feat(ui): add a WeChat-inspired skin with light/dark variants and synchronized candidate and toolbar previews
This commit is contained in:
1 parent
3a65b88bbe
commit
e104beef22
11 files changed
+3677
-56
No files matched your search
@@ -0,0 +1,599 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>(IME)WechatHCandWnd</title>
|
||||
<style>
|
||||
:root {
|
||||
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||
--cand-font-size: 16px;
|
||||
--cand-text: #b7b7b7;
|
||||
--cand-num: #858585;
|
||||
/* Fallback before C++ injects host-width DIP. Wrap limit = 宿主宽度. */
|
||||
--msime-max-width-dip: 100%;
|
||||
--msime-max-height-dip: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--cand-font-family);
|
||||
font-size: var(--cand-font-size);
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.containerParent {
|
||||
width: fit-content;
|
||||
max-width: var(--msime-max-width-dip);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
background: #202020;
|
||||
/* background: url("https://appassets/background.jpg"); */
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
|
||||
min-width: 7em;
|
||||
/* Wrap only when content reaches the host (injected) max width. */
|
||||
width: min(max-content, var(--msime-max-width-dip));
|
||||
max-width: var(--msime-max-width-dip);
|
||||
/* Host height in DIP (injected). Do not use min(100%, …): parent height is
|
||||
content-sized so 100% never resolves and tall vertical lists get clipped
|
||||
by the HWND instead of scrolling. */
|
||||
max-height: var(--msime-max-height-dip);
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
user-select: none;
|
||||
border: 1.5px solid #9b9b9b2e;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 1.5px;
|
||||
height: 1.2em;
|
||||
background: #6b69d6;
|
||||
transform: translate(1.3px, 10%);
|
||||
}
|
||||
|
||||
.row {
|
||||
justify-content: space-between;
|
||||
/* padding: 2px; */
|
||||
/* padding-right: 12px; */
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.cand {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hover-active .cand:hover {
|
||||
background-color: #414141;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.first-row-wrapper {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.first {
|
||||
background-color: #3e3e3eb9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* bar */
|
||||
.first::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 53%;
|
||||
transform: translateY(-50%);
|
||||
height: 0.8em;
|
||||
width: 0.2em;
|
||||
background: #6b69d6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: calc(0.2em + 3px);
|
||||
padding-right: 5px;
|
||||
color: var(--cand-text);
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cand .text {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 0.8em;
|
||||
color: var(--cand-num);
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.pinyin {
|
||||
/* Keep preedit in sync with the configured candidate-window font size. */
|
||||
font-size: var(--cand-font-size);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hidden-pinyin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Hide candidate-window top preedit when configured as empty */
|
||||
.container.preedit-hidden > .row.pinyin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide container when empty */
|
||||
.container:empty {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.context-menu { display:none; position:fixed; z-index:1000; background:#2d2d2d; color:#e9e8e8;
|
||||
border:1px solid #9b9b9b2e; border-radius:6px; padding:1px 0; min-width:86px; font-size:14px; line-height:20px; }
|
||||
.context-menu-item { position:relative; padding:1px 20px 1px 4px; margin:2px; border-radius:4px; cursor:default; white-space:nowrap; }
|
||||
.context-menu-item:hover { background:#414141; }
|
||||
.context-menu-parent::after { content:"›"; position:absolute; right:6px; }
|
||||
.context-submenu { display:none; position:absolute; left:100%; top:-2px; min-width:0;
|
||||
background:#2d2d2d; border:1px solid #9b9b9b2e; border-radius:6px; padding:1px 0; }
|
||||
.context-submenu .context-menu-item { padding-right: 8px; }
|
||||
.context-menu-parent:hover>.context-submenu { display:block; }
|
||||
.context-menu-separator { height:1px; margin:3px 5px; background:#ffffff20; }
|
||||
|
||||
/* WeChat-inspired candidate skin. Kept in a standalone document so the
|
||||
host can switch skins by navigating WebView2 to a different HTML file. */
|
||||
body { border-radius: 5px; }
|
||||
.container {
|
||||
background: #151515;
|
||||
padding: 2px;
|
||||
border: 1px solid #292929;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, .42);
|
||||
}
|
||||
.cand { border-radius: 4px; }
|
||||
.cursor { background: #07c160; }
|
||||
.hover-active .cand:hover { background-color: rgba(7, 193, 96, .22); }
|
||||
.first {
|
||||
background-color: #07c160;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.first::before { display: none; }
|
||||
.first .text,
|
||||
.first .num { color: #ffffff; }
|
||||
.context-menu,
|
||||
.context-submenu { background: #1f1f1f; border-color: #343434; color: #d0d0d0; }
|
||||
.context-menu-item:hover { background: #2a2a2a; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let a = false;
|
||||
let currentRightClickedCand = null;
|
||||
let contextMenuTopExpansion = 0;
|
||||
let contextMenuLeftExpansion = 0;
|
||||
function positionContextMenu(menu, x, y) {
|
||||
menu.style.left = x + 'px'; menu.style.top = y + 'px';
|
||||
menu.style.visibility = 'hidden'; menu.style.display = 'block';
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const adjustedX = x - Math.max(0, window.screenX + x + menu.offsetWidth + 4 - screenRight);
|
||||
const adjustedY = y - Math.max(0, window.screenY + y + menu.offsetHeight + 4 - screenBottom);
|
||||
menu.style.left = Math.max(0, adjustedX) + 'px';
|
||||
menu.style.top = adjustedY + 'px';
|
||||
menu.style.visibility = '';
|
||||
return { x: Math.max(0, adjustedX), y: adjustedY, topExpansion: Math.max(0, 2 - adjustedY) };
|
||||
}
|
||||
function positionContextSubmenu(menu, menuY) {
|
||||
const parent = menu.querySelector('.context-menu-parent');
|
||||
const submenu = menu.querySelector('.context-submenu');
|
||||
if (!parent || !submenu) return { width: 80, height: 180, bottom: menuY + menu.offsetHeight };
|
||||
submenu.style.top = '-2px';
|
||||
submenu.style.visibility = 'hidden';
|
||||
submenu.style.display = 'block';
|
||||
const width = submenu.offsetWidth;
|
||||
const naturalHeight = submenu.offsetHeight;
|
||||
const menuX = Number.parseFloat(menu.style.left) || 0;
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const openLeft = window.screenX + menuX + menu.offsetWidth + width + 4 > screenRight;
|
||||
const leftExpansion = openLeft ? Math.max(0, width - menuX + 2) : 0;
|
||||
submenu.style.left = openLeft ? 'auto' : '100%';
|
||||
submenu.style.right = openLeft ? '100%' : 'auto';
|
||||
const defaultScreenTop = window.screenY + menuY + parent.offsetTop - 2;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const targetScreenTop = Math.max(window.screen.availTop + 4,
|
||||
Math.min(defaultScreenTop, screenBottom - naturalHeight - 4));
|
||||
const topExpansion = Math.max(0, window.screenY + 2 - targetScreenTop);
|
||||
submenu.style.top = (targetScreenTop - window.screenY - menuY - parent.offsetTop) + 'px';
|
||||
const height = submenu.offsetHeight;
|
||||
submenu.style.display = '';
|
||||
submenu.style.visibility = '';
|
||||
return { width, height, topExpansion, leftExpansion, bottom: menuY + parent.offsetTop +
|
||||
Number.parseFloat(submenu.style.top) + height };
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
const cand = e.target instanceof Element ? e.target.closest('.cand') : null;
|
||||
if (!cand) return;
|
||||
currentRightClickedCand = cand;
|
||||
const menu = document.getElementById('contextMenu');
|
||||
document.getElementById('menuDelete').style.display =
|
||||
Array.from((cand.textContent || '').replace(/^\s*\d+\s*/, '').trim()).length === 1 ? 'none' : 'block';
|
||||
const menuPosition=positionContextMenu(menu,e.clientX,e.clientY);
|
||||
const submenuSize=positionContextSubmenu(menu,menuPosition.y), content=document.getElementById('realContainer');
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuResize',data:{
|
||||
width:Math.ceil(menuPosition.x+menu.offsetWidth+submenuSize.width+4),
|
||||
height:Math.ceil(Math.max(content?.offsetHeight||0,menuPosition.y+menu.offsetHeight+4,submenuSize.bottom+4)),
|
||||
topExpansion:Math.ceil(Math.max(menuPosition.topExpansion||0,submenuSize.topExpansion||0))
|
||||
,leftExpansion:Math.ceil(submenuSize.leftExpansion||0)
|
||||
}}));
|
||||
});
|
||||
// Dismiss only when clicking blank areas inside the rendered candidate card.
|
||||
document.addEventListener('mousedown', function (e) {
|
||||
if (e.button !== 0) return;
|
||||
if (!(e.target instanceof Element)) return;
|
||||
if (e.target.closest('.context-menu')) return;
|
||||
if (!e.target.closest('#realContainer')) return;
|
||||
hideContextMenu();
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candElement = e.target.closest('.cand');
|
||||
if (!candElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('realContainer');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const candidates = Array.from(container.querySelectorAll('.cand'));
|
||||
const candIndex = candidates.indexOf(candElement) + 1;
|
||||
if (candIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'candidate', data: candIndex }));
|
||||
} catch (error) {
|
||||
console.error('Failed to send candidate click:', error);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function () {
|
||||
if (a === true) {
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.add('hover-active');
|
||||
}
|
||||
a = true;
|
||||
});
|
||||
});
|
||||
function hideContextMenu() {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
if (!menu || menu.style.display !== 'block') return;
|
||||
menu.style.display = 'none';
|
||||
currentRightClickedCand = null;
|
||||
window.RestoreContextMenuTopExpansion();
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuClosed'}));
|
||||
}
|
||||
function menuIndex(){ return Array.from(document.querySelectorAll('#realContainer .cand')).indexOf(currentRightClickedCand)+1; }
|
||||
function sendMenu(type,data){ window.chrome.webview.postMessage(JSON.stringify({type,data}));
|
||||
hideContextMenu(); }
|
||||
function onPinToTop(e){ if(e.button===0) sendMenu('pin',menuIndex()); }
|
||||
function onDelete(e){ if(e.button===0) sendMenu('delete',menuIndex()); }
|
||||
function onFixPosition(e,position){ if(e.button===0) sendMenu('fixPosition',{index:menuIndex(),position}); }
|
||||
function onClearPosition(e){ if(e.button===0) sendMenu('clearPosition',menuIndex()); }
|
||||
|
||||
window.ClearState = function () {
|
||||
a = false;
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.remove('hover-active');
|
||||
hideContextMenu();
|
||||
}
|
||||
window.ApplyContextMenuTopExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuTopExpansion;
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) + delta) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) + delta) + 'px';
|
||||
contextMenuTopExpansion = pixels;
|
||||
}
|
||||
window.ApplyContextMenuLeftExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuLeftExpansion;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) + delta) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) + delta) + 'px';
|
||||
contextMenuLeftExpansion = pixels;
|
||||
}
|
||||
window.RestoreContextMenuTopExpansion = function () {
|
||||
if (!contextMenuTopExpansion && !contextMenuLeftExpansion) return;
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) - contextMenuTopExpansion) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) - contextMenuTopExpansion) + 'px';
|
||||
contextMenuTopExpansion = 0;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
contextMenuLeftExpansion = 0;
|
||||
}
|
||||
|
||||
window.SetCandidateSelection = function (index) {
|
||||
const candidates = document.querySelectorAll('#realContainer .cand');
|
||||
candidates.forEach((candidate) => candidate.classList.remove('first'));
|
||||
if (index >= 0 && index < candidates.length) {
|
||||
candidates[index].classList.add('first');
|
||||
}
|
||||
}
|
||||
|
||||
window.SetCandidatePreeditVisible = function (visible) {
|
||||
const hide = !visible;
|
||||
document.getElementById('realContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
document.getElementById('measureContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
}
|
||||
|
||||
window.SetPreeditCaret = function () {
|
||||
const preedit = document.querySelector('#realContainer .pinyin .text');
|
||||
if (!preedit) return;
|
||||
const marker = '\uE000';
|
||||
const text = preedit.textContent || '';
|
||||
const index = text.indexOf(marker);
|
||||
const cleanText = text.replace(marker, '');
|
||||
const caretIndex = index >= 0 ? index : cleanText.length;
|
||||
preedit.textContent = '';
|
||||
preedit.append(document.createTextNode(cleanText.slice(0, caretIndex)));
|
||||
const caret = document.createElement('span');
|
||||
caret.className = 'cursor';
|
||||
preedit.append(caret);
|
||||
preedit.append(document.createTextNode(cleanText.slice(caretIndex)));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="realBody">
|
||||
<div class="containerParent" id="realContainerParent">
|
||||
<div class="container" id="realContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">ni'mf<span class="cursor"></span></div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper first-row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="num">1</span> 你们(rR)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">2</span> 你(rX)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">3</span> 尼(uV)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">4</span> 妮(nV)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">5</span> 泥(dV)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">6</span> 逆(zQ)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">7</span> 拟(fR)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">8</span> 腻(oD)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">9</span> 拧(nG)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="measureBody" style="visibility: hidden;">
|
||||
<div class="containerParent" id="measureContainerParent">
|
||||
<div class="container" id="measureContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">{0}</div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper first-row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="num">1</span> {1} 你</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">2</span> {2} 我</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">3</span> {3} 什么</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">4</span> {4} 量子</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">5</span> {5} 可恶</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">6</span> {6} 不是</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">7</span> {7} 那么</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">8</span> {8} 如果</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">9</span> {9}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contextMenu" class="context-menu">
|
||||
<div class="context-menu-item" onmouseup="onPinToTop(event)">置顶</div>
|
||||
<div class="context-menu-item context-menu-parent">固定排位<div class="context-submenu">
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,1)">第 1 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,2)">第 2 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,3)">第 3 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,4)">第 4 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,5)">第 5 位</div>
|
||||
<div class="context-menu-separator"></div><div class="context-menu-item" onmouseup="onClearPosition(event)">取消固定</div>
|
||||
</div></div>
|
||||
<div id="menuDelete" class="context-menu-item" onmouseup="onDelete(event)">删除</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var SURFACE = 'cand';
|
||||
var SELECTOR = '#realContainer';
|
||||
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(JSON.stringify({
|
||||
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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,594 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>(IME)WechatLightHCandWnd</title>
|
||||
<style>
|
||||
:root {
|
||||
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||
--cand-font-size: 16px;
|
||||
--cand-text: #333333;
|
||||
--cand-num: #757575;
|
||||
/* Fallback before C++ injects host-width DIP. Wrap limit = 宿主宽度. */
|
||||
--msime-max-width-dip: 100%;
|
||||
--msime-max-height-dip: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--cand-font-family);
|
||||
font-size: var(--cand-font-size);
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.containerParent {
|
||||
width: fit-content;
|
||||
max-width: var(--msime-max-width-dip);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
background: #ffffff;
|
||||
/* background: url("https://appassets/background.jpg"); */
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.12);
|
||||
min-width: 7em;
|
||||
/* Wrap only when content reaches the host (injected) max width. */
|
||||
width: min(max-content, var(--msime-max-width-dip));
|
||||
max-width: var(--msime-max-width-dip);
|
||||
/* Host height in DIP (injected). Do not use min(100%, …): parent height is
|
||||
content-sized so 100% never resolves and tall vertical lists get clipped
|
||||
by the HWND instead of scrolling. */
|
||||
max-height: var(--msime-max-height-dip);
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
user-select: none;
|
||||
border: 1.5px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 1.5px;
|
||||
height: 1.2em;
|
||||
background: #6b69d6;
|
||||
transform: translate(1.3px, 10%);
|
||||
}
|
||||
|
||||
.row {
|
||||
justify-content: space-between;
|
||||
/* padding: 2px; */
|
||||
/* padding-right: 12px; */
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.cand {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hover-active .cand:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.first-row-wrapper {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.first {
|
||||
background-color: #e8e8e8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* bar */
|
||||
.first::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 53%;
|
||||
transform: translateY(-50%);
|
||||
height: 0.8em;
|
||||
width: 0.2em;
|
||||
background: #6b69d6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: calc(0.2em + 3px);
|
||||
padding-right: 5px;
|
||||
color: var(--cand-text);
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cand .text {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 0.8em;
|
||||
color: var(--cand-num);
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.pinyin {
|
||||
/* Keep preedit in sync with the configured candidate-window font size. */
|
||||
font-size: var(--cand-font-size);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hidden-pinyin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Hide candidate-window top preedit when configured as empty */
|
||||
.container.preedit-hidden > .row.pinyin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide container when empty */
|
||||
.container:empty {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.context-menu { display:none; position:fixed; z-index:1000; background:#fff; color:#1a1a1a;
|
||||
border:1px solid #00000020; border-radius:6px; padding:1px 0; min-width:86px; font-size:14px; line-height:20px; }
|
||||
.context-menu-item { position:relative; padding:1px 20px 1px 4px; margin:2px; border-radius:4px; cursor:default; white-space:nowrap; }
|
||||
.context-menu-item:hover { background:#ececec; }
|
||||
.context-menu-parent::after { content:"›"; position:absolute; right:6px; }
|
||||
.context-submenu { display:none; position:absolute; left:100%; top:-2px; min-width:0;
|
||||
background:#fff; border:1px solid #00000020; border-radius:6px; padding:1px 0; }
|
||||
.context-submenu .context-menu-item { padding-right: 8px; }
|
||||
.context-menu-parent:hover>.context-submenu { display:block; }
|
||||
.context-menu-separator { height:1px; margin:3px 5px; background:#00000020; }
|
||||
/* Light variant of the standalone WeChat-inspired candidate skin. */
|
||||
body { border-radius: 5px; }
|
||||
.container {
|
||||
background: #f7f7f7;
|
||||
padding: 2px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, .14);
|
||||
}
|
||||
.cand { border-radius: 4px; }
|
||||
.cursor { background: #07c160; }
|
||||
.cand:hover { background-color: rgba(7, 193, 96, .14); }
|
||||
.first { background-color: #07c160; border-radius: 5px; }
|
||||
.first::before { display: none; }
|
||||
.first .text,
|
||||
.first .num { color: #ffffff; }
|
||||
.context-menu,
|
||||
.context-submenu { background: #ffffff; border-color: #d9d9d9; color: #333333; }
|
||||
.context-menu-item:hover { background: #eeeeee; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let a = false;
|
||||
let currentRightClickedCand = null;
|
||||
let contextMenuTopExpansion = 0;
|
||||
let contextMenuLeftExpansion = 0;
|
||||
function positionContextMenu(menu, x, y) {
|
||||
menu.style.left = x + 'px'; menu.style.top = y + 'px';
|
||||
menu.style.visibility = 'hidden'; menu.style.display = 'block';
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const adjustedX = x - Math.max(0, window.screenX + x + menu.offsetWidth + 4 - screenRight);
|
||||
const adjustedY = y - Math.max(0, window.screenY + y + menu.offsetHeight + 4 - screenBottom);
|
||||
menu.style.left = Math.max(0, adjustedX) + 'px';
|
||||
menu.style.top = adjustedY + 'px';
|
||||
menu.style.visibility = '';
|
||||
return { x: Math.max(0, adjustedX), y: adjustedY, topExpansion: Math.max(0, 2 - adjustedY) };
|
||||
}
|
||||
function positionContextSubmenu(menu, menuY) {
|
||||
const parent = menu.querySelector('.context-menu-parent');
|
||||
const submenu = menu.querySelector('.context-submenu');
|
||||
if (!parent || !submenu) return { width: 80, height: 180, bottom: menuY + menu.offsetHeight };
|
||||
submenu.style.top = '-2px';
|
||||
submenu.style.visibility = 'hidden';
|
||||
submenu.style.display = 'block';
|
||||
const width = submenu.offsetWidth;
|
||||
const naturalHeight = submenu.offsetHeight;
|
||||
const menuX = Number.parseFloat(menu.style.left) || 0;
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const openLeft = window.screenX + menuX + menu.offsetWidth + width + 4 > screenRight;
|
||||
const leftExpansion = openLeft ? Math.max(0, width - menuX + 2) : 0;
|
||||
submenu.style.left = openLeft ? 'auto' : '100%';
|
||||
submenu.style.right = openLeft ? '100%' : 'auto';
|
||||
const defaultScreenTop = window.screenY + menuY + parent.offsetTop - 2;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const targetScreenTop = Math.max(window.screen.availTop + 4,
|
||||
Math.min(defaultScreenTop, screenBottom - naturalHeight - 4));
|
||||
const topExpansion = Math.max(0, window.screenY + 2 - targetScreenTop);
|
||||
submenu.style.top = (targetScreenTop - window.screenY - menuY - parent.offsetTop) + 'px';
|
||||
const height = submenu.offsetHeight;
|
||||
submenu.style.display = '';
|
||||
submenu.style.visibility = '';
|
||||
return { width, height, topExpansion, leftExpansion, bottom: menuY + parent.offsetTop +
|
||||
Number.parseFloat(submenu.style.top) + height };
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
const cand = e.target instanceof Element ? e.target.closest('.cand') : null;
|
||||
if (!cand) return;
|
||||
currentRightClickedCand = cand;
|
||||
const menu = document.getElementById('contextMenu');
|
||||
document.getElementById('menuDelete').style.display =
|
||||
Array.from((cand.textContent || '').replace(/^\s*\d+\s*/, '').trim()).length === 1 ? 'none' : 'block';
|
||||
const menuPosition=positionContextMenu(menu,e.clientX,e.clientY);
|
||||
const submenuSize=positionContextSubmenu(menu,menuPosition.y), content=document.getElementById('realContainer');
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuResize',data:{
|
||||
width:Math.ceil(menuPosition.x+menu.offsetWidth+submenuSize.width+4),
|
||||
height:Math.ceil(Math.max(content?.offsetHeight||0,menuPosition.y+menu.offsetHeight+4,submenuSize.bottom+4)),
|
||||
topExpansion:Math.ceil(Math.max(menuPosition.topExpansion||0,submenuSize.topExpansion||0))
|
||||
,leftExpansion:Math.ceil(submenuSize.leftExpansion||0)
|
||||
}}));
|
||||
});
|
||||
// Dismiss only when clicking blank areas inside the rendered candidate card.
|
||||
document.addEventListener('mousedown', function (e) {
|
||||
if (e.button !== 0) return;
|
||||
if (!(e.target instanceof Element)) return;
|
||||
if (e.target.closest('.context-menu')) return;
|
||||
if (!e.target.closest('#realContainer')) return;
|
||||
hideContextMenu();
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candElement = e.target.closest('.cand');
|
||||
if (!candElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('realContainer');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const candidates = Array.from(container.querySelectorAll('.cand'));
|
||||
const candIndex = candidates.indexOf(candElement) + 1;
|
||||
if (candIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'candidate', data: candIndex }));
|
||||
} catch (error) {
|
||||
console.error('Failed to send candidate click:', error);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function () {
|
||||
if (a === true) {
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.add('hover-active');
|
||||
}
|
||||
a = true;
|
||||
});
|
||||
});
|
||||
function hideContextMenu() {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
if (!menu || menu.style.display !== 'block') return;
|
||||
menu.style.display = 'none';
|
||||
currentRightClickedCand = null;
|
||||
window.RestoreContextMenuTopExpansion();
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuClosed'}));
|
||||
}
|
||||
function menuIndex(){ return Array.from(document.querySelectorAll('#realContainer .cand')).indexOf(currentRightClickedCand)+1; }
|
||||
function sendMenu(type,data){ window.chrome.webview.postMessage(JSON.stringify({type,data}));
|
||||
hideContextMenu(); }
|
||||
function onPinToTop(e){ if(e.button===0) sendMenu('pin',menuIndex()); }
|
||||
function onDelete(e){ if(e.button===0) sendMenu('delete',menuIndex()); }
|
||||
function onFixPosition(e,position){ if(e.button===0) sendMenu('fixPosition',{index:menuIndex(),position}); }
|
||||
function onClearPosition(e){ if(e.button===0) sendMenu('clearPosition',menuIndex()); }
|
||||
|
||||
window.ClearState = function () {
|
||||
a = false;
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.remove('hover-active');
|
||||
hideContextMenu();
|
||||
}
|
||||
window.ApplyContextMenuTopExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuTopExpansion;
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) + delta) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) + delta) + 'px';
|
||||
contextMenuTopExpansion = pixels;
|
||||
}
|
||||
window.ApplyContextMenuLeftExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuLeftExpansion;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) + delta) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) + delta) + 'px';
|
||||
contextMenuLeftExpansion = pixels;
|
||||
}
|
||||
window.RestoreContextMenuTopExpansion = function () {
|
||||
if (!contextMenuTopExpansion && !contextMenuLeftExpansion) return;
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) - contextMenuTopExpansion) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) - contextMenuTopExpansion) + 'px';
|
||||
contextMenuTopExpansion = 0;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
contextMenuLeftExpansion = 0;
|
||||
}
|
||||
|
||||
window.SetCandidateSelection = function (index) {
|
||||
const candidates = document.querySelectorAll('#realContainer .cand');
|
||||
candidates.forEach((candidate) => candidate.classList.remove('first'));
|
||||
if (index >= 0 && index < candidates.length) {
|
||||
candidates[index].classList.add('first');
|
||||
}
|
||||
}
|
||||
|
||||
window.SetCandidatePreeditVisible = function (visible) {
|
||||
const hide = !visible;
|
||||
document.getElementById('realContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
document.getElementById('measureContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
}
|
||||
|
||||
window.SetPreeditCaret = function () {
|
||||
const preedit = document.querySelector('#realContainer .pinyin .text');
|
||||
if (!preedit) return;
|
||||
const marker = '\uE000';
|
||||
const text = preedit.textContent || '';
|
||||
const index = text.indexOf(marker);
|
||||
const cleanText = text.replace(marker, '');
|
||||
const caretIndex = index >= 0 ? index : cleanText.length;
|
||||
preedit.textContent = '';
|
||||
preedit.append(document.createTextNode(cleanText.slice(0, caretIndex)));
|
||||
const caret = document.createElement('span');
|
||||
caret.className = 'cursor';
|
||||
preedit.append(caret);
|
||||
preedit.append(document.createTextNode(cleanText.slice(caretIndex)));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="realBody">
|
||||
<div class="containerParent" id="realContainerParent">
|
||||
<div class="container" id="realContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">ni'mf<span class="cursor"></span></div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper first-row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="num">1</span> 你们(rR)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">2</span> 你(rX)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">3</span> 尼(uV)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">4</span> 妮(nV)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">5</span> 泥(dV)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">6</span> 逆(zQ)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">7</span> 拟(fR)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">8</span> 腻(oD)</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">9</span> 拧(nG)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="measureBody" style="visibility: hidden;">
|
||||
<div class="containerParent" id="measureContainerParent">
|
||||
<div class="container" id="measureContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">{0}</div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper first-row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="num">1</span> {1} 你</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">2</span> {2} 我</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">3</span> {3} 什么</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">4</span> {4} 量子</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">5</span> {5} 可恶</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">6</span> {6} 不是</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">7</span> {7} 那么</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">8</span> {8} 如果</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="num">9</span> {9}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contextMenu" class="context-menu">
|
||||
<div class="context-menu-item" onmouseup="onPinToTop(event)">置顶</div>
|
||||
<div class="context-menu-item context-menu-parent">固定排位<div class="context-submenu">
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,1)">第 1 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,2)">第 2 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,3)">第 3 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,4)">第 4 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,5)">第 5 位</div>
|
||||
<div class="context-menu-separator"></div><div class="context-menu-item" onmouseup="onClearPosition(event)">取消固定</div>
|
||||
</div></div>
|
||||
<div id="menuDelete" class="context-menu-item" onmouseup="onDelete(event)">删除</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var SURFACE = 'cand';
|
||||
var SELECTOR = '#realContainer';
|
||||
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(JSON.stringify({
|
||||
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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,727 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>(IME)WechatVCandWnd</title>
|
||||
<style>
|
||||
:root {
|
||||
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||
--cand-font-size: 16px;
|
||||
--cand-text: #b7b7b7;
|
||||
--cand-num: #858585;
|
||||
/* Fallback before C++ injects host-width DIP. Wrap limit = 宿主宽度. */
|
||||
--msime-max-width-dip: 100%;
|
||||
--msime-max-height-dip: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--cand-font-family);
|
||||
font-size: var(--cand-font-size);
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.containerParent {
|
||||
width: fit-content;
|
||||
max-width: var(--msime-max-width-dip);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
background: #202020;
|
||||
/* background: url("https://appassets/background.jpg"); */
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
|
||||
min-width: 7em;
|
||||
width: min(fit-content, var(--msime-max-width-dip));
|
||||
max-width: var(--msime-max-width-dip);
|
||||
/* Host height in DIP (injected). Do not use min(100%, …): parent height is
|
||||
content-sized so 100% never resolves and tall vertical lists get clipped
|
||||
by the HWND instead of scrolling. */
|
||||
max-height: var(--msime-max-height-dip);
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
user-select: none;
|
||||
border: 1.5px solid #9b9b9b2e;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 1.5px;
|
||||
height: 1.2em;
|
||||
background: #6b69d6;
|
||||
transform: translate(1.4px, 10%);
|
||||
}
|
||||
|
||||
.row {
|
||||
justify-content: space-between;
|
||||
padding: 2px;
|
||||
padding-right: 12px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cand {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hover-active .cand:hover {
|
||||
background-color: #414141;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.first {
|
||||
background-color: #3e3e3eb9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* bar */
|
||||
.first::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 0.8em;
|
||||
width: 0.2em;
|
||||
background: #6b69d6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: calc(0.2em + 3px);
|
||||
color: var(--cand-text);
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cand .text {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.cand-no {
|
||||
font-size: 0.8em;
|
||||
color: var(--cand-num);
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.pinyin {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hidden-pinyin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Hide candidate-window top preedit when configured as empty */
|
||||
.container.preedit-hidden > .row.pinyin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide container when empty */
|
||||
.container:empty {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Custom context menu */
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #9b9b9b2e;
|
||||
border-radius: 6px;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
|
||||
padding: 1px 0;
|
||||
/* min-width: 80px; */
|
||||
z-index: 1000;
|
||||
/* 默认隐藏 */
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 1px 4px;
|
||||
margin: 2px 2px;
|
||||
color: #e9e8e8;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background-color: #414141;
|
||||
}
|
||||
.context-menu-parent { position: relative; padding-right: 20px; }
|
||||
.context-menu-parent::after { content: "›"; position: absolute; right: 6px; }
|
||||
.context-submenu { display: none; position: absolute; left: 100%; top: -2px; min-width: 0;
|
||||
background: #2d2d2d; border: 1px solid #9b9b9b2e; border-radius: 6px; padding: 1px 0; }
|
||||
.context-menu-parent:hover > .context-submenu { display: block; }
|
||||
.context-menu-separator { height: 1px; margin: 3px 5px; background: #ffffff20; }
|
||||
|
||||
/* WeChat-inspired candidate skin. Kept in a standalone document so the
|
||||
host can switch skins by navigating WebView2 to a different HTML file. */
|
||||
body { border-radius: 5px; }
|
||||
.container {
|
||||
background: #151515;
|
||||
padding: 2px;
|
||||
border: 1px solid #292929;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, .42);
|
||||
}
|
||||
.cand { border-radius: 4px; }
|
||||
.cursor { background: #07c160; }
|
||||
.hover-active .cand:hover { background-color: rgba(7, 193, 96, .22); }
|
||||
.first {
|
||||
background-color: #07c160;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.first::before { display: none; }
|
||||
.first .text,
|
||||
.first .cand-no { color: #ffffff; }
|
||||
.context-menu,
|
||||
.context-submenu { background: #1f1f1f; border-color: #343434; }
|
||||
.context-menu-item { color: #d0d0d0; }
|
||||
.context-menu-item:hover { background: #2a2a2a; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let a = false;
|
||||
let currentRightClickedCand = null;
|
||||
let contextMenuTopExpansion = 0;
|
||||
let contextMenuLeftExpansion = 0;
|
||||
|
||||
|
||||
//
|
||||
// 判断候选项是否为单个汉字
|
||||
//
|
||||
function isSingleCharCandidate(candElement) {
|
||||
const contentEl = candElement.querySelector('.cand-content');
|
||||
if (!contentEl) return false;
|
||||
|
||||
let text = contentEl.textContent.trim();
|
||||
|
||||
// 去掉括号里的编码,例如:你们(rR) → 你们
|
||||
text = text.replace(/\(.*?\)$/, '').trim();
|
||||
|
||||
// 按 Unicode code point 计数
|
||||
return Array.from(text).length === 1;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Disable native context menu globally
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Handle right-click on candidate items
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
const candElement = e.target.closest('.cand');
|
||||
if (!candElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
currentRightClickedCand = candElement;
|
||||
|
||||
// Move .first class to the clicked candidate
|
||||
// TODO: .first 类名需要改为更合适的
|
||||
const container = document.getElementById('realContainer');
|
||||
const currentFirst = container.querySelector('.cand.first');
|
||||
if (currentFirst) {
|
||||
currentFirst.classList.remove('first');
|
||||
}
|
||||
candElement.classList.add('first');
|
||||
|
||||
// 根据候选项是否为单字符决定是否显示删除菜单项
|
||||
const menuDelete = document.getElementById('menuDelete');
|
||||
const isSingle = isSingleCharCandidate(candElement);
|
||||
if (isSingle) {
|
||||
menuDelete.style.display = 'none';
|
||||
} else {
|
||||
menuDelete.style.display = 'block';
|
||||
}
|
||||
|
||||
showContextMenu(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
//
|
||||
// Dismiss only when clicking blank areas inside the rendered candidate card.
|
||||
//
|
||||
document.addEventListener('mousedown', function (e) {
|
||||
if (e.button !== 0) return;
|
||||
if (!(e.target instanceof Element)) return;
|
||||
if (e.target.closest('.context-menu')) return;
|
||||
if (!e.target.closest('#realContainer')) return;
|
||||
hideContextMenu();
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
const candElement = e.target.closest('.cand');
|
||||
if (candElement && e.button === 0) {
|
||||
const candNoElement = candElement.querySelector('.cand-no');
|
||||
const candIndex = candNoElement ? Number(candNoElement.textContent.trim()) : null;
|
||||
if (candIndex !== null) {
|
||||
console.log('Candidate left clicked:', candIndex);
|
||||
try {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'candidate', data: Number(candIndex) }));
|
||||
// TODO: 考虑一下,这里要不要也去做一下 html 界面的隐藏。
|
||||
} catch (e) {
|
||||
console.error('Failed to send message:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function () {
|
||||
if (a === true) {
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.add('hover-active');
|
||||
}
|
||||
a = true;
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Show context menu
|
||||
//
|
||||
function positionContextMenu(menu, x, y) {
|
||||
menu.style.left = x + 'px';
|
||||
menu.style.top = y + 'px';
|
||||
menu.style.visibility = 'hidden';
|
||||
menu.style.display = 'block';
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const adjustedX = x - Math.max(0, window.screenX + x + menu.offsetWidth + 4 - screenRight);
|
||||
const adjustedY = y - Math.max(0, window.screenY + y + menu.offsetHeight + 4 - screenBottom);
|
||||
menu.style.left = Math.max(0, adjustedX) + 'px';
|
||||
menu.style.top = adjustedY + 'px';
|
||||
menu.style.visibility = '';
|
||||
return { x: Math.max(0, adjustedX), y: adjustedY, topExpansion: Math.max(0, 2 - adjustedY) };
|
||||
}
|
||||
|
||||
function positionContextSubmenu(menu, menuY) {
|
||||
const parent = menu.querySelector('.context-menu-parent');
|
||||
const submenu = menu.querySelector('.context-submenu');
|
||||
if (!parent || !submenu) return { width: 80, bottom: menuY + menu.offsetHeight };
|
||||
submenu.style.top = '-2px';
|
||||
submenu.style.visibility = 'hidden';
|
||||
submenu.style.display = 'block';
|
||||
const width = submenu.offsetWidth;
|
||||
const naturalHeight = submenu.offsetHeight;
|
||||
const menuX = Number.parseFloat(menu.style.left) || 0;
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const openLeft = window.screenX + menuX + menu.offsetWidth + width + 4 > screenRight;
|
||||
const leftExpansion = openLeft ? Math.max(0, width - menuX + 2) : 0;
|
||||
submenu.style.left = openLeft ? 'auto' : '100%';
|
||||
submenu.style.right = openLeft ? '100%' : 'auto';
|
||||
const defaultScreenTop = window.screenY + menuY + parent.offsetTop - 2;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const targetScreenTop = Math.max(window.screen.availTop + 4,
|
||||
Math.min(defaultScreenTop, screenBottom - naturalHeight - 4));
|
||||
const topExpansion = Math.max(0, window.screenY + 2 - targetScreenTop);
|
||||
submenu.style.top = (targetScreenTop - window.screenY - menuY - parent.offsetTop) + 'px';
|
||||
const height = submenu.offsetHeight;
|
||||
submenu.style.display = '';
|
||||
submenu.style.visibility = '';
|
||||
return { width, topExpansion, leftExpansion, bottom: menuY + parent.offsetTop +
|
||||
Number.parseFloat(submenu.style.top) + height };
|
||||
}
|
||||
|
||||
function showContextMenu(x, y) {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const menuPosition = positionContextMenu(menu, x, y);
|
||||
const submenuSize = positionContextSubmenu(menu, menuPosition.y);
|
||||
const content = document.getElementById('realContainer');
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuResize', data:{
|
||||
width: Math.ceil(menuPosition.x + menu.offsetWidth + submenuSize.width + 4),
|
||||
height: Math.ceil(Math.max(content?.offsetHeight || 0, menuPosition.y + menu.offsetHeight + 4, submenuSize.bottom + 4)),
|
||||
topExpansion: Math.ceil(Math.max(menuPosition.topExpansion || 0, submenuSize.topExpansion || 0))
|
||||
,leftExpansion: Math.ceil(submenuSize.leftExpansion || 0)
|
||||
}}));
|
||||
}
|
||||
|
||||
//
|
||||
// Hide context menu
|
||||
//
|
||||
function hideContextMenu() {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
if (!menu || menu.style.display !== 'block') return;
|
||||
menu.style.display = 'none';
|
||||
currentRightClickedCand = null;
|
||||
window.RestoreContextMenuTopExpansion();
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuClosed'}));
|
||||
}
|
||||
|
||||
//
|
||||
// 置顶当前候选项
|
||||
//
|
||||
function onPinToTop(event) {
|
||||
// 只针对鼠标左键的点击有效
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentRightClickedCand) {
|
||||
// Get the candidate index from the cand-no element
|
||||
const candNoElement = currentRightClickedCand.querySelector('.cand-no');
|
||||
const candIndex = candNoElement ? Number(candNoElement.textContent.trim()) : null;
|
||||
const candContentElement = currentRightClickedCand.querySelector('.cand-content');
|
||||
const candContent = candContentElement ? candContentElement.textContent.trim() : null;
|
||||
console.log('Pin to top:', candIndex, candContent);
|
||||
// 通知 C++ 端置顶
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'pin', data: Number(candIndex) }));
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: 处理异常
|
||||
} finally {
|
||||
hideContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 删除当前候选项
|
||||
//
|
||||
function onDelete(event) {
|
||||
// 只针对鼠标左键的点击有效
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentRightClickedCand) {
|
||||
// Get the candidate index from the cand-no element
|
||||
const candNoElement = currentRightClickedCand.querySelector('.cand-no');
|
||||
const candIndex = candNoElement ? Number(candNoElement.textContent.trim()) : null;
|
||||
const candContentElement = currentRightClickedCand.querySelector('.cand-content');
|
||||
const candContent = candContentElement ? candContentElement.textContent.trim() : null;
|
||||
console.log('Delete:', candIndex, candContent);
|
||||
// 通知 C++ 端删除
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'delete', data: Number(candIndex) }));
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: 处理异常
|
||||
} finally {
|
||||
hideContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
window.ClearState = function () {
|
||||
a = false;
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.remove('hover-active');
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
function candidateMenuIndex() {
|
||||
const number = currentRightClickedCand?.querySelector('.cand-no');
|
||||
return number ? Number(number.textContent.trim()) : null;
|
||||
}
|
||||
function onFixPosition(event, position) {
|
||||
if (event.button !== 0) return;
|
||||
const index = candidateMenuIndex();
|
||||
if (index !== null) window.chrome.webview.postMessage(JSON.stringify({type:'fixPosition', data:{index,position}}));
|
||||
hideContextMenu();
|
||||
}
|
||||
window.ApplyContextMenuTopExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuTopExpansion;
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) + delta) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) + delta) + 'px';
|
||||
contextMenuTopExpansion = pixels;
|
||||
}
|
||||
window.ApplyContextMenuLeftExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuLeftExpansion;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) + delta) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) + delta) + 'px';
|
||||
contextMenuLeftExpansion = pixels;
|
||||
}
|
||||
window.RestoreContextMenuTopExpansion = function () {
|
||||
if (!contextMenuTopExpansion && !contextMenuLeftExpansion) return;
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) - contextMenuTopExpansion) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) - contextMenuTopExpansion) + 'px';
|
||||
contextMenuTopExpansion = 0;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
contextMenuLeftExpansion = 0;
|
||||
}
|
||||
function onClearPosition(event) {
|
||||
if (event.button !== 0) return;
|
||||
const index = candidateMenuIndex();
|
||||
if (index !== null) window.chrome.webview.postMessage(JSON.stringify({type:'clearPosition', data:index}));
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
window.SetCandidateSelection = function (index) {
|
||||
const candidates = document.querySelectorAll('#realContainer .cand');
|
||||
candidates.forEach((candidate) => candidate.classList.remove('first'));
|
||||
if (index >= 0 && index < candidates.length) {
|
||||
candidates[index].classList.add('first');
|
||||
}
|
||||
}
|
||||
|
||||
window.SetCandidatePreeditVisible = function (visible) {
|
||||
const hide = !visible;
|
||||
document.getElementById('realContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
document.getElementById('measureContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
}
|
||||
|
||||
window.SetPreeditCaret = function () {
|
||||
const preedit = document.querySelector('#realContainer .pinyin .text');
|
||||
if (!preedit) return;
|
||||
const marker = '\uE000';
|
||||
const text = preedit.textContent || '';
|
||||
const index = text.indexOf(marker);
|
||||
const cleanText = text.replace(marker, '');
|
||||
const caretIndex = index >= 0 ? index : cleanText.length;
|
||||
preedit.textContent = '';
|
||||
preedit.append(document.createTextNode(cleanText.slice(0, caretIndex)));
|
||||
const caret = document.createElement('span');
|
||||
caret.className = 'cursor';
|
||||
preedit.append(caret);
|
||||
preedit.append(document.createTextNode(cleanText.slice(caretIndex)));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="realBody">
|
||||
<div class="containerParent" id="realContainerParent">
|
||||
<div class="container" id="realContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">ni'mf<span class="cursor"></span></div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="cand-no">1</span><span class="cand-content">你们(rR)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">2</span><span class="cand-content">你(rX)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">3</span><span class="cand-content">尼(uV)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">4</span><span class="cand-content">妮(nV)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">5</span><span class="cand-content">泥(dV)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">6</span><span class="cand-content">逆(zQ)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">7</span><span class="cand-content">拟(fR)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">8</span><span class="cand-content">腻(oD)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">9</span><span class="cand-content">拧(nG)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="measureBody" style="visibility: hidden;">
|
||||
<div class="containerParent" id="measureContainerParent">
|
||||
<div class="container" id="measureContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">{0}</div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="cand-no">1</span><span class="cand-content"> {1} 你</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">2</span><span class="cand-content"> {2} 我</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">3</span><span class="cand-content"> {3} 什么</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">4</span><span class="cand-content"> {4} 量子</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">5</span><span class="cand-content"> {5} 可恶</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">6</span><span class="cand-content"> {6} 不是</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">7</span><span class="cand-content"> {7} 那么</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">8</span><span class="cand-content"> {8} 如果</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">9</span><span class="cand-content"> {9}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Custom Context Menu -->
|
||||
<div id="contextMenu" class="context-menu">
|
||||
<div id="menuPin" class="context-menu-item" onmouseup="onPinToTop(event)">置顶</div>
|
||||
<div class="context-menu-item context-menu-parent">固定排位
|
||||
<div class="context-submenu">
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,1)">第 1 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,2)">第 2 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,3)">第 3 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,4)">第 4 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,5)">第 5 位</div>
|
||||
<div class="context-menu-separator"></div>
|
||||
<div class="context-menu-item" onmouseup="onClearPosition(event)">取消固定</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="menuDelete" class="context-menu-item" onmouseup="onDelete(event)">删除</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var SURFACE = 'cand';
|
||||
var SELECTOR = '#realContainer';
|
||||
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(JSON.stringify({
|
||||
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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,722 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>(IME)WechatLightVCandWnd</title>
|
||||
<style>
|
||||
:root {
|
||||
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||
--cand-font-size: 16px;
|
||||
--cand-text: #333333;
|
||||
--cand-num: #757575;
|
||||
/* Fallback before C++ injects host-width DIP. Wrap limit = 宿主宽度. */
|
||||
--msime-max-width-dip: 100%;
|
||||
--msime-max-height-dip: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--cand-font-family);
|
||||
font-size: var(--cand-font-size);
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.containerParent {
|
||||
width: fit-content;
|
||||
max-width: var(--msime-max-width-dip);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
background: #ffffff;
|
||||
/* background: url("https://appassets/background.jpg"); */
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.12);
|
||||
min-width: 7em;
|
||||
width: min(fit-content, var(--msime-max-width-dip));
|
||||
max-width: var(--msime-max-width-dip);
|
||||
/* Host height in DIP (injected). Do not use min(100%, …): parent height is
|
||||
content-sized so 100% never resolves and tall vertical lists get clipped
|
||||
by the HWND instead of scrolling. */
|
||||
max-height: var(--msime-max-height-dip);
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
user-select: none;
|
||||
border: 1.5px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 1.5px;
|
||||
height: 1.2em;
|
||||
background: #6b69d6;
|
||||
transform: translate(1.4px, 10%);
|
||||
}
|
||||
|
||||
.row {
|
||||
justify-content: space-between;
|
||||
padding: 2px;
|
||||
padding-right: 12px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cand {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hover-active .cand:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.first {
|
||||
background-color: #e8e8e8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* bar */
|
||||
.first::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 0.8em;
|
||||
width: 0.2em;
|
||||
background: #6b69d6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: calc(0.2em + 3px);
|
||||
color: var(--cand-text);
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cand .text {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.cand-no {
|
||||
font-size: 0.8em;
|
||||
color: var(--cand-num);
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.pinyin {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hidden-pinyin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Hide candidate-window top preedit when configured as empty */
|
||||
.container.preedit-hidden > .row.pinyin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide container when empty */
|
||||
.container:empty {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Custom context menu */
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 6px;
|
||||
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.12);
|
||||
padding: 1px 0;
|
||||
/* min-width: 80px; */
|
||||
z-index: 1000;
|
||||
/* 默认隐藏 */
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 1px 4px;
|
||||
margin: 2px 2px;
|
||||
color: #1a1a1a;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
.context-menu-parent { position: relative; padding-right: 20px; }
|
||||
.context-menu-parent::after { content: "›"; position: absolute; right: 6px; }
|
||||
.context-submenu { display: none; position: absolute; left: 100%; top: -2px; min-width: 0;
|
||||
background: #fff; border: 1px solid rgba(0,0,0,.12); border-radius: 6px; padding: 1px 0; }
|
||||
.context-menu-parent:hover > .context-submenu { display: block; }
|
||||
.context-menu-separator { height: 1px; margin: 3px 5px; background: #00000020; }
|
||||
/* Light variant of the standalone WeChat-inspired candidate skin. */
|
||||
body { border-radius: 5px; }
|
||||
.container {
|
||||
background: #f7f7f7;
|
||||
padding: 2px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, .14);
|
||||
}
|
||||
.cand { border-radius: 4px; }
|
||||
.cursor { background: #07c160; }
|
||||
.cand:hover { background-color: rgba(7, 193, 96, .14); }
|
||||
.first { background-color: #07c160; border-radius: 5px; }
|
||||
.first::before { display: none; }
|
||||
.first .text,
|
||||
.first .cand-no { color: #ffffff; }
|
||||
.context-menu,
|
||||
.context-submenu { background: #ffffff; border-color: #d9d9d9; }
|
||||
.context-menu-item { color: #333333; }
|
||||
.context-menu-item:hover { background: #eeeeee; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let a = false;
|
||||
let currentRightClickedCand = null;
|
||||
let contextMenuTopExpansion = 0;
|
||||
let contextMenuLeftExpansion = 0;
|
||||
|
||||
|
||||
//
|
||||
// 判断候选项是否为单个汉字
|
||||
//
|
||||
function isSingleCharCandidate(candElement) {
|
||||
const contentEl = candElement.querySelector('.cand-content');
|
||||
if (!contentEl) return false;
|
||||
|
||||
let text = contentEl.textContent.trim();
|
||||
|
||||
// 去掉括号里的编码,例如:你们(rR) → 你们
|
||||
text = text.replace(/\(.*?\)$/, '').trim();
|
||||
|
||||
// 按 Unicode code point 计数
|
||||
return Array.from(text).length === 1;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Disable native context menu globally
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Handle right-click on candidate items
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
const candElement = e.target.closest('.cand');
|
||||
if (!candElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
currentRightClickedCand = candElement;
|
||||
|
||||
// Move .first class to the clicked candidate
|
||||
// TODO: .first 类名需要改为更合适的
|
||||
const container = document.getElementById('realContainer');
|
||||
const currentFirst = container.querySelector('.cand.first');
|
||||
if (currentFirst) {
|
||||
currentFirst.classList.remove('first');
|
||||
}
|
||||
candElement.classList.add('first');
|
||||
|
||||
// 根据候选项是否为单字符决定是否显示删除菜单项
|
||||
const menuDelete = document.getElementById('menuDelete');
|
||||
const isSingle = isSingleCharCandidate(candElement);
|
||||
if (isSingle) {
|
||||
menuDelete.style.display = 'none';
|
||||
} else {
|
||||
menuDelete.style.display = 'block';
|
||||
}
|
||||
|
||||
showContextMenu(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
//
|
||||
// Dismiss only when clicking blank areas inside the rendered candidate card.
|
||||
//
|
||||
document.addEventListener('mousedown', function (e) {
|
||||
if (e.button !== 0) return;
|
||||
if (!(e.target instanceof Element)) return;
|
||||
if (e.target.closest('.context-menu')) return;
|
||||
if (!e.target.closest('#realContainer')) return;
|
||||
hideContextMenu();
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
const candElement = e.target.closest('.cand');
|
||||
if (candElement && e.button === 0) {
|
||||
const candNoElement = candElement.querySelector('.cand-no');
|
||||
const candIndex = candNoElement ? Number(candNoElement.textContent.trim()) : null;
|
||||
if (candIndex !== null) {
|
||||
console.log('Candidate left clicked:', candIndex);
|
||||
try {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'candidate', data: Number(candIndex) }));
|
||||
// TODO: 考虑一下,这里要不要也去做一下 html 界面的隐藏。
|
||||
} catch (e) {
|
||||
console.error('Failed to send message:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function () {
|
||||
if (a === true) {
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.add('hover-active');
|
||||
}
|
||||
a = true;
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Show context menu
|
||||
//
|
||||
function positionContextMenu(menu, x, y) {
|
||||
menu.style.left = x + 'px';
|
||||
menu.style.top = y + 'px';
|
||||
menu.style.visibility = 'hidden';
|
||||
menu.style.display = 'block';
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const adjustedX = x - Math.max(0, window.screenX + x + menu.offsetWidth + 4 - screenRight);
|
||||
const adjustedY = y - Math.max(0, window.screenY + y + menu.offsetHeight + 4 - screenBottom);
|
||||
menu.style.left = Math.max(0, adjustedX) + 'px';
|
||||
menu.style.top = adjustedY + 'px';
|
||||
menu.style.visibility = '';
|
||||
return { x: Math.max(0, adjustedX), y: adjustedY, topExpansion: Math.max(0, 2 - adjustedY) };
|
||||
}
|
||||
|
||||
function positionContextSubmenu(menu, menuY) {
|
||||
const parent = menu.querySelector('.context-menu-parent');
|
||||
const submenu = menu.querySelector('.context-submenu');
|
||||
if (!parent || !submenu) return { width: 80, bottom: menuY + menu.offsetHeight };
|
||||
submenu.style.top = '-2px';
|
||||
submenu.style.visibility = 'hidden';
|
||||
submenu.style.display = 'block';
|
||||
const width = submenu.offsetWidth;
|
||||
const naturalHeight = submenu.offsetHeight;
|
||||
const menuX = Number.parseFloat(menu.style.left) || 0;
|
||||
const screenRight = window.screen.availLeft + window.screen.availWidth;
|
||||
const openLeft = window.screenX + menuX + menu.offsetWidth + width + 4 > screenRight;
|
||||
const leftExpansion = openLeft ? Math.max(0, width - menuX + 2) : 0;
|
||||
submenu.style.left = openLeft ? 'auto' : '100%';
|
||||
submenu.style.right = openLeft ? '100%' : 'auto';
|
||||
const defaultScreenTop = window.screenY + menuY + parent.offsetTop - 2;
|
||||
const screenBottom = window.screen.availTop + window.screen.availHeight;
|
||||
const targetScreenTop = Math.max(window.screen.availTop + 4,
|
||||
Math.min(defaultScreenTop, screenBottom - naturalHeight - 4));
|
||||
const topExpansion = Math.max(0, window.screenY + 2 - targetScreenTop);
|
||||
submenu.style.top = (targetScreenTop - window.screenY - menuY - parent.offsetTop) + 'px';
|
||||
const height = submenu.offsetHeight;
|
||||
submenu.style.display = '';
|
||||
submenu.style.visibility = '';
|
||||
return { width, topExpansion, leftExpansion, bottom: menuY + parent.offsetTop +
|
||||
Number.parseFloat(submenu.style.top) + height };
|
||||
}
|
||||
|
||||
function showContextMenu(x, y) {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const menuPosition = positionContextMenu(menu, x, y);
|
||||
const submenuSize = positionContextSubmenu(menu, menuPosition.y);
|
||||
const content = document.getElementById('realContainer');
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuResize', data:{
|
||||
width: Math.ceil(menuPosition.x + menu.offsetWidth + submenuSize.width + 4),
|
||||
height: Math.ceil(Math.max(content?.offsetHeight || 0, menuPosition.y + menu.offsetHeight + 4, submenuSize.bottom + 4)),
|
||||
topExpansion: Math.ceil(Math.max(menuPosition.topExpansion || 0, submenuSize.topExpansion || 0))
|
||||
,leftExpansion: Math.ceil(submenuSize.leftExpansion || 0)
|
||||
}}));
|
||||
}
|
||||
|
||||
//
|
||||
// Hide context menu
|
||||
//
|
||||
function hideContextMenu() {
|
||||
const menu = document.getElementById('contextMenu');
|
||||
if (!menu || menu.style.display !== 'block') return;
|
||||
menu.style.display = 'none';
|
||||
currentRightClickedCand = null;
|
||||
window.RestoreContextMenuTopExpansion();
|
||||
window.chrome.webview.postMessage(JSON.stringify({type:'contextMenuClosed'}));
|
||||
}
|
||||
|
||||
//
|
||||
// 置顶当前候选项
|
||||
//
|
||||
function onPinToTop(event) {
|
||||
// 只针对鼠标左键的点击有效
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentRightClickedCand) {
|
||||
// Get the candidate index from the cand-no element
|
||||
const candNoElement = currentRightClickedCand.querySelector('.cand-no');
|
||||
const candIndex = candNoElement ? Number(candNoElement.textContent.trim()) : null;
|
||||
const candContentElement = currentRightClickedCand.querySelector('.cand-content');
|
||||
const candContent = candContentElement ? candContentElement.textContent.trim() : null;
|
||||
console.log('Pin to top:', candIndex, candContent);
|
||||
// 通知 C++ 端置顶
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'pin', data: Number(candIndex) }));
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: 处理异常
|
||||
} finally {
|
||||
hideContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 删除当前候选项
|
||||
//
|
||||
function onDelete(event) {
|
||||
// 只针对鼠标左键的点击有效
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentRightClickedCand) {
|
||||
// Get the candidate index from the cand-no element
|
||||
const candNoElement = currentRightClickedCand.querySelector('.cand-no');
|
||||
const candIndex = candNoElement ? Number(candNoElement.textContent.trim()) : null;
|
||||
const candContentElement = currentRightClickedCand.querySelector('.cand-content');
|
||||
const candContent = candContentElement ? candContentElement.textContent.trim() : null;
|
||||
console.log('Delete:', candIndex, candContent);
|
||||
// 通知 C++ 端删除
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'delete', data: Number(candIndex) }));
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: 处理异常
|
||||
} finally {
|
||||
hideContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
window.ClearState = function () {
|
||||
a = false;
|
||||
const container = document.getElementById('realContainer');
|
||||
container.classList.remove('hover-active');
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
function candidateMenuIndex() {
|
||||
const number = currentRightClickedCand?.querySelector('.cand-no');
|
||||
return number ? Number(number.textContent.trim()) : null;
|
||||
}
|
||||
function onFixPosition(event, position) {
|
||||
if (event.button !== 0) return;
|
||||
const index = candidateMenuIndex();
|
||||
if (index !== null) window.chrome.webview.postMessage(JSON.stringify({type:'fixPosition', data:{index,position}}));
|
||||
hideContextMenu();
|
||||
}
|
||||
window.ApplyContextMenuTopExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuTopExpansion;
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) + delta) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) + delta) + 'px';
|
||||
contextMenuTopExpansion = pixels;
|
||||
}
|
||||
window.ApplyContextMenuLeftExpansion = function (pixels) {
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
const delta = pixels - contextMenuLeftExpansion;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) + delta) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) + delta) + 'px';
|
||||
contextMenuLeftExpansion = pixels;
|
||||
}
|
||||
window.RestoreContextMenuTopExpansion = function () {
|
||||
if (!contextMenuTopExpansion && !contextMenuLeftExpansion) return;
|
||||
const content = document.getElementById('realContainerParent');
|
||||
const menu = document.getElementById('contextMenu');
|
||||
content.style.marginTop = ((Number.parseFloat(content.style.marginTop) || 0) - contextMenuTopExpansion) + 'px';
|
||||
menu.style.top = ((Number.parseFloat(menu.style.top) || 0) - contextMenuTopExpansion) + 'px';
|
||||
contextMenuTopExpansion = 0;
|
||||
content.style.marginLeft = ((Number.parseFloat(content.style.marginLeft) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
menu.style.left = ((Number.parseFloat(menu.style.left) || 0) - contextMenuLeftExpansion) + 'px';
|
||||
contextMenuLeftExpansion = 0;
|
||||
}
|
||||
function onClearPosition(event) {
|
||||
if (event.button !== 0) return;
|
||||
const index = candidateMenuIndex();
|
||||
if (index !== null) window.chrome.webview.postMessage(JSON.stringify({type:'clearPosition', data:index}));
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
window.SetCandidateSelection = function (index) {
|
||||
const candidates = document.querySelectorAll('#realContainer .cand');
|
||||
candidates.forEach((candidate) => candidate.classList.remove('first'));
|
||||
if (index >= 0 && index < candidates.length) {
|
||||
candidates[index].classList.add('first');
|
||||
}
|
||||
}
|
||||
|
||||
window.SetCandidatePreeditVisible = function (visible) {
|
||||
const hide = !visible;
|
||||
document.getElementById('realContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
document.getElementById('measureContainer')?.classList.toggle('preedit-hidden', hide);
|
||||
}
|
||||
|
||||
window.SetPreeditCaret = function () {
|
||||
const preedit = document.querySelector('#realContainer .pinyin .text');
|
||||
if (!preedit) return;
|
||||
const marker = '\uE000';
|
||||
const text = preedit.textContent || '';
|
||||
const index = text.indexOf(marker);
|
||||
const cleanText = text.replace(marker, '');
|
||||
const caretIndex = index >= 0 ? index : cleanText.length;
|
||||
preedit.textContent = '';
|
||||
preedit.append(document.createTextNode(cleanText.slice(0, caretIndex)));
|
||||
const caret = document.createElement('span');
|
||||
caret.className = 'cursor';
|
||||
preedit.append(caret);
|
||||
preedit.append(document.createTextNode(cleanText.slice(caretIndex)));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="realBody">
|
||||
<div class="containerParent" id="realContainerParent">
|
||||
<div class="container" id="realContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">ni'mf<span class="cursor"></span></div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="cand-no">1</span><span class="cand-content">你们(rR)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">2</span><span class="cand-content">你(rX)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">3</span><span class="cand-content">尼(uV)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">4</span><span class="cand-content">妮(nV)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">5</span><span class="cand-content">泥(dV)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">6</span><span class="cand-content">逆(zQ)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">7</span><span class="cand-content">拟(fR)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">8</span><span class="cand-content">腻(oD)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">9</span><span class="cand-content">拧(nG)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="measureBody" style="visibility: hidden;">
|
||||
<div class="containerParent" id="measureContainerParent">
|
||||
<div class="container" id="measureContainer">
|
||||
<!--0Anchor-->
|
||||
<div class="row pinyin">
|
||||
<div class="text">{0}</div>
|
||||
<div class="hidden-pinyin">n</div>
|
||||
</div>
|
||||
<!--1Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand first">
|
||||
<div class="text"><span class="cand-no">1</span><span class="cand-content"> {1} 你</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--2Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">2</span><span class="cand-content"> {2} 我</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--3Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">3</span><span class="cand-content"> {3} 什么</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--4Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">4</span><span class="cand-content"> {4} 量子</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--5Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">5</span><span class="cand-content"> {5} 可恶</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--6Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">6</span><span class="cand-content"> {6} 不是</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--7Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">7</span><span class="cand-content"> {7} 那么</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--8Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">8</span><span class="cand-content"> {8} 如果</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--9Anchor-->
|
||||
<div class="row-wrapper">
|
||||
<div class="row cand">
|
||||
<div class="text"><span class="cand-no">9</span><span class="cand-content"> {9}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Custom Context Menu -->
|
||||
<div id="contextMenu" class="context-menu">
|
||||
<div id="menuPin" class="context-menu-item" onmouseup="onPinToTop(event)">置顶</div>
|
||||
<div class="context-menu-item context-menu-parent">固定排位
|
||||
<div class="context-submenu">
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,1)">第 1 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,2)">第 2 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,3)">第 3 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,4)">第 4 位</div>
|
||||
<div class="context-menu-item" onmouseup="onFixPosition(event,5)">第 5 位</div>
|
||||
<div class="context-menu-separator"></div>
|
||||
<div class="context-menu-item" onmouseup="onClearPosition(event)">取消固定</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="menuDelete" class="context-menu-item" onmouseup="onDelete(event)">删除</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var SURFACE = 'cand';
|
||||
var SELECTOR = '#realContainer';
|
||||
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(JSON.stringify({
|
||||
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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,429 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>IME(WeChat 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;
|
||||
}
|
||||
|
||||
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: "Segoe UI Symbol", "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;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
/* Standalone WeChat-inspired toolbar skin. */
|
||||
.status-bar {
|
||||
background-color: #151515;
|
||||
border-color: #292929;
|
||||
box-shadow: 0 5px 14px rgba(0, 0, 0, .38);
|
||||
}
|
||||
.drag-handle { background: #07c160; }
|
||||
.icon:hover { background-color: rgba(7, 193, 96, .16); }
|
||||
</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">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.92 6.78H20.04V16.18H18.5V8.24H5.42V16.28H3.92V6.78ZM4.72 13.56H19.4V15.04H4.72V13.56ZM11.16 3.2H12.74V21.58H11.16V3.2Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 英文状态 -->
|
||||
<div class="icon" id="en">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.14 14.44H20.9V15.86H3.14V14.44ZM5.2 9.76H18.92V15.02H17.42V11.08H6.64V15.02H5.2V9.76ZM11.14 7.46H12.7V13C12.7 13.6933 12.6333 14.3733 12.5 15.04C12.38 15.6933 12.1467 16.34 11.8 16.98C11.4533 17.6067 10.9467 18.2067 10.28 18.78C9.61333 19.34 8.74 19.86 7.66 20.34C6.59333 20.8333 5.26667 21.2667 3.68 21.64C3.62667 21.5333 3.54667 21.4 3.44 21.24C3.33333 21.08 3.22 20.92 3.1 20.76C2.99333 20.6133 2.88 20.4867 2.76 20.38C4.28 20.0467 5.54667 19.6667 6.56 19.24C7.57333 18.8133 8.38667 18.3533 9 17.86C9.61333 17.3667 10.0733 16.8533 10.38 16.32C10.6867 15.7867 10.8867 15.24 10.98 14.68C11.0867 14.12 11.14 13.5533 11.14 12.98V7.46ZM12.94 14.96C13.58 16.4667 14.6067 17.66 16.02 18.54C17.4467 19.4067 19.2333 19.9867 21.38 20.28C21.26 20.3867 21.14 20.52 21.02 20.68C20.9 20.84 20.7867 21.0067 20.68 21.18C20.5733 21.3533 20.4867 21.5067 20.42 21.64C18.9267 21.3733 17.6067 20.9733 16.46 20.44C15.3267 19.9067 14.3533 19.2133 13.54 18.36C12.74 17.5067 12.0867 16.4867 11.58 15.3L12.94 14.96ZM3.38 5.04H20.56V6.4H3.38V5.04ZM7.62 3.2H9.1V8.5H7.62V3.2ZM14.8 3.2H16.3V8.5H14.8V3.2Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
</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">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M21 14.2894C17.8146 15.1746 14.3993 14.2763 12.0615 11.9385C9.72366 9.60074 8.82541 6.18545 9.71056 3C5.44557 4.18647 2.63796 8.25133 3.03782 12.6602C3.43767 17.069 6.93101 20.5623 11.3399 20.9622C15.7487 21.362 19.8136 18.5544 21 14.2894Z"
|
||||
stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 全角 -->
|
||||
<div class="icon" id="fullwidth">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="9.25" stroke="white" stroke-width="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="punctuation-mode" data-toolbar-item="punctuation">
|
||||
<!-- 中文标点 -->
|
||||
<div class="icon" id="puncCn">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="7.5" cy="7.5" r="2.5" stroke="white" />
|
||||
<circle cx="17" cy="15" r="2" fill="white" stroke="white" />
|
||||
<path d="M19 15C19 17 17.7059 18.6765 16 20.5" stroke="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 英文标点 -->
|
||||
<div class="icon" id="puncEn">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="7.5" cy="7.5" r="1.5" fill="white" stroke="white" />
|
||||
<path d="M18 16L15.5 21H14L16 16H18Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="icon" id="character-set" data-toolbar-item="character_set">
|
||||
<svg id="character-set-simplified" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.78 4.96H11.86V6.22H5.78V4.96ZM13.34 4.98H20.6V6.24H13.34V4.98ZM6.14 3.14L7.56 3.5C7.30667 4.20667 7.00667 4.88667 6.66 5.54C6.31333 6.19333 5.92667 6.80667 5.5 7.38C5.08667 7.94 4.66 8.44 4.22 8.88C4.12667 8.78667 4 8.69333 3.84 8.6C3.69333 8.49333 3.54 8.39333 3.38 8.3C3.23333 8.19333 3.1 8.10667 2.98 8.04C3.64667 7.45333 4.25333 6.72667 4.8 5.86C5.36 4.99333 5.80667 4.08667 6.14 3.14ZM13.92 3.18L15.36 3.5C15.0667 4.47333 14.6733 5.4 14.18 6.28C13.6867 7.14667 13.16 7.88 12.6 8.48C12.52 8.4 12.4067 8.30667 12.26 8.2C12.1133 8.09333 11.96 7.98667 11.8 7.88C11.64 7.77333 11.4933 7.69333 11.36 7.64C11.9333 7.09333 12.44 6.43333 12.88 5.66C13.3333 4.87333 13.68 4.04667 13.92 3.18ZM7.28 5.9L8.52 5.44C8.8 5.85333 9.07333 6.32 9.34 6.84C9.62 7.34667 9.82 7.78 9.94 8.14L8.6 8.68C8.49333 8.30667 8.31333 7.86 8.06 7.34C7.80667 6.80667 7.54667 6.32667 7.28 5.9ZM15.48 5.88L16.72 5.36C17.0533 5.8 17.3867 6.28667 17.72 6.82C18.0533 7.35333 18.3 7.81333 18.46 8.2L17.16 8.78C17.0267 8.39333 16.7933 7.92667 16.46 7.38C16.14 6.82 15.8133 6.32 15.48 5.88ZM9.02 15.1H15.08V16.22H9.02V15.1ZM4.14 10.92H5.6V21.56H4.14V10.92ZM9 9.24H19.2V10.6H9V9.24ZM5.04 9.22L6.14 8.46C6.56667 8.82 7 9.23333 7.44 9.7C7.88 10.1533 8.21333 10.56 8.44 10.92L7.28 11.74C7.06667 11.3933 6.74 10.98 6.3 10.5C5.87333 10.02 5.45333 9.59333 5.04 9.22ZM18.4 9.24H19.88V19.8C19.88 20.2133 19.8267 20.5267 19.72 20.74C19.6133 20.9667 19.4067 21.14 19.1 21.26C18.7933 21.3667 18.3867 21.4267 17.88 21.44C17.3733 21.4667 16.7467 21.48 16 21.48C15.9733 21.28 15.9133 21.0533 15.82 20.8C15.7267 20.56 15.6267 20.3467 15.52 20.16C15.8933 20.16 16.2467 20.1667 16.58 20.18C16.9267 20.1933 17.2267 20.1933 17.48 20.18C17.7333 20.18 17.9067 20.18 18 20.18C18.16 20.1667 18.2667 20.1333 18.32 20.08C18.3733 20.0267 18.4 19.9267 18.4 19.78V9.24ZM9.7 13.42V18.02H14.4V13.42H9.7ZM8.4 12.26H15.76V19.18H8.4V12.26Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
<span id="character-set-traditional" style="display: none">繁</span>
|
||||
</div>
|
||||
<div class="icon" id="emoji-panel" data-toolbar-item="emoji">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z"
|
||||
stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M15.6364 13.625C14.9091 14.8371 13.4546 15.625 12 15.625C10.5455 15.625 9.09092 14.8371 8.36365 13.625"
|
||||
stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9.01 10H9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M15.01 10H15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon" id="screen-keyboard" data-toolbar-item="screen_keyboard" style="display: none">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3.25" y="4.25" width="17.5" height="12.5" rx="1.75" stroke="white" stroke-width="1.5"/>
|
||||
<path d="M8 20H16M12 17V20" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M6.5 8H7.5M10 8H11M13.5 8H14.5M17 8H18M6.5 11H7.5M10 11H11M13.5 11H14.5M17 11H18M8 14H16"
|
||||
stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon" id="settings" data-toolbar-item="settings">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.07699 14.3816L3.59497 13.9054C3.24043 13.7915 3 13.4617 3 13.0894V10.9106C3 10.5383 3.24043 10.2085 3.59497 10.0946L5.07699 9.61845C5.52769 9.47366 5.77568 8.99091 5.63089 8.54021C5.60187 8.44991 5.55808 8.36505 5.50127 8.28908L4.86839 7.44272C4.61323 7.10148 4.64746 6.62461 4.94875 6.32332L6.32332 4.94875C6.62461 4.64746 7.10148 4.61323 7.44272 4.86839L8.28908 5.50127C8.6682 5.78476 9.20535 5.70724 9.48884 5.32812C9.54564 5.25216 9.58944 5.1673 9.61845 5.07699L10.0946 3.59497C10.2085 3.24043 10.5383 3 10.9106 3H13.0894C13.4617 3 13.7915 3.24043 13.9054 3.59497L14.3816 5.07699C14.5263 5.52769 15.0091 5.77568 15.4598 5.63089C15.5501 5.60187 15.635 5.55808 15.7109 5.50127L16.5573 4.86839C16.8985 4.61323 17.3754 4.64746 17.6767 4.94875L19.0512 6.32332C19.3525 6.62461 19.3868 7.10148 19.1316 7.44272L18.4987 8.28908C18.2152 8.6682 18.2928 9.20535 18.6719 9.48884C18.7478 9.54564 18.8327 9.58944 18.923 9.61845L20.405 10.0946C20.7596 10.2085 21 10.5383 21 10.9106V13.0894C21 13.4617 20.7596 13.7915 20.405 13.9054L18.923 14.3816C18.4723 14.5263 18.2243 15.0091 18.3691 15.4598C18.3981 15.5501 18.4419 15.635 18.4987 15.7109L19.1316 16.5573C19.3868 16.8985 19.3525 17.3754 19.0512 17.6767L17.6767 19.0512C17.3754 19.3525 16.8985 19.3868 16.5573 19.1316L15.7109 18.4987C15.3318 18.2152 14.7947 18.2928 14.5112 18.6719C14.4544 18.7478 14.4106 18.8327 14.3816 18.923L13.9054 20.405C13.7915 20.7596 13.4617 21 13.0894 21H10.9106C10.5383 21 10.2085 20.7596 10.0946 20.405L9.61845 18.923C9.47366 18.4723 8.99091 18.2243 8.54021 18.3691C8.44991 18.3981 8.36505 18.4419 8.28908 18.4987L7.44272 19.1316C7.10148 19.3868 6.62461 19.3525 6.32332 19.0512L4.94875 17.6767C4.64746 17.3754 4.61323 16.8985 4.86839 16.5573L5.50127 15.7109C5.78476 15.3318 5.70724 14.7947 5.32812 14.5112C5.25216 14.4544 5.1673 14.4106 5.07699 14.3816Z"
|
||||
stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M14.1213 9.87868C12.9497 8.70711 11.0503 8.70711 9.87868 9.87868C8.70711 11.0503 8.70711 12.9497 9.87868 14.1213C11.0503 15.2929 12.9497 15.2929 14.1213 14.1213C15.2929 12.9497 15.2929 11.0503 14.1213 9.87868Z"
|
||||
stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<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(JSON.stringify({ 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';
|
||||
// document.getElementById('cn').style.display = 'none';
|
||||
// 中英切换点击,向 Cpp 端发送消息
|
||||
document.getElementById('cn').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changeIMEMode', data: 'en' }));
|
||||
});
|
||||
document.getElementById('en').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changeIMEMode', data: 'cn' }));
|
||||
});
|
||||
document.getElementById('en-candidate').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'exitEnglishInputMode' }));
|
||||
});
|
||||
|
||||
//
|
||||
// 半角全角切换
|
||||
//
|
||||
// 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(JSON.stringify({ type: 'changeCharMode', data: 'fullwidth' }));
|
||||
});
|
||||
document.getElementById('fullwidth').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ 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(JSON.stringify({ type: 'changePuncMode', data: 'puncEn' }));
|
||||
});
|
||||
document.getElementById('puncEn').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changePuncMode', data: 'puncCn' }));
|
||||
});
|
||||
|
||||
//
|
||||
// 简繁输入切换
|
||||
//
|
||||
document.getElementById('character-set').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changeCharacterSet' }));
|
||||
});
|
||||
|
||||
document.getElementById('emoji-panel').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'openEmojiPanel' }));
|
||||
});
|
||||
|
||||
document.getElementById('screen-keyboard').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'openKeyboardPanel' }));
|
||||
});
|
||||
|
||||
//
|
||||
// 设置按钮
|
||||
//
|
||||
document.getElementById('settings').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'openSettings' }));
|
||||
});
|
||||
|
||||
// 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(JSON.stringify({ 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(JSON.stringify({
|
||||
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>
|
||||
@@ -0,0 +1,428 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>IME(WeChat Light 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;
|
||||
--msime-max-width-dip: 50vw;
|
||||
--msime-max-height-dip: 50vh;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
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: #ffffff;
|
||||
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(0, 0, 0, 0.12);
|
||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.12);
|
||||
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(0, 0, 0, 0.12);
|
||||
margin-left: calc(4px * var(--ftb-scale));
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #1a1a1a;
|
||||
font-size: calc(var(--ftb-icon-size) * 0.833 * var(--ftb-scale));
|
||||
font-family: "Segoe UI Symbol", "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;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
/* Light variant of the standalone WeChat-inspired toolbar skin. */
|
||||
.status-bar {
|
||||
background-color: #f7f7f7;
|
||||
border-color: #dedede;
|
||||
box-shadow: 0 5px 14px rgba(0, 0, 0, .12);
|
||||
}
|
||||
.drag-handle { background: #07c160; }
|
||||
.icon:hover { background-color: rgba(7, 193, 96, .12); }
|
||||
</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">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.92 6.78H20.04V16.18H18.5V8.24H5.42V16.28H3.92V6.78ZM4.72 13.56H19.4V15.04H4.72V13.56ZM11.16 3.2H12.74V21.58H11.16V3.2Z"
|
||||
fill="#1a1a1a" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 英文状态 -->
|
||||
<div class="icon" id="en">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.14 14.44H20.9V15.86H3.14V14.44ZM5.2 9.76H18.92V15.02H17.42V11.08H6.64V15.02H5.2V9.76ZM11.14 7.46H12.7V13C12.7 13.6933 12.6333 14.3733 12.5 15.04C12.38 15.6933 12.1467 16.34 11.8 16.98C11.4533 17.6067 10.9467 18.2067 10.28 18.78C9.61333 19.34 8.74 19.86 7.66 20.34C6.59333 20.8333 5.26667 21.2667 3.68 21.64C3.62667 21.5333 3.54667 21.4 3.44 21.24C3.33333 21.08 3.22 20.92 3.1 20.76C2.99333 20.6133 2.88 20.4867 2.76 20.38C4.28 20.0467 5.54667 19.6667 6.56 19.24C7.57333 18.8133 8.38667 18.3533 9 17.86C9.61333 17.3667 10.0733 16.8533 10.38 16.32C10.6867 15.7867 10.8867 15.24 10.98 14.68C11.0867 14.12 11.14 13.5533 11.14 12.98V7.46ZM12.94 14.96C13.58 16.4667 14.6067 17.66 16.02 18.54C17.4467 19.4067 19.2333 19.9867 21.38 20.28C21.26 20.3867 21.14 20.52 21.02 20.68C20.9 20.84 20.7867 21.0067 20.68 21.18C20.5733 21.3533 20.4867 21.5067 20.42 21.64C18.9267 21.3733 17.6067 20.9733 16.46 20.44C15.3267 19.9067 14.3533 19.2133 13.54 18.36C12.74 17.5067 12.0867 16.4867 11.58 15.3L12.94 14.96ZM3.38 5.04H20.56V6.4H3.38V5.04ZM7.62 3.2H9.1V8.5H7.62V3.2ZM14.8 3.2H16.3V8.5H14.8V3.2Z"
|
||||
fill="#1a1a1a" />
|
||||
</svg>
|
||||
</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">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M21 14.2894C17.8146 15.1746 14.3993 14.2763 12.0615 11.9385C9.72366 9.60074 8.82541 6.18545 9.71056 3C5.44557 4.18647 2.63796 8.25133 3.03782 12.6602C3.43767 17.069 6.93101 20.5623 11.3399 20.9622C15.7487 21.362 19.8136 18.5544 21 14.2894Z"
|
||||
stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 全角 -->
|
||||
<div class="icon" id="fullwidth">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="9.25" stroke="#1a1a1a" stroke-width="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="punctuation-mode" data-toolbar-item="punctuation">
|
||||
<!-- 中文标点 -->
|
||||
<div class="icon" id="puncCn">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="7.5" cy="7.5" r="2.5" stroke="#1a1a1a" />
|
||||
<circle cx="17" cy="15" r="2" fill="#1a1a1a" stroke="#1a1a1a" />
|
||||
<path d="M19 15C19 17 17.7059 18.6765 16 20.5" stroke="#1a1a1a" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 英文标点 -->
|
||||
<div class="icon" id="puncEn">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="7.5" cy="7.5" r="1.5" fill="#1a1a1a" stroke="#1a1a1a" />
|
||||
<path d="M18 16L15.5 21H14L16 16H18Z" fill="#1a1a1a" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="icon" id="character-set" data-toolbar-item="character_set">
|
||||
<svg id="character-set-simplified" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.78 4.96H11.86V6.22H5.78V4.96ZM13.34 4.98H20.6V6.24H13.34V4.98ZM6.14 3.14L7.56 3.5C7.30667 4.20667 7.00667 4.88667 6.66 5.54C6.31333 6.19333 5.92667 6.80667 5.5 7.38C5.08667 7.94 4.66 8.44 4.22 8.88C4.12667 8.78667 4 8.69333 3.84 8.6C3.69333 8.49333 3.54 8.39333 3.38 8.3C3.23333 8.19333 3.1 8.10667 2.98 8.04C3.64667 7.45333 4.25333 6.72667 4.8 5.86C5.36 4.99333 5.80667 4.08667 6.14 3.14ZM13.92 3.18L15.36 3.5C15.0667 4.47333 14.6733 5.4 14.18 6.28C13.6867 7.14667 13.16 7.88 12.6 8.48C12.52 8.4 12.4067 8.30667 12.26 8.2C12.1133 8.09333 11.96 7.98667 11.8 7.88C11.64 7.77333 11.4933 7.69333 11.36 7.64C11.9333 7.09333 12.44 6.43333 12.88 5.66C13.3333 4.87333 13.68 4.04667 13.92 3.18ZM7.28 5.9L8.52 5.44C8.8 5.85333 9.07333 6.32 9.34 6.84C9.62 7.34667 9.82 7.78 9.94 8.14L8.6 8.68C8.49333 8.30667 8.31333 7.86 8.06 7.34C7.80667 6.80667 7.54667 6.32667 7.28 5.9ZM15.48 5.88L16.72 5.36C17.0533 5.8 17.3867 6.28667 17.72 6.82C18.0533 7.35333 18.3 7.81333 18.46 8.2L17.16 8.78C17.0267 8.39333 16.7933 7.92667 16.46 7.38C16.14 6.82 15.8133 6.32 15.48 5.88ZM9.02 15.1H15.08V16.22H9.02V15.1ZM4.14 10.92H5.6V21.56H4.14V10.92ZM9 9.24H19.2V10.6H9V9.24ZM5.04 9.22L6.14 8.46C6.56667 8.82 7 9.23333 7.44 9.7C7.88 10.1533 8.21333 10.56 8.44 10.92L7.28 11.74C7.06667 11.3933 6.74 10.98 6.3 10.5C5.87333 10.02 5.45333 9.59333 5.04 9.22ZM18.4 9.24H19.88V19.8C19.88 20.2133 19.8267 20.5267 19.72 20.74C19.6133 20.9667 19.4067 21.14 19.1 21.26C18.7933 21.3667 18.3867 21.4267 17.88 21.44C17.3733 21.4667 16.7467 21.48 16 21.48C15.9733 21.28 15.9133 21.0533 15.82 20.8C15.7267 20.56 15.6267 20.3467 15.52 20.16C15.8933 20.16 16.2467 20.1667 16.58 20.18C16.9267 20.1933 17.2267 20.1933 17.48 20.18C17.7333 20.18 17.9067 20.18 18 20.18C18.16 20.1667 18.2667 20.1333 18.32 20.08C18.3733 20.0267 18.4 19.9267 18.4 19.78V9.24ZM9.7 13.42V18.02H14.4V13.42H9.7ZM8.4 12.26H15.76V19.18H8.4V12.26Z"
|
||||
fill="#1a1a1a" />
|
||||
</svg>
|
||||
<span id="character-set-traditional" style="display: none">繁</span>
|
||||
</div>
|
||||
<div class="icon" id="emoji-panel" data-toolbar-item="emoji">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z"
|
||||
stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M15.6364 13.625C14.9091 14.8371 13.4546 15.625 12 15.625C10.5455 15.625 9.09092 14.8371 8.36365 13.625"
|
||||
stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9.01 10H9" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M15.01 10H15" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon" id="screen-keyboard" data-toolbar-item="screen_keyboard" style="display: none">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3.25" y="4.25" width="17.5" height="12.5" rx="1.75" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M8 20H16M12 17V20" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M6.5 8H7.5M10 8H11M13.5 8H14.5M17 8H18M6.5 11H7.5M10 11H11M13.5 11H14.5M17 11H18M8 14H16"
|
||||
stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon" id="settings" data-toolbar-item="settings">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.07699 14.3816L3.59497 13.9054C3.24043 13.7915 3 13.4617 3 13.0894V10.9106C3 10.5383 3.24043 10.2085 3.59497 10.0946L5.07699 9.61845C5.52769 9.47366 5.77568 8.99091 5.63089 8.54021C5.60187 8.44991 5.55808 8.36505 5.50127 8.28908L4.86839 7.44272C4.61323 7.10148 4.64746 6.62461 4.94875 6.32332L6.32332 4.94875C6.62461 4.64746 7.10148 4.61323 7.44272 4.86839L8.28908 5.50127C8.6682 5.78476 9.20535 5.70724 9.48884 5.32812C9.54564 5.25216 9.58944 5.1673 9.61845 5.07699L10.0946 3.59497C10.2085 3.24043 10.5383 3 10.9106 3H13.0894C13.4617 3 13.7915 3.24043 13.9054 3.59497L14.3816 5.07699C14.5263 5.52769 15.0091 5.77568 15.4598 5.63089C15.5501 5.60187 15.635 5.55808 15.7109 5.50127L16.5573 4.86839C16.8985 4.61323 17.3754 4.64746 17.6767 4.94875L19.0512 6.32332C19.3525 6.62461 19.3868 7.10148 19.1316 7.44272L18.4987 8.28908C18.2152 8.6682 18.2928 9.20535 18.6719 9.48884C18.7478 9.54564 18.8327 9.58944 18.923 9.61845L20.405 10.0946C20.7596 10.2085 21 10.5383 21 10.9106V13.0894C21 13.4617 20.7596 13.7915 20.405 13.9054L18.923 14.3816C18.4723 14.5263 18.2243 15.0091 18.3691 15.4598C18.3981 15.5501 18.4419 15.635 18.4987 15.7109L19.1316 16.5573C19.3868 16.8985 19.3525 17.3754 19.0512 17.6767L17.6767 19.0512C17.3754 19.3525 16.8985 19.3868 16.5573 19.1316L15.7109 18.4987C15.3318 18.2152 14.7947 18.2928 14.5112 18.6719C14.4544 18.7478 14.4106 18.8327 14.3816 18.923L13.9054 20.405C13.7915 20.7596 13.4617 21 13.0894 21H10.9106C10.5383 21 10.2085 20.7596 10.0946 20.405L9.61845 18.923C9.47366 18.4723 8.99091 18.2243 8.54021 18.3691C8.44991 18.3981 8.36505 18.4419 8.28908 18.4987L7.44272 19.1316C7.10148 19.3868 6.62461 19.3525 6.32332 19.0512L4.94875 17.6767C4.64746 17.3754 4.61323 16.8985 4.86839 16.5573L5.50127 15.7109C5.78476 15.3318 5.70724 14.7947 5.32812 14.5112C5.25216 14.4544 5.1673 14.4106 5.07699 14.3816Z"
|
||||
stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M14.1213 9.87868C12.9497 8.70711 11.0503 8.70711 9.87868 9.87868C8.70711 11.0503 8.70711 12.9497 9.87868 14.1213C11.0503 15.2929 12.9497 15.2929 14.1213 14.1213C15.2929 12.9497 15.2929 11.0503 14.1213 9.87868Z"
|
||||
stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<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(JSON.stringify({ 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';
|
||||
// document.getElementById('cn').style.display = 'none';
|
||||
// 中英切换点击,向 Cpp 端发送消息
|
||||
document.getElementById('cn').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changeIMEMode', data: 'en' }));
|
||||
});
|
||||
document.getElementById('en').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changeIMEMode', data: 'cn' }));
|
||||
});
|
||||
document.getElementById('en-candidate').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'exitEnglishInputMode' }));
|
||||
});
|
||||
|
||||
//
|
||||
// 半角全角切换
|
||||
//
|
||||
// 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(JSON.stringify({ type: 'changeCharMode', data: 'fullwidth' }));
|
||||
});
|
||||
document.getElementById('fullwidth').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ 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(JSON.stringify({ type: 'changePuncMode', data: 'puncEn' }));
|
||||
});
|
||||
document.getElementById('puncEn').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changePuncMode', data: 'puncCn' }));
|
||||
});
|
||||
|
||||
//
|
||||
// 简繁输入切换
|
||||
//
|
||||
document.getElementById('character-set').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'changeCharacterSet' }));
|
||||
});
|
||||
|
||||
document.getElementById('emoji-panel').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'openEmojiPanel' }));
|
||||
});
|
||||
|
||||
document.getElementById('screen-keyboard').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'openKeyboardPanel' }));
|
||||
});
|
||||
|
||||
//
|
||||
// 设置按钮
|
||||
//
|
||||
document.getElementById('settings').addEventListener('click', () => {
|
||||
window.chrome.webview.postMessage(JSON.stringify({ type: 'openSettings' }));
|
||||
});
|
||||
|
||||
// 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(JSON.stringify({ 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(JSON.stringify({
|
||||
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>
|
||||
@@ -5,6 +5,7 @@ import { applyVoiceConfig } from './voice';
|
||||
import { applyAiConfig } from './ai-settings';
|
||||
import { applyFloatingToolbarAppearanceConfig, applyFloatingToolbarItemsConfig } from './floating-toolbar';
|
||||
import { applyShortcutConfig } from './shortcut';
|
||||
import { applyCandidateSkin } from './skin';
|
||||
|
||||
function safeParseJson(value: string): unknown {
|
||||
try {
|
||||
@@ -26,6 +27,7 @@ export function setupConfigSync(): void {
|
||||
}
|
||||
|
||||
applyCandidateArrange(payload.data?.appearance?.candidate_window_layout);
|
||||
applyCandidateSkin(payload.data?.appearance?.candidate_skin);
|
||||
applyAppearanceConfig(
|
||||
payload.data?.appearance?.candidate_window_preedit_style,
|
||||
payload.data?.appearance?.tsf_preedit_style,
|
||||
|
||||
@@ -2,11 +2,21 @@ import { loadHTML } from '../utils/common-utils';
|
||||
import ftbHTML from '../../../../ftb/default.html?raw';
|
||||
|
||||
export type SkinPreviewTheme = 'dark' | 'light';
|
||||
export type CandidateSkin = 'fluent' | 'wechat';
|
||||
|
||||
let previewOverride: SkinPreviewTheme | null = null;
|
||||
const SKINS: CandidateSkin[] = ['fluent', 'wechat'];
|
||||
const previewOverrides: Record<CandidateSkin, SkinPreviewTheme | null> = {
|
||||
fluent: null,
|
||||
wechat: null
|
||||
};
|
||||
let activeTheme: SkinPreviewTheme = 'dark';
|
||||
let activeSkin: CandidateSkin = 'fluent';
|
||||
const SKIN_PREVIEW_PAGE_SIZE = 6;
|
||||
|
||||
function normalizeCandidateSkin(value: unknown): CandidateSkin {
|
||||
return value === 'wechat' ? 'wechat' : 'fluent';
|
||||
}
|
||||
|
||||
function limitCandidatePreview(host: HTMLElement): void {
|
||||
host.querySelectorAll<HTMLElement>('.row-wrapper').forEach((wrapper, index) => {
|
||||
wrapper.style.display = index < SKIN_PREVIEW_PAGE_SIZE ? '' : 'none';
|
||||
@@ -22,58 +32,108 @@ function fillToolbar(host: HTMLElement): void {
|
||||
host.replaceChildren(statusBar);
|
||||
}
|
||||
|
||||
function applySkinPreviewTheme(theme: SkinPreviewTheme): void {
|
||||
document.querySelectorAll('#skinCandidateHorizontal, #skinCandidateVertical').forEach((element) => {
|
||||
element.classList.toggle('theme-light', theme === 'light');
|
||||
element.classList.toggle('theme-dark', theme === 'dark');
|
||||
});
|
||||
document.querySelectorAll('#skinToolbarPreview').forEach((element) => {
|
||||
element.classList.toggle('theme-light', theme === 'light');
|
||||
element.classList.toggle('theme-dark', theme === 'dark');
|
||||
});
|
||||
function applySkinPreviewTheme(skin: CandidateSkin, theme: SkinPreviewTheme): void {
|
||||
const card = document.querySelector<HTMLElement>(`[data-candidate-skin="${skin}"]`);
|
||||
if (!card) return;
|
||||
|
||||
const title = document.getElementById('skinThemeTitle');
|
||||
const button = document.getElementById('skinPreviewSwitch');
|
||||
card.querySelectorAll<HTMLElement>('[data-skin-horizontal], [data-skin-vertical], [data-skin-toolbar]')
|
||||
.forEach((element) => {
|
||||
element.classList.toggle('theme-light', theme === 'light');
|
||||
element.classList.toggle('theme-dark', theme === 'dark');
|
||||
});
|
||||
|
||||
const title = card.querySelector<HTMLElement>('.section-title');
|
||||
if (title) {
|
||||
title.textContent = theme === 'light' ? 'Fluent 主题(Light)' : 'Fluent 主题(Dark)';
|
||||
const name = skin === 'wechat' ? '微信绿主题' : 'Fluent 主题';
|
||||
title.textContent = `${name}(${theme === 'light' ? 'Light' : 'Dark'})`;
|
||||
}
|
||||
if (button) {
|
||||
card.querySelectorAll<HTMLButtonElement>('[data-skin-preview-switch]').forEach((button) => {
|
||||
button.textContent = theme === 'light' ? '预览深色' : '预览浅色';
|
||||
});
|
||||
}
|
||||
|
||||
function resolvedPreviewTheme(skin: CandidateSkin): SkinPreviewTheme {
|
||||
return previewOverrides[skin] ?? activeTheme;
|
||||
}
|
||||
|
||||
function syncSkinSwitches(): void {
|
||||
document.querySelectorAll<HTMLElement>('[data-skin-switch]').forEach((element) => {
|
||||
const selected = element.dataset.skinSwitch === activeSkin;
|
||||
element.classList.toggle('active', selected);
|
||||
element.setAttribute('aria-checked', String(selected));
|
||||
});
|
||||
}
|
||||
|
||||
function syncAppearancePreviews(): void {
|
||||
const useWechat = activeSkin === 'wechat';
|
||||
document.querySelectorAll<HTMLElement>('.cand-preview .candidate').forEach((element) => {
|
||||
element.classList.toggle('skin-wechat', useWechat);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('.ftb-preview-host:not([data-skin-toolbar])').forEach((element) => {
|
||||
element.classList.toggle('skin-wechat', useWechat);
|
||||
});
|
||||
}
|
||||
|
||||
function selectSkin(value: unknown, persist: boolean): void {
|
||||
activeSkin = normalizeCandidateSkin(value);
|
||||
syncSkinSwitches();
|
||||
syncAppearancePreviews();
|
||||
if (persist) {
|
||||
window.chrome?.webview?.postMessage(JSON.stringify({
|
||||
type: 'configUpdate',
|
||||
data: { path: 'appearance.candidate_skin', value: activeSkin }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function resolvedPreviewTheme(): SkinPreviewTheme {
|
||||
return previewOverride ?? activeTheme;
|
||||
export function applyCandidateSkin(value: unknown): void {
|
||||
selectSkin(value, false);
|
||||
}
|
||||
|
||||
export function syncSkinPreviewTheme(theme: SkinPreviewTheme): void {
|
||||
activeTheme = theme;
|
||||
// When the real theme changes, drop manual override so preview follows config again.
|
||||
previewOverride = null;
|
||||
applySkinPreviewTheme(resolvedPreviewTheme());
|
||||
SKINS.forEach((skin) => {
|
||||
previewOverrides[skin] = null;
|
||||
applySkinPreviewTheme(skin, resolvedPreviewTheme(skin));
|
||||
});
|
||||
}
|
||||
|
||||
export async function setupSkin(): Promise<void> {
|
||||
const vertical = document.getElementById('skinCandidateVertical');
|
||||
const horizontal = document.getElementById('skinCandidateHorizontal');
|
||||
if (vertical) {
|
||||
vertical.innerHTML = await loadHTML('/src/partials/candidate/candidate-wnd-v.html');
|
||||
limitCandidatePreview(vertical);
|
||||
}
|
||||
if (horizontal) {
|
||||
horizontal.innerHTML = await loadHTML('/src/partials/candidate/candidate-wnd-h.html');
|
||||
limitCandidatePreview(horizontal);
|
||||
}
|
||||
const horizontalHtml = await loadHTML('/src/partials/candidate/candidate-wnd-h.html');
|
||||
const verticalHtml = await loadHTML('/src/partials/candidate/candidate-wnd-v.html');
|
||||
|
||||
const toolbar = document.getElementById('skinToolbarPreview');
|
||||
if (toolbar) fillToolbar(toolbar);
|
||||
document.querySelectorAll<HTMLElement>('[data-skin-horizontal]').forEach((host) => {
|
||||
host.innerHTML = horizontalHtml;
|
||||
limitCandidatePreview(host);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('[data-skin-vertical]').forEach((host) => {
|
||||
host.innerHTML = verticalHtml;
|
||||
limitCandidatePreview(host);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('[data-skin-toolbar]').forEach(fillToolbar);
|
||||
|
||||
applySkinPreviewTheme(resolvedPreviewTheme());
|
||||
SKINS.forEach((skin) => applySkinPreviewTheme(skin, resolvedPreviewTheme(skin)));
|
||||
syncSkinSwitches();
|
||||
syncAppearancePreviews();
|
||||
|
||||
const switchBtn = document.getElementById('skinPreviewSwitch');
|
||||
switchBtn?.addEventListener('click', () => {
|
||||
const current = resolvedPreviewTheme();
|
||||
previewOverride = current === 'light' ? 'dark' : 'light';
|
||||
applySkinPreviewTheme(previewOverride);
|
||||
document.querySelectorAll<HTMLElement>('[data-skin-switch]').forEach((element) => {
|
||||
const activate = () => selectSkin(element.dataset.skinSwitch, true);
|
||||
element.addEventListener('click', activate);
|
||||
element.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
activate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-skin-preview-switch]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const card = button.closest<HTMLElement>('[data-candidate-skin]');
|
||||
const skin = normalizeCandidateSkin(card?.dataset.candidateSkin);
|
||||
const current = resolvedPreviewTheme(skin);
|
||||
previewOverrides[skin] = current === 'light' ? 'dark' : 'light';
|
||||
applySkinPreviewTheme(skin, previewOverrides[skin]!);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -179,8 +179,8 @@ function updateSkinThemeCard(
|
||||
`全局 ${modeLabel} · 设置 ${settingsResolved === 'light' ? '浅色' : '深色'} · 候选 ${candResolved === 'light' ? '浅色' : '深色'}`;
|
||||
}
|
||||
|
||||
// Skin gallery follows the global theme; the switch button can preview the other one.
|
||||
syncSkinPreviewTheme(resolveTheme(mode, 'follow'));
|
||||
// Fluent preview follows the same resolved theme as the real candidate surface.
|
||||
syncSkinPreviewTheme(candResolved);
|
||||
}
|
||||
|
||||
export function setThemeMode(value: string | undefined): void {
|
||||
|
||||
@@ -1,33 +1,56 @@
|
||||
<div class="content" id="skin-settings">
|
||||
<div class="content-header"><span>皮肤</span></div>
|
||||
|
||||
<div class="section skin-theme-card">
|
||||
<div class="section skin-theme-card" data-candidate-skin="fluent">
|
||||
<div class="skin-theme-header">
|
||||
<div>
|
||||
<div class="section-title" id="skinThemeTitle">Fluent 主题(Dark)</div>
|
||||
<div class="skin-theme-description" id="skinThemeStatus">当前默认主题</div>
|
||||
</div>
|
||||
<div class="skin-theme-actions">
|
||||
<div class="ftb-toggle-btn active skin-theme-toggle" role="switch" aria-checked="true" aria-disabled="true">
|
||||
<div class="ftb-toggle-btn active skin-theme-toggle" data-skin-switch="fluent" role="switch" aria-checked="true" tabindex="0">
|
||||
<div class="ftb-toggle-knob"></div>
|
||||
</div>
|
||||
<button type="button" class="skin-preview-switch" id="skinPreviewSwitch" title="切换明暗预览">
|
||||
预览浅色
|
||||
</button>
|
||||
<button type="button" class="skin-preview-switch" data-skin-preview-switch title="切换明暗预览">预览浅色</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skin-preview-list">
|
||||
<div class="skin-preview-row skin-preview-horizontal">
|
||||
<div class="skin-preview-stage"><div class="candidate wnd-h" id="skinCandidateHorizontal"></div></div>
|
||||
<div class="skin-preview-stage"><div class="candidate wnd-h" data-skin-horizontal></div></div>
|
||||
</div>
|
||||
|
||||
<div class="skin-preview-row skin-preview-vertical">
|
||||
<div class="skin-preview-stage"><div class="candidate wnd-v" id="skinCandidateVertical"></div></div>
|
||||
<div class="skin-preview-stage"><div class="candidate wnd-v" data-skin-vertical></div></div>
|
||||
</div>
|
||||
|
||||
<div class="skin-preview-row skin-preview-toolbar">
|
||||
<div class="skin-preview-stage"><div class="ftb-preview-host" id="skinToolbarPreview"></div></div>
|
||||
<div class="skin-preview-stage"><div class="ftb-preview-host" data-skin-toolbar></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section skin-theme-card" data-candidate-skin="wechat">
|
||||
<div class="skin-theme-header">
|
||||
<div>
|
||||
<div class="section-title" id="wechatSkinTitle">微信绿主题(Dark)</div>
|
||||
<div class="skin-theme-description">微信绿候选窗与悬浮工具栏</div>
|
||||
</div>
|
||||
<div class="skin-theme-actions">
|
||||
<div class="ftb-toggle-btn skin-theme-toggle" data-skin-switch="wechat" role="switch" aria-checked="false" tabindex="0">
|
||||
<div class="ftb-toggle-knob"></div>
|
||||
</div>
|
||||
<button type="button" class="skin-preview-switch" data-skin-preview-switch title="切换明暗预览">预览浅色</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skin-preview-list">
|
||||
<div class="skin-preview-row skin-preview-horizontal">
|
||||
<div class="skin-preview-stage"><div class="candidate wnd-h skin-wechat" data-skin-horizontal></div></div>
|
||||
</div>
|
||||
<div class="skin-preview-row skin-preview-vertical">
|
||||
<div class="skin-preview-stage"><div class="candidate wnd-v skin-wechat" data-skin-vertical></div></div>
|
||||
</div>
|
||||
<div class="skin-preview-row skin-preview-toolbar">
|
||||
<div class="skin-preview-stage"><div class="ftb-preview-host skin-wechat" data-skin-toolbar></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.skin-theme-card { padding: 0; overflow: hidden; }
|
||||
.skin-theme-card + .skin-theme-card { margin-top: 18px; }
|
||||
.skin-theme-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 24px; }
|
||||
.skin-theme-description { margin-top: 5px; color: var(--text-muted); font-size: 13px; }
|
||||
.skin-theme-actions {
|
||||
@@ -8,7 +9,7 @@
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.skin-theme-toggle { cursor: default; }
|
||||
.skin-theme-toggle { cursor: pointer; }
|
||||
.skin-preview-switch {
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
@@ -21,17 +22,12 @@
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.skin-preview-switch:hover {
|
||||
color: var(--text-color);
|
||||
background: var(--button-secondary-bg);
|
||||
}
|
||||
.skin-preview-switch:hover { color: var(--text-color); background: var(--button-secondary-bg); }
|
||||
.skin-preview-list { padding: 9px 0; background: var(--skin-preview-stage-bg); }
|
||||
.skin-preview-row { min-width: 0; }
|
||||
.skin-preview-stage { display: flex; align-items: flex-start; justify-content: flex-start; min-width: 0; padding: 9px 24px; overflow: hidden; }
|
||||
.skin-preview-row .candidate { flex: 0 0 auto; align-items: flex-start; margin-top: 0; transform-origin: left center; }
|
||||
.skin-preview-horizontal .container {
|
||||
/* Match the real candidate window: shrink to content, but wrap when the
|
||||
available preview width is too small. */
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
@@ -40,3 +36,44 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
.skin-preview-toolbar .ftb-preview-host { display: flex; align-items: center; justify-content: flex-start; padding: 0; }
|
||||
|
||||
/* Preview equivalents of the standalone WeChat skin HTML files. */
|
||||
.candidate.skin-wechat.theme-dark {
|
||||
--accent-strong: #07c160;
|
||||
--cand-bg: #151515;
|
||||
--cand-border: #292929;
|
||||
--cand-shadow: 0 6px 18px rgba(0, 0, 0, .42);
|
||||
--cand-text: #b7b7b7;
|
||||
--cand-num: #858585;
|
||||
--cand-hover: rgba(7, 193, 96, .22);
|
||||
--cand-selected: #07c160;
|
||||
}
|
||||
.candidate.skin-wechat.theme-light {
|
||||
--accent-strong: #07c160;
|
||||
--cand-bg: #f7f7f7;
|
||||
--cand-border: #dedede;
|
||||
--cand-shadow: 0 5px 15px rgba(0, 0, 0, .14);
|
||||
--cand-text: #333333;
|
||||
--cand-num: #757575;
|
||||
--cand-hover: rgba(7, 193, 96, .14);
|
||||
--cand-selected: #07c160;
|
||||
}
|
||||
.candidate.skin-wechat .container { padding: 2px; border-radius: 5px; }
|
||||
.candidate.skin-wechat .cand { border-radius: 4px; }
|
||||
.candidate.skin-wechat .first { border-radius: 5px; }
|
||||
.candidate.skin-wechat .first::before { display: none; }
|
||||
.candidate.skin-wechat .first .text,
|
||||
.candidate.skin-wechat .first .num,
|
||||
.candidate.skin-wechat .first .cand-no { color: #fff; }
|
||||
|
||||
.ftb-preview-host.skin-wechat.theme-dark .status-bar {
|
||||
background-color: #151515;
|
||||
border-color: #292929;
|
||||
box-shadow: 0 5px 14px rgba(0, 0, 0, .38);
|
||||
}
|
||||
.ftb-preview-host.skin-wechat.theme-light .status-bar {
|
||||
background-color: #f7f7f7;
|
||||
border-color: #dedede;
|
||||
box-shadow: 0 5px 14px rgba(0, 0, 0, .12);
|
||||
}
|
||||
.ftb-preview-host.skin-wechat .drag-handle { background: #07c160; }
|
||||
Reference in new issue
Block a user