Files
msime-windows/ui-html/webview2/ftb/default.html
T
Lu Fan(SonnyCalcr) 42c2666a4d feat(server): 悬浮工具栏改用候选窗的柔和阴影,开着阴影也能贴边,并可在设置中关闭阴影 (#809)
* 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
  缝隙;关掉阴影时同样贴边
- 切换阴影时保持工具栏本体位置不动,并按工作区重新判定边界,
  避免工具栏一部分跑到任务栏下面
- 设置页「工具栏阴影」说明改为「在工具栏周围显示与候选窗一致的柔和阴影」
2026-10-11 13:07:27 +08:00

467 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="中">&#xE982;</span>
</div>
<!-- 日文状态 -->
<div class="icon" id="ja" style="display: none" title="日文">
<span class="icon-glyph" data-fallback="日">&#xE7DE;</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="英">&#xE983;</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="半">&#xEC46;</span>
</div>
<!-- 全角 -->
<div class="icon" id="fullwidth">
<span class="icon-glyph" data-fallback="全">&#xF138;</span>
</div>
</div>
<div id="punctuation-mode" data-toolbar-item="punctuation">
<!-- 中文标点 -->
<div class="icon" id="puncCn">
<span class="icon-glyph" data-fallback="。">&#xF111;</span>
</div>
<!-- 英文标点 -->
<div class="icon" id="puncEn">
<span class="icon-glyph" data-fallback=",">&#xF110;</span>
</div>
</div>
<div class="icon" id="character-set" data-toolbar-item="character_set">
<span id="character-set-simplified" class="icon-glyph" data-fallback="简">&#xE88D;</span>
<span id="character-set-traditional" style="display: none" class="icon-glyph" data-fallback="繁">&#xE88C;</span>
</div>
<div class="icon" id="emoji-panel" data-toolbar-item="emoji">
<span class="icon-glyph" data-fallback="表">&#xE76E;</span>
</div>
<div class="icon" id="screen-keyboard" data-toolbar-item="screen_keyboard" style="display: none">
<span class="icon-glyph" data-fallback="键">&#xE765;</span>
</div>
<div class="icon" id="settings" data-toolbar-item="settings">
<span class="icon-glyph" data-fallback="设">&#xE713;</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>