* 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 页面
13 KiB
外部皮肤 manifest(TOML)
每个皮肤目录需要 skin.toml,id 必须与文件夹名一致。schema_version 支持 1 与 2:v2 是面向全贴图与复杂立绘皮肤的规范,必须以 canvas 为 base,由 Direct2D 候选窗完整绘制(九宫格面板、上下两层挂件、动图)。
候选框和悬浮工具栏都由 skin.toml 声明,不要再提供 cand.css 或工具栏 CSS。所有图片路径都相对于皮肤目录,且不能指向目录之外。
顶层字段
- 必填:
schema_version(1或2)、id、name、version、base(继承的内置皮肤:fluent、wechat、graphite、willow_green、autumn_osmanthus、microsoft;或与 msime 共享的基底:system、shuishan、light、paper、night、ink,按fluent的结构绘制并补上基底配色,固定明暗的基底只在对应的themes下生效;或全贴图基底canvas,只用于 v2) schema_version与base的搭配:schema_version = 2必须写base = "canvas"。写了 2 而base是别的基底时,皮肤仍可使用,但按schema_version = 1加载,下面标「v2」的表和键全部忽略(连同其中的错误),设置页的诊断信息里会多一条说明。schema_version = 1不能用canvas:没有面板贴图它只是一张透明卡片,这种皮肤不会出现在可选列表里。
- 可选:
author、description [supports]:layouts(horizontal/vertical)、themes(dark/light)[license]:code、assets
[candidate_window]
min_width_dip:卡片最小宽度;实际最小宽度还会被装饰图的width_dip撑大corner_radius_dip:卡片圆角,0–32;不写则沿用baseborder_width_dip:卡片外框线宽,0–4;颜色取border,不写border时取base的边框色(杨柳青、秋桂没有外框,此时仍不可见)item_corner_radius_dip:选中 / 悬停高亮的圆角,0–16;贴着卡片四角的那几个角仍跟corner_radius_dipshadow:卡片阴影,none|soft|strong;不写则沿用base。只影响候选卡片,不影响右键菜单font_family:候选字体名,UTF-8 编码后最长 64 字节(约 21 个汉字),不能含引号、反斜杠、逗号、分号、尖括号、花括号和反引号。排在用户设置的字体前面,缺字时仍回落到用户字体;字号不由皮肤决定page_arrows:候选框里的翻页箭头,true|false。横排时是候选右侧贴着最后一行的一小列,竖排时是最后一行下方的一矮行、“‹”与序号左对齐;颜色取number,悬停底色取hover。不写则沿用base的默认皮肤设置(数据目录skins\default\<base>\skin.toml,出厂为false)fit_content:卡片是否严格按候选文字所需的宽度收紧,true|false。打开后不再垫出厂的最小宽度(160 DIP),同一次输入里后到的翻译等内容消失时宽度也立刻缩回;min_width_dip和装饰图width_dip仍然生效。不写则沿用base的默认皮肤设置(出厂为false)
[candidate_window.decoration](可选)
卡片上方的装饰图。写了这张表就必须同时给出 image、top_inset_dip、width_dip。
image:图片路径top_inset_dip/width_dip:装饰框的高和宽,框贴在卡片上方,不与卡片重叠;图片在框内等比缩放align:left|center|right,相对卡片
[candidate_window.background](可选)
卡片背景图,画在底色之上、文字之下,按圆角裁剪,对深浅两套主题都生效。
image:图片路径fit:cover(铺满裁切)|contain(完整显示)|stretch(拉伸)opacity:0–1
canvas 基底(v2)
全贴图皮肤的地基:面板透明、没有描边、直角、默认没有阴影,候选框长什么样完全由下面的九宫格面板图和挂件决定。它只能作为 base,不能在设置页单独选用。
- 文字与高亮用不依赖底色的中性色:深色主题近白文字(
#f2f2f2)、白色半透明的选中 / 悬停底,浅色主题近黑文字(#1a1a1a)、黑色半透明的选中 / 悬停底;高亮圆角 4,可用item_corner_radius_dip改。贴图偏深或偏浅时用[candidate.dark]/[candidate.light]改配色。 - v1 的键照样可用:配色、
font_family、page_arrows、fit_content、min_width_dip、corner_radius_dip、border_width_dip、shadow(写soft/strong才有阴影)、[candidate_window.decoration]、[candidate_window.background]、[toolbar]。写了border_image时,卡片的底色、描边、圆角、阴影和background背景图都让给面板图。 - 右键菜单与悬浮工具栏没有贴图,沿用
fluent的样式,可用[candidate.*.menu]与[toolbar]改。翻页箭头、fit_content的出厂值也取fluent的默认皮肤设置。
[candidate_window.border_image](v2 可选)
九宫格面板图,语义同 CSS border-image(border-image-repeat: stretch):四角按边宽缩放,四边沿长度方向拉伸,中心双向拉伸。
source:图片路径,.png、.apng、.gif或.webpslice_dip:切片线位置,按源图像素计(同 CSSborder-image-slice的数值,键名里的_dip是历史写法),1、2 或 4 个0–500的数,按 CSS 简写展开成上右下左(1 个值四边相同,2 个值是「上下 左右」)stretch_dip:可选,同为源图像素,给出时代替slice_dip。面板图里画死的装饰必须落在切片之内(可拉伸区之外),否则会被拉长;例如帆船画在右侧,就把右侧切片放到船身左侧width_dip:可选,四边在屏幕上画多宽,按 DIP 计(同 CSSborder-image-width),写法同上。不写时取切片的数值当 DIP。给 2 倍图时写成切片的一半,200% 缩放下逐像素清晰、100% 下是缩小一半的同一张图。两条对边加起来比面板还宽时四边按同一比例缩小fill:可选,布尔值,是否画中心块,默认true;false时中心留空、透出桌面
面板图画在卡片的底色之上、文字之下,不按圆角裁剪(同 CSS)。动图面板也会播放,但面板一般建议用静态图,把动画留给挂件。
[candidate_window.content_insets_dip](v2 可选)
候选框内容与文字的安全内边距,从面板外缘量起,用来避开贴图四周自带的花纹或立绘。某一边小于九宫格该边的边宽(width_dip)时按边宽算,文字不会压在边框上(同 CSS 的 border + padding)。不写时是边宽再加 5 DIP。
top/right/bottom/left:四边内边距,top/bottom0–200,left/right0–500
[candidate_window] 的 line_gap_dip(v2 可选)
预编辑行与候选之间的距离,0–100。只影响这一处,竖排时候选与翻页箭头之间的间距不变。不写则沿用 base 的行距。
[candidate_window.outer_insets_dip](v2 可选)
挂件挂在卡片外沿时,窗口需要额外预留的透明空白,从卡片四边往外量。
top/right/bottom/left:四边预留量浮点数
不声明时窗口只按卡片大小开,挂到卡片外面的立绘会被窗口边界裁掉。
[[candidate_window.decorations]](v2 可选,数组表)
自由锚定的立绘挂件,可以叠加角色立绘与动图,最多 32 层。
image:图片路径,.png、.apng、.gif或.webpanchor:挂件框贴着面板的哪个位置:top-left|top-right|bottom-left|bottom-right|left|right|top|bottom|center,默认rightwidth_dip/height_dip:挂件框的尺寸,0–2000(不含 0);图片在框里等比缩放、居中(同 CSSbackground-size: contain)offset_x_dip/offset_y_dip:相对锚点的平移量,-2000–2000,正数向右、向下z_index:层级,-1000–1000的整数,默认2。面板自身相当于1:z_index < 1的挂件画在面板之下(只有伸出面板的部分看得见),其余画在面板与文字之上。同层按z_index从小到大、相同时按书写顺序叠放
挂件不参与布局,也不接收点击;伸到面板外面时要用 outer_insets_dip 给窗口留出空白。
动图(v2)
v2 皮肤里的图片可以是动图:APNG(按 APNG 规范 解码)、GIF,以及系统 WebP 解码器能逐帧读出的动画 WebP(每帧 100 ms)。
- 帧时长与浏览器一致:
delay_den为 0 按 100 算,10 ms 及以下按 100 ms;APNG 的num_plays、GIF 的 NETSCAPE 循环次数生效,播完停在最后一帧。 - 动画从候选窗出现时开始,同一次显示里打字不会让它重头开始;候选窗隐藏或换皮肤后,下一次显示从第一帧开始。
- 系统关闭了动画效果(设置 → 辅助功能 → 视觉效果 → 动画效果)时显示静态图:APNG 显示它的默认图(通常就是第一帧),GIF 显示第一帧。
- 一张动图超过 1024 帧或解码后超过 64 MB、文件损坏或不合规范时只显示第一帧(静态图)。所有动图的帧合计受 64 MB 显存预算约束。
- v1 皮肤里的图片始终是静态首帧,与以往一致。
[candidate.dark] / [candidate.light]
accent、selected、hover、surface、border、text、number、translation、show_selected_bar
细分配色,都可省略,不写时按右列回落:
| 键 | 作用 | 不写时 |
|---|---|---|
candidate_text |
普通候选的文字 | text |
preedit_text |
预编辑(拼音)文字 | text |
preedit_caret |
预编辑光标 | accent |
selected_text |
选中候选的文字 | base 的选中行文字色 |
selected_number |
选中候选的序号 | base 的选中行序号色 |
translation_pos |
翻译里的词性(n.、adj.、proper n. 等) |
与翻译同色 |
translation_separator |
翻译里义项之间的分隔符(;、;) |
与翻译同色 |
selected_translation |
选中候选的翻译 | base 的选中行翻译色(微信、杨柳青、秋桂铺实色高亮,各有一组);base 没有时用 translation,再不写则取选中文字色的 62% 不透明度 |
selected_translation_pos |
选中候选翻译里的词性 | 写了 selected_translation 时与它同色;否则 base 的选中行词性色,再回落到 translation_pos |
selected_translation_separator |
选中候选翻译里的分隔符 | 写了 selected_translation 时与它同色;否则 base 的选中行分隔符色,再回落到 translation_separator |
selected_bar |
选中项左侧竖条 | accent |
preedit_background |
预编辑行底色,圆角同高亮圆角 | 透明 |
preedit_divider |
预编辑行下方 1px 分隔线 | 无 |
设置页里的「候选文字颜色」优先于 text、candidate_text 和 preedit_text。
内置皮肤的翻译用 Notion 调色板:单词分别是 fluent 紫、微信蓝、杨柳青绿、石墨棕、微软粉,秋桂浅色橙、深色黄;词性换成相邻的另一个 Notion 色(紫配粉、蓝配绿、绿配蓝、棕配橙、橙配红、黄配橙、粉配紫)再加 80 透明度调淡,与单词区分开;分隔符是单词色加 73 透明度。包写了 translation 而没写 translation_pos / translation_separator 时,base 的词性和分隔符配色一并作废,跟随新的翻译色。想要同样的效果,词性取另一个色相、在色值后补两位十六进制透明度即可:
translation = "#529cca"
translation_pos = "#4dab9a80"
translation_separator = "#529cca73"
[candidate.dark.menu] / [candidate.light.menu](可选)
候选窗右键菜单:background、border、text、hover。
[toolbar]
悬浮工具栏样式。所有键都可省略,不写的沿用 base。
corner_radius_dip:0–32[toolbar.dark]/[toolbar.light]:background、border、handle(左侧拖动条)、divider、icon、hover
颜色格式
所有颜色键——[candidate.dark] / [candidate.light] 里除 show_selected_bar 外的键、[candidate.*.menu] 和 [toolbar.dark] / [toolbar.light] 的全部键——都接受以下写法。数字、布尔、枚举和路径类的键不是颜色,不能写成颜色值。
| 写法 | 示例 | 说明 |
|---|---|---|
#rgb / #rrggbb |
#e8a、#e08aa8 |
不透明 |
#rgba / #rrggbbaa |
#e8a4、#e08aa847 |
末尾一位或两位是不透明度 |
rgb(r, g, b) |
rgb(224, 138, 168) |
r、g、b 为 0–255 |
rgba(r, g, b, a) |
rgba(224, 138, 168, 0.28) |
a 为 0–1 |
transparent |
transparent |
全透明,等同 #0000 |
- 十六进制大小写均可,但必须带
#:这是清单格式的约定,与 CSS 写法一致;渲染器对不带#的写法有容错,但不要依赖它。 - 不透明度在末尾(
#RRGGBBAA),不是 Windows 常见的#AARRGGBB。例如#e9e8e89d约为 62% 不透明,rgba(224, 138, 168, 0.28)与#e08aa847等价。