工具栏按钮由 SVG 改为 Segoe Fluent Icons / Segoe MDL2 Assets 码位,与 D2D 工具栏逐一对应;两者都缺某个字形时换成与 D2D 相同的回退文字 (issue #232)。图标颜色因此跟随各皮肤的 .icon color,石墨皮肤的 图标色此前被 SVG 写死的描边覆盖,现在生效。设置页的工具栏与皮肤 预览同步改用图标字体和相同的回退。
466 lines
16 KiB
HTML
466 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);
|
|
box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.4);
|
|
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>
|