Commit Graph
42 Commits
Author SHA1 Message Date
時崎 ギャラクシー bcbbdcf74c Merge pull request #903 from Tokisaki-Galaxy/feat/skin-schema-v2
feat(server): 外部皮肤新增 schema_version 2,WebView2 与 D2D 双后端支持九宫格贴图与自由挂件
2026-10-13 00:57:14 +08:00
fanlusky 797c0f5652 feat(server,ui,ui-html): 候选窗可严格按文字宽度收紧
皮肤新增 [candidate_window] fit_content,设置页「外观」新增「候选窗口按文字收紧宽度」
(appearance.candidate_window_fit_content),两者取或。打开后:

- D2D 不再垫 160 DIP 卡片最小宽度和列表 80 DIP 下限,行尾不再多留 6 DIP,
  右侧留白与序号左侧留白相同;
- WebView2 去掉 .container 的 7em 下限;
- 同一输入里宽度不再只增不减,高度仍保持。

皮肤的 min_width_dip 与装饰图宽度照常生效;内置皮肤清单出厂为 false。
2026-10-12 02:03:59 +08:00
Lu Fan(SonnyCalcr) 0d7ae4ca79 feat(server): 翻译里的词性与分隔符单独配色 (#848)
候选翻译(如 "adj. spreading; n. sound of laughter")由 Server 切成单词、词性、
分隔符三段:WebView2 端包成 .cand-pos / .cand-sep,D2D 端经
CandidateList::Item::translationRuns 用分段画刷绘制,两端同一切法。

新增皮肤键 translation_pos / translation_separator 及选中行的
selected_translation_pos / selected_translation_separator,留空时与翻译同色。

内置皮肤的翻译改用 Notion 调色板:词性取相邻的另一个色相并降到 50% 不透明度,
分隔符为单词色 45%;铺实色高亮的选中行另配一组。仓库内的外部皮肤同步补上词性与
分隔符配色。
2026-10-11 23:28:30 +08:00
fanlusky b986c3496f refactor(server): D2D 候选组页时只读一次翻译开关 2026-10-05 13:02:46 +08:00
fanlusky ec00892121 feat(server): 新增仿微软拼音的内置皮肤 microsoft
- 候选窗:深色 #2C2C2C 底、#383838 选中底、#E183D9 紫色选中条,序号与候选同字号,
  横排每项最小 4.35em、间距 0.28em(换行的行间同样隔 0.28em);尺寸均按字号缩放
- 翻页:默认开启,实心圆角三角,与候选行等高,横排前加分隔线
- 悬浮工具栏沿用默认页面,拖拽条改为紫色
- ui:PagerArrows 新增 Glyph::Triangle 与分隔线,三角用单一路径填充,半透明时不出现描边重叠;
  CandidateList 新增 minItemWidth 与 selectedBarInside
- 设置页新增皮肤卡片与预览;更新内置皮肤列表、文档与配置注释,补充测试
2026-09-30 17:35:38 +08:00
fanlusky dde06afd7f feat(server): 候选框显示翻页箭头并接入设置页预览
- D2D:横排在候选右侧多一窄列,竖排在最后一行下方、「‹」与序号左对齐;开关或排列方式变化时重建场景
- WebView2:页面按 data-page-arrows 显示箭头,点击经 candidatePage 翻页
- 箭头点击走 WM_PAGE_CANDIDATE_ARROW,不受鼠标滚轮翻页开关影响;页面快照带上一页 / 下一页可用状态
- 两个设置窗口宿主下发内置皮肤的 page_arrows,外部皮肤条目带解析后的 pageArrows
- 安装包把 skins/default 以 onlyifdoesntexist 放到数据目录,升级不覆盖用户改过的清单
2026-09-30 12:42:25 +08:00
fanlusky 52041513b2 fix(server): 杨柳青横排翻译贴底、行尾留白且末项高亮不铺满
行高里的底部内边距原先夹在候选与翻译之间,改为放在翻译下面。
卡片为防闪烁保持同一输入下更宽的尺寸时,多出的宽度均分给同一行各项,末项高亮贴到卡片右边。
2026-09-30 01:33:47 +08:00
fanlusky 7ad6a42f12 feat(server): 外部皮肤新增细分配色、菜单、外框线宽、高亮圆角、阴影与字体族 2026-09-29 22:08:00 +08:00
fanlusky 24b2444a3d feat(server): 外部皮肤支持候选框背景图、自定义圆角与装饰图对齐
- skin.toml 新增 candidate_window.corner_radius_dip、candidate_window.background(image / fit / opacity)和 decoration.image / decoration.align
- decoration 表改为可选;存在时 image、top_inset_dip、width_dip 必填且图片必须存在,不再回退到 preview
- D2D 候选框的装饰图与卡片同列排布,按 width_dip × top_inset_dip 等比缩放、贴在卡片上方且不重叠,卡片最小宽度遵守 min_width_dip 和装饰宽度,与 WebView2 渲染一致
- WebView2 端生成对应的背景、圆角和对齐 CSS;皮肤重载后 D2D 场景随之重建
2026-09-28 19:36:37 +08:00
fanlusky b8c6a18ca2 fix(server): D2D 候选窗横排首尾高亮与外框同 R,自定义皮肤按 base 取几何
fluent、微信绿横排设置 outerItemRadius,对应 CSS 的首尾项圆角规则。
自定义皮肤包先加载,皮肤分支按 package->base 判断,与 WebView2 注入
base 皮肤 CSS 的行为一致,颜色仍由包覆盖。
2026-09-28 13:11:40 +08:00
fanlusky dff42f92df perf(server): D2D 候选窗每次显示只绘制一遍
PlaceAndShow 同步 Present 之后,前面各个 setter 的 InvalidateRect 留下的
WM_PAINT 又把整张卡片(含阴影模糊)画一遍;宿主改尺寸时 WM_SIZE 也会在
显式 Present 之前同步画一遍。同步 Present 后 ValidateRect 丢弃过时的
WM_PAINT;PlaceAndShow 自己触发的 WM_SIZE 只调整交换链尺寸,由随后的
Present 出帧。
2026-09-27 15:03:17 +08:00
fanlusky e829a1b1ab fix(server): 候选卡片只在同一输入内保留尺寸,换键或翻页后不再留空白
3a7beb18 让卡片在整次组合内只增不减,自然尺寸低于保留值 70% 才收回。
这本是为了吸收同一输入下陆续到达的译文、云/AI 候选和混输英文,但换键
后候选本就换了一页,也沿用了旧尺寸:gw(4 行)→ gw'r(3 行)时自然高度
约为 80%,卡片保持旧高度,底部留出一行空白;翻到末页同理。

保留尺寸改为按输入计算,键为 preedit(含光标标记)+ 页码。键不变时仍只
增不减;换键、移动光标或翻页后从自然尺寸开始。WebView2 由 C++ 把键传给
MsimeStickyCandidateCard,D2D 用 stickyCardKey_,两端逻辑一致。
2026-09-27 14:36:48 +08:00
fanlusky 6a172d448d fix(server): D2D 候选宿主窗口不再越过当前显示器工作区
卡片本身已经被夹在工作区内,但宿主窗口还带着阴影内边距和 64px 取整余量,贴近
右缘或下缘时会伸到相邻显示器上。DWM 分别应用窗口移动和交换链呈现,而且先后不
固定,某一帧可能把新卡片配上旧的宿主原点(或反过来),落在卡片外侧的内容就闪
到了旁边的屏幕上。诊断日志显示每次放置时卡片都在屏幕内(右缘 3837 < 3840),
但宿主一直延伸到 3927 左右。

宿主的宽高现在截到工作区的右缘和下缘,不管 DWM 怎么配对,都不会在光标所在显示器
之外画出任何东西。卡片完整保留,只裁掉伸出边缘的那部分阴影。移窗与呈现的先后
排序保留,它只决定哪种中间帧更常见。
2026-09-27 09:13:15 +08:00
fanlusky dcf1b6e2d3 fix(server): D2D 候选窗隐藏时清空交换链,避免重新显示时闪出上一张卡片
Hide() 只把宿主窗口 cloak 起来,交换链里还留着最后一帧。下次显示时先呈现、再
取消 cloak,但 DWM 可能在拿到新帧之前就先合成了已取消 cloak 的窗口,于是有一帧
在新位置画出了上一张卡片。上一张卡片较宽时,新的窄卡片贴屏幕右缘,位置更靠右,
旧画面就会越过边缘落到右侧扩展屏上。删光输入后重新打字、或上屏后开始下一次输入
时最容易出现。

隐藏前先呈现一帧透明画面,交换链里不再留下旧卡片。同时在 candidate-d2d place
诊断日志里补上最终宿主位置、先移动到的位置、卡片像素矩形和显示器工作区。
2026-09-27 09:06:49 +08:00
fanlusky 3a7beb18ab fix(server): 同一次输入内候选卡片只增不减
译文、云/AI 候选、混输英文等内容是陆续到达的,每一批都让卡片在两帧之间改变
宽高。每一帧本身画得完整,但尺寸来回变化看起来就是闪烁。

同一次组合内卡片只增大;自然尺寸低于保留尺寸的 70% 时(页面确实变小了)才跟着
收回,候选窗隐藏后重新开始。D2D 给 card 设置 MinWidth/MinHeight 后重新排版;
WebView2 在更新内容的同一个 JS 任务里给 #realContainer 和 #measureContainer
设置最小宽高,绘制前就是最终尺寸,原生区域测量的也是同一个盒子。两端的收回比例
常量互相注明了对应关系。
2026-09-27 09:00:47 +08:00
fanlusky 5fc9485abf fix(server): D2D 候选窗按移动方向决定移窗与呈现的先后
DWM 分别合成窗口移动和交换链呈现,窗口可见时总有一帧只完成了其中一步。原先
总是先移窗后呈现:卡片在屏幕右缘变窄时要向右挪,上一帧更宽的画面会先出现在新
位置,越过屏幕边缘落到右侧扩展屏上,下一帧再弹回来。

现在按轴分别处理:向左或向上移动的轴先移窗再呈现,向右或向下移动的轴先呈现再
移窗,这样中间帧不会超出新旧两张卡片覆盖的范围。DPI override 提前到调整窗口
尺寸之前,因为 WM_SIZE 里已经会呈现一次。
2026-09-27 09:00:35 +08:00
fanlusky d852a05687 fix(windows,server): 选词回复超时不再上屏未显示的候选
电池模式下数字选词常上屏与所见不同的候选。链路是:TSF 只等 50ms
回复;Server 在回复前同步写调频,渲染同步 30ms 到点后按未绘制的页面
选词;超时后 TSF 断管重放按键,Server 又调一次频。

- server:选词调频(拼音/五笔/英文)改为 worker 队列任务,在回复之后、
  下一键之前执行,不再占用回复时间。
- server:渲染同步改为条件变量,绘制回显即唤醒;上限 30ms 提到 80ms。
- windows:上屏类回复等待 300ms;请求已送达仍超时时视为投递不确定,
  只重建前缀并丢弃上屏键,不再无条件重放。
- server:同一选择 3s 内在新连接上再次到达时跳过调频,避免重放重复计数。
2026-09-27 00:47:36 +08:00
fanlusky 7d66b2556c feat(server,ui,ui-html): 候选翻译文本支持在皮肤中单独配色
外部皮肤 skin.toml 的 [candidate.dark] / [candidate.light] 新增 translation 键。
不写时沿用候选文字色叠 62% 不透明度;写了就按原值绘制(含选中行)。
D2D(CandidateList::Appearance::translationColor)、WebView2 注入 CSS
和设置页外部皮肤预览三处一致;预览顺带补上此前漏掉的 number 配色。
2026-09-26 23:38:37 +08:00
fanlusky 8278c065c9 feat(server,ui-html): 新增内置皮肤秋桂,杨柳青 D2D 高亮对齐 WebView2
秋桂 Autumn osmanthus(autumn_osmanthus):暗色底 #7D929F、高亮 #F97D0A,
亮色底 #D6ECF0、高亮 #FFE399。高亮内缩于外框,首项顶部两角与尾项底部两角
和外框同为 10px;预编辑行隐藏时首项才贴顶。WebView2 候选窗与工具栏、D2D
候选窗、设置页卡片与预览一并提供。

ApplyCandidateFrame 只隐藏多余的候选行,:last-child 找不到最后一个可见行,
因此为其加上 .last-visible,杨柳青的 CSS 同步改用它,候选不满一页时尾项的
补边与贴边高亮才会生效。

杨柳青在 D2D 下改为直角、左右贴边的整行高亮,只在外框四角处用 9px 贴合,
行距、行内边距与预编辑行边距按 CSS 对齐。
2026-09-26 21:49:04 +08:00
AaGMix 902989abc4 feat(server): 显示光标输入状态提示 2026-09-22 23:52:28 +08:00
Jia Sui b149a500b0 fix(server): 按数字选词偶发选错——以屏幕上的候选列表为准
按数字 2 想选第 2 个词,有时上屏的却是第 1 个:后台按手里的最新候选列表
数序号结算,而用户看到的是窗口还没重画完的旧画面;调频(一次置顶)每次
选完都改顺序,两者的时间差就会撞出「看 2 选 1」。关掉调频或固定候选位置
后顺序不再变化,问题消失——与报障观察一致。

修法:候选列表带版本号,窗口画完后回报画到第几版;按数字或空格时如果
窗口还没画到最新,后台最多等 30 毫秒再按用户看到的那一版结算。等待上限
是硬约束:必须留在 TSF 50ms 回复窗口内,超时会触发管道重连与按键重放。
窗口不可见、宿主自绘(UILess)、鼠标点击不等待。

- 发布候选页时递增版本号,快照携带该值;
- D2D 绘制完成、WebView2 DOM 回调(含 dedup 分支)回写实际渲染快照的版本号,
  异步回调捕获快照值而非回读实时值;
- 单测覆盖等待判定与 30ms 常量;阴性验证:等待失效时「落后 → 等待」用例
  失败,恢复后 333 项测试全绿;
- 真机验证(调频 pin + 快速同拼音 + 连按数字)待跟进。
2026-09-15 11:02:39 +08:00
Jia Sui 3dea733f64 fix(ui,server): 候选窗 D2D 阴影按皮肤对齐 webview——Card 支持 pass 列表
D2D 候选窗的 Card 阴影是内置 Win11 漫射三层(σ11/6/2.8,α0.42/0.30/0.48):
四边对称、无偏移,紧贴边缘合成 α≈0.49、暗晕铺到 15~20 DIP。webview 端候选窗
皮肤 CSS 用的是带右下偏移的双层软阴影(light: 8px 10px 24px rgba(0,0,0,.18)
+ 2px 3px 8px rgba(0,0,0,.10))。用户实机截图剖面实测:webview 底边峰值 α≈0.16、
D2D 0.41,差 2.5 倍,即「D2D 阴影偏重」的根因。对称三层无论怎么调参都拟合不了
带偏移的衰减曲线;浮动工具栏已有的 SetShadowScale 只缩模糊半径压不了深度
(高斯在边缘恒贡献 α/2)。

给 Card 拆出数据驱动的阴影 API,阴影参数跟皮肤走(与颜色/圆角同模式):

- ui/include/msimeui/Layout.h、ui/src/Layout.cpp:ShadowPass{σ,α,offset} +
  SetShadowPasses(空列表保持内置 Mist 三层,逐位不变)、SetShadowEnabled
  (整体开关,语义对齐 Popup 同名 API)、SetShadowOpacity(uniform 缩 α,与
  SetShadowScale 正交);渲染统一走 DrawGaussianShadowPass,无 device context
  时兜底 DrawLayeredMistShadow
- server/src/window/candidate_presenter.cpp:ApplySkin 按皮肤设阴影——内置四皮肤
  与自定义包用皮肤 CSS 标准对(σ=blur÷2)light {12,0.18,(8,10)}+{4,0.10,(2,3)}、
  dark α .34/.22;默认皮肤 webview 端 body HTML 无 box-shadow,D2D 同步关掉
- 浮动工具栏(scale=0.45)与托盘菜单(全默认)不触新 API,观感不变
- 透明垫余量重验:新 pass 可见范围(α>0.02 阈值)左16/上14/右32/下34 DIP,
  均在左32/上20/右32/下40 垫内,无裁切切痕
2026-09-14 12:34:10 +08:00
fanlusky eadcca9f1b fix(server): 候选窗贴边后不再随内容变化上下左右乱飘
候选窗一旦贴住屏幕右边缘,删字后右边缘就会离开边缘;靠近屏幕下边缘时,它有时
正常停在当前行上方,有时却盖住当前行的文字。两者是同一个病根:最终定位用的不是
候选卡片本身,或者压根没有重新定位。

D2D:PlaceAndShow 末尾的水平夹取用的是宿主窗口宽度。宿主带透明阴影内边距、向上
取整到 64px、而且打字过程中只增不减,于是卡片被这些余量整体推离右边缘;删字后过
大的宿主宽度被保留,卡片左边缘就被冻住。改成夹取不透明卡片,和紧随其后的垂直夹取
用同一条规则,透明宿主允许越过屏幕边缘。

WebView2:内容更新只走「重绘不重排」的两条路径,它们从不重做上下翻转判定,只是
(a) 用「MarginTop -= 高度差」这个累加器去粘住底边,(b) 交给
KeepCandidateCardInsideHostAndMonitor 夹取,而后者在卡片底边越过工作区下边界时
直接把卡片往上推。于是本来在光标下方勉强放得下的候选窗,一旦下一个键多出一行释义
变高,就被径直推到当前行文字上面。现在两条路径都改为用实际绘制出来的尺寸重新推导
锚点(ReanchorCandidateHostToPaintedCard)——翻转判定只能信这个高度——两个累加器
随之删除。

另有两处一致性修补:MarginLeft/MarginTop 超出宿主可用余量时改为平移宿主而不是截断,
截断会让卡片毫无理由地吸到宿主的另一端;翻转判定的下边界改用 coordinates.bottom -
edgePadPx,与下游所有夹取共用一条线,否则会判定「下方放得下」,再被 2 DIP 边距顶
回来压到文字上。

已知未处理:翻转时的行高仍是写死的 24 DIP,因为 IPC 只传了文本范围的 left 和
bottom,行高在 GetPhysicalTextAnchor 里就被丢掉了。宿主报出的行比 24 DIP 高时,
翻转后的卡片底边仍会压进那一行——但现在是稳定的偏低,不再忽上忽下。
2026-09-13 03:07:52 +08:00
fanlusky 426bb9d544 feat(server): WebView2 候选窗口支持滚轮翻页,并加上开关
PR #351 只给 D2D 候选窗口加了滚轮翻页,WebView2 后端还没有。

网页端只上报一个以 WHEEL_DELTA 为单位的原始 delta,notch 累加仍然
交给 CandidateWheel::ConsumeWheelDelta,两个宿主最终都汇聚到
WM_PAGE_CANDIDATE,翻页逻辑一份不重复。Chromium 的 wheelDeltaY 本身
就是 ±120/格,正是原生累加器期望的单位,deltaY 作标准回退。

同时把滚轮翻页做成可配置的 general.paging_mouse_wheel,默认关闭:
候选宿主是 NOACTIVATE 且从不获得焦点,滚轮只有在 Windows「当我悬停
在非活动窗口上方时滚动它们」开启时才会送达,无法无条件承诺,所以设为
opt-in,设置页里也写明了这个前提。开关只在 WM_PAGE_CANDIDATE 一处
判断;D2D 那边在 WM_MOUSEWHEEL 顶部提前返回 false,关闭时消息照旧
交给 DefWindowProc,与功能加入前一致。
2026-09-13 01:02:02 +08:00
fanluskyandCarry 63eab2f88f refactor(server): 滚轮翻页复用键盘翻页逻辑并移出按键热路径
在 #351 的基础上收敛几处实现:

- CloseContextMenu 里新增的 hover 清理和 Present 原本无条件执行,而
  ShowFromGlobalState 和 Hide 每次按键都会调它。没有菜单时按键路径上会多出
  一次整屏重绘,画的还是更新前的旧场景。改为只在确实有 popup 要销毁时才做。
- 分页与动态扩充的逻辑从键盘路径复制了一份。抽成
  ExpandCandidatesKeepingPagePosition 和 MoveCandidatePage,键盘与滚轮共用。
- 每一格滚轮直接投一个任务,而每个任务都可能回引擎和用户词库。改为在队列里
  合并成 page_steps,一次快速滚动只重绘一次。
- 窗口层不再直接调 EnqueueCandidateUiAction,改为与其它候选窗动作一致,
  PostMessage 到宿主 WndProc(WM_PAGE_CANDIDATE)。EnqueueCandidateUiAction
  的序号校验随之恢复原样。
- 滚轮累加拆成纯函数 CandidateWheel::ConsumeWheelDelta 并补上单元测试;
  候选窗隐藏时忽略迟到的滚轮消息。

滚轮翻页仍然把页内高亮重置为首项,键盘翻页仍然保留原有的页内索引。

Co-authored-by: Carry <xu762556@outlook.com>
2026-09-13 00:16:18 +08:00
Carry 0fb8001022 feat(server): support mouse wheel paging on candidate window (fixes #81) 2026-09-12 19:46:24 +08:00
fanlusky a7779d2b05 refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。

Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。

引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。

CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。

README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00
Jia Sui 753312f32c fix(server): RDP 会话中候选窗缩放跟随宿主应用实际渲染比例 2026-09-11 22:21:57 +08:00
Jia Sui bd7cf1114d fix(server): D2D 内置皮肤补齐 light 配色,对齐 WebView2 CSS
Signed-off-by: Jia Sui <jsfaint@gmail.com>
2026-09-11 22:19:24 +08:00
Jia Sui 1ae63ad13f fix(server): D2D 候选窗明暗主题跟随设定与系统切换实时刷新
ApplySkin 的指纹缓存只含 theme_cand 原始值,follow 之下改 theme_mode
或系统明暗切换时指纹不变,候选窗停留在首次渲染的暗色 token。
把 ResolveConfiguredTheme 的解析结果并入指纹,让明暗变化穿透缓存。

同时补上系统明暗切换的实时刷新路径:WM_SETTINGCHANGE(ImmersiveColorSet)
时候选窗可见则重绘、常驻的悬浮工具栏即时 ApplyTheme;托盘菜单每次
打开已先 ApplyTheme,无需处理。
2026-09-11 22:19:24 +08:00
fanlusky 093b485124 Merge branch 'develop' into fix/issue-242-settings-cannot-save
冲突解决:

- windows/src/Utils/FanyUtils.h:develop 通过新增的 .gitattributes 把整个文件
  从 CRLF 规范化成 LF,与本分支删除 GetIMEDataDirPath() 声明的改动整文件冲突。
  取 develop 的 LF 版本,再删掉该声明。
- server/tests/src/test_config_non_ascii_path.cpp:仅 include 块冲突,取两侧并集
  (algorithm/filesystem/fstream/iterator/string/system_error)。另修正一处错误的
  自动合并——develop 新增的候选回退字体断言本属于
  config_round_trips_under_non_ascii_profile_path,git 却按上下文并进了本分支新增的
  config_migrates_legacy_acp_mangled_path,已移回原测试。
2026-09-10 19:33:43 +08:00
Neptrue_Lin ff7650bba2 Merge pull request #293 from Aegon8719/feat/issue-55-fallback-fonts
issue-55 候选窗支持可配置的有序回落字体
2026-09-10 17:14:29 +08:00
Aegon8719 dea9292814 issue-55 候选窗支持可配置的有序回落字体 2026-09-09 18:03:56 +08:00
Aegon8719 848cf6a288 issue-288 修复候选窗在光标坐标无效时提前显示的问题 2026-09-09 11:40:26 +08:00
linym 075fbb2a84 fix(server): persist settings after leftover non-ASCII config paths
Elevated setup left high-IL config.toml that medium-IL Settings never
rewrote on machines hit by the ANSI path bug. Make the data directory
writable, migrate ACP-mangled leftovers, rebuild unparseable files, and
keep remaining asset paths on wchar_t.

Fixes #242
2026-09-08 22:42:26 +08:00
Ouchihao 3fc7ba295f feat(server): show translations in horizontal candidate windows 2026-09-06 18:09:01 +08:00
Evan 42238a2af6 style: 用 .clang-format 重排 server / windows / ui / log
一次性重排,无任何行为改动。四个目录各自的 .clang-format 放在仓库里已久,但全组织只有
MSIME-Linux 在 CI 里执行 clang-format 检查,这边 149 个文件里有 149 个不符合自己声明的风格。

已逐文件核对语义等价:去掉空白、注释与相邻字符串字面量拆分之后,全部 149 个文件与重排前逐字节
相同。差异只有三类——缩进与换行、clang-format 补的 } // namespace 结尾注释、以及长字符串被拆成
相邻字面量(C++ 保证自动拼接)。

这个提交会写进 .git-blame-ignore-revs。
2026-09-06 12:13:47 +09:00
Evan 2562026a2a fix: 修复审计发现的候选页竞态、管道权限、TSF 空指针与皮肤 CSS 逃逸等八项缺陷 (#169)
* fix(server): publish candidate pages as immutable snapshots

Global::candidate_ui is a plain global with no lock. The IPC worker rebuilds and destroys its vectors while the UI thread paints from them, and the handoff is an asynchronous PostMessage, so the worker returns to its loop and pops the next task without waiting. A second keystroke, or an async english.db candidate arrival, could free the page_views buffer and its strings while FillItemsFromUi held a reference into it. ShowItemContextMenu reads the same vectors from a mouse callback with no message handoff at all.

The worker now copies each finished page into an immutable snapshot and publishes it at the single point where the rebuild completes; every UI-thread reader loads that snapshot once and reads only its own copy. PostMessage is deliberately left alone: the worker also serves UI-originated tasks, so a synchronous handoff would invite a cross-thread deadlock.

* fix(server): bound the aux and diagnostic pipe reads

Both listeners serve a single pipe instance inline and polled a NOWAIT handle for as long as the process ran, breaking only on a read that succeeded or failed with something other than ERROR_NO_DATA. A client that connected and never wrote held the only instance forever, so the loop never returned to ConnectNamedPipe and no later client was accepted, permanently losing the langbar right-click channel and the terminal-deactivation teardown fallback.

Give both loops the same deadline the hello reads already use and go back to accepting when it expires.

* fix(server): snapshot the voice config and replace config.toml atomically

GetConfiguredVoiceInput handed out a reference to a global that LoadImeConfig rewrites from the IPC worker thread, clearing and refilling two std::map members and around twenty strings. The voice control thread and the low-level keyboard hook, which copies the struct on every keystroke, walked those nodes as they were freed. LoadImeConfig now parses into a local and publishes it in one step under a process-local shared_mutex, and the getter returns a snapshot by value. The file lock is deliberately not reused for readers: it is a named cross-process mutex with a five second wait, and taking it in a keyboard hook would stall typing system-wide.

WriteConfiguredValue also reopened the live config.toml with ios::trunc, so a crash mid-write destroyed the user settings and could leave the file unparseable for good. It now goes through WriteFileTextAtomically, which this file already uses elsewhere. That makes the directory entry swap atomic; it does not flush, so it is not a durability guarantee.

* fix(ipc): restrict the server pipes to the owning account

The pipe DACL granted Everyone FILE_ALL_ACCESS. That bit set includes FILE_CREATE_PIPE_INSTANCE, so any local account could add an instance to a name this server already owned and answer part of the connections in its place. The names are machine-global with no per-user component, and the TSF client opened them with no SQOS, so a squatter could also impersonate the connecting thread.

Build the descriptor from the process owner SID and admit only LocalSystem, that account, and app containers with connect-only rights so UWP text input still works; fail pipe creation outright when the SID cannot be resolved rather than falling back to something permissive. Pass FILE_FLAG_FIRST_PIPE_INSTANCE on the first instance of each name so a squatted name fails loudly, and request SECURITY_IDENTIFICATION on the client side.

Authenticating the peer process after connect is a larger change to the handshake and is not attempted here.

* fix(tsf): guard the null focus paths and unadvise the compartment sink

_CandidateChangeNotification treated ITfThreadMgr::GetFocus and ITfDocumentMgr::GetTop as if they could only fail through a FAILED HRESULT. Both return S_OK with a null out-pointer when nothing has focus or the document stack is empty, and the results were dereferenced unconditionally, so an external UILess host calling Finalize while focus moved away crashed the host application. The sibling _RequestCancelComposition in the same class already checks both.

CLangBarItemButton::CleanUp only unregistered the compartment inside the SUCCEEDED branch of a CoCreateInstance, then deleted the sink regardless. This TIP registers COM-less activation support, where that call does fail, and the sink was freed while TSF still held it advised. Unadvising never needed the thread manager, so it now runs unconditionally before the deletes.

* fix(ui): drop the cached visuals when the scene is replaced

Window caches focused, hovered and captured Visual pointers into the scene tree, and SetScene destroyed that tree without clearing them. Toggling the input mode rebuilds the floating toolbar scene, so the next mouse move dispatched a virtual call through a freed object.

Clear all three before the assignment, and release the capture only when this window actually holds it: an adopted HWND may never have called SetCapture, and capturedVisual_ is set even when it did not.

* fix(settings): scope external skin CSS through the CSS parser

injectScopedSkinCss concatenated a skin toolbar.css into an @scope block as text. Block nesting is textual, so a leading close brace in the skin ended the block early and the rest styled the whole settings window. The host validates only the skin file name, never the body, and candidatePreviewCss interpolated unvalidated colour strings into rule bodies the same way.

Create the scope rule empty and re-insert every rule parsed out of the skin as a child of it, so nothing lands outside however the braces are balanced, and accept a colour only in the plain notations we actually render.

* fix(release): gate the dictionary source commit on its default branch

verify_published iterated only data["repositories"], but the dictionary block pins a source_commit exactly like they do. The manifest check merely proves the release agrees with the lock about that commit, which a branch commit satisfies just as happily, so a dictionary built from an unmerged branch shipped with the provenance the gate exists to refuse.

Run the dictionary through the same loop under its own name, resolving its repository from the lock. A retired dictionary repository that no longer answers is reported rather than failing the release: its published artifacts are immutable history, and being unable to re-check them is not the same as finding them unmerged.
2026-09-06 10:31:48 +09:00
Evan Hu 6783ff7175 refactor(session): use shared composition and local queries with one candidate view model 2026-09-06 00:26:38 +09:00
fanlusky 895abeee08 Optimize the width expansion logic of the vertical window so that it does not leave large white spaces. 2026-09-02 19:31:44 +08:00
fanlusky 9f4c8efad7 Reuse the candidate D2D scene and keep the host size stable while typing
ApplySkin was rebuilding the card/scene on every keystroke and
SetWindowPos resized the HWND by a few pixels each time, which forced
DXGI ResizeBuffers and looked like a leak. Skip skin work when the
fingerprint is unchanged, update the existing Card in place, and only
grow the host in 64px buckets. Point msimeui at the emoji-fallback and
swapchain fixes.
2026-09-02 16:26:14 +08:00
fanlusky 5c02503139 Render the candidate window with D2D instead of WebView2
Drive the existing candidate HWND through CandidatePresenter and msimeui,
keep WebView2 for the menu, floating toolbar, and settings, and load
external skins from skin.toml color tokens. Restore cloud/AI badges on
D2D items, and keep click, pin, fix-position, and delete on the same
window messages as before.
2026-09-02 15:30:03 +08:00