Files
msime-windows/server/src/window/candidate_skin_layers.cpp
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

68 lines
2.3 KiB
C++

#include "window/candidate_skin_layers.h"
#include <algorithm>
namespace CandidateSkinLayers
{
Edges ExpandBox(const std::vector<double> &values)
{
Edges edges;
if (values.size() == 1)
{
edges = {values[0], values[0], values[0], values[0]};
}
else if (values.size() == 2)
{
edges = {values[0], values[1], values[0], values[1]};
}
else if (values.size() == 4)
{
edges = {values[0], values[1], values[2], values[3]};
}
return edges;
}
NineSlice ResolveNineSlice(const CandidateSkinCatalog::BorderImage &borderImage)
{
NineSlice result;
const std::vector<double> &slice = borderImage.stretchDip.empty() ? borderImage.sliceDip : borderImage.stretchDip;
result.slice = ExpandBox(slice);
result.width = borderImage.widthDip.empty() ? result.slice : ExpandBox(borderImage.widthDip);
result.fill = borderImage.fill;
return result;
}
Edges ResolveContentPadding(const CandidateSkinCatalog::Package &package, double basePadding)
{
Edges border;
if (package.borderImage && !package.borderImage->source.empty())
{
border = ResolveNineSlice(*package.borderImage).width;
}
if (package.contentInsetsDip)
{
const CandidateSkinCatalog::InsetsDip &insets = *package.contentInsetsDip;
return {(std::max)(insets.top, border.top), (std::max)(insets.right, border.right),
(std::max)(insets.bottom, border.bottom), (std::max)(insets.left, border.left)};
}
return {border.top + basePadding, border.right + basePadding, border.bottom + basePadding,
border.left + basePadding};
}
Layers SplitDecorations(const std::vector<CandidateSkinCatalog::FreeDecoration> &decorations)
{
std::vector<CandidateSkinCatalog::FreeDecoration> sorted = decorations;
std::stable_sort(sorted.begin(), sorted.end(),
[](const CandidateSkinCatalog::FreeDecoration &a, const CandidateSkinCatalog::FreeDecoration &b) {
return a.zIndex < b.zIndex;
});
Layers layers;
for (auto &decoration : sorted)
{
(decoration.zIndex < CandidateSkinCatalog::kDecorationFrontZIndex ? layers.below : layers.above)
.push_back(std::move(decoration));
}
return layers;
}
} // namespace CandidateSkinLayers