* feat(server): 悬浮工具栏改用候选窗的柔和阴影,并可在设置中关闭阴影 - D2D 工具栏改用与候选窗相同的两层阴影(light α .18/.10,dark α .34/.22), 透明边距与候选窗一致(16/14/32/34 DIP) - 新增配置 general.floating_toolbar_shadow(默认开启),设置页「悬浮工具栏」 加「工具栏阴影」开关;关闭后去掉阴影边距,工具栏可贴住屏幕边缘 - 关闭阴影时保留 1 DIP 边距,避免居中描边的外半边被窗口边缘裁掉 - WebView2 工具栏:开关同样生效;默认页阴影改为候选窗的近接层(宿主只留 8px) * feat(server): 悬浮工具栏开着阴影也能贴住屏幕边缘 - 阴影边距允许伸出屏幕:拖动结束、改尺寸和默认位置都只要求工具栏本体 (含描边)留在工作区内;伸出工作区的部分用窗口区域裁掉,不绘制、 不挡任务栏点击,拖动时实时更新 - 伸出量按宿主实际像素尺寸和卡片位置计算,消除向上取整留下的 1-3 px 缝隙;关掉阴影时同样贴边 - 切换阴影时保持工具栏本体位置不动,并按工作区重新判定边界, 避免工具栏一部分跑到任务栏下面 - 设置页「工具栏阴影」说明改为「在工具栏周围显示与候选窗一致的柔和阴影」
467 lines
16 KiB
HTML
467 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' https://msime-contracts; style-src 'unsafe-inline'; img-src data: https://appassets https://candidate-skins; font-src data: https://appassets https://candidate-skins">
|
||
<meta charset="UTF-8">
|
||
<title>IME(FTB)</title>
|
||
<style>
|
||
:root {
|
||
--ftb-scale: 1;
|
||
--ftb-bar-height: 35px;
|
||
--ftb-icon-size: 24px;
|
||
--ftb-gap: 6px;
|
||
--ftb-pad-left: 8px;
|
||
--ftb-pad-right: 4px;
|
||
--ftb-radius: 8px;
|
||
--ftb-handle-width: 2.5px;
|
||
--ftb-handle-height: 14px;
|
||
/* Filled by C++ from half-monitor DIPs (DPI-aware). */
|
||
--msime-max-width-dip: 50vw;
|
||
--msime-max-height-dip: 50vh;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
background-color: transparent;
|
||
margin: 0;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
|
||
body {
|
||
-webkit-app-region: drag;
|
||
}
|
||
|
||
/* 状态栏整体样式 */
|
||
.status-bar {
|
||
position: absolute;
|
||
box-sizing: border-box;
|
||
height: auto;
|
||
min-height: calc(var(--ftb-bar-height) * var(--ftb-scale));
|
||
max-width: min(100%, var(--msime-max-width-dip));
|
||
max-height: min(100%, var(--msime-max-height-dip));
|
||
top: 0px;
|
||
left: 0px;
|
||
display: flex;
|
||
/* FTB is a single-row control. Wrapping the three top-level sections
|
||
puts the drag handle on a separate row when the host is 1-2px narrow. */
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
/* The native host remeasures overflow via scrollWidth/scrollHeight.
|
||
Never expose Chromium scrollbars for sub-pixel DPI rounding. */
|
||
overflow: hidden;
|
||
background-color: #1a1a1a;
|
||
border-radius: calc(var(--ftb-radius) * var(--ftb-scale));
|
||
padding: 0px calc(var(--ftb-pad-right) * var(--ftb-scale)) 0px calc(var(--ftb-pad-left) * var(--ftb-scale));
|
||
gap: calc(var(--ftb-gap) * var(--ftb-scale));
|
||
|
||
border: 1.4px solid rgba(255, 255, 255, 0.15);
|
||
/* 候选窗阴影的近接层;宿主只在右下留 8px(FTB_WND_SHADOW_WIDTH),放不下 24px 的扩散层。 */
|
||
box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.22);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
/* 拖动条 */
|
||
.drag-handle {
|
||
width: calc(var(--ftb-handle-width) * var(--ftb-scale));
|
||
height: calc(var(--ftb-handle-height) * var(--ftb-scale));
|
||
border-radius: calc(2px * var(--ftb-scale));
|
||
background: #8E8CD8;
|
||
margin-right: 0px;
|
||
position: relative;
|
||
}
|
||
|
||
/* 分隔线 */
|
||
.divider {
|
||
width: calc(1.2px * var(--ftb-scale));
|
||
align-self: stretch;
|
||
background-color: rgba(255, 255, 255, 0.15);
|
||
margin-left: calc(4px * var(--ftb-scale));
|
||
margin-right: 0px;
|
||
}
|
||
|
||
.icon {
|
||
color: #fff;
|
||
font-size: calc(var(--ftb-icon-size) * 0.833 * var(--ftb-scale));
|
||
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: calc(var(--ftb-icon-size) * var(--ftb-scale));
|
||
height: calc(var(--ftb-icon-size) * var(--ftb-scale));
|
||
border-radius: calc(6px * var(--ftb-scale));
|
||
transition: background-color 0.15s ease;
|
||
cursor: default;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
|
||
.lang-label {
|
||
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||
font-size: calc(var(--ftb-icon-size) * 0.8 * var(--ftb-scale));
|
||
line-height: 1;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
|
||
.lang-label::selection,
|
||
.english-candidate-label::selection {
|
||
background: transparent;
|
||
color: inherit;
|
||
}
|
||
|
||
/* 与 D2D 工具栏同一套图标字体(msimeui::ResolveIconGlyph):Segoe Fluent Icons
|
||
优先,Segoe MDL2 Assets 兜底;两者都缺的码位由脚本换成 data-fallback 文字。 */
|
||
.icon-glyph {
|
||
font-family: "Segoe Fluent Icons", "Segoe MDL2 Assets";
|
||
font-size: calc(var(--ftb-icon-size) * 0.82 * var(--ftb-scale));
|
||
font-style: normal;
|
||
line-height: 1;
|
||
}
|
||
|
||
.icon-glyph.is-fallback {
|
||
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||
font-size: calc(var(--ftb-icon-size) * 0.8 * var(--ftb-scale));
|
||
}
|
||
|
||
.icon:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.english-candidate-label {
|
||
position: relative;
|
||
font-family: "Segoe UI", sans-serif;
|
||
font-size: calc(var(--ftb-icon-size) * 0.62 * var(--ftb-scale));
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
}
|
||
|
||
.english-candidate-label::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 8%;
|
||
right: 8%;
|
||
bottom: calc(-3px * var(--ftb-scale));
|
||
height: calc(1.5px * var(--ftb-scale));
|
||
border-radius: 1px;
|
||
background: currentColor;
|
||
}
|
||
|
||
.left-section {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
gap: calc(4px * var(--ftb-scale));
|
||
}
|
||
|
||
.right-section {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
gap: calc(var(--ftb-gap) * var(--ftb-scale));
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<div class="status-bar">
|
||
<div class="left-section">
|
||
<div class="drag-handle"></div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="right-section">
|
||
<!-- 1. 输入法状态 -->
|
||
<!-- 中文状态 -->
|
||
<div id="cn-en" data-toolbar-item="language">
|
||
<div class="icon" id="cn" title="中文">
|
||
<span class="icon-glyph" data-fallback="中"></span>
|
||
</div>
|
||
<!-- 日文状态 -->
|
||
<div class="icon" id="ja" style="display: none" title="日文">
|
||
<span class="icon-glyph" data-fallback="日"></span>
|
||
</div>
|
||
<!-- 大写锁定 -->
|
||
<div class="icon" id="cap" style="display: none" title="大写锁定">
|
||
<span class="lang-label">A</span>
|
||
</div>
|
||
<!-- 英文状态 -->
|
||
<div class="icon" id="en" title="英文">
|
||
<span class="icon-glyph" data-fallback="英"></span>
|
||
</div>
|
||
<!-- 英文候选输入状态 -->
|
||
<div class="icon" id="en-candidate" style="display: none" title="英文候选输入">
|
||
<span class="english-candidate-label">En</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="char-width-mode" data-toolbar-item="fullwidth">
|
||
<!-- 2. 字符状态 -->
|
||
<!-- 半角 -->
|
||
<div class="icon" id="halfwidth">
|
||
<span class="icon-glyph" data-fallback="半"></span>
|
||
</div>
|
||
<!-- 全角 -->
|
||
<div class="icon" id="fullwidth">
|
||
<span class="icon-glyph" data-fallback="全"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="punctuation-mode" data-toolbar-item="punctuation">
|
||
<!-- 中文标点 -->
|
||
<div class="icon" id="puncCn">
|
||
<span class="icon-glyph" data-fallback="。"></span>
|
||
</div>
|
||
<!-- 英文标点 -->
|
||
<div class="icon" id="puncEn">
|
||
<span class="icon-glyph" data-fallback=","></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="icon" id="character-set" data-toolbar-item="character_set">
|
||
<span id="character-set-simplified" class="icon-glyph" data-fallback="简"></span>
|
||
<span id="character-set-traditional" style="display: none" class="icon-glyph" data-fallback="繁"></span>
|
||
</div>
|
||
<div class="icon" id="emoji-panel" data-toolbar-item="emoji">
|
||
<span class="icon-glyph" data-fallback="表"></span>
|
||
</div>
|
||
<div class="icon" id="screen-keyboard" data-toolbar-item="screen_keyboard" style="display: none">
|
||
<span class="icon-glyph" data-fallback="键"></span>
|
||
</div>
|
||
<div class="icon" id="settings" data-toolbar-item="settings">
|
||
<span class="icon-glyph" data-fallback="设"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
<script src="https://msime-contracts/schema.js"></script>
|
||
<script src="https://msime-contracts/runtime.js"></script>
|
||
<script>const serializeHostMessage = message => MsimeProtocol.serializeClientMessage(message, "toolbar");</script>
|
||
<script>
|
||
//
|
||
// 左侧拖拽区域
|
||
//
|
||
let dividerX = 0;
|
||
|
||
// 初始化时或窗口尺寸变化时更新一次
|
||
function updateDividerX() {
|
||
const divider = document.querySelector('.divider');
|
||
if (divider) {
|
||
dividerX = divider.getBoundingClientRect().left;
|
||
}
|
||
}
|
||
|
||
let isDragging = false, startX, startY;
|
||
|
||
window.addEventListener('mousedown', e => {
|
||
if (e.clientX < dividerX) { // 左部区域
|
||
isDragging = true;
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'dragStart' }));
|
||
}
|
||
});
|
||
|
||
window.addEventListener('mouseup', () => {
|
||
isDragging = false;
|
||
});
|
||
|
||
window.addEventListener('resize', updateDividerX);
|
||
// load handler is registered below together with the ready signal.
|
||
|
||
//
|
||
// 中英切换
|
||
//
|
||
// Default status is CN
|
||
document.getElementById('en').style.display = 'none';
|
||
document.getElementById('en-candidate').style.display = 'none';
|
||
const jaIcon = document.getElementById('ja');
|
||
if (jaIcon) jaIcon.style.display = 'none';
|
||
const capIcon = document.getElementById('cap');
|
||
if (capIcon) capIcon.style.display = 'none';
|
||
// document.getElementById('cn').style.display = 'none';
|
||
// 中英切换点击,向 Cpp 端发送消息
|
||
document.getElementById('cn').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeIMEMode', data: 'en' }));
|
||
});
|
||
const jaButton = document.getElementById('ja');
|
||
if (jaButton) {
|
||
jaButton.addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeIMEMode', data: 'en' }));
|
||
});
|
||
}
|
||
document.getElementById('en').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeIMEMode', data: 'cn' }));
|
||
});
|
||
document.getElementById('en-candidate').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'exitEnglishInputMode' }));
|
||
});
|
||
const capButton = document.getElementById('cap');
|
||
if (capButton) {
|
||
capButton.addEventListener('click', () => {
|
||
const lang = (document.getElementById('cn-en').dataset.lang || 'cn');
|
||
if (lang === 'en') {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeIMEMode', data: 'cn' }));
|
||
} else if (lang === 'en-candidate') {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'exitEnglishInputMode' }));
|
||
} else {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeIMEMode', data: 'en' }));
|
||
}
|
||
});
|
||
}
|
||
|
||
//
|
||
// 半角全角切换
|
||
//
|
||
// Default status is Halfwidth
|
||
document.getElementById('fullwidth').style.display = 'none';
|
||
// document.getElementById('halfwidth').style.display = 'none';
|
||
document.getElementById('halfwidth').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeCharMode', data: 'fullwidth' }));
|
||
});
|
||
document.getElementById('fullwidth').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeCharMode', data: 'halfwidth' }));
|
||
});
|
||
|
||
//
|
||
// 中英文标点切换
|
||
//
|
||
// Default status is CN Punc
|
||
document.getElementById('puncEn').style.display = 'none';
|
||
// document.getElementById('puncCn').style.display = 'none';
|
||
document.getElementById('puncCn').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changePuncMode', data: 'puncEn' }));
|
||
});
|
||
document.getElementById('puncEn').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changePuncMode', data: 'puncCn' }));
|
||
});
|
||
|
||
//
|
||
// 简繁输入切换
|
||
//
|
||
document.getElementById('character-set').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'changeCharacterSet' }));
|
||
});
|
||
|
||
document.getElementById('emoji-panel').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'openEmojiPanel' }));
|
||
});
|
||
|
||
document.getElementById('screen-keyboard').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'openKeyboardPanel' }));
|
||
});
|
||
|
||
//
|
||
// 设置按钮
|
||
//
|
||
document.getElementById('settings').addEventListener('click', () => {
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'openSettings' }));
|
||
});
|
||
|
||
//
|
||
// 图标字形回退:Segoe Fluent Icons 只随 Windows 11 提供,旧版 Windows 10 的 Segoe MDL2 Assets
|
||
// 也可能缺少部分输入法码位。Chromium 缺字时静默换字体,按钮会变成空白方框(issue #232),
|
||
// 所以和 D2D 工具栏一样,缺字形的按钮改用文字。U+10FFFD 不在任何字体里,拿它的渲染结果当
|
||
// “缺字形”的参照。
|
||
//
|
||
(function applyIconGlyphFallbacks() {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 32;
|
||
canvas.height = 32;
|
||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||
if (!context) return;
|
||
const render = (text) => {
|
||
context.clearRect(0, 0, 32, 32);
|
||
context.font = '24px "Segoe Fluent Icons", "Segoe MDL2 Assets"';
|
||
context.textBaseline = 'top';
|
||
context.fillText(text, 4, 4);
|
||
return context.getImageData(0, 0, 32, 32).data;
|
||
};
|
||
const missing = render('\u{10FFFD}');
|
||
document.querySelectorAll('.icon-glyph[data-fallback]').forEach((node) => {
|
||
const pixels = render(node.textContent);
|
||
if (pixels.every((value, index) => value === missing[index])) {
|
||
node.textContent = node.dataset.fallback;
|
||
node.classList.add('is-fallback');
|
||
}
|
||
});
|
||
})();
|
||
|
||
// Tell the host the first frame is ready so it can stop the paint-grace show
|
||
// and apply the real visibility decision (IME active / configured / etc.).
|
||
function notifyToolbarReady() {
|
||
if (!window.chrome || !window.chrome.webview) return;
|
||
window.chrome.webview.postMessage(serializeHostMessage({ type: 'ready' }));
|
||
}
|
||
window.addEventListener('load', () => {
|
||
updateDividerX();
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(notifyToolbarReady);
|
||
});
|
||
});
|
||
|
||
</script>
|
||
<script>
|
||
(function () {
|
||
var SURFACE = 'ftb';
|
||
var SELECTOR = '.status-bar';
|
||
var COOLDOWN_MS = 450;
|
||
var lastSent = 0;
|
||
var pending = false;
|
||
function el() { return document.querySelector(SELECTOR); }
|
||
function truncated(node) {
|
||
if (!node) return false;
|
||
var vw = document.documentElement.clientWidth;
|
||
var vh = document.documentElement.clientHeight;
|
||
var r = node.getBoundingClientRect();
|
||
var pad = 1.5;
|
||
return node.scrollWidth > vw + pad || node.scrollHeight > vh + pad ||
|
||
r.width > vw + pad || r.height > vh + pad ||
|
||
r.right > vw + pad || r.bottom > vh + pad;
|
||
}
|
||
function report() {
|
||
var node = el();
|
||
if (!node || !truncated(node)) return;
|
||
if (!window.chrome || !window.chrome.webview) return;
|
||
var now = Date.now();
|
||
if (now - lastSent < COOLDOWN_MS) return;
|
||
lastSent = now;
|
||
var r = node.getBoundingClientRect();
|
||
window.chrome.webview.postMessage(serializeHostMessage({
|
||
type: 'contentTruncated',
|
||
data: {
|
||
surface: SURFACE,
|
||
width: Math.ceil(Math.max(r.width, node.scrollWidth || 0)),
|
||
height: Math.ceil(Math.max(r.height, node.scrollHeight || 0)),
|
||
viewportWidth: document.documentElement.clientWidth,
|
||
viewportHeight: document.documentElement.clientHeight
|
||
}
|
||
}));
|
||
}
|
||
function schedule() {
|
||
if (pending) return;
|
||
pending = true;
|
||
requestAnimationFrame(function () {
|
||
requestAnimationFrame(function () {
|
||
pending = false;
|
||
report();
|
||
});
|
||
});
|
||
}
|
||
window.CheckContentTruncation = schedule;
|
||
window.addEventListener('resize', schedule);
|
||
window.addEventListener('load', schedule);
|
||
if (typeof ResizeObserver !== 'undefined') {
|
||
var ro = new ResizeObserver(schedule);
|
||
ro.observe(document.documentElement);
|
||
var node = el();
|
||
if (node) ro.observe(node);
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
</html>
|