176 Commits
Author SHA1 Message Date
一把伞南星andfanlusky 71960b8301 feat(server,ui): 悬浮工具栏图片模式与装饰图叠加候选词 (#1046)
* feat(server,ui): 悬浮工具栏图片模式与装饰图叠加候选词

在 #1046 的基础上按最新 develop 重做。

悬浮工具栏 [toolbar] mode = "image":一整张背景图,按钮按
[toolbar.buttons.<按钮>] 的 x_dip / y_dip / width_dip / height_dip 摆放,
可按状态换图([images]),没图时画矢量图标。窗口按背景图的透明区域裁成
异形,透明处点得穿;图上除按钮外都能拖动。v2 皮肤的动图会播放,工具栏
隐藏或淡出后停帧。

候选窗装饰图可用 candidate_overlay_* 叠加一个候选词,太宽时缩字号;
设置页的皮肤预览一并显示。

ui 新增 OverlayPanel(绝对定位)与 ImageMask(按 alpha 生成区域)。

* fix(ui): 动图工具栏的窗口形状取所有帧的并集

窗口区域连画面一起裁,只按首帧裁形状时,动画里伸出首帧轮廓的部分
(抬头、飞出的纸)会被裁掉。改为把所有帧的 alpha 合起来再生成区域。

* fix(server): 工具栏按钮框始终算在异形窗口里

窗口形状只取背景图不透明的部分时,摆在图片透明处的按钮会被一起裁掉、
点不到。形状改为背景图轮廓并上每个按钮的框,按钮可以放在人物旁边的空白处,
不必在背景图里垫底。

* feat(server): 工具栏按钮可单独设内置图标的字号

[toolbar.buttons.<按钮>] 新增 icon_size_dip,内置 Segoe Fluent Icons 图标的
字号不再只能跟着按钮框走:按钮框铺满背景图里的圆底、图标单独调大小。
不写时仍是按钮框短边的 0.82 倍。

* feat(server): 设置页按图片模式预览悬浮工具栏

皮肤页的卡片与悬浮工具栏页的预览原来只会画矩形条。皮肤是图片模式时,
改按工具栏的画法预览:背景图(v2 皮肤的动图会播放)、按皮肤坐标摆放的
按钮、按钮贴图或内置图标(含 icon_size_dip),设置页关掉的按钮不画,
缩放与图标尺寸设置一并生效。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-20 23:08:01 +08:00
Lu Fan(SonnyCalcr) 6e15526e75 perf(server,ui,engine): 降低 Server 常驻内存并加快候选去重 (#1078)
- ui: 同一线程上的窗口共用一个 D3D11 设备。候选窗、悬浮工具栏、托盘菜单原先各建一个,
  每多一个硬件设备约多占 10 MB 私有内存;设备被移除时下一次取用会换新的。
- engine: U 模式拆字表改为单块文本池加 16 字节定长条目,常驻内存从约 12.6 MB 降到约 3.9 MB,
  查询结果不变。
- server: 未开启语音输入时,提示音播放器推迟到第一次播放才打开 miniaudio 引擎,
  不再常驻 WASAPI 播放设备和音频线程;开启时行为不变。
- engine: append_unique_words 由逐条线性比对改为哈希集合去重,顺序与结果不变。
2026-10-19 00:55:46 +08:00
Lu Fan(SonnyCalcr) e701cebbe4 feat(server,ui,installer): 内置皮肤改由皮肤清单定义,新增微信绿(卷轴)内置皮肤 (#1061)
* feat(ui): CandidateList 支持等宽网格列与关闭选中态

- Appearance::gridColumns:横排时按可用宽度均分成等宽列(列数不超过 gridColumns,每列不窄于
  minItemWidth),放不下一列的项占多列。
- Appearance::selectable:为 false 时不画任何选中态,首项也能正常显示悬停与按下效果。

* feat(server,installer): 内置皮肤改由皮肤清单定义,新增微信绿(卷轴)内置皮肤

内置皮肤数据化:
- skins/default/<id>/skin.toml 从只放开关改为完整的出厂清单(配色、圆角、版式、工具栏、名称与说明),
  并新增 canvas 基底的清单。候选窗、悬浮工具栏、光标状态徽标不再在代码里写死任何内置皮肤的参数。
- 出厂清单在构建时编进程序;数据目录里的清单写了的键覆盖出厂值,缺失或写错时退回出厂外观。
  安装包改为每次整份覆盖 skins\default。
- 皮肤清单新增版式键(padding、item_gap、number_inset 等,长度可写 DIP 或 "0.3em"),以及
  [candidate_window.horizontal] / [candidate_window.vertical] 分排列覆盖、page_arrow_divider、
  toolbar.icon_corner_radius_dip。
- test_candidate_skin_tokens 保留改动前的内置分支作参照,逐个皮肤、主题、排列与字号比对清单解析结果。

共用候选窗场景:
- 候选框的场景搭建与套皮肤抽成 CandidateSkinScene,候选窗与设置页的皮肤预览共用;设置页预览改用
  真实的候选窗控件绘制,删除原先移植自网页样式的预览绘制与各皮肤硬编码。

卷轴展开(expand_panel)与微信绿(卷轴):
- 皮肤开关 expand_panel / expand_rows:横排候选行右端的展开按钮,展开后首行与后续候选网格一起滚动,
  右侧细滚动条;展开与收起只认按钮,展开时滚轮上下滚动、不翻页。
- 网格点选按绝对下标上屏并在工作线程核对显示文字;滚到底部时自动再展开一批候选。
- 新增内置皮肤 wechat_scroll「微信绿(卷轴)」。
2026-10-18 20:42:06 +08:00
Lu Fan(SonnyCalcr) e9825a521f feat(server): 优化设置窗口滚动手感并加入启动画面 (#1045)
* feat(ui): 滚轮平滑滚动贴近浏览器手感

- 每次滚轮的平滑滚动改为 cubic ease-out,接续当前速度,不再有指数逼近的长尾
- 滚动距离跟随系统「一次滚动的行数」,「一次滚动一屏」时按整页滚动
- 合成交换链的帧时间对齐到 DWM 的刷新网格,消除绘制开始时刻抖动带来的位移抖动

* fix(server): 设置页滚动到 Niya Demo 皮肤时不再卡顿

Niya Demo 的 background.png 实际是 1280x720 的 WebP,预览第一次滚入视野时在界面线程同步解码,首次加载 WebP 解码器要 100-200ms。皮肤预览的静态图改为打开页面时就在后台线程解码,界面线程只创建位图。

设置页与词库表格的滚轮步长同时调到 100(与 Chrome 一致),平滑时长 260ms。

* feat(server): 设置窗口加入启动画面与开关

打开设置时先在窗口中央显示齿轮图标(Win11 用 Segoe Fluent Icons U+E713,Win10 回退 Segoe MDL2 Assets),等首个 configSnapshot 填好控件后再淡入主界面,避免输入方案先显示全拼再跳到双拼。齿轮至少停留 200ms,淡出 140ms。

「关于」页新增「启动动画」开关(appearance.settings_launch_animation,默认开启),关闭后只短暂显示纯色背景。新键已写入出厂模板和开发配置,并补了默认值与读写往返测试。
2026-10-18 00:27:50 +08:00
Lu Fan(SonnyCalcr) a98acfee24 feat: 候选窗支持系统亚克力与云母背景材质 (#1033)
皮肤在 candidate_window.backdrop 写 acrylic 或 mica 时,候选卡片圆角范围内垫一层系统材质:
acrylic 模糊后面的窗口,mica 模糊桌面壁纸(Windows 11,更早的系统按 acrylic)。材质按
Fluent 配方叠「模糊背景 → 亮度层 → 色调」,色调取 surface,深浅主题各自用
backdrop_opacity / backdrop_luminosity 调色调与亮度层的不透明度。

msimeui 的 DeviceResources 新增 backdrop composition:经 Windows.UI.Composition 把 D2D
画进 CompositionDrawingSurface,与材质的形状在同一次提交里更新,卡片伸缩时边缘不错位;
交换链 Present 与合成提交分属两条路径,实测会错开。没写 backdrop 的皮肤仍走交换链。
有材质时卡片阴影只画在卡片外,ShadowCache 改按 D2D 设备判断缓存是否可用。

系统关闭透明效果或建不起 Windows.UI.Composition 时退回交换链,surface 按不透明画。
附 acrylic-demo、mica-demo 两个样例皮肤。
2026-10-17 16:02:38 +08:00
Lu Fan(SonnyCalcr) 9c45526cd2 fix: 缓存的皮肤图片不再一直占着文件 (#1017)
* fix(ui): 缓存的位图不再占着图片文件

GetBitmapFromFile 直接用 WIC 解码器建 D2D 位图,位图会一直引用解码器,缓存期间文件句柄不放:输入法运行时皮肤图删不掉、覆盖不了。改为先把像素解进内存里的 IWICBitmap 再建 D2D 位图,文件随解码器一起释放。补一条回归用例。

* fix(server): 设置页皮肤预览不再占着皮肤图文件

预览的位图缓存与 msimeui 同样直接用解码器建 D2D 位图,设置窗口开着时皮肤图同样被占用;改为经内存里的 IWICBitmap 中转。
2026-10-17 04:02:21 +08:00
Lu Fan(SonnyCalcr) cc868151a8 feat: 新增 canvas 基底,D2D 候选窗支持 v2 九宫格、挂件分层与 APNG/GIF 动图 (#1014)
* feat(ui): 新增 APNG/GIF 动图解码与动画帧时钟

AnimatedImage 按 APNG 规范解析 acTL/fcTL/fdAT 并校验序号、CRC 与帧边界,逐帧重建 PNG 交给 WIC 解码后按 dispose_op/blend_op 合成;GIF 走 WIC 多帧解码。延时规则与浏览器一致,超限或损坏的动画退回静态首帧。

DeviceResources::GetImageFrame 按动画时钟取帧并汇报下一次换帧时刻,动图帧按 64 MB 预算缓存;Scene 与 Window 据此按需重绘,可整体关闭动画。DrawNineSlice 按 CSS border-image 语义区分源切片与目标边宽、支持 fill,并用夹取画刷消除接缝渗色。

* feat(server): 新增 canvas 基础皮肤并要求 v2 皮肤基于它

canvas 是全贴图基底:透明面板、无描边、无圆角、无阴影,外观来自九宫格面板图与挂件。它只能作为外部皮肤的 base,不进内置皮肤列表;v1 皮肤用它直接报错。

schema_version = 2 而 base 不是 canvas 的皮肤按 v1 加载、忽略全部 v2 字段,并在扫描结果里记一条问题。v2 字段补齐校验:图片限 .png/.apng/.gif/.webp,fill 必须是布尔,偏移与 z_index 有范围,挂件最多 32 层。

* feat(server): D2D 候选窗补齐 v2 九宫格、挂件分层与动图

canvas 皮肤在 D2D 候选窗里按 CSS border-image 语义绘制九宫格面板:切片以源图像素计、边宽以 DIP 计、fill = false 不画中心;有面板图时卡片的底色、描边、圆角与阴影让给贴图。content_insets_dip 从面板外缘量起且不小于边框宽度,line_gap_dip 只作用于预编辑行与候选之间。

挂件拆成卡片前后两层:z_index < 1 在面板之下,其余在面板之上,同层按 z_index 与声明顺序稳定排序;图片在框内等比居中。动图按场景给出的下一次换帧时刻排一次性定时器续帧,只在候选窗显示时运行,隐藏、换皮肤时停;系统关闭动画效果时只显示首帧,v1 皮肤保持静态。

设置页原生预览认得 canvas 基底并绘制面板图。

* feat(examples): 新增基于 canvas 的 v2 全贴图皮肤样例

canvas-demo 用 2 倍九宫格面板、面板下层的叶子挂件和面板上层循环播放的 APNG 星光演示 v2 皮肤,素材均为程序生成的几何图形,随目录以 MIT 授权。样例测试同步校验 schema 2 与 canvas 互为前提、v2 图片扩展名与数值范围。

* docs(examples): 说明 canvas 基底、v2 九宫格与挂件分层及动图规则

schema 说明去掉「仅 WebView2」,写明 v2 必须以 canvas 为 base、canvas 只能当 base;九宫格切片按源图像素、边宽按 DIP,content_insets_dip 不小于边宽,line_gap_dip 只管预编辑行与候选之间;挂件以 z_index 1 为界分在面板上下两层;动图格式、帧时长、上限与系统减少动画时的行为。server/docs/ui-backends.md 与 skins/README.md 同步更新。

* docs(server): 候选窗与皮肤目录的注释不再引用已删除的 WebView2 页面
2026-10-17 02:57:33 +08:00
Lu Fan(SonnyCalcr) 1b875267db refactor: 移除 WebView2,界面只保留 Direct2D (#1013)
* refactor(engine): 语音独立工具改用原生托盘菜单,移除 WebView2

托盘菜单改用 Win32 弹出菜单,设置项改为用关联编辑器打开 config.toml,关于页改为消息框。删除 window_webview2 与两份 HTML 资源,windows-app 特性不再依赖 webview2 端口。

* refactor(server): 移除 WebView2,候选窗、工具栏、托盘菜单与设置窗口只保留 Direct2D

删除 src/webview2 与 webview_utils,小窗口的置顶、皮肤重载和工具栏状态缓存移到 window/small_windows,presenter 成为唯一路径,D2D 下的置顶时序与 cloak 行为保持不变。设置进程去掉 WebView2 宿主、鼠标转发与启动图,只构建原生窗口;旧快捷方式传入的 --d2d / --webview2 被忽略。删除 appearance.ui_backend 与 appearance.settings_ui_backend(读取、写入、设置页下拉框与两份配置模板),升级时由模板合并丢弃旧值。设置窗口的图片资源移到 server/assets/settings,安装到数据目录的 assets\settings。不再链接 WebView2Loader,vcpkg 去掉 webview2 端口。

* build(installer): 打包与 CI 不再处理 WebView2 页面和运行时

Prepare-PackageFiles 去掉 -UiHtmlDirectory 与 html 暂存,改为暂存 server/assets/settings;安装器不再检查 WebView2 Runtime、不再创建 ProgramData 下的 WebView2 用户数据目录,升级和卸载时清理旧版留下的 webview2 / webview2-settings 与数据目录里的 html。CI 删除设置页构建、契约同步检查和候选窗 DOM 测试,release 的打包任务不再依赖网页产物;dependabot 去掉 npm。第三方声明去掉 WebView2Loader。

* refactor(ui-html): 删除 WebView2 页面与设置页前端工程

候选窗、工具栏、托盘菜单、设置页、主题和契约副本都已没有使用者;设置窗口用到的图片资源已移到 server/assets/settings。sync-contracts.py 与 pnpm/Vite/TypeScript 工程一并删除,契约仍以 engine/contracts/webview 为唯一来源。

* docs(server): 文档改为只描述 Direct2D 界面

仓库地图去掉 ui-html,组件边界、uiAccess 与置顶时序、设置窗口和配置读写的说明改写为 Direct2D 唯一界面;安装说明去掉 WebView2 Runtime 检查,打包命令去掉 -UiHtmlDirectory;engine/contracts/webview 的说明改为设置窗口是唯一使用者,名字是历史遗留。

* docs(skins): 皮肤说明不再区分 WebView2 与 D2D 渲染器

v2 字段去掉「仅 WebView2」,改为说明由 Direct2D 原生渲染、部分字段仍在补全;颜色必须带 # 改为清单格式约定。随包的 skins/niya-demo/skin.toml 不改,免得用户机器上的同名皮肤在升级时被当成已修改而改名备份。

* docs(ui): demo 说明文字不再提 WebView2
2026-10-17 02:48:02 +08:00
Lu Fan(SonnyCalcr) ef28cf690d fix(server,ui,ui-html): 打磨 Direct2D 设置窗口:图标、检查更新、热力图、滚动与冷启动 (#1001)
* fix(ui): SVG 按整像素栅格化、滚动改为跟手的指数滑动,并可预热 D3D 设备

- GetSvgBitmap 按四舍五入的像素尺寸建位图,避免 D2D 向上取整后被挤压采样
- ScrollViewer 平滑滚动改为按帧时间逼近目标:连续滚轮不再每格从零速重起,
  内容按整设备像素排布,滑动中文字不再抖动
- 新增 DeviceResources::PrewarmComposition,在后台线程提前创建 D3D11 设备,
  首帧复用,省去约 200ms 的驱动初始化

* fix(server): 修复 Direct2D 设置窗口的图标、检查更新与热力图交互,加快冷启动

- 侧栏等 SVG 图标对齐设备像素绘制,不再发糊
- 关于页「检查更新」:页面持有状态,点击不再无反应;按钮加悬停描边与按下位移;
  后台任务结果改为投递消息回窗口,窗口被遮挡时不再因 WM_TIMER 被压制而卡在「正在检查…」
- 弹窗按显式宽度测量内容,换行文字不再与下一行重叠
- 以 --about 打开时侧栏指示条落在「关于」
- 日历热力图可横向滚动(拖动滑块、点击轨道、Shift+滚轮),滚动条与竖向一致:
  8px、无箭头、2px 圆角;悬停格子 500ms 后显示日期与字数
- 滚轮每格 60px;暗色主题首帧前不再闪白(按主题色擦底并在首帧前隐藏窗口)
- 启动时预热 D3D 设备,首帧从约 300ms 降到约 140ms

* fix(ui-html): 热力图滚动条与页面竖向滚动条一致,检查更新按钮加悬停与按下反馈

与 Direct2D 设置窗口的同步改动保持一致。
2026-10-16 18:51:21 +08:00
Lu Fan(SonnyCalcr) 53649d9d12 feat(server,ui,ui-html): 新增实验性的 Direct2D 设置窗口 (#999)
* feat(ui): 动画时钟、渲染剔除与文本框扩展,并优化 msimeui 性能

- 新增 Animation.h:cubic-bezier 缓动、Tween、颜色插值,Window 提供
  RequestAnimationFrame / GetFrameTime,动画帧随显示刷新率推进
- Window 可选 DirectComposition 渲染(含不透明模式以支持 ClearType)、
  双击按下、捕获丢失时结束按压、悬停刷新、可关闭销毁时退出消息循环
- Scene 把滚轮交给光标下最内层可滚动元素并逐级冒泡
- DeviceResources:裁剪栈驱动的离屏剔除、按帧回收的笔刷缓存、SVG 栅格缓存
- ScrollViewer 可配置外观与平滑滚动;Popup 支持阴影通道与出现动画;
  卡片阴影缓存抽成可复用的 ShadowCache
- TextBox 支持 SetText、密码掩码、多行滚动、字重/字间距/行高与外观设置
- 调试日志默认关闭(MSIMEUI_DEBUG_LOG=1 才写文件),不再在每次点击和
  TSF GetTextExt 时打开文件
- 字体回退在 CJK/emoji 映射之后保留系统回退,缺字不再显示方框
- 新增动画与剔除单元测试

* feat(server): 新增实验性的 Direct2D 设置窗口

设置窗口可通过 appearance.settings_ui_backend 选择渲染方式:默认 webview2,
d2d 为实验性的 msimeui 原生实现(server/src/settings/native/),1:1 复刻
网页设置页,包括侧栏切换时竖向高亮条的滑动动画。

- 两个前端共用同一套 webview 契约消息与 HandleClientMessage,配置读写
  映射仍只在 settings_app.cpp
- 切换渲染方式后设置进程自动以新方式重新打开;--d2d / --webview2 可临时指定
- 新配置项写入出厂模板与开发配置,UiBackendPolicy 与测试同步更新
- METASEQUOIA_IME_SETTINGS_INSTANCE 允许开发版与已安装的设置窗口并存
- 更新 ui-backends 文档与 server/AGENTS.md

* feat(ui-html): 外观页新增设置窗口渲染方式选项
2026-10-16 16:13:13 +08:00
時崎 ギャラクシー 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 2ab73b4816 fix(ui): snap native-size images in device space and crop unstretched images
- Account for the render target transform when snapping, so images drawn under
  translated or scaled content (TextBox, Menu, EmojiPanel) still land on the
  device pixel grid; skip snapping for non-axis-aligned transforms or invalid DPI.
- Move the clip together with the snapped image so None/UniformToFill no longer
  shave half a pixel off an edge.
- With ImageStretch::None, crop to the bounds via a source rect instead of
  squeezing the whole bitmap in.
- Make the test fixture exception-safe, tolerate an existing MTA apartment, give
  the test image an opaque border, and cover transforms, clipping and cropping.
2026-10-08 15:14:27 +08:00
Ouchihao 7e6e568b07 fix(ui): align native-size images to physical pixels 2026-10-07 15:04:00 +08:00
fanlusky d256385264 perf(ui): 候选文字布局按内容跨帧复用
原来每次 Measure 都清空按项缓存的 DirectWrite 布局,每帧为每个候选重建全部
布局。改为按文字、字号与排版框缓存:移动选中、后补的翻译或云候选重绘同一批
字符串时直接命中,下一键的页面也常在别的下标复用上一页的字。字体或外观变化
时整体清空,条目数有上限。
2026-10-05 13:02:46 +08:00
fanlusky fef743e0b1 perf(ui): 卡片阴影只模糊一次,按九宫格拉伸到任意尺寸
阴影缓存按卡片精确尺寸作键,打字时卡片几乎每键都变大小,每帧都要重建两遍
高斯模糊,约占候选帧一半的开销。离四角足够远处模糊后的边缘沿边不变,所以
只为刚好够到这段直边的尺寸模糊一次,绘制时切成九块、拉伸中间的条带;切口
落在直边内部,逐像素平铺无缝。

新增像素对比测试:4 种 DPI、2 种圆角、6 种尺寸下与逐帧完整模糊最大相差
7/255(原缓存为 6/255),切片接缝会差到阴影本身的 alpha。
2026-10-05 13:02:46 +08:00
fanlusky e2bdeb418b perf(ui): 位图缓存加 LRU 上限
GetBitmapFromFile 的缓存从不淘汰,换过的皮肤图片按原始分辨率解码后一直留到设备
重建。调用方每帧按路径重新取、不持有指针,超出 4 张时丢掉最久未用的即可。
2026-09-30 19:58:18 +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 9f5572c525 fix(build): 所有 MSVC 目标显式按 UTF-8 编译源文件
#548 修了 ngram,同类问题还有几处:源文件或头文件是无 BOM 的 UTF-8,
目标却没有 /utf-8,在代码页 936 的系统上注释会吞掉换行。

- ui:Controls.h 让 msimeui 与各 demo 单独构建失败,/utf-8 设为 PUBLIC
- server/tests/portable:input_key_policy.h 让 MSVC 构建失败
- engine test_windows_ipc_contract、tests/pipe-probe、windows/tests:C4819
- windows DLL:此前只靠 fmt 的 INTERFACE 选项带来 /utf-8,改为显式设置,
  否则 L"“" 等宽字符标点会在去掉 fmt 后静默变成乱码
2026-09-30 15:37:13 +08:00
fanlusky 806a4b1977 feat(ui): 新增 PagerArrows 翻页箭头控件
两个用线条绘制的「上一页 / 下一页」小按钮,不依赖字体;支持置灰、悬停与按下态,
按钮贴在槽位右下角,并提供 GlyphWidth 供调用方对齐箭头。
2026-09-30 12:42:14 +08:00
fanlusky 19bd5deae0 feat(ui): 候选列表支持行底内边距与横排行铺满
contentPadBottom:换到第二行的翻译和辅助码从行底内边距上方开始排,内边距挪到最后一行下面,行高不变。
justifyHorizontalRows:横排时把列表多出的宽度均分给同一行各项,末项贴着列表右边。
2026-09-30 01:33:47 +08:00
fanlusky 07c9dea9f9 feat(ui): 候选选中行翻译色与 TextBlock 背景、底部分隔线
CandidateList::Appearance 新增 rowTranslationSelected,选中行的翻译可以单独配色。TextBlock 新增 SetBackground 和 SetBottomRule,底部分隔线计入测量高度;高度折叠到分隔线以内时两者都不绘制。
2026-09-29 21:57:23 +08:00
fanlusky 0f627ef41d feat(ui): Card 支持按圆角裁剪的背景图
Card::SetBackgroundImage 在底色之上、边框和子元素之下绘制图片,支持 Fill / Uniform / UniformToFill 与不透明度。ImageStretch 从 Controls.h 移到 Layout.h,供 Card 和 Image 共用。
2026-09-28 19:36:37 +08:00
fanlusky c3bfa69a29 refactor(ui): 按控件拆分 Controls.cpp
Controls.cpp 有 4200 行,Image、Button、CheckBox、ProgressBar、Slider、Separator、
Popup 及其宿主、菜单行、ComboBox、ListView、CandidateList、TreeView、TabControl、
Accordion 的实现,连同它们共用的文字测量与绘制工具全挤在一个文件里。沿用
TextBox.cpp 的做法,按控件拆成以控件命名的翻译单元:

- Image.cpp:Image(含只它用到的 WIC 读图尺寸)
- Button.cpp:Button 与 CheckBox(含只 Button 用到的 DrawTextBlock)
- RangeControls.cpp:ProgressBar 与 Slider
- Separator.cpp:Separator
- Popup.cpp:Popup(含 LayoutOverlay)、PopupHost、ContextMenuHost,以及只有
  Popup 用到的 DeflateSizeLocal / DeflateRectLocal 和 DrawPopupShadow
- Menu.cpp:MenuFlyoutItem 与 MenuSeparator(含托盘菜单字号常量和
  EstimateTrayLabelWidth)
- ComboBox.cpp:ComboBox 与下拉列表 ComboBoxPopupContent
- ListView.cpp:ListView
- CandidateList.cpp:CandidateList 及其文字度量缓存(含 FillRoundedRectCorners
  和彩色字体裁剪选项)
- TreeView.cpp、TabControl.cpp、Accordion.cpp:各自的控件
- ControlsInternal.h / ControlsInternal.cpp:多个控件共用的 GetSharedDWriteFactory、
  PointInRect、PointInRoundedRect、MeasureText、CreateCachedTextLayout、
  FillRoundedRect、DrawLabel 和两个共用圆角常量,只给 src/ 下的实现文件包含

纯搬移,不改行为:函数体、注释和格式都没动。多个文件共用的工具放进
msimeui::controls_detail 命名空间,声明在 ControlsInternal.h、定义只在
ControlsInternal.cpp 里有一份——CreateCachedTextLayout 的函数内静态
formatCache 和 GetSharedDWriteFactory 的静态工厂因此仍是全库唯一的一份,
不会按翻译单元各复制一份。只被一个控件用到的工具和常量仍留在该文件的匿名
命名空间。原来没人用的 MakeInsetRect 和 kCandidateItemGap 原样保留,分别放在
ControlsInternal.cpp 和 CandidateList.cpp 的匿名命名空间里。
using Microsoft::WRL::ComPtr 挪进内部头文件。Controls.cpp 已经没有剩余内容,
删除;ui/CMakeLists.txt 的源文件列表相应更新,Server 通过 add_subdirectory
引入 msimeui,不需要改。

非搬移改动只有两处:CreateCachedTextLayout 和 DrawLabel 的默认实参从定义挪到了
头文件里的声明上,DrawLabel 定义的形参因此由 clang-format 重新折行。

ui/scripts/check-boundary.py 仍然通过,没有新增对产品的依赖。用脚本比对了原文件
与拆分后全部文件的非空、非 #include 行多重集:除上述两个函数签名外,差异只有
新增的声明、命名空间包装、using 指令、#pragma once 和文件头注释。可用
git diff --color-moved 复核。
2026-09-27 17:28:16 +08:00
fanlusky 7bbb9042fc perf(ui): 缓存卡片阴影、TextBlock 字体格式和候选列表文字度量
- Card:每帧为每道阴影新建离屏位图、画刷和高斯模糊特效。改为把全部阴影
  合成到一张位图,按尺寸、圆角、DPI、阴影参数和 render target 复用;
  尺寸不变的重绘(选中项移动、译文与云候选补齐)直接贴图。合成失败时仍
  走原来的逐帧绘制。
- TextBlock:每次测量都新建 IDWriteTextFormat,自定义回退字体还要把系统
  回退表整份复制进新 builder。格式与文字、光标无关,改为只在字体、字号、
  对齐或回退字体变化时重建。
- CandidateList:一帧里会测量多次(自然尺寸、保留尺寸、按最终宽度排版),
  每次都为每段文字新建 DirectWrite layout,一页九项每键约一两百个。文字
  宽高只取决于文字、字号、宽度和字体,改为记忆化,字体或外观变化时清空,
  超过上限整体重来。空的辅助码和译文不再创建 layout。
2026-09-27 15:03:16 +08:00
fanlusky 1348ca8eb7 fix(server): D2D 托盘菜单项圆角与外框同心对齐,放宽行高与内边距 2026-09-27 11:19:07 +08:00
fanlusky 573169105f fix(ui): D2D 预编辑光标不再贴住首项高亮
光标原先取整行高度并下移 8%,底部越过预编辑行、碰到候选列表首项的
高亮背景。改为与 CSS 相同的几何:高 1.2em,底边在基线下 0.12em。
2026-09-26 23:38:37 +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 0b4d9172f8 feat(ui): CandidateList 让贴着列表四角的项使用外框圆角
Appearance 新增 outerCornerRadius 与 outerTopCornersEnabled:选中或悬停的
填充里,真正落在列表四角上的角改用外框半径,其余角保持 cornerRadius。
既支持内缩高亮与外框同 R,也支持整行铺满、只在外框四角处贴合的皮肤。
2026-09-26 21:49:04 +08:00
fanlusky 3135af32ea fix(ui): 菜单项回调不再读已释放的闭包
在 D2D 候选框上右键删除一个候选,server 进程必定闪退重启,
0xc0000005 访问违例。

MenuFlyoutItem::OnMouseUp 直接调 onClick_()。候选框右键菜单的四个
动作都是「先 CloseContextMenu(true) 再 PostMessageW」的形状,而
CloseContextMenu 会同步 RemovePopup 并 reset 掉 contextMenu,也就是
在 operator() 还在栈上时销毁了这个 MenuFlyoutItem,连带销毁正在执行
的 std::function —— 小闭包按 MSVC 的 small-object optimization 就存
在 MenuFlyoutItem 那个堆块里。紧接着的 RestoreHostAfterMenu() 同步
走 SetWindowPos,派发 WM_SIZE/WM_PAINT 触发重新分配,把那块内存回收
掉;lambda 体回来再读 this 时读到的已经是别人的数据,mov rcx,[rcx+8]
(hwnd_)就炸在这里。

四个动作形状相同,都有这个缺陷;置顶碰巧不崩只是因为 MSVC 没有在那
一处重新从闭包存储里加载捕获,不是它安全。

改法是调用前把 ClickHandler 拷到栈上,让可调用对象活过自己这次调用。
Button::OnClick 是同样的形状,一并改掉。

回归测试不去捕捉内存破坏——能不能复现取决于分配器是否恰好回收了那一
块、以及编译器有没有重新加载捕获,而且读已释放内存本身就是未定义行为。
改为断言修复所建立的不变式:句柄在自己 operator() 执行期间始终存活。
2026-09-19 15:45:25 +08:00
fanlusky a54e2e08e8 fix(ui): 候选窗高亮行的辅助码与翻译跟随选中文字色
微信绿皮肤下高亮行的辅助码压在 #07C160 绿底上仍是暗色,与 webview 不一致。

候选行绘制里 rowTextSelected/rowLabelSelected 只覆盖候选文字和序号,辅助码
(annotation)和翻译一直无条件使用 appearance_.annotationColor。而皮肤 CSS 里
这两者是 .text 的子节点(.cand-content / .cand-translation),颜色继承自 .text,
选中行的 `.first .text { color: ... }` 会一并覆盖它们——D2D 端分开绘制,这层继承
关系就丢了。

改为在高亮行显式跟随 rowTextSelected,翻译再乘 CSS 的 opacity .62 保持原有观感。
判据是 rowTextSelected 的 alpha,所以各皮肤自动与各自 CSS 对齐:wechat 与
willow_green 白色,graphite #111827/#f1f3f5,fluent 没有 .first .text 覆盖,
沿用普通文字色不变。
2026-09-14 14:45:11 +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 ee5b50133f fix(ui): 竖排候选窗选中项高亮铺满整行
d2d 渲染下,竖排候选窗被卡片最小宽度撑大时,候选项的自然宽度小于卡片内宽,
选中高亮与命中区域只覆盖到文字宽度,右侧留下大片空白(候选项较长、宽度超过
卡片最小宽度时表现正常)。

根因:候选项行矩形取自按自然宽度测量得到的 itemGeometry。列表虽被拉伸到卡片
内宽,但绘制前 Present 阶段会以窗口宽度重新测量,把每项宽度还原为自然宽度,而
此时排布命中缓存(矩形未变)被跳过,铺满逻辑无法重新生效。

改为在 ItemRect 中直接以列表最终布局宽度 bounds_ 计算竖排行矩形,不再依赖逐项
自然宽度,绘制与命中同时受益,且不受测量/排布缓存影响。右侧留白由卡片内边距
提供,与左侧保持一致。横排路径不变。

新增 GetItemBounds 访问器与三个回归测试,其中一个复刻了 Present 重新测量的时序,
可捕获此前方案的退化。
2026-09-14 02:04:58 +08:00
Lu Fan(SonnyCalcr) e6136ddcd2 Merge pull request #337 from Komeiji-Shiki/codex/fix-317-candidate-wrap
fix(ui): 修复候选列表溢出与换行后的点击区域
2026-09-12 14:52:52 +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
古明四季 42bfd9fc49 fix(ui): 修复候选列表溢出与换行后的点击区域 2026-09-11 23:34:57 +08:00
Jia Sui 62b4688aa1 feat(ui): DeviceResources 与 Window 支持渲染 DPI 覆写 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
Carry aa56486cc6 fix(ui): pass /utf-8 to msimeui-tests on MSVC to prevent C4819 on CP936 2026-09-11 21:03:03 +08:00
Neptrue_Lin 2b6bfafeec Merge pull request #327 from ouchihao/codex/fix-295-retained-dpi
fix(ui): refresh DPI when reusing render targets
2026-09-11 20:34:43 +08:00
Ouchihao f3fcd322e9 fix(ui): refresh DPI when reusing render targets 2026-09-11 19:35:07 +08:00
Ouchihao f9a021dc02 fix(ui): collapse text selections before moving the caret 2026-09-11 19:32:34 +08:00
Aegon8719 dea9292814 issue-55 候选窗支持可配置的有序回落字体 2026-09-09 18:03:56 +08:00
fanlusky 43576b75ca fix(ui): make the icon font resolver recover from DirectWrite failures
ResolveIconGlyph() guarded its cache with a mutex, but the work under that
lock reached SharedFactory(), whose lazy DWriteCreateFactory() call was
unsynchronized -- and UiFontFamily()/ApplyUiFontFallback() call the same
helper without that mutex from render paths. Two window threads could both
see a null factory, both create one into the same ComPtr, leak one and race
on the raw pointer. Give SharedFactory() a lock of its own.

IconFonts() memoized its result in a function-local static initialized
exactly once. If SharedFactory() returned null or GetSystemFontCollection()
failed on that first call, the empty list was cached for the process
lifetime and every icon degraded to its text label permanently, with no
retry -- while SharedFactory() itself retried on each call, making the
failure mode asymmetric and hard to diagnose. Keep the list only once the
probe finds a font, and skip the ResolveIconGlyph cache while it is empty
so the retry is actually reachable.
2026-09-08 16:11:16 +08:00
fanlusky 43555460db fix(server): fall back to text when the icon font lacks the glyph
The floating toolbar and the emoji panel hardcoded "Segoe Fluent Icons",
which ships with Windows 11 only. DirectWrite substitutes a font silently
instead of failing, so on Windows 10 every private-use codepoint the toolbar
draws (U+E982, U+F110, U+F138, ...) rendered as a blank box: the buttons
still worked but no longer showed any state.

Resolve the font per codepoint rather than per family, because "Segoe MDL2
Assets" is a subset of Segoe Fluent Icons and swapping the family alone would
still draw tofu wherever a glyph is missing:
- add msimeui::ResolveIconGlyph(), which probes the installed icon fonts with
  IDWriteFont::HasCharacter() and returns the first family that really carries
  the glyph, or no family at all;
- give every toolbar and tab icon a text label to fall back on (中/英, 半/全,
  简/繁, ...) when neither font has the glyph;
- drop the unused ToolbarIconButton::SetGlyph/SetLabel, which would bypass the
  resolved family.

Add unit tests for the resolver, covering the missing-glyph path that cannot
be reached on a host that has Segoe Fluent Icons installed.
2026-09-08 12:41:01 +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 4fc16952b3 wip(windows): 补齐隐私与许可交付,修好外部人的构建入口
- 新增 PRIVACY.md:逐项写明每个联网端点、发送内容、默认开关与关闭方式,明确云候选默认开启、
  token 在 config.toml 明文存储、无任何遥测
- 设置页「用户协议」「隐私政策」两个无 handler 的死按钮接上;前者改名为「开源许可协议」指向
  LICENSE,开源产品的用户条款就是许可证本身,不再承诺一份不存在的 EULA
- 安装包随包装入 GPL 正文并在安装向导显示(GPLv3 §4/§6),补上打包测试断言
- release.yml 增加 build provenance attestation,并在发布后向 MSIME-Web 发 repository_dispatch,
  让 msime.app/update.json 不再等两小时一次的 cron
- 所有 CMakePresets 的 vcpkg 路径改用 $env{VCPKG_ROOT},server 的 Boost 回退不再指向个人目录
- ui/README.md 指向本机绝对路径的 markdown 死链改成相对链接
- 新增 check-developer-paths.sh 并接入 required 的 quality 检查,防止路径回潮
- docs/installation.md 不再写死版本号
2026-09-06 12:05:06 +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