Merge pull request #903 from Tokisaki-Galaxy/feat/skin-schema-v2
feat(server): 外部皮肤新增 schema_version 2,WebView2 与 D2D 双后端支持九宫格贴图与自由挂件
This commit is contained in:
1 parent
242f8b2bc8
commit
bcbbdcf74c
17 files changed
+1012
-36
No files matched your search
@@ -1,12 +1,12 @@
|
||||
# 外部皮肤 manifest(TOML)
|
||||
|
||||
每个皮肤目录需要 `skin.toml`,`id` 必须与文件夹名一致。`schema_version` 当前为 `1`。
|
||||
每个皮肤目录需要 `skin.toml`,`id` 必须与文件夹名一致。`schema_version` 支持 `1` 与 `2`(v2 为面向全贴图与复杂立绘皮肤的高级规范,仅在 WebView2 端全面启用,Direct2D 原生端保底降级)。
|
||||
|
||||
候选框和悬浮工具栏都由 `skin.toml` 声明,不要再提供 `cand.css` 或工具栏 CSS。所有图片路径都相对于皮肤目录,且不能指向目录之外。
|
||||
|
||||
## 顶层字段
|
||||
|
||||
- 必填:`schema_version`、`id`、`name`、`version`、`base`(继承的内置皮肤:`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`、`microsoft`)
|
||||
- 必填:`schema_version`(`1` 或 `2`)、`id`、`name`、`version`、`base`(继承的内置皮肤:`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`、`microsoft`)
|
||||
- 可选:`author`、`description`
|
||||
- `[supports]`:`layouts`(`horizontal` / `vertical`)、`themes`(`dark` / `light`)
|
||||
- `[license]`:`code`、`assets`
|
||||
@@ -38,6 +38,44 @@
|
||||
- `fit`:`cover`(铺满裁切)| `contain`(完整显示)| `stretch`(拉伸)
|
||||
- `opacity`:`0`–`1`
|
||||
|
||||
### `[candidate_window.border_image]`(v2 可选,仅 WebView2)
|
||||
|
||||
九宫格背景拉伸,面板整体贴图,用于全贴图与复杂纹理面板。
|
||||
|
||||
- `source`:图片路径(如 `assets/skin1.png`,支持 APNG/WebP/PNG)
|
||||
- `slice_dip`:`[top, right, bottom, left]` 切片线位置,正数数组,支持 1~4 个值
|
||||
- `stretch_dip`:可选,可拉伸区,语义同 `slice_dip` 但更直白,给出时覆盖它。**面板图里画死的装饰必须落在可拉伸区之外**,否则会被横向拉长;例如帆船画在右侧,就把右侧切片放到船身左侧
|
||||
- `width_dip`:可选,边框绘制宽度,支持 1~4 个值
|
||||
- `fill`:可选,布尔值,是否保留九宫格中心内容区域,默认为 `true`
|
||||
|
||||
### `[candidate_window.content_insets_dip]`(v2 可选,仅 WebView2)
|
||||
|
||||
候选框内容与文字的安全内边距,**从面板外缘量起**,用来避开贴图四周自带的花纹或立绘。渲染时会把该方向的边框宽度扣掉,落到内容盒上。
|
||||
|
||||
- `top` / `right` / `bottom` / `left`:四边内边距浮点数
|
||||
|
||||
### `[candidate_window]` 的 `line_gap_dip`(v2 可选)
|
||||
|
||||
预编辑行与候选行之间的行距,`0`–`100`。不写则沿用 `base` 的行距。
|
||||
|
||||
### `[candidate_window.outer_insets_dip]`(v2 可选)
|
||||
|
||||
挂件挂在卡片外沿时,窗口需要额外预留的透明空白,从卡片四边往外量。
|
||||
|
||||
- `top` / `right` / `bottom` / `left`:四边预留量浮点数
|
||||
|
||||
**不声明时窗口只按卡片大小开,挂到卡片外面的立绘会被窗口边界裁掉。**
|
||||
|
||||
### `[[candidate_window.decorations]]`(v2 可选,数组表,仅 WebView2)
|
||||
|
||||
自由锚定立绘挂件层,支持叠加角色立绘与 APNG 动图,层数不限。
|
||||
|
||||
- `image`:图片路径
|
||||
- `anchor`:`top-left` | `top-right` | `bottom-left` | `bottom-right` | `left` | `right` | `top` | `bottom` | `center`
|
||||
- `width_dip` / `height_dip`:挂件的尺寸
|
||||
- `offset_x_dip` / `offset_y_dip`:相对锚点的平移量,正数向右、向下,两个后端同一套语义
|
||||
- `z_index`:层级,默认为 `2`
|
||||
|
||||
## `[candidate.dark]` / `[candidate.light]`
|
||||
|
||||
`accent`、`selected`、`hover`、`surface`、`border`、`text`、`number`、`translation`、`show_selected_bar`
|
||||
|
||||
@@ -18,7 +18,7 @@ class ManifestTests(unittest.TestCase):
|
||||
for path in manifests:
|
||||
with self.subTest(skin=path.parent.name):
|
||||
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(data["schema_version"], 1)
|
||||
self.assertIn(data["schema_version"], (1, 2))
|
||||
self.assertEqual(data["id"], path.parent.name)
|
||||
self.assertRegex(data["id"], r"^[a-z0-9._-]{1,64}$")
|
||||
# Mirrors the Server catalog: name, version and base are required, author is not.
|
||||
@@ -30,6 +30,11 @@ class ManifestTests(unittest.TestCase):
|
||||
for table in ("decoration", "background"):
|
||||
if table in window:
|
||||
assets[f"candidate_window.{table}.image"] = window[table]["image"]
|
||||
if "border_image" in window:
|
||||
assets["candidate_window.border_image.source"] = window["border_image"]["source"]
|
||||
for i, dec in enumerate(window.get("decorations", [])):
|
||||
if "image" in dec:
|
||||
assets[f"candidate_window.decorations[{i}].image"] = dec["image"]
|
||||
if "decoration" in window:
|
||||
for key in ("top_inset_dip", "width_dip"):
|
||||
self.assertGreater(window["decoration"][key], 0, key)
|
||||
|
||||
@@ -269,6 +269,8 @@ set(SRC_FILES
|
||||
"./src/webview2/windows_webview2.h"
|
||||
"./src/window/candidate_presenter.cpp"
|
||||
"./src/window/candidate_presenter.h"
|
||||
"./src/window/candidate_skin_overlay.cpp"
|
||||
"./src/window/candidate_skin_overlay.h"
|
||||
"./src/window/candidate_skin_palette.cpp"
|
||||
"./src/window/candidate_skin_palette.h"
|
||||
"./src/window/tray_menu_presenter.cpp"
|
||||
|
||||
@@ -30,3 +30,14 @@ Sciter is not an active product backend. The old unreferenced handwritten D2D
|
||||
prototype is removed; native small windows use msimeui presenters. The msimeui
|
||||
library owns generic controls and rendering, never Server configuration, pipe
|
||||
sessions, dictionary access, or global input state.
|
||||
|
||||
## v2 皮肤在两个后端下的渲染
|
||||
|
||||
`schema_version = 2` 的皮肤(九宫格贴图面板、自由锚定挂件、面板外空白)两个后端都支持,但走的是不同的实现:
|
||||
|
||||
- WebView2 把面板翻成 CSS:`border-image` 铺九宫格,`content_insets_dip` 落成容器内边距,挂件用 `.containerParent` 的伪元素绝对定位。
|
||||
- D2D 用 `CandidatePresenter` 的本地场景:面板由 `msimeui::Card::SetBackgroundImageNineSlice` 画成九宫格,挂件由 `CandidateSkinOverlay` 这一层浮在卡片上,`outer_insets_dip` 换成窗口在卡片四边多留的空白。
|
||||
|
||||
两侧读同一份 `skin.toml`,字段语义必须一致,尤其是 `anchor` 与 `offset_*_dip`:正数向右、向下。改一侧的定位规则要同时改另一侧。
|
||||
|
||||
`content_insets_dip` 在两个后端都从面板外缘量起,D2D 侧由卡片内边距承担,不需要再扣边框。
|
||||
@@ -330,12 +330,14 @@ std::optional<Package> Load(const std::filesystem::path &skinsRoot, const std::s
|
||||
return std::nullopt;
|
||||
}
|
||||
const toml::table &root = *parsed;
|
||||
if (root["schema_version"].value_or(0) != 1)
|
||||
const int schemaVersion = static_cast<int>(root["schema_version"].value_or(0));
|
||||
if (schemaVersion != 1 && schemaVersion != 2)
|
||||
{
|
||||
SetError(error, "仅支持 schema_version 1");
|
||||
SetError(error, "仅支持 schema_version 1 或 2");
|
||||
return std::nullopt;
|
||||
}
|
||||
Package package;
|
||||
package.schemaVersion = schemaVersion;
|
||||
if (!ReadString(root, "id", package.id, 64, true) || package.id != id ||
|
||||
!ReadString(root, "name", package.name, 80, true) ||
|
||||
!ReadString(root, "version", package.version, 32, true) ||
|
||||
@@ -444,6 +446,164 @@ std::optional<Package> Load(const std::filesystem::path &skinsRoot, const std::s
|
||||
}
|
||||
}
|
||||
}
|
||||
if (package.schemaVersion >= 2)
|
||||
{
|
||||
if (const toml::node *borderImageNode = window->get("border_image"))
|
||||
{
|
||||
const auto *borderImageTable = borderImageNode->as_table();
|
||||
BorderImage bi;
|
||||
if (!borderImageTable || !borderImageTable->contains("source") ||
|
||||
!ReadResource(*borderImageTable, "source", bi.source))
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.source 无效");
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto *sliceArr = (*borderImageTable)["slice_dip"].as_array();
|
||||
if (!sliceArr || (sliceArr->size() != 1 && sliceArr->size() != 2 && sliceArr->size() != 4))
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.slice_dip 必须包含 1、2 或 4 个数值");
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &val : *sliceArr)
|
||||
{
|
||||
const auto num = val.value<double>();
|
||||
if (!num || *num < 0.0 || *num > 500.0)
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.slice_dip 超出范围");
|
||||
return std::nullopt;
|
||||
}
|
||||
bi.sliceDip.push_back(*num);
|
||||
}
|
||||
if (const auto *stretchArr = (*borderImageTable)["stretch_dip"].as_array())
|
||||
{
|
||||
if (stretchArr->size() != 1 && stretchArr->size() != 2 && stretchArr->size() != 4)
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.stretch_dip 必须包含 1、2 或 4 个数值");
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &val : *stretchArr)
|
||||
{
|
||||
const auto num = val.value<double>();
|
||||
if (!num || *num < 0.0 || *num > 500.0)
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.stretch_dip 超出范围");
|
||||
return std::nullopt;
|
||||
}
|
||||
bi.stretchDip.push_back(*num);
|
||||
}
|
||||
}
|
||||
if (const auto *widthArr = (*borderImageTable)["width_dip"].as_array())
|
||||
{
|
||||
if (widthArr->size() != 1 && widthArr->size() != 2 && widthArr->size() != 4)
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.width_dip 必须包含 1、2 或 4 个数值");
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &val : *widthArr)
|
||||
{
|
||||
const auto num = val.value<double>();
|
||||
if (!num || *num < 0.0 || *num > 500.0)
|
||||
{
|
||||
SetError(error, "candidate_window.border_image.width_dip 超出范围");
|
||||
return std::nullopt;
|
||||
}
|
||||
bi.widthDip.push_back(*num);
|
||||
}
|
||||
}
|
||||
bi.fill = (*borderImageTable)["fill"].value_or(true);
|
||||
package.borderImage = bi;
|
||||
}
|
||||
|
||||
if (const toml::node *insetsNode = window->get("content_insets_dip"))
|
||||
{
|
||||
const auto *insetsTable = insetsNode->as_table();
|
||||
if (!insetsTable)
|
||||
{
|
||||
SetError(error, "candidate_window.content_insets_dip 必须是表");
|
||||
return std::nullopt;
|
||||
}
|
||||
InsetsDip insets;
|
||||
insets.top = BoundedNumber(*insetsTable, "top", 200.0);
|
||||
insets.right = BoundedNumber(*insetsTable, "right", 500.0);
|
||||
insets.bottom = BoundedNumber(*insetsTable, "bottom", 200.0);
|
||||
insets.left = BoundedNumber(*insetsTable, "left", 500.0);
|
||||
if (insets.top < 0.0 || insets.right < 0.0 || insets.bottom < 0.0 || insets.left < 0.0)
|
||||
{
|
||||
SetError(error, "candidate_window.content_insets_dip 数值超出范围");
|
||||
return std::nullopt;
|
||||
}
|
||||
package.contentInsetsDip = insets;
|
||||
}
|
||||
|
||||
if (!ReadOptionalBounded(*window, "line_gap_dip", 100.0, package.lineGapDip))
|
||||
{
|
||||
SetError(error, "candidate_window.line_gap_dip 超出范围");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (const toml::node *outerNode = window->get("outer_insets_dip"))
|
||||
{
|
||||
const auto *outerTable = outerNode->as_table();
|
||||
if (!outerTable)
|
||||
{
|
||||
SetError(error, "candidate_window.outer_insets_dip 必须是表");
|
||||
return std::nullopt;
|
||||
}
|
||||
InsetsDip outer;
|
||||
outer.top = BoundedNumber(*outerTable, "top", 500.0);
|
||||
outer.right = BoundedNumber(*outerTable, "right", 500.0);
|
||||
outer.bottom = BoundedNumber(*outerTable, "bottom", 500.0);
|
||||
outer.left = BoundedNumber(*outerTable, "left", 500.0);
|
||||
if (outer.top < 0.0 || outer.right < 0.0 || outer.bottom < 0.0 || outer.left < 0.0)
|
||||
{
|
||||
SetError(error, "candidate_window.outer_insets_dip 数值超出范围");
|
||||
return std::nullopt;
|
||||
}
|
||||
package.outerInsetsDip = outer;
|
||||
}
|
||||
|
||||
if (const toml::node *decorationsNode = window->get("decorations"))
|
||||
{
|
||||
const auto *decorationsArr = decorationsNode->as_array();
|
||||
if (!decorationsArr)
|
||||
{
|
||||
SetError(error, "candidate_window.decorations 必须是数组");
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &decNode : *decorationsArr)
|
||||
{
|
||||
const auto *decTable = decNode.as_table();
|
||||
FreeDecoration dec;
|
||||
if (!decTable || !decTable->contains("image") || !ReadResource(*decTable, "image", dec.image))
|
||||
{
|
||||
SetError(error, "candidate_window.decorations.image 无效");
|
||||
return std::nullopt;
|
||||
}
|
||||
if (decTable->contains("anchor") &&
|
||||
!ReadEnum(*decTable, "anchor",
|
||||
{"top-left", "top-right", "bottom-left", "bottom-right", "left", "right", "top",
|
||||
"bottom", "center"},
|
||||
dec.anchor))
|
||||
{
|
||||
SetError(error, "candidate_window.decorations.anchor 无效");
|
||||
return std::nullopt;
|
||||
}
|
||||
// 这三个键可选,缺省时 table::get 返回空指针,必须经 node_view 取值
|
||||
dec.offsetXDip = (*decTable)["offset_x_dip"].value_or(0.0);
|
||||
dec.offsetYDip = (*decTable)["offset_y_dip"].value_or(0.0);
|
||||
dec.widthDip = BoundedNumber(*decTable, "width_dip", 2000.0);
|
||||
dec.heightDip = BoundedNumber(*decTable, "height_dip", 2000.0);
|
||||
dec.zIndex = static_cast<int>((*decTable)["z_index"].value_or(int64_t{2}));
|
||||
if (dec.widthDip <= 0.0 || dec.heightDip <= 0.0)
|
||||
{
|
||||
SetError(error, "candidate_window.decorations 的 width_dip 与 height_dip 必须大于 0");
|
||||
return std::nullopt;
|
||||
}
|
||||
package.freeDecorations.push_back(dec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto *candidate = root["candidate"].as_table();
|
||||
if (candidate && (!ReadColors((*candidate)["dark"].as_table(), package.dark) ||
|
||||
!ReadColors((*candidate)["light"].as_table(), package.light)))
|
||||
@@ -484,6 +644,21 @@ std::optional<Package> Load(const std::filesystem::path &skinsRoot, const std::s
|
||||
SetError(error, "找不到 candidate_window.background.image 文件");
|
||||
return std::nullopt;
|
||||
}
|
||||
if (package.borderImage && !package.borderImage->source.empty() &&
|
||||
!std::filesystem::is_regular_file(directory / std::filesystem::u8path(package.borderImage->source), ec))
|
||||
{
|
||||
SetError(error, "找不到 candidate_window.border_image.source 文件");
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &dec : package.freeDecorations)
|
||||
{
|
||||
if (!dec.image.empty() &&
|
||||
!std::filesystem::is_regular_file(directory / std::filesystem::u8path(dec.image), ec))
|
||||
{
|
||||
SetError(error, "找不到 candidate_window.decorations.image 文件");
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return package;
|
||||
}
|
||||
catch (const toml::parse_error &)
|
||||
|
||||
@@ -57,8 +57,44 @@ struct ToolbarColors
|
||||
std::string hover;
|
||||
};
|
||||
|
||||
struct BorderImage
|
||||
{
|
||||
std::string source;
|
||||
// 四边切片大小(DIP):top, right, bottom, left
|
||||
std::vector<double> sliceDip;
|
||||
// 可拉伸区(上右下左,从面板外缘量起):给出时覆盖 sliceDip。
|
||||
// 面板图里画死的装饰必须落在可拉伸区之外,否则会被横向拉长。
|
||||
std::vector<double> stretchDip;
|
||||
// 四边宽度(DIP):top, right, bottom, left;若为空则沿用 sliceDip
|
||||
std::vector<double> widthDip;
|
||||
// 是否填充中间内容区域
|
||||
bool fill = true;
|
||||
};
|
||||
|
||||
struct InsetsDip
|
||||
{
|
||||
double top = 0.0;
|
||||
double right = 0.0;
|
||||
double bottom = 0.0;
|
||||
double left = 0.0;
|
||||
};
|
||||
|
||||
struct FreeDecoration
|
||||
{
|
||||
std::string image;
|
||||
// 锚定基准:top-left, top-right, bottom-left, bottom-right, left, right, top, bottom, center
|
||||
std::string anchor = "right";
|
||||
double offsetXDip = 0.0;
|
||||
double offsetYDip = 0.0;
|
||||
double widthDip = 0.0;
|
||||
double heightDip = 0.0;
|
||||
int zIndex = 2;
|
||||
};
|
||||
|
||||
struct Package
|
||||
{
|
||||
// 皮肤规范版本:1 为通用现代卡片皮肤,2 为支持九宫格与自由挂件的高级皮肤。
|
||||
int schemaVersion = 1;
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string version;
|
||||
@@ -97,6 +133,17 @@ struct Package
|
||||
std::optional<double> toolbarCornerRadiusDip;
|
||||
ToolbarColors toolbarDark;
|
||||
ToolbarColors toolbarLight;
|
||||
// v2 专有特性:九宫格贴图边框
|
||||
std::optional<BorderImage> borderImage;
|
||||
// v2 专有特性:面板内容/文字安全内边距,从面板外缘量起(渲染时扣掉边框宽度)。
|
||||
std::optional<InsetsDip> contentInsetsDip;
|
||||
// v2 专有特性:预编辑行与候选行之间的行距(DIP,0–100);未设置时沿用 base 的行距。
|
||||
std::optional<double> lineGapDip;
|
||||
// v2 专有特性:面板之外需要额外预留的窗口空间(上右下左 DIP),给挂在卡片外沿的挂件用。
|
||||
// 不声明时窗口只按卡片大小开,挂到外面的立绘会被窗口边界裁掉。
|
||||
std::optional<InsetsDip> outerInsetsDip;
|
||||
// v2 专有特性:多层自由锚定挂件(支持动图)
|
||||
std::vector<FreeDecoration> freeDecorations;
|
||||
};
|
||||
|
||||
struct Issue
|
||||
|
||||
@@ -75,6 +75,11 @@ bool ApplyConfiguredFloatingToolbarAppearance(std::function<void()> onComplete);
|
||||
// Geometry declared by the active external skin package. Built-in skins return 0.
|
||||
double GetActiveCandidateSkinDecorationTopDip();
|
||||
double GetActiveCandidateSkinDecorationWidthDip();
|
||||
// v2 皮肤为挂在卡片外沿的挂件声明的预留空白(DIP)。没有就返回 0。
|
||||
double GetActiveCandidateSkinOuterTopDip();
|
||||
double GetActiveCandidateSkinOuterRightDip();
|
||||
double GetActiveCandidateSkinOuterBottomDip();
|
||||
double GetActiveCandidateSkinOuterLeftDip();
|
||||
// Push half-monitor CSS max width/height (DIP) into a small-window page.
|
||||
void InjectSurfaceViewportLimits(ICoreWebView2 *webview, HWND hwnd);
|
||||
// WebView2 rasterization scale includes both monitor DPI and the user's text
|
||||
|
||||
@@ -38,6 +38,45 @@ double GetActiveCandidateSkinDecorationWidthDip()
|
||||
return activeExternalCandidateSkin ? activeExternalCandidateSkin->decorationWidthDip : 0.0;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
double ActiveOuterInset(bool top, bool right, bool bottom)
|
||||
{
|
||||
if (!activeExternalCandidateSkin || !activeExternalCandidateSkin->outerInsetsDip)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
const CandidateSkinCatalog::InsetsDip &insets = *activeExternalCandidateSkin->outerInsetsDip;
|
||||
if (top)
|
||||
return insets.top;
|
||||
if (right)
|
||||
return insets.right;
|
||||
if (bottom)
|
||||
return insets.bottom;
|
||||
return insets.left;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
double GetActiveCandidateSkinOuterTopDip()
|
||||
{
|
||||
return ActiveOuterInset(true, false, false);
|
||||
}
|
||||
|
||||
double GetActiveCandidateSkinOuterRightDip()
|
||||
{
|
||||
return ActiveOuterInset(false, true, false);
|
||||
}
|
||||
|
||||
double GetActiveCandidateSkinOuterBottomDip()
|
||||
{
|
||||
return ActiveOuterInset(false, false, true);
|
||||
}
|
||||
|
||||
double GetActiveCandidateSkinOuterLeftDip()
|
||||
{
|
||||
return ActiveOuterInset(false, false, false);
|
||||
}
|
||||
|
||||
std::wstring ReadHtmlFile(const std::wstring &filePath)
|
||||
{
|
||||
std::ifstream file(filePath, std::ios::binary);
|
||||
@@ -367,6 +406,13 @@ std::wstring BuildExternalCandidateSkinCss(const CandidateSkinCatalog::Package &
|
||||
css.append(L" center / contain no-repeat; pointer-events: none; }\n"
|
||||
L".container { position: relative; z-index: 1; }\n");
|
||||
}
|
||||
if (skin.outerInsetsDip && (skin.outerInsetsDip->left > 0.0 || skin.outerInsetsDip->top > 0.0))
|
||||
{
|
||||
// MarginLeft / MarginTop 指向外留白信封的左上角,这里把卡片连同挂件相对定位回卡片原位。
|
||||
// 用相对定位而不是 padding:.containerParent 是挂件的定位容器,加 padding 会把挂件锚点挪到信封上。
|
||||
css.append(fmt::format(L".containerParent {{ position: relative; left: {}px; top: {}px; }}\n",
|
||||
skin.outerInsetsDip->left, skin.outerInsetsDip->top));
|
||||
}
|
||||
AppendExternalCandidateCornerCss(css, skin);
|
||||
const bool light = ResolveConfiguredTheme(GetConfiguredThemeCand()) == "light";
|
||||
const CandidateSkinCatalog::CandidateColors &colors = light ? skin.light : skin.dark;
|
||||
@@ -394,9 +440,151 @@ std::wstring BuildExternalCandidateSkinCss(const CandidateSkinCatalog::Package &
|
||||
L"background-clip: border-box; }}\n",
|
||||
size));
|
||||
}
|
||||
if (skin.borderImage)
|
||||
{
|
||||
// v2 专有九宫格贴图边框。stretch_dip 给出时它才是可拉伸区,语义比 slice_dip 直白:
|
||||
// 面板图里画死的装饰放在它之外就保持 1:1,不会被横向拉长。
|
||||
const auto &bi = *skin.borderImage;
|
||||
const std::vector<double> &sliceSrc = bi.stretchDip.empty() ? bi.sliceDip : bi.stretchDip;
|
||||
std::wstring sliceStr;
|
||||
for (size_t i = 0; i < sliceSrc.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
sliceStr += L" ";
|
||||
sliceStr += fmt::format(L"{}", sliceSrc[i]);
|
||||
}
|
||||
if (bi.fill)
|
||||
sliceStr += L" fill";
|
||||
|
||||
const std::vector<double> &widthSrc = bi.widthDip.empty() ? sliceSrc : bi.widthDip;
|
||||
std::wstring widthStr;
|
||||
for (size_t i = 0; i < widthSrc.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
widthStr += L" ";
|
||||
widthStr += fmt::format(L"{}px", widthSrc[i]);
|
||||
}
|
||||
|
||||
css.append(L".container:not(:empty) { border-style: solid; border-image-source: ");
|
||||
css.append(ManifestImageCssUrl(skinsRoot, skin.id, bi.source));
|
||||
// border-width 决定内容盒的内缩量,必须与 border-image-width 一致;只写 border-image-width
|
||||
// 时内容盒仍按基础皮肤的 border 宽度内缩,文字会压在贴图上。
|
||||
css.append(fmt::format(L"; border-width: {}; border-image-slice: {}; border-image-width: {}; "
|
||||
L"border-image-repeat: stretch; "
|
||||
L"background-color: transparent; border-color: transparent; border-radius: 0; }}\n",
|
||||
widthStr, sliceStr, widthStr));
|
||||
}
|
||||
if (skin.contentInsetsDip)
|
||||
{
|
||||
// 四值展开成「上右下左」,1 值全边、2 值纵横、4 值四边,与 CSS 一致。
|
||||
const auto expand4 = [](const std::vector<double> &v, double out[4]) {
|
||||
if (v.size() == 1)
|
||||
out[0] = out[1] = out[2] = out[3] = v[0];
|
||||
else if (v.size() == 2)
|
||||
{
|
||||
out[0] = out[2] = v[0];
|
||||
out[1] = out[3] = v[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
out[i] = i < v.size() ? v[i] : 0.0;
|
||||
}
|
||||
};
|
||||
double borderPx[4] = {0.0, 0.0, 0.0, 0.0};
|
||||
if (skin.borderImage)
|
||||
{
|
||||
const auto &bi = *skin.borderImage;
|
||||
const std::vector<double> &widthSrc =
|
||||
bi.widthDip.empty() ? (bi.stretchDip.empty() ? bi.sliceDip : bi.stretchDip) : bi.widthDip;
|
||||
expand4(widthSrc, borderPx);
|
||||
}
|
||||
// content_insets_dip 从面板外缘量起,而 CSS 的 padding 从边框内缘起算,这里把边框宽度扣掉。
|
||||
const auto &insets = *skin.contentInsetsDip;
|
||||
const double raw[4] = {insets.top, insets.right, insets.bottom, insets.left};
|
||||
double padding[4] = {0.0, 0.0, 0.0, 0.0};
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
padding[i] = (std::max)(0.0, raw[i] - borderPx[i]);
|
||||
css.append(fmt::format(L".container:not(:empty) {{ padding: {}px {}px {}px {}px !important; }}\n", padding[0],
|
||||
padding[1], padding[2], padding[3]));
|
||||
}
|
||||
if (skin.lineGapDip)
|
||||
{
|
||||
// 预编辑行与候选行之间的行距。基础皮肤把它并进 .row 的上下边距,这里显式覆盖。
|
||||
css.append(fmt::format(L".row.pinyin {{ margin-bottom: {}px; }}\n", *skin.lineGapDip));
|
||||
}
|
||||
// 自由挂件:数量不限。候选框页面里没有能承载任意数量挂件的元素,所以按皮肤声明逐个生成
|
||||
// 一份 CSS 规则,由 InjectExternalCandidateSkin 注入对应数量的空 div,挂件层数不再受
|
||||
// ::before / ::after 两层限制。
|
||||
if (!skin.freeDecorations.empty())
|
||||
{
|
||||
css.append(L".containerParent { position: relative; }\n");
|
||||
css.append(L".msime-skin-decor { position: absolute; pointer-events: none; "
|
||||
L"background-position: center; background-repeat: no-repeat; background-size: contain; }\n");
|
||||
}
|
||||
for (size_t idx = 0; idx < skin.freeDecorations.size(); ++idx)
|
||||
{
|
||||
const auto &dec = skin.freeDecorations[idx];
|
||||
// offset 语义统一:正数向右、向下,与 D2D 侧 CandidateSkinOverlay 一致。
|
||||
const double ox = dec.offsetXDip;
|
||||
const double oy = dec.offsetYDip;
|
||||
std::wstring posRule;
|
||||
if (dec.anchor == "top-left")
|
||||
posRule = fmt::format(L"left: {}px; top: {}px;", ox, oy);
|
||||
else if (dec.anchor == "top-right")
|
||||
posRule = fmt::format(L"right: {}px; top: {}px;", -ox, oy);
|
||||
else if (dec.anchor == "bottom-left")
|
||||
posRule = fmt::format(L"left: {}px; bottom: {}px;", ox, -oy);
|
||||
else if (dec.anchor == "bottom-right")
|
||||
posRule = fmt::format(L"right: {}px; bottom: {}px;", -ox, -oy);
|
||||
else if (dec.anchor == "left")
|
||||
posRule = fmt::format(L"left: {}px; top: 50%; transform: translateY(calc(-50% + {}px));", ox, oy);
|
||||
else if (dec.anchor == "right")
|
||||
posRule = fmt::format(L"right: {}px; top: 50%; transform: translateY(calc(-50% + {}px));", -ox, oy);
|
||||
else if (dec.anchor == "top")
|
||||
posRule = fmt::format(L"top: {}px; left: 50%; transform: translateX(calc(-50% + {}px));", oy, ox);
|
||||
else if (dec.anchor == "bottom")
|
||||
posRule = fmt::format(L"bottom: {}px; left: 50%; transform: translateX(calc(-50% + {}px));", -oy, ox);
|
||||
else
|
||||
posRule = fmt::format(L"left: 50%; top: 50%; "
|
||||
L"transform: translate(calc(-50% + {}px), calc(-50% + {}px));",
|
||||
ox, oy);
|
||||
|
||||
css.append(fmt::format(L".msime-skin-decor[data-i=\"{}\"] {{ z-index: {}; {} width: {}px; height: {}px; "
|
||||
L"background-image: ",
|
||||
idx, dec.zIndex, posRule, dec.widthDip, dec.heightDip));
|
||||
css.append(ManifestImageCssUrl(skinsRoot, skin.id, dec.image));
|
||||
css.append(L"; }\n");
|
||||
}
|
||||
return css;
|
||||
}
|
||||
|
||||
// 挂件层数的 CSS 已经按 data-i 生成,这里把对应数量的空 div 补进 .containerParent。
|
||||
// 页面自己的脚本只换 .container 的内容,不动 .containerParent,所以这些 div 不会被打掉。
|
||||
std::wstring BuildFreeDecorationLayerScript(const CandidateSkinCatalog::Package &skin)
|
||||
{
|
||||
if (skin.freeDecorations.empty())
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
std::wstring indices;
|
||||
for (size_t i = 0; i < skin.freeDecorations.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
indices += L",";
|
||||
indices += std::to_wstring(i);
|
||||
}
|
||||
return L"<script id=\"external-candidate-skin-decor\">(function(){"
|
||||
L"function add(){var parent=document.getElementById('realContainerParent');if(!parent){return;}"
|
||||
L"[" +
|
||||
indices +
|
||||
L"].forEach(function(i){if(parent.querySelector('.msime-skin-decor[data-i=\"'+i+'\"]')){return;}"
|
||||
L"var node=document.createElement('div');node.className='msime-skin-decor';"
|
||||
L"node.setAttribute('data-i',String(i));parent.appendChild(node);});}"
|
||||
L"if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',add);}else{add();}"
|
||||
L"})();</script>";
|
||||
}
|
||||
|
||||
bool InjectExternalCandidateSkin(std::wstring &html, const CandidateSkinCatalog::Package &skin,
|
||||
const std::wstring &skinsRoot)
|
||||
{
|
||||
@@ -420,7 +608,7 @@ bool InjectExternalCandidateSkin(std::wstring &html, const CandidateSkinCatalog:
|
||||
{
|
||||
return false;
|
||||
}
|
||||
html.insert(headEnd, vars + generated);
|
||||
html.insert(headEnd, vars + generated + BuildFreeDecorationLayerScript(skin));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "ipc/ipc.h"
|
||||
#include "log/candidate_diag_log.h"
|
||||
#include "skin/candidate_skin_catalog.h"
|
||||
#include "window/candidate_skin_overlay.h"
|
||||
#include "utils/common_utils.h"
|
||||
#include "utils/ime_utils.h"
|
||||
#include "utils/window_utils.h"
|
||||
@@ -335,6 +336,9 @@ struct CandidatePresenter::Impl
|
||||
std::shared_ptr<msimeui::StackPanel> root;
|
||||
std::shared_ptr<msimeui::Image> decoration;
|
||||
std::shared_ptr<msimeui::Card> card;
|
||||
// 卡片与 v2 挂件层放在同一格里:挂件层测量为 0,格子仍由卡片决定,挂件按 anchor 画到卡片外沿。
|
||||
std::shared_ptr<msimeui::Grid> overlayGrid;
|
||||
std::shared_ptr<CandidateSkinOverlay> skinOverlay;
|
||||
// 装饰图与卡片同列:列宽即卡片宽,装饰图在列内按 align 对齐,对应 CSS 里
|
||||
// .containerParent 为 fit-content、::before 相对它定位。
|
||||
std::shared_ptr<msimeui::StackPanel> cardColumn;
|
||||
@@ -449,11 +453,19 @@ void CandidatePresenter::RebuildScene()
|
||||
impl_->card->SetHorizontalAlignment(msimeui::HorizontalAlignment::Leading);
|
||||
impl_->card->AddChild(impl_->body);
|
||||
impl_->root->SetHorizontalContentAlignment(msimeui::HorizontalAlignment::Leading);
|
||||
impl_->overlayGrid = std::make_shared<msimeui::Grid>();
|
||||
impl_->overlayGrid->AddRow(msimeui::GridLength{msimeui::GridUnitType::Auto, 1.0f});
|
||||
impl_->overlayGrid->AddColumn(msimeui::GridLength{msimeui::GridUnitType::Auto, 1.0f});
|
||||
impl_->overlayGrid->AddChild(impl_->card, 0, 0);
|
||||
impl_->skinOverlay = std::make_shared<CandidateSkinOverlay>();
|
||||
impl_->skinOverlay->SetHorizontalAlignment(msimeui::HorizontalAlignment::Stretch);
|
||||
impl_->skinOverlay->SetVerticalAlignment(msimeui::VerticalAlignment::Stretch);
|
||||
impl_->overlayGrid->AddChild(impl_->skinOverlay, 0, 0);
|
||||
impl_->cardColumn = std::make_shared<msimeui::StackPanel>(0.0f);
|
||||
impl_->cardColumn->SetHorizontalAlignment(msimeui::HorizontalAlignment::Leading);
|
||||
impl_->cardColumn->SetHorizontalContentAlignment(msimeui::HorizontalAlignment::Stretch);
|
||||
impl_->cardColumn->AddChild(impl_->decoration);
|
||||
impl_->cardColumn->AddChild(impl_->card);
|
||||
impl_->cardColumn->AddChild(impl_->overlayGrid);
|
||||
impl_->frame = std::make_shared<msimeui::Container>();
|
||||
impl_->frame->SetPadding({kShadowPadLeft, kShadowPadTop, kShadowPadRight, kShadowPadBottom});
|
||||
impl_->frame->SetChild(impl_->cardColumn);
|
||||
@@ -1000,9 +1012,49 @@ void CandidatePresenter::ApplySkin()
|
||||
impl_->decoration->SetHorizontalAlignment(DecorationAlignment(package->decorationAlign));
|
||||
framePadTop = (std::max)(0.0f, kShadowPadTop - decorationTopDip_);
|
||||
}
|
||||
// v2 挂件挂在卡片外沿,窗口要按 outer_insets_dip 往四边多留出空白,否则立绘被窗口边界裁掉。
|
||||
float framePadLeft = kShadowPadLeft;
|
||||
float framePadRight = kShadowPadRight;
|
||||
float framePadBottom = kShadowPadBottom;
|
||||
if (package && package->outerInsetsDip)
|
||||
{
|
||||
const CandidateSkinCatalog::InsetsDip &outer = *package->outerInsetsDip;
|
||||
framePadLeft += static_cast<float>(outer.left);
|
||||
framePadRight += static_cast<float>(outer.right);
|
||||
framePadTop += static_cast<float>(outer.top);
|
||||
framePadBottom += static_cast<float>(outer.bottom);
|
||||
}
|
||||
if (impl_->frame)
|
||||
{
|
||||
impl_->frame->SetPadding({kShadowPadLeft, framePadTop, kShadowPadRight, kShadowPadBottom});
|
||||
impl_->frame->SetPadding({framePadLeft, framePadTop, framePadRight, framePadBottom});
|
||||
}
|
||||
|
||||
// v2 自由挂件交给 overlay 层绘制;它测量为 0,不参与卡片布局。
|
||||
if (impl_->skinOverlay)
|
||||
{
|
||||
std::vector<CandidateSkinOverlay::Item> items;
|
||||
if (package)
|
||||
{
|
||||
for (const CandidateSkinCatalog::FreeDecoration &dec : package->freeDecorations)
|
||||
{
|
||||
CandidateSkinOverlay::Item item;
|
||||
item.path = packageRoot + string_to_wstring(dec.image);
|
||||
item.anchor = dec.anchor;
|
||||
item.offsetXDip = static_cast<float>(dec.offsetXDip);
|
||||
item.offsetYDip = static_cast<float>(dec.offsetYDip);
|
||||
item.widthDip = static_cast<float>(dec.widthDip);
|
||||
item.heightDip = static_cast<float>(dec.heightDip);
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
}
|
||||
impl_->skinOverlay->SetItems(std::move(items));
|
||||
}
|
||||
// v2 的 line_gap_dip 落到预编辑行与候选列表之间的间距上。
|
||||
// TODO(v2 皮肤): 暂定 WebView2 独占。SetSpacing 作用于 body 的全部子元素,竖排时列表与翻页栏之间也会被拉开,
|
||||
// WebView2 只加在 .row.pinyin 下方。
|
||||
if (impl_->body)
|
||||
{
|
||||
impl_->body->SetSpacing(package && package->lineGapDip ? static_cast<float>(*package->lineGapDip) : 2.0f);
|
||||
}
|
||||
|
||||
msimeui::Brush brush;
|
||||
@@ -1014,10 +1066,50 @@ void CandidatePresenter::ApplySkin()
|
||||
if (impl_->card)
|
||||
{
|
||||
impl_->card->SetBrush(brush);
|
||||
impl_->card->SetPadding(tokens.containerPad);
|
||||
if (package && package->contentInsetsDip)
|
||||
{
|
||||
// v2 的 content_insets_dip 从面板外缘量起,D2D 侧直接落成卡片内边距(这里没有 CSS 的边框内缘概念)。
|
||||
const CandidateSkinCatalog::InsetsDip &insets = *package->contentInsetsDip;
|
||||
impl_->card->SetPadding(msimeui::Thickness{static_cast<float>(insets.left), static_cast<float>(insets.top),
|
||||
static_cast<float>(insets.right),
|
||||
static_cast<float>(insets.bottom)});
|
||||
}
|
||||
else
|
||||
{
|
||||
impl_->card->SetPadding(tokens.containerPad);
|
||||
}
|
||||
impl_->card->SetMinWidth(cardMinWidthDip_);
|
||||
impl_->card->SetBackgroundImage(backgroundPath, BackgroundStretch(package ? package->backgroundFit : ""),
|
||||
package ? static_cast<float>(package->backgroundOpacity) : 1.0f);
|
||||
if (package && package->borderImage && !package->borderImage->source.empty())
|
||||
{
|
||||
// v2 九宫格:四边原样、其余拉伸。切片按 1/2/4 值展开成「上右下左」。
|
||||
// TODO(v2 皮肤): 暂定 WebView2 独占。D2D 尚未处理 width_dip 与 fill = false,九宫格仍叠在卡片底色、
|
||||
// 描边与阴影之上并被卡片圆角裁剪;WebView2 会把这些清成透明、圆角归零。
|
||||
const CandidateSkinCatalog::BorderImage &bi = *package->borderImage;
|
||||
const std::vector<double> &values = bi.stretchDip.empty() ? bi.sliceDip : bi.stretchDip;
|
||||
const auto expand4 = [](const std::vector<double> &v, float out[4]) {
|
||||
if (v.size() == 1)
|
||||
out[0] = out[1] = out[2] = out[3] = static_cast<float>(v[0]);
|
||||
else if (v.size() == 2)
|
||||
{
|
||||
out[0] = out[2] = static_cast<float>(v[0]);
|
||||
out[1] = out[3] = static_cast<float>(v[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
out[i] = i < v.size() ? static_cast<float>(v[i]) : 0.0f;
|
||||
}
|
||||
};
|
||||
float slice[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
expand4(values, slice);
|
||||
impl_->card->SetBackgroundImageNineSlice(packageRoot + string_to_wstring(bi.source), slice[3], slice[0],
|
||||
slice[1], slice[2], 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
impl_->card->SetBackgroundImage(backgroundPath, BackgroundStretch(package ? package->backgroundFit : ""),
|
||||
package ? static_cast<float>(package->backgroundOpacity) : 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "window/candidate_skin_overlay.h"
|
||||
|
||||
#include "msimeui/DeviceResources.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
void CandidateSkinOverlay::SetItems(std::vector<Item> items)
|
||||
{
|
||||
items_ = std::move(items);
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
msimeui::SizeF CandidateSkinOverlay::Measure(const msimeui::SizeF & /*availableSize*/)
|
||||
{
|
||||
// 挂件是浮在卡片外面的装饰,不参与测量:测量为 0 时父级网格的格子仍由卡片决定,
|
||||
// 卡片不会被立绘撑大或被挤偏。
|
||||
return {0.0f, 0.0f};
|
||||
}
|
||||
|
||||
void CandidateSkinOverlay::Arrange(const msimeui::RectF &finalRect)
|
||||
{
|
||||
bounds_ = finalRect;
|
||||
}
|
||||
|
||||
bool CandidateSkinOverlay::HitTest(const msimeui::PointF & /*point*/) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(v2 皮肤): v2 自由挂件暂定 WebView2 独占,D2D 这里尚未按 z_index 分层,全部按声明顺序画在卡片之上。
|
||||
void CandidateSkinOverlay::Render(msimeui::DeviceResources &deviceResources)
|
||||
{
|
||||
if (items_.empty() || bounds_.width <= 0.0f || bounds_.height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ID2D1RenderTarget *target = deviceResources.GetRenderTarget();
|
||||
if (!target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const Item &item : items_)
|
||||
{
|
||||
D2D1_SIZE_F sourceSize = {};
|
||||
ID2D1Bitmap *bitmap = deviceResources.GetBitmapFromFile(item.path, &sourceSize);
|
||||
if (!bitmap || sourceSize.width <= 0.0f || sourceSize.height <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float width = item.widthDip > 0.0f ? item.widthDip : sourceSize.width;
|
||||
const float height = item.heightDip > 0.0f ? item.heightDip : sourceSize.height;
|
||||
const float right = bounds_.x + bounds_.width;
|
||||
const float bottom = bounds_.y + bounds_.height;
|
||||
float x = right - width;
|
||||
float y = bounds_.y + (bounds_.height - height) * 0.5f;
|
||||
const std::string &anchor = item.anchor;
|
||||
if (anchor == "top-left")
|
||||
{
|
||||
x = bounds_.x;
|
||||
y = bounds_.y;
|
||||
}
|
||||
else if (anchor == "top-right")
|
||||
{
|
||||
x = right - width;
|
||||
y = bounds_.y;
|
||||
}
|
||||
else if (anchor == "bottom-left")
|
||||
{
|
||||
x = bounds_.x;
|
||||
y = bottom - height;
|
||||
}
|
||||
else if (anchor == "bottom-right")
|
||||
{
|
||||
x = right - width;
|
||||
y = bottom - height;
|
||||
}
|
||||
else if (anchor == "left")
|
||||
{
|
||||
x = bounds_.x;
|
||||
}
|
||||
else if (anchor == "top")
|
||||
{
|
||||
x = bounds_.x + (bounds_.width - width) * 0.5f;
|
||||
y = bounds_.y;
|
||||
}
|
||||
else if (anchor == "bottom")
|
||||
{
|
||||
x = bounds_.x + (bounds_.width - width) * 0.5f;
|
||||
y = bottom - height;
|
||||
}
|
||||
else if (anchor == "center")
|
||||
{
|
||||
x = bounds_.x + (bounds_.width - width) * 0.5f;
|
||||
y = bounds_.y + (bounds_.height - height) * 0.5f;
|
||||
}
|
||||
x += item.offsetXDip;
|
||||
y += item.offsetYDip;
|
||||
const D2D1_RECT_F destination = D2D1::RectF(x, y, x + width, y + height);
|
||||
const D2D1_RECT_F source = D2D1::RectF(0.0f, 0.0f, sourceSize.width, sourceSize.height);
|
||||
target->DrawBitmap(bitmap, destination, 1.0f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, &source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "msimeui/Layout.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace msimeui
|
||||
{
|
||||
class DeviceResources;
|
||||
}
|
||||
|
||||
// v2 皮肤的自由挂件层。它不参与布局(测量恒为 0),由父级摆到卡片那一格,
|
||||
// 内部按 anchor + offset 把挂件画到卡片边界之外,所以窗口必须按 outer_insets_dip 多留出空间。
|
||||
class CandidateSkinOverlay : public msimeui::Visual
|
||||
{
|
||||
public:
|
||||
struct Item
|
||||
{
|
||||
std::wstring path;
|
||||
// top-left / top-right / bottom-left / bottom-right / left / right / top / bottom / center
|
||||
std::string anchor = "right";
|
||||
// 相对锚点的平移量,正数向右、向下。
|
||||
float offsetXDip = 0.0f;
|
||||
float offsetYDip = 0.0f;
|
||||
float widthDip = 0.0f;
|
||||
float heightDip = 0.0f;
|
||||
};
|
||||
|
||||
void SetItems(std::vector<Item> items);
|
||||
|
||||
msimeui::SizeF Measure(const msimeui::SizeF &availableSize) override;
|
||||
void Arrange(const msimeui::RectF &finalRect) override;
|
||||
void Render(msimeui::DeviceResources &deviceResources) override;
|
||||
// 挂件纯装饰,叠在卡片那一格上方,点击必须穿透给候选。
|
||||
bool HitTest(const msimeui::PointF &point) const override;
|
||||
|
||||
private:
|
||||
std::vector<Item> items_;
|
||||
};
|
||||
@@ -89,6 +89,26 @@ double GetCandidateDecorationWidthDip()
|
||||
{
|
||||
return GetActiveCandidateSkinDecorationWidthDip();
|
||||
}
|
||||
|
||||
double GetCandidateOuterTopDip()
|
||||
{
|
||||
return GetActiveCandidateSkinOuterTopDip();
|
||||
}
|
||||
|
||||
double GetCandidateOuterRightDip()
|
||||
{
|
||||
return GetActiveCandidateSkinOuterRightDip();
|
||||
}
|
||||
|
||||
double GetCandidateOuterBottomDip()
|
||||
{
|
||||
return GetActiveCandidateSkinOuterBottomDip();
|
||||
}
|
||||
|
||||
double GetCandidateOuterLeftDip()
|
||||
{
|
||||
return GetActiveCandidateSkinOuterLeftDip();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace ime_windows_detail
|
||||
@@ -104,11 +124,15 @@ int GetCandidatePackingMarginTopDip()
|
||||
std::pair<double, double> AddCandidateDecorationToSize(const std::pair<double, double> &cardSize)
|
||||
{
|
||||
const double decorationTopDip = GetCandidateDecorationTopDip();
|
||||
if (decorationTopDip <= 0.0)
|
||||
std::pair<double, double> size = cardSize;
|
||||
if (decorationTopDip > 0.0)
|
||||
{
|
||||
return cardSize;
|
||||
size = {(std::max)(size.first, GetCandidateDecorationWidthDip()), size.second + decorationTopDip};
|
||||
}
|
||||
return {(std::max)(cardSize.first, GetCandidateDecorationWidthDip()), cardSize.second + decorationTopDip};
|
||||
// v2 挂件挂在卡片外沿:窗口在卡片四周再各留出一块透明空白,立绘才不会被窗口边界裁掉。
|
||||
size.first += GetCandidateOuterLeftDip() + GetCandidateOuterRightDip();
|
||||
size.second += GetCandidateOuterTopDip() + GetCandidateOuterBottomDip();
|
||||
return size;
|
||||
}
|
||||
|
||||
int GetCandidateOuterTopPx(int cardAnchorY, int packingTopDip, FLOAT scale)
|
||||
@@ -117,10 +141,20 @@ int GetCandidateOuterTopPx(int cardAnchorY, int packingTopDip, FLOAT scale)
|
||||
{
|
||||
scale = 1.0f;
|
||||
}
|
||||
const double outerOffsetDip = static_cast<double>(packingTopDip) - GetCandidateDecorationTopDip();
|
||||
const double outerOffsetDip =
|
||||
static_cast<double>(packingTopDip) - GetCandidateDecorationTopDip() - GetCandidateOuterTopDip();
|
||||
return cardAnchorY + static_cast<int>(std::lround(outerOffsetDip * static_cast<double>(scale)));
|
||||
}
|
||||
|
||||
int GetCandidateOuterLeftPx(int cardAnchorX, FLOAT scale)
|
||||
{
|
||||
if (scale <= 0.0f)
|
||||
{
|
||||
scale = 1.0f;
|
||||
}
|
||||
return cardAnchorX - static_cast<int>(std::lround(GetCandidateOuterLeftDip() * static_cast<double>(scale)));
|
||||
}
|
||||
|
||||
void RememberCandidateClip(const std::pair<double, double> &decoratedSize, FLOAT scale, int caretX, int caretY)
|
||||
{
|
||||
g_has_last_candidate_clip = decoratedSize.first > 1.0 && decoratedSize.second > 1.0;
|
||||
@@ -257,8 +291,9 @@ void ReanchorCandidateHostToPaintedCard(const POINT &layoutCaret, const std::pai
|
||||
hostY = coordinates.top + edgePadPx;
|
||||
}
|
||||
}
|
||||
const int desiredOuterLeftPx = GetCandidateOuterLeftPx(anchorPos->first, scale);
|
||||
Global::MarginLeft =
|
||||
(std::max)(0, static_cast<int>(std::lround((anchorPos->first - hostX) / static_cast<double>(scale))));
|
||||
(std::max)(0, static_cast<int>(std::lround((desiredOuterLeftPx - hostX) / static_cast<double>(scale))));
|
||||
Global::MarginTop = GetCandidateOuterMarginDip(desiredOuterTopPx, hostY, scale);
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,8 @@ void PlaceCandidateHostNearCaret(HWND hwnd, std::pair<double, double> requestedC
|
||||
RememberCandidateFlip(properPos->second, caretPt.y);
|
||||
|
||||
MonitorCoordinates coordinates = GetMonitorCoordinatesFromPoint(caretPt);
|
||||
int hostX = properPos->first -
|
||||
const int desiredOuterLeftPx = GetCandidateOuterLeftPx(properPos->first, layoutScale);
|
||||
int hostX = desiredOuterLeftPx -
|
||||
static_cast<int>(std::lround(::CANDIDATE_SHADOW_PAD_LEFT * static_cast<double>(layoutScale)));
|
||||
const int packingMarginTop = GetCandidatePackingMarginTopDip();
|
||||
int desiredOuterTopPx = GetCandidateOuterTopPx(properPos->second, packingMarginTop, layoutScale);
|
||||
@@ -212,7 +213,8 @@ void PlaceCandidateHostNearCaret(HWND hwnd, std::pair<double, double> requestedC
|
||||
hostY = coordinates.top + edgePadPx;
|
||||
}
|
||||
|
||||
const int offsetXDip = static_cast<int>(std::lround((properPos->first - hostX) / static_cast<double>(layoutScale)));
|
||||
const int offsetXDip =
|
||||
static_cast<int>(std::lround((desiredOuterLeftPx - hostX) / static_cast<double>(layoutScale)));
|
||||
Global::MarginLeft = (std::max)(0, offsetXDip);
|
||||
Global::MarginTop = GetCandidateOuterMarginDip(desiredOuterTopPx, hostY, layoutScale);
|
||||
const int marginLeftBeforeKeep = Global::MarginLeft;
|
||||
@@ -574,7 +576,8 @@ int FineTuneWindow(HWND hwnd)
|
||||
int hostHeightPx = hostPx.second;
|
||||
|
||||
MonitorCoordinates coordinates = GetMonitorCoordinatesFromPoint(pt);
|
||||
int hostX = properPos->first -
|
||||
int desiredOuterLeftPx = GetCandidateOuterLeftPx(properPos->first, layoutScale);
|
||||
int hostX = desiredOuterLeftPx -
|
||||
static_cast<int>(std::lround(::CANDIDATE_SHADOW_PAD_LEFT * static_cast<double>(layoutScale)));
|
||||
const int packingMarginTop = GetCandidatePackingMarginTopDip();
|
||||
int desiredOuterTopPx = GetCandidateOuterTopPx(properPos->second, packingMarginTop, layoutScale);
|
||||
@@ -603,7 +606,7 @@ int FineTuneWindow(HWND hwnd)
|
||||
|
||||
auto applyCardMargins = [&](FLOAT marginScale) {
|
||||
const int offsetXDip =
|
||||
static_cast<int>(std::lround((properPos->first - hostX) / static_cast<double>(marginScale)));
|
||||
static_cast<int>(std::lround((desiredOuterLeftPx - hostX) / static_cast<double>(marginScale)));
|
||||
Global::MarginLeft = (std::max)(0, offsetXDip);
|
||||
Global::MarginTop = GetCandidateOuterMarginDip(desiredOuterTopPx, hostY, marginScale);
|
||||
};
|
||||
@@ -693,7 +696,8 @@ int FineTuneWindow(HWND hwnd)
|
||||
AdjustCandidateWindowPosition(&pt, containerSize, properPos, layoutScale, containerSize.first);
|
||||
RememberCandidateFlip(properPos->second, caretY);
|
||||
const int resyncPackingMarginTop = GetCandidatePackingMarginTopDip();
|
||||
hostX = properPos->first -
|
||||
desiredOuterLeftPx = GetCandidateOuterLeftPx(properPos->first, layoutScale);
|
||||
hostX = desiredOuterLeftPx -
|
||||
static_cast<int>(std::lround(::CANDIDATE_SHADOW_PAD_LEFT * static_cast<double>(layoutScale)));
|
||||
desiredOuterTopPx = GetCandidateOuterTopPx(properPos->second, resyncPackingMarginTop, layoutScale);
|
||||
hostY = desiredOuterTopPx -
|
||||
@@ -718,7 +722,7 @@ int FineTuneWindow(HWND hwnd)
|
||||
}
|
||||
|
||||
const int offsetXDip =
|
||||
static_cast<int>(std::lround((properPos->first - hostX) / static_cast<double>(layoutScale)));
|
||||
static_cast<int>(std::lround((desiredOuterLeftPx - hostX) / static_cast<double>(layoutScale)));
|
||||
Global::MarginLeft = (std::max)(0, offsetXDip);
|
||||
Global::MarginTop = GetCandidateOuterMarginDip(desiredOuterTopPx, hostY, layoutScale);
|
||||
KeepCandidateCardInsideHostAndMonitor(hostX, hostY, hostWidthPx, hostHeightPx, decoratedSize.first,
|
||||
@@ -875,8 +879,9 @@ int FineTuneWindow(HWND hwnd)
|
||||
const int hostHeightPx = pass2Host.second;
|
||||
|
||||
MonitorCoordinates coordinates = GetMonitorCoordinatesFromPoint(pt);
|
||||
int hostX = properPos->first - static_cast<int>(std::lround(::CANDIDATE_SHADOW_PAD_LEFT *
|
||||
static_cast<double>(pass2Scale)));
|
||||
const int desiredOuterLeftPx = GetCandidateOuterLeftPx(properPos->first, pass2Scale);
|
||||
int hostX = desiredOuterLeftPx - static_cast<int>(std::lround(::CANDIDATE_SHADOW_PAD_LEFT *
|
||||
static_cast<double>(pass2Scale)));
|
||||
const int desiredOuterTopPx = GetCandidateOuterTopPx(properPos->second, packingTop, pass2Scale);
|
||||
int hostY =
|
||||
desiredOuterTopPx -
|
||||
@@ -900,8 +905,8 @@ int FineTuneWindow(HWND hwnd)
|
||||
hostY = coordinates.top + edgePadPx;
|
||||
}
|
||||
|
||||
const int offsetXDip =
|
||||
static_cast<int>(std::lround((properPos->first - hostX) / static_cast<double>(pass2Scale)));
|
||||
const int offsetXDip = static_cast<int>(
|
||||
std::lround((desiredOuterLeftPx - hostX) / static_cast<double>(pass2Scale)));
|
||||
Global::MarginLeft = (std::max)(0, offsetXDip);
|
||||
Global::MarginTop = GetCandidateOuterMarginDip(desiredOuterTopPx, hostY, pass2Scale);
|
||||
KeepCandidateCardInsideHostAndMonitor(hostX, hostY, hostWidthPx, hostHeightPx,
|
||||
|
||||
@@ -109,6 +109,9 @@ double GetCandidateDecorationTopDip();
|
||||
int GetCandidatePackingMarginTopDip();
|
||||
std::pair<double, double> AddCandidateDecorationToSize(const std::pair<double, double> &cardSize);
|
||||
int GetCandidateOuterTopPx(int cardAnchorY, int packingTopDip, FLOAT scale);
|
||||
// 外留白信封的左边(屏幕 px):卡片左边再往左让出 v2 的 outer_insets_dip.left。
|
||||
// MarginLeft 与 MarginTop 一样指向信封左上角,卡片由皮肤 CSS 相对定位回原位。
|
||||
int GetCandidateOuterLeftPx(int cardAnchorX, FLOAT scale);
|
||||
void ClipCandidateWindowToContent(HWND hwnd, const std::pair<double, double> &containerSize, FLOAT scale,
|
||||
double extraTopDip = 0.0);
|
||||
void ClearCandidateWindowRegion(HWND hwnd);
|
||||
|
||||
@@ -192,3 +192,140 @@ TEST_CASE(candidate_skin_catalog_rejects_invalid_toolbar_tables)
|
||||
REQUIRE(LoadFails(L"tb-radius", window + "[toolbar]\ncorner_radius_dip = 40\n"));
|
||||
REQUIRE(LoadFails(L"tb-radius-type", window + "[toolbar]\ncorner_radius_dip = \"8px\"\n"));
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_skin_catalog_reads_v2_features)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path skins_root =
|
||||
fs::temp_directory_path() / (L"msime-skin-v2-" + std::to_wstring(GetCurrentProcessId())) / L"skins";
|
||||
std::error_code ec;
|
||||
fs::remove_all(skins_root, ec);
|
||||
fs::create_directories(skins_root / L"art" / L"assets", ec);
|
||||
REQUIRE(!ec);
|
||||
for (const wchar_t *image : {L"assets\\border.png", L"assets\\char.png"})
|
||||
{
|
||||
std::ofstream file(skins_root / L"art" / image, std::ios::binary | std::ios::trunc);
|
||||
REQUIRE(file.is_open());
|
||||
}
|
||||
std::ofstream manifest(skins_root / L"art" / L"skin.toml", std::ios::binary | std::ios::trunc);
|
||||
REQUIRE(manifest.is_open());
|
||||
manifest << R"toml(schema_version = 2
|
||||
id = "art"
|
||||
name = "Art"
|
||||
version = "2.0.0"
|
||||
base = "fluent"
|
||||
|
||||
[supports]
|
||||
layouts = ["horizontal"]
|
||||
themes = ["dark"]
|
||||
|
||||
[candidate_window]
|
||||
line_gap_dip = 6
|
||||
|
||||
[candidate_window.border_image]
|
||||
source = "assets/border.png"
|
||||
slice_dip = [16, 44, 16, 0]
|
||||
stretch_dip = [16, 90, 16, 0]
|
||||
width_dip = [16, 44, 16, 0]
|
||||
fill = true
|
||||
|
||||
[candidate_window.content_insets_dip]
|
||||
top = 4
|
||||
right = 130
|
||||
bottom = 14
|
||||
left = 55
|
||||
|
||||
[candidate_window.outer_insets_dip]
|
||||
top = 87
|
||||
right = 16
|
||||
bottom = 4
|
||||
left = 0
|
||||
|
||||
[[candidate_window.decorations]]
|
||||
image = "assets/char.png"
|
||||
anchor = "right"
|
||||
offset_x_dip = 10
|
||||
offset_y_dip = -15
|
||||
width_dip = 149
|
||||
height_dip = 161
|
||||
z_index = 3
|
||||
)toml";
|
||||
manifest.close();
|
||||
|
||||
std::string error;
|
||||
const auto package = CandidateSkinCatalog::Load(skins_root, "art", &error);
|
||||
REQUIRE(error.empty());
|
||||
REQUIRE(package.has_value());
|
||||
REQUIRE_EQ(package->schemaVersion, 2);
|
||||
REQUIRE(package->borderImage.has_value());
|
||||
REQUIRE_EQ(package->borderImage->source, std::string("assets/border.png"));
|
||||
REQUIRE_EQ(package->borderImage->sliceDip.size(), static_cast<size_t>(4));
|
||||
REQUIRE_EQ(package->borderImage->sliceDip[1], 44.0);
|
||||
// stretch_dip 与 slice_dip 分开存:前者才是可拉伸区,渲染时优先用它。
|
||||
REQUIRE_EQ(package->borderImage->stretchDip.size(), static_cast<size_t>(4));
|
||||
REQUIRE_EQ(package->borderImage->stretchDip[1], 90.0);
|
||||
REQUIRE(package->borderImage->fill);
|
||||
|
||||
REQUIRE(package->contentInsetsDip.has_value());
|
||||
REQUIRE_EQ(package->contentInsetsDip->left, 55.0);
|
||||
REQUIRE_EQ(package->contentInsetsDip->right, 130.0);
|
||||
|
||||
REQUIRE(package->lineGapDip.has_value());
|
||||
REQUIRE_EQ(*package->lineGapDip, 6.0);
|
||||
|
||||
REQUIRE(package->outerInsetsDip.has_value());
|
||||
REQUIRE_EQ(package->outerInsetsDip->top, 87.0);
|
||||
REQUIRE_EQ(package->outerInsetsDip->right, 16.0);
|
||||
|
||||
REQUIRE_EQ(package->freeDecorations.size(), static_cast<size_t>(1));
|
||||
REQUIRE_EQ(package->freeDecorations[0].anchor, std::string("right"));
|
||||
REQUIRE_EQ(package->freeDecorations[0].widthDip, 149.0);
|
||||
REQUIRE_EQ(package->freeDecorations[0].zIndex, 3);
|
||||
|
||||
fs::remove_all(skins_root, ec);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_skin_catalog_defaults_optional_decoration_keys)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path skins_root =
|
||||
fs::temp_directory_path() / (L"msime-skin-v2-decor-" + std::to_wstring(GetCurrentProcessId())) / L"skins";
|
||||
std::error_code ec;
|
||||
fs::remove_all(skins_root, ec);
|
||||
fs::create_directories(skins_root / L"art" / L"assets", ec);
|
||||
REQUIRE(!ec);
|
||||
{
|
||||
std::ofstream file(skins_root / L"art" / L"assets" / L"char.png", std::ios::binary | std::ios::trunc);
|
||||
REQUIRE(file.is_open());
|
||||
}
|
||||
std::ofstream manifest(skins_root / L"art" / L"skin.toml", std::ios::binary | std::ios::trunc);
|
||||
REQUIRE(manifest.is_open());
|
||||
// offset_x_dip、offset_y_dip、z_index 都可省略;缺省时不能解引用空节点。
|
||||
manifest << R"toml(schema_version = 2
|
||||
id = "art"
|
||||
name = "Art"
|
||||
version = "2.0.0"
|
||||
base = "fluent"
|
||||
|
||||
[supports]
|
||||
layouts = ["horizontal"]
|
||||
themes = ["dark"]
|
||||
|
||||
[[candidate_window.decorations]]
|
||||
image = "assets/char.png"
|
||||
width_dip = 40
|
||||
height_dip = 30
|
||||
)toml";
|
||||
manifest.close();
|
||||
|
||||
std::string error;
|
||||
const auto package = CandidateSkinCatalog::Load(skins_root, "art", &error);
|
||||
REQUIRE(error.empty());
|
||||
REQUIRE(package.has_value());
|
||||
REQUIRE_EQ(package->freeDecorations.size(), static_cast<size_t>(1));
|
||||
REQUIRE_EQ(package->freeDecorations[0].offsetXDip, 0.0);
|
||||
REQUIRE_EQ(package->freeDecorations[0].offsetYDip, 0.0);
|
||||
REQUIRE_EQ(package->freeDecorations[0].zIndex, 2);
|
||||
|
||||
fs::remove_all(skins_root, ec);
|
||||
}
|
||||
@@ -168,6 +168,7 @@ class StackPanel : public Panel
|
||||
{
|
||||
public:
|
||||
explicit StackPanel(float spacing = 0.0f);
|
||||
void SetSpacing(float spacing);
|
||||
void SetHorizontalContentAlignment(HorizontalAlignment alignment);
|
||||
void SetVerticalContentAlignment(VerticalAlignment alignment);
|
||||
|
||||
@@ -362,6 +363,8 @@ class Card : public Panel
|
||||
|
||||
void SetBrush(Brush brush);
|
||||
void SetPadding(float padding);
|
||||
// 四边独立的内边距,给 v2 皮肤的 content_insets_dip 用。
|
||||
void SetPadding(Thickness padding);
|
||||
void SetShadowScale(float scale);
|
||||
// Multiplies every shadow pass alpha; 1.0 keeps the built-in intensity.
|
||||
void SetShadowOpacity(float opacity);
|
||||
@@ -374,6 +377,10 @@ class Card : public Panel
|
||||
// card shape. An empty path removes it.
|
||||
void SetBackgroundImage(std::wstring filePath, ImageStretch stretch = ImageStretch::UniformToFill,
|
||||
float opacity = 1.0f);
|
||||
// 九宫格背景:四边按切片原样绘制,其余单向/双向拉伸,用来铺「面板即贴图」的 v2 皮肤。
|
||||
// 切片单位与源图像素一致;给出后覆盖 SetBackgroundImage 的普通背景。
|
||||
void SetBackgroundImageNineSlice(std::wstring filePath, float left, float top, float right, float bottom,
|
||||
float opacity = 1.0f);
|
||||
|
||||
SizeF Measure(const SizeF &availableSize) override;
|
||||
void Arrange(const RectF &finalRect) override;
|
||||
@@ -386,10 +393,15 @@ class Card : public Panel
|
||||
const D2D1_ROUNDED_RECT &shape);
|
||||
|
||||
Brush brush_;
|
||||
float padding_ = 0.0f;
|
||||
Thickness padding_ = {};
|
||||
std::wstring backgroundImage_;
|
||||
ImageStretch backgroundStretch_ = ImageStretch::UniformToFill;
|
||||
float backgroundOpacity_ = 1.0f;
|
||||
bool backgroundNineSlice_ = false;
|
||||
float sliceLeft_ = 0.0f;
|
||||
float sliceTop_ = 0.0f;
|
||||
float sliceRight_ = 0.0f;
|
||||
float sliceBottom_ = 0.0f;
|
||||
float shadowScale_ = 1.0f;
|
||||
float shadowOpacity_ = 1.0f;
|
||||
std::vector<ShadowPass> shadowPasses_;
|
||||
|
||||
+88
-9
@@ -933,6 +933,16 @@ StackPanel::StackPanel(float spacing) : spacing_(spacing)
|
||||
{
|
||||
}
|
||||
|
||||
void StackPanel::SetSpacing(float spacing)
|
||||
{
|
||||
if (spacing_ == spacing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
spacing_ = spacing;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
void StackPanel::SetHorizontalContentAlignment(HorizontalAlignment alignment)
|
||||
{
|
||||
horizontalContentAlignment_ = alignment;
|
||||
@@ -1835,7 +1845,7 @@ void Border::Render(DeviceResources &deviceResources)
|
||||
Container::Render(deviceResources);
|
||||
}
|
||||
|
||||
Card::Card(Brush brush, float padding) : brush_(brush), padding_(padding)
|
||||
Card::Card(Brush brush, float padding) : brush_(brush), padding_{padding, padding, padding, padding}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1847,7 +1857,13 @@ void Card::SetBrush(Brush brush)
|
||||
|
||||
void Card::SetPadding(float padding)
|
||||
{
|
||||
if (padding_ == padding)
|
||||
SetPadding(Thickness{padding, padding, padding, padding});
|
||||
}
|
||||
|
||||
void Card::SetPadding(Thickness padding)
|
||||
{
|
||||
if (padding_.left == padding.left && padding_.top == padding.top && padding_.right == padding.right &&
|
||||
padding_.bottom == padding.bottom)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1899,8 +1915,8 @@ void Card::SetShadowEnabled(bool enabled)
|
||||
|
||||
SizeF Card::Measure(const SizeF &availableSize)
|
||||
{
|
||||
const SizeF inner = {std::max(availableSize.width - padding_ * 2.0f, 0.0f),
|
||||
std::max(availableSize.height - padding_ * 2.0f, 0.0f)};
|
||||
const SizeF inner = {std::max(availableSize.width - padding_.left - padding_.right, 0.0f),
|
||||
std::max(availableSize.height - padding_.top - padding_.bottom, 0.0f)};
|
||||
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
@@ -1911,16 +1927,16 @@ SizeF Card::Measure(const SizeF &availableSize)
|
||||
height = std::max(height, childSize_.height);
|
||||
}
|
||||
|
||||
return {width + padding_ * 2.0f, height + padding_ * 2.0f};
|
||||
return {width + padding_.left + padding_.right, height + padding_.top + padding_.bottom};
|
||||
}
|
||||
|
||||
void Card::Arrange(const RectF &finalRect)
|
||||
{
|
||||
bounds_ = finalRect;
|
||||
|
||||
const RectF inner = {finalRect.x + padding_, finalRect.y + padding_,
|
||||
std::max(finalRect.width - padding_ * 2.0f, 0.0f),
|
||||
std::max(finalRect.height - padding_ * 2.0f, 0.0f)};
|
||||
const RectF inner = {finalRect.x + padding_.left, finalRect.y + padding_.top,
|
||||
std::max(finalRect.width - padding_.left - padding_.right, 0.0f),
|
||||
std::max(finalRect.height - padding_.top - padding_.bottom, 0.0f)};
|
||||
|
||||
for (const auto &child : children_)
|
||||
{
|
||||
@@ -1964,16 +1980,41 @@ void Card::Render(DeviceResources &deviceResources)
|
||||
void Card::SetBackgroundImage(std::wstring filePath, ImageStretch stretch, float opacity)
|
||||
{
|
||||
const float clamped = std::clamp(opacity, 0.0f, 1.0f);
|
||||
if (backgroundImage_ == filePath && backgroundStretch_ == stretch && backgroundOpacity_ == clamped)
|
||||
if (backgroundImage_ == filePath && !backgroundNineSlice_ && backgroundStretch_ == stretch &&
|
||||
backgroundOpacity_ == clamped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
backgroundImage_ = std::move(filePath);
|
||||
backgroundNineSlice_ = false;
|
||||
backgroundStretch_ = stretch;
|
||||
backgroundOpacity_ = clamped;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
void Card::SetBackgroundImageNineSlice(std::wstring filePath, float left, float top, float right, float bottom,
|
||||
float opacity)
|
||||
{
|
||||
const float clamped = std::clamp(opacity, 0.0f, 1.0f);
|
||||
const float l = (std::max)(0.0f, left);
|
||||
const float t = (std::max)(0.0f, top);
|
||||
const float r = (std::max)(0.0f, right);
|
||||
const float b = (std::max)(0.0f, bottom);
|
||||
if (backgroundImage_ == filePath && backgroundNineSlice_ && sliceLeft_ == l && sliceTop_ == t && sliceRight_ == r &&
|
||||
sliceBottom_ == b && backgroundOpacity_ == clamped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
backgroundImage_ = std::move(filePath);
|
||||
backgroundNineSlice_ = true;
|
||||
sliceLeft_ = l;
|
||||
sliceTop_ = t;
|
||||
sliceRight_ = r;
|
||||
sliceBottom_ = b;
|
||||
backgroundOpacity_ = clamped;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
void Card::RenderBackgroundImage(DeviceResources &deviceResources, ID2D1RenderTarget *target,
|
||||
const D2D1_ROUNDED_RECT &shape)
|
||||
{
|
||||
@@ -2019,6 +2060,44 @@ void Card::RenderBackgroundImage(DeviceResources &deviceResources, ID2D1RenderTa
|
||||
return;
|
||||
}
|
||||
target->PushLayer(D2D1::LayerParameters(D2D1::InfiniteRect(), clip.Get()), layer.Get());
|
||||
if (backgroundNineSlice_)
|
||||
{
|
||||
// 九宫格:四角原样、四边单向拉伸、中心双向拉伸。切片单位与源图像素一致,
|
||||
// 图像按 96 DPI 绘制时 1 源像素 = 1 DIP。
|
||||
const float srcW = sourceSize.width;
|
||||
const float srcH = sourceSize.height;
|
||||
const float l = (std::min)(sliceLeft_, srcW);
|
||||
const float t = (std::min)(sliceTop_, srcH);
|
||||
const float r = (std::min)(sliceRight_, srcW - l);
|
||||
const float b = (std::min)(sliceBottom_, srcH - t);
|
||||
const float dl = (std::min)(l, bounds_.width);
|
||||
const float dt = (std::min)(t, bounds_.height);
|
||||
const float dr = (std::min)(r, bounds_.width - dl);
|
||||
const float db = (std::min)(b, bounds_.height - dt);
|
||||
const float sx[3] = {0.0f, l, srcW - r};
|
||||
const float sw[3] = {l, srcW - l - r, r};
|
||||
const float dx[3] = {bounds_.x, bounds_.x + dl, bounds_.x + bounds_.width - dr};
|
||||
const float dw[3] = {dl, bounds_.width - dl - dr, dr};
|
||||
const float sy[3] = {0.0f, t, srcH - b};
|
||||
const float sh[3] = {t, srcH - t - b, b};
|
||||
const float dy[3] = {bounds_.y, bounds_.y + dt, bounds_.y + bounds_.height - db};
|
||||
const float dh[3] = {dt, bounds_.height - dt - db, db};
|
||||
for (int row = 0; row < 3; ++row)
|
||||
{
|
||||
for (int col = 0; col < 3; ++col)
|
||||
{
|
||||
if (sw[col] <= 0.0f || sh[row] <= 0.0f || dw[col] <= 0.0f || dh[row] <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
target->DrawBitmap(bitmap, D2D1::RectF(dx[col], dy[row], dx[col] + dw[col], dy[row] + dh[row]),
|
||||
backgroundOpacity_, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
|
||||
D2D1::RectF(sx[col], sy[row], sx[col] + sw[col], sy[row] + sh[row]));
|
||||
}
|
||||
}
|
||||
target->PopLayer();
|
||||
return;
|
||||
}
|
||||
target->DrawBitmap(bitmap,
|
||||
D2D1::RectF(destination.x, destination.y, destination.x + destination.width,
|
||||
destination.y + destination.height),
|
||||
|
||||
Reference in new issue
Block a user