Files
msime-windows/examples/skin-examples/schema/README.md
T
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

13 KiB
Raw Blame History

外部皮肤 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;不写则沿用 base
  • border_width_dip:卡片外框线宽,0–4;颜色取 border,不写 border 时取 base 的边框色(杨柳青、秋桂没有外框,此时仍不可见)
  • item_corner_radius_dip:选中 / 悬停高亮的圆角,0–16;贴着卡片四角的那几个角仍跟 corner_radius_dip
  • shadow:卡片阴影,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 或 .webp
  • slice_dip:切片线位置,按源图像素计(同 CSS border-image-slice 的数值,键名里的 _dip 是历史写法),1、2 或 4 个 0–500 的数,按 CSS 简写展开成上右下左(1 个值四边相同,2 个值是「上下 左右」)
  • stretch_dip:可选,同为源图像素,给出时代替 slice_dip。面板图里画死的装饰必须落在切片之内(可拉伸区之外),否则会被拉长;例如帆船画在右侧,就把右侧切片放到船身左侧
  • width_dip:可选,四边在屏幕上画多宽,按 DIP 计(同 CSS border-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 / bottom 0–200,left / right 0–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 或 .webp
  • anchor:挂件框贴着面板的哪个位置:top-left | top-right | bottom-left | bottom-right | left | right | top | bottom | center,默认 right
  • width_dip / height_dip:挂件框的尺寸,0–2000(不含 0);图片在框里等比缩放、居中(同 CSS background-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 等价。