24 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) 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) 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) 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
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
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 fef743e0b1 perf(ui): 卡片阴影只模糊一次,按九宫格拉伸到任意尺寸
阴影缓存按卡片精确尺寸作键,打字时卡片几乎每键都变大小,每帧都要重建两遍
高斯模糊,约占候选帧一半的开销。离四角足够远处模糊后的边缘沿边不变,所以
只为刚好够到这段直边的尺寸模糊一次,绘制时切成九块、拉伸中间的条带;切口
落在直边内部,逐像素平铺无缝。

新增像素对比测试:4 种 DPI、2 种圆角、6 种尺寸下与逐帧完整模糊最大相差
7/255(原缓存为 6/255),切片接缝会差到阴影本身的 alpha。
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 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 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 ee5b50133f fix(ui): 竖排候选窗选中项高亮铺满整行
d2d 渲染下,竖排候选窗被卡片最小宽度撑大时,候选项的自然宽度小于卡片内宽,
选中高亮与命中区域只覆盖到文字宽度,右侧留下大片空白(候选项较长、宽度超过
卡片最小宽度时表现正常)。

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

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

新增 GetItemBounds 访问器与三个回归测试,其中一个复刻了 Present 重新测量的时序,
可捕获此前方案的退化。
2026-09-14 02:04:58 +08:00
古明四季 42bfd9fc49 fix(ui): 修复候选列表溢出与换行后的点击区域 2026-09-11 23:34:57 +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 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 Hu 210c38faff test: fail an empty suite and report how many tests ran
CI 日志里 ctest 只显示「1 个测试通过」,因为它把整个套件当作一个测试项,而 --output-on-failure 只在失败时打印用例明细。这样一个一个用例都没注册的套件,看起来和全部通过完全一样。

runner 改为:注册数为零时直接判失败,并在结尾打印实际跑了多少个用例。ctest 改用 --verbose,让用例明细进入 CI 日志。
2026-09-05 17:23:34 +09:00
Evan Hu b8ac8e0b00 test: cover the layout core with unit tests
本仓库此前没有任何测试,CI 只能证明它编译得过。布局系统是这个框架里被所有控件依赖、也最容易在重构中被改坏的部分,而且是纯几何计算,不需要 D2D 设备即可测。

新增 13 个用例,覆盖 Visual 的测量与排列语义(margin 收缩可用尺寸并回填外框、显式尺寸覆盖测量值、min/max 夹取、可用空间封顶、测量缓存的命中与失效、Stretch/Center/Trailing 对齐定位),以及 StackPanel 与 HorizontalStackPanel 的堆叠、间距和 padding 行为。

断言值全部按 src/Layout.cpp 的现有实现推导,用于锁定当前行为,不是按我认为应该如何来写。测试框架沿用 MSIME-Server 的极简写法,但去掉了 fmt 依赖,本仓库不引入任何新依赖。

Grid、WrapPanel 和 ScrollViewer 本次未覆盖,留作后续。
2026-09-05 17:21:10 +09:00