// 皮肤与外观:读取页面 HTML、外部候选皮肤的 CSS 注入与内嵌资源、PrepareHtmlForWnds,
// 以及候选窗、悬浮工具栏的布局 / 主题 / 字体等外观应用。
#include "webview2/windows_webview2_internal.h"
#include "webview2/inline_protocol.h"
#include "webview2/skin_css_policy.h"
#include "config/ime_config.h"
#include "defines/globals.h"
#include "skin/candidate_skin_catalog.h"
#include "utils/common_utils.h"
#include "window/candidate_presenter.h"
#include "window/candidate_skin_palette.h"
#include "window/floating_toolbar_presenter.h"
#include "fmt/xchar.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace
{
std::optional activeExternalCandidateSkin;
} // namespace
double GetActiveCandidateSkinDecorationTopDip()
{
return activeExternalCandidateSkin ? activeExternalCandidateSkin->decorationTopDip : 0.0;
}
double GetActiveCandidateSkinDecorationWidthDip()
{
return activeExternalCandidateSkin ? activeExternalCandidateSkin->decorationWidthDip : 0.0;
}
std::wstring ReadHtmlFile(const std::wstring &filePath)
{
std::ifstream file(filePath, std::ios::binary);
if (!file)
return L"";
std::ostringstream buffer;
buffer << file.rdbuf();
return string_to_wstring(buffer.str());
}
// An unreadable themed asset must never turn into an empty NavigateToString:
// that renders a blank, title-less page which looks like a dead WebView.
std::wstring ReadHtmlFileWithFallback(const std::wstring &primaryPath, const std::wstring &fallbackPath)
{
std::wstring content = ReadHtmlFile(primaryPath);
if (content.empty() && primaryPath != fallbackPath)
{
content = ReadHtmlFile(fallbackPath);
}
return content;
}
namespace
{
std::wstring NormalizeSkinCssUrl(std::wstring url)
{
while (!url.empty() && iswspace(url.front()))
{
url.erase(url.begin());
}
while (!url.empty() && iswspace(url.back()))
{
url.pop_back();
}
while (url.size() >= 2 && url[0] == L'.' && url[1] == L'/')
{
url.erase(0, 2);
}
std::replace(url.begin(), url.end(), L'\\', L'/');
return url;
}
std::wstring EncodeBase64(const std::vector &bytes)
{
static constexpr wchar_t kTable[] = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::wstring out;
out.reserve(((bytes.size() + 2) / 3) * 4);
size_t i = 0;
while (i + 2 < bytes.size())
{
const unsigned int triple = (static_cast(bytes[i]) << 16) |
(static_cast(bytes[i + 1]) << 8) |
static_cast(bytes[i + 2]);
out.push_back(kTable[(triple >> 18) & 63]);
out.push_back(kTable[(triple >> 12) & 63]);
out.push_back(kTable[(triple >> 6) & 63]);
out.push_back(kTable[triple & 63]);
i += 3;
}
if (i < bytes.size())
{
unsigned int triple = static_cast(bytes[i]) << 16;
if (i + 1 < bytes.size())
{
triple |= static_cast(bytes[i + 1]) << 8;
}
out.push_back(kTable[(triple >> 18) & 63]);
out.push_back(kTable[(triple >> 12) & 63]);
out.push_back(i + 1 < bytes.size() ? kTable[(triple >> 6) & 63] : L'=');
out.push_back(L'=');
}
return out;
}
std::wstring MimeForSkinAsset(const std::wstring &relativePath)
{
std::wstring lower = relativePath;
for (wchar_t &ch : lower)
{
ch = static_cast(towlower(ch));
}
if (lower.size() >= 4 && lower.compare(lower.size() - 4, 4, L".png") == 0)
return L"image/png";
if (lower.size() >= 4 && lower.compare(lower.size() - 4, 4, L".jpg") == 0)
return L"image/jpeg";
if (lower.size() >= 5 && lower.compare(lower.size() - 5, 5, L".jpeg") == 0)
return L"image/jpeg";
if (lower.size() >= 5 && lower.compare(lower.size() - 5, 5, L".webp") == 0)
return L"image/webp";
if (lower.size() >= 4 && lower.compare(lower.size() - 4, 4, L".gif") == 0)
return L"image/gif";
if (lower.size() >= 4 && lower.compare(lower.size() - 4, 4, L".svg") == 0)
return L"image/svg+xml";
return L"application/octet-stream";
}
std::wstring EmbedSkinCssUrl(const std::wstring &skinsRoot, const std::string &skinId, const std::wstring &rawUrl)
{
const std::wstring relative = NormalizeSkinCssUrl(rawUrl);
const std::filesystem::path filePath = std::filesystem::path(skinsRoot) / std::filesystem::u8path(skinId) /
std::filesystem::u8path(wstring_to_string(relative));
std::error_code ec;
constexpr std::uintmax_t kMaxEmbedBytes = 1500 * 1024;
const auto fileSize = std::filesystem::file_size(filePath, ec);
if (!ec && fileSize > 0 && fileSize <= kMaxEmbedBytes)
{
std::ifstream stream(filePath, std::ios::binary);
std::vector bytes(static_cast(fileSize));
if (stream && stream.read(reinterpret_cast(bytes.data()), static_cast(fileSize)))
{
return L"url(\"data:" + MimeForSkinAsset(relative) + L";base64," + EncodeBase64(bytes) + L"\")";
}
}
return L"url(\"https://candidate-skins/" + string_to_wstring(skinId) + L"/" + relative + L"\")";
}
void NeutralizeEmbeddedStyleClosers(std::wstring &css)
{
for (size_t i = 0; i + 7 < css.size(); ++i)
{
if (css[i] != L'<' || css[i + 1] != L'/')
{
continue;
}
std::wstring tag = css.substr(i + 2, 5);
for (wchar_t &ch : tag)
{
ch = static_cast(towlower(ch));
}
if (tag == L"style")
{
css.insert(i + 1, 1, L' ');
i += 2;
}
}
}
void AppendExternalCandidateColorCss(std::wstring &css, const CandidateSkinCatalog::CandidateColors &colors)
{
auto add = [&](const std::string &value, const wchar_t *selector, const wchar_t *property) {
if (value.empty())
{
return;
}
css.append(selector);
css.append(L" { ");
css.append(property);
css.append(L": ");
css.append(string_to_wstring(value));
css.append(L"; }\n");
};
add(colors.accent, L".cursor, .first::before", L"background");
// 悬停在选中项上保持选中色;base 皮肤的 .cand.first:hover(如微信绿的 #07c160)会盖过低特异度的 .cand.first。
add(colors.selected, L".first, .cand.first, .hover-active .cand.first:hover", L"background-color");
add(colors.hover, L".hover-active .cand:not(.first):hover", L"background-color");
add(colors.surface, L".container", L"background");
add(colors.border, L".container", L"border-color");
// .text 画的是 var(--cand-text),只改 .container 的 color 够不着它。设置页的候选文字色写在 :root 的内联样式里,
// 仍然压过这一条,与 D2D 的优先级一致。
add(colors.text, L":root", L"--cand-text");
add(colors.text, L".container", L"color");
add(colors.number, L".num, .cand-no", L"color");
// 翻译默认继承 .text 的颜色再叠 opacity .62;单独配色时取原值,不再叠透明度。
add(colors.translation, L".cand-translation", L"color");
if (!colors.translation.empty())
{
css.append(L".cand-translation { opacity: 1; }\n");
}
if (colors.showSelectedBar.has_value() && !*colors.showSelectedBar)
{
css.append(L".first::before { display: none; }\n");
}
// 细分配色。候选与预编辑文字先读设置页的 --msime-user-text,与 D2D 里用户文字色压过皮肤一致。
// :where(.cand) 把特异度压到与基础 .text 相同,基础皮肤 `.first .text` 的选中行配色仍然生效。
if (!colors.candidateText.empty())
css.append(L":where(.cand) .text { color: var(--msime-user-text, " + string_to_wstring(colors.candidateText) +
L"); }\n");
if (!colors.preeditText.empty())
css.append(L".pinyin .text { color: var(--msime-user-text, " + string_to_wstring(colors.preeditText) +
L"); }\n");
add(colors.preeditCaret, L".cursor", L"background");
add(colors.selectedBar, L".first::before", L"background");
add(colors.selectedText, L".cand.first .text", L"color");
add(colors.selectedNumber, L".cand.first .num, .cand.first .cand-no", L"color");
add(colors.selectedTranslation, L".cand.first .cand-translation", L"color");
if (!colors.selectedTranslation.empty())
{
css.append(L".cand.first .cand-translation { opacity: 1; }\n");
}
add(colors.menuBackground, L".context-menu, .context-submenu", L"background");
add(colors.menuBorder, L".context-menu, .context-submenu", L"border-color");
add(colors.menuText, L".context-menu, .context-submenu, .context-menu-item", L"color");
add(colors.menuHover, L".context-menu-item:hover", L"background");
}
std::wstring CssRgba(D2D1_COLOR_F color, double alpha)
{
return fmt::format(L"rgba({}, {}, {}, {:.3f})", static_cast(std::lround(color.r * 255.0f)),
static_cast(std::lround(color.g * 255.0f)), static_cast(std::lround(color.b * 255.0f)),
alpha);
}
// 预编辑底色带、分隔线、外框线宽、高亮圆角与卡片阴影。D2D 端在 CandidatePresenter::ApplySkin 里用同一组值。
void AppendExternalCandidateGeometryCss(std::wstring &css, const CandidateSkinCatalog::Package &skin, bool light)
{
const CandidateSkinCatalog::CandidateColors &colors = light ? skin.light : skin.dark;
const double itemRadius =
skin.itemCornerRadiusDip ? *skin.itemCornerRadiusDip : CandidateSkinBaseItemRadiusDip(skin.base);
if (!colors.preeditBackground.empty())
{
css.append(fmt::format(L".row.pinyin {{ background: {}; border-radius: {}px; }}\n",
string_to_wstring(colors.preeditBackground), itemRadius));
}
if (!colors.preeditDivider.empty())
{
css.append(L".row.pinyin { border-bottom: 1px solid " + string_to_wstring(colors.preeditDivider) + L"; }\n");
}
if (skin.borderWidthDip)
{
// 杨柳青、秋桂的基础 CSS 是 border: none,只改线宽会没有线型,所以连同线型和解析后的边框色一起写。
const D2D1_COLOR_F border = ResolveCandidateSkinPalette(skin.base, light, {}, &colors, skin.base).border;
css.append(fmt::format(L".container:not(:empty) {{ border: {}px solid {}; }}\n", *skin.borderWidthDip,
CssRgba(border, border.a)));
}
if (skin.itemCornerRadiusDip)
{
// 贴着外框四角的那几个角由更高特异度的贴角规则接管,仍跟外框圆角。
const std::wstring radius = fmt::format(L"{}px", *skin.itemCornerRadiusDip);
css.append(L".container { --ao-item-radius: " + radius + L"; }\n");
css.append(L".container .cand, .container .cand.first { border-radius: " + radius + L"; }\n");
}
if (!skin.shadow.empty())
{
// 杨柳青的 .container 带 clip-path,阴影挂在 .containerParent 上;其余基础皮肤挂在 .container 上。
const wchar_t *selector =
skin.base == "willow_green" ? L".containerParent:not(:empty)" : L".container:not(:empty)";
std::wstring value = L"none";
if (skin.shadow != "none")
{
const double scale = skin.shadow == "soft" ? 0.5 : 1.6;
const D2D1_COLOR_F black = D2D1::ColorF(0, 1.0f);
value = L"8px 10px 24px " + CssRgba(black, (std::min)((light ? 0.18 : 0.34) * scale, 1.0)) +
L", 2px 3px 8px " + CssRgba(black, (std::min)((light ? 0.10 : 0.22) * scale, 1.0));
}
css.append(std::wstring(selector) + L" { box-shadow: " + value + L"; }\n");
}
}
// Image paths come from the package's own skin.toml, so they get the same treatment as a URL
// written inside the stylesheet rather than being trusted because they arrived through a
// manifest field.
std::wstring ManifestImageCssUrl(const std::wstring &skinsRoot, const std::string &skinId, const std::string &path)
{
const std::wstring raw = string_to_wstring(path);
if (path.empty() || msime::skin_css::ClassifyUrl(raw) != msime::skin_css::UrlAction::Embed)
{
return L"none";
}
return EmbedSkinCssUrl(skinsRoot, skinId, raw);
}
void AppendExternalCandidateCornerCss(std::wstring &css, const CandidateSkinCatalog::Package &skin)
{
if (!skin.cornerRadiusDip)
{
return;
}
const std::wstring radius = fmt::format(L"{}px", *skin.cornerRadiusDip);
// 杨柳青、秋桂的外框与贴角高亮都读这两个变量,改变量即可;杨柳青的外框阴影挂在 .containerParent 上。
css.append(L"body, .container { border-radius: " + radius + L"; }\n");
css.append(L".container { --wg-radius: " + radius + L"; --ao-radius: " + radius + L"; }\n");
if (skin.base == "willow_green")
{
css.append(L".containerParent { border-radius: " + radius + L"; }\n");
}
// Fluent 与微信横排把贴着外框四角的高亮角写死成外框圆角,这里用同一组选择器改成 R。
if ((skin.base == "fluent" || skin.base == "wechat") && GetConfiguredCandidateWindowLayout() == "horizontal")
{
css.append(L".container > .pinyin + .row-wrapper > .cand { border-bottom-left-radius: " + radius +
L"; }\n"
L".container.preedit-hidden > .pinyin + .row-wrapper > .cand { border-top-left-radius: " +
radius +
L"; }\n"
L".container > .row-wrapper:is(:last-child, .last-visible) > .cand { "
L"border-bottom-right-radius: " +
radius +
L"; }\n"
L".container.preedit-hidden > .row-wrapper:is(:last-child, .last-visible) > .cand { "
L"border-top-right-radius: " +
radius + L"; }\n");
}
}
std::wstring BuildExternalCandidateSkinCss(const CandidateSkinCatalog::Package &skin, const std::wstring &skinsRoot)
{
// 卡片至少与装饰图同宽,装饰图因此总落在卡片宽度之内;D2D 端用同一规则撑开卡片。
std::wstring css = L".container:not(:empty) { min-width: max(7em, var(--msime-skin-min-width, 0px), "
L"var(--msime-skin-decoration-width, 0px)); }\n";
if (skin.decorationTopDip > 0.0)
{
// 装饰盒底边贴卡片顶边、不与卡片重叠,按 align 贴卡片左/中/右,图片在盒内 contain 居中。
const wchar_t *horizontal = L"right: 0;";
if (skin.decorationAlign == "left")
horizontal = L"left: 0;";
else if (skin.decorationAlign == "center")
horizontal = L"left: 0; right: 0; margin-inline: auto;";
css.append(L".containerParent { padding-top: var(--msime-skin-decoration-top, 0px); "
L"position: relative; box-sizing: border-box; }\n"
L".containerParent:not(:empty)::before { content: \"\"; position: absolute; "
L"z-index: 0; top: 0; ");
css.append(horizontal);
css.append(L" width: var(--msime-skin-decoration-width, 0px); "
L"height: var(--msime-skin-decoration-top, 0px); background: ");
css.append(ManifestImageCssUrl(skinsRoot, skin.id, skin.decorationImage));
css.append(L" center / contain no-repeat; pointer-events: none; }\n"
L".container { position: relative; z-index: 1; }\n");
}
AppendExternalCandidateCornerCss(css, skin);
const bool light = ResolveConfiguredTheme(GetConfiguredThemeCand()) == "light";
const CandidateSkinCatalog::CandidateColors &colors = light ? skin.light : skin.dark;
AppendExternalCandidateColorCss(css, colors);
AppendExternalCandidateGeometryCss(css, skin, light);
if (!skin.backgroundImage.empty())
{
// 与 D2D 一致:背景图铺满整个卡片(含边框下方),边框画在它上面。不能用 ::before 垫图——
// 卡片是 overflow 滚动容器,子元素只能画到内边距盒,半透明边框下会露出一圈底色。
// 图层不能单独设 opacity,于是在图上再盖一层 (1 - opacity) 的底色,效果等同于把图按 opacity 叠在底色上。
const wchar_t *size = L"cover";
if (skin.backgroundFit == "contain")
size = L"contain";
else if (skin.backgroundFit == "stretch")
size = L"100% 100%";
const D2D1_COLOR_F surface = ResolveCandidateSkinPalette(skin.base, light, {}, &colors, skin.base).surface;
const std::wstring veil =
fmt::format(L"rgba({}, {}, {}, {:.3f})", static_cast(std::lround(surface.r * 255.0f)),
static_cast(std::lround(surface.g * 255.0f)),
static_cast(std::lround(surface.b * 255.0f)), surface.a * (1.0 - skin.backgroundOpacity));
css.append(L".container:not(:empty) { background-image: linear-gradient(" + veil + L", " + veil + L"), ");
css.append(ManifestImageCssUrl(skinsRoot, skin.id, skin.backgroundImage));
css.append(fmt::format(L"; background-size: auto, {}; background-position: center; "
L"background-repeat: no-repeat; background-origin: border-box; "
L"background-clip: border-box; }}\n",
size));
}
return css;
}
bool InjectExternalCandidateSkin(std::wstring &html, const CandidateSkinCatalog::Package &skin,
const std::wstring &skinsRoot)
{
if (html.empty() || skinsRoot.empty())
{
return false;
}
const std::wstring vars =
fmt::format(L"",
skin.minWidthDip, skin.decorationTopDip, skin.decorationWidthDip);
std::wstring css = BuildExternalCandidateSkinCss(skin, skinsRoot);
NeutralizeEmbeddedStyleClosers(css);
std::wstring generated;
if (!css.empty())
{
generated = L"";
}
const size_t headEnd = html.find(L"");
if (headEnd == std::wstring::npos)
{
return false;
}
html.insert(headEnd, vars + generated);
return true;
}
// 工具栏页面是按深浅主题各一张的,这里只生成当前主题的覆盖;D2D 端由 ApplyFloatingToolbarSkinOverrides 读同一份值。
std::wstring BuildExternalToolbarSkinCss(const CandidateSkinCatalog::Package &skin, bool light)
{
std::wstring css;
auto add = [&](const std::string &value, const wchar_t *selector, const wchar_t *property) {
if (value.empty())
{
return;
}
css.append(selector);
css.append(L" { ");
css.append(property);
css.append(L": ");
css.append(string_to_wstring(value));
css.append(L"; }\n");
};
const CandidateSkinCatalog::ToolbarColors &colors = light ? skin.toolbarLight : skin.toolbarDark;
add(colors.background, L".status-bar", L"background-color");
add(colors.border, L".status-bar", L"border-color");
add(colors.handle, L".drag-handle", L"background");
add(colors.divider, L".divider", L"background-color");
add(colors.icon, L".icon", L"color");
add(colors.hover, L".icon:hover", L"background-color");
if (skin.toolbarCornerRadiusDip)
{
// 部分 base 页面把圆角直接写成 calc(Npx * var(--ftb-scale)),所以覆盖 border-radius 本身而不是 --ftb-radius。
css.append(fmt::format(L".status-bar {{ border-radius: calc({}px * var(--ftb-scale)); }}\n",
*skin.toolbarCornerRadiusDip));
}
return css;
}
bool InjectExternalToolbarSkin(std::wstring &html, const CandidateSkinCatalog::Package &skin, bool light)
{
std::wstring css = BuildExternalToolbarSkinCss(skin, light);
const size_t headEnd = html.find(L"");
if (css.empty() || headEnd == std::wstring::npos)
{
return false;
}
NeutralizeEmbeddedStyleClosers(css);
html.insert(headEnd, L"");
return true;
}
void InjectCandidateDocumentSkin(std::wstring &html, const std::wstring &builtInCss, const std::string &skin,
const std::string &base, const std::string &layout, const std::string &theme)
{
if (html.empty())
return;
const size_t htmlTagEnd = html.find(L'>', html.find(L"");
if (headEnd != std::wstring::npos && !builtInCss.empty())
html.insert(headEnd, L"");
}
} // namespace
int PrepareHtmlForWnds()
{
// 用户数据目录,默认 %LOCALAPPDATA%\metasequoiaime,安装时可以改到别的盘。
std::wstring assetPath = CommonUtils::get_ime_data_path_w();
//
// 候选窗口
//
const bool isHorizontal = GetConfiguredCandidateWindowLayout() == "horizontal";
const bool candLight = ResolveConfiguredTheme(GetConfiguredThemeCand()) == "light";
const std::string candidateSkin = GetConfiguredCandidateSkin();
const std::string candidateLayout = isHorizontal ? "horizontal" : "vertical";
const std::string candidateTheme = candLight ? "light" : "dark";
activeExternalCandidateSkin.reset();
std::string baseCandidateSkin = candidateSkin;
if (!CandidateSkinCatalog::IsBuiltIn(candidateSkin))
{
const std::wstring skinsRoot = assetPath + L"\\skins";
activeExternalCandidateSkin = CandidateSkinCatalog::Load(skinsRoot, candidateSkin);
if (activeExternalCandidateSkin &&
!CandidateSkinCatalog::Supports(*activeExternalCandidateSkin, candidateLayout, candidateTheme))
activeExternalCandidateSkin.reset();
baseCandidateSkin = activeExternalCandidateSkin ? activeExternalCandidateSkin->base : "fluent";
}
std::wstring htmlCandWnd;
std::wstring bodyHtmlCandWnd;
std::wstring measureHtmlCandWnd;
if (isHorizontal)
{
htmlCandWnd = L"/html/webview2/candwnd/horizontal_candidate_window.html";
// Body/measure fragments are theme-agnostic markup; keep the existing dark assets.
bodyHtmlCandWnd = L"/html/webview2/candwnd/body/horizontal_candidate_window_dark.html";
measureHtmlCandWnd = L"/html/webview2/candwnd/body/horizontal_candidate_window_dark_measure.html";
}
else
{
htmlCandWnd = L"/html/webview2/candwnd/vertical_candidate_window.html";
bodyHtmlCandWnd = L"/html/webview2/candwnd/body/vertical_candidate_window_dark.html";
measureHtmlCandWnd = L"/html/webview2/candwnd/body/vertical_candidate_window_dark_measure.html";
}
std::wstring entireHtmlPathCandWnd = assetPath + htmlCandWnd;
::HTMLStringCandWnd = ReadHtmlFile(entireHtmlPathCandWnd);
const std::wstring candidateStyleName =
fmt::format(L"{}_{}.css", string_to_wstring(candidateLayout), string_to_wstring(candidateTheme));
const std::wstring candidateStylePath =
fmt::format(L"/html/webview2/candwnd/skins/{}/{}", string_to_wstring(baseCandidateSkin), candidateStyleName);
std::wstring builtInCandidateCss = ReadHtmlFile(assetPath + candidateStylePath);
if (builtInCandidateCss.empty())
{
builtInCandidateCss = ReadHtmlFile(assetPath + L"/html/webview2/candwnd/skins/fluent/" + candidateStyleName);
}
if (builtInCandidateCss.empty() && candLight)
{
builtInCandidateCss = ReadHtmlFile(assetPath + L"/html/webview2/candwnd/skins/fluent/" +
string_to_wstring(candidateLayout) + L"_dark.css");
}
InjectCandidateDocumentSkin(::HTMLStringCandWnd, builtInCandidateCss, candidateSkin, baseCandidateSkin,
candidateLayout, candidateTheme);
if (activeExternalCandidateSkin)
{
const std::wstring skinsRoot = assetPath + L"\\skins";
if (!InjectExternalCandidateSkin(::HTMLStringCandWnd, *activeExternalCandidateSkin, skinsRoot))
{
// Without the stylesheet, native decoration insets would clip the card.
activeExternalCandidateSkin.reset();
}
}
std::wstring bodyHtmlPathCandWnd = assetPath + bodyHtmlCandWnd;
::BodyStringCandWnd = ReadHtmlFile(bodyHtmlPathCandWnd);
std::wstring measureHtmlPathCandWnd = assetPath + measureHtmlCandWnd;
::MeasureStringCandWnd = ReadHtmlFile(measureHtmlPathCandWnd);
(void)0;
//
// 托盘语言区菜单窗口
//
const bool menuLight = ResolveConfiguredTheme(GetConfiguredThemeMenu()) == "light";
std::wstring htmlMenuWnd =
menuLight ? L"/html/webview2/menu/default_light.html" : L"/html/webview2/menu/default.html";
std::wstring entireHtmlPathMenuWnd = assetPath + htmlMenuWnd;
::HTMLStringMenuWnd =
ReadHtmlFileWithFallback(entireHtmlPathMenuWnd, assetPath + L"/html/webview2/menu/default.html");
//
// settings 窗口
// 这里暂时没有用到,因为 settings 窗口使用的是映射 url 导航
//
/*
std::wstring htmlSettingsWnd = L"/html/webview2/settings/default.html";
std::wstring entireHtmlPathSettingsWnd = assetPath + htmlSettingsWnd;
::HTMLStringSettingsWnd = ReadHtmlFile(entireHtmlPathSettingsWnd);
*/
//
// floating toolbar 窗口
//
const bool ftbLight = ResolveConfiguredTheme(GetConfiguredThemeFtb()) == "light";
std::wstring htmlFtbWnd;
if (baseCandidateSkin == "wechat")
{
htmlFtbWnd = ftbLight ? L"/html/webview2/ftb/wechat_light.html" : L"/html/webview2/ftb/wechat.html";
}
else if (baseCandidateSkin == "graphite")
{
htmlFtbWnd = ftbLight ? L"/html/webview2/ftb/graphite_light.html" : L"/html/webview2/ftb/graphite.html";
}
else if (baseCandidateSkin == "willow_green")
{
htmlFtbWnd = ftbLight ? L"/html/webview2/ftb/willow_green_light.html" : L"/html/webview2/ftb/willow_green.html";
}
else if (baseCandidateSkin == "autumn_osmanthus")
{
htmlFtbWnd =
ftbLight ? L"/html/webview2/ftb/autumn_osmanthus_light.html" : L"/html/webview2/ftb/autumn_osmanthus.html";
}
else
{
htmlFtbWnd = ftbLight ? L"/html/webview2/ftb/default_light.html" : L"/html/webview2/ftb/default.html";
}
std::wstring entireHtmlPathFtbWnd = assetPath + htmlFtbWnd;
::HTMLStringFtbWnd = ReadHtmlFileWithFallback(entireHtmlPathFtbWnd, assetPath + L"/html/webview2/ftb/default.html");
if (activeExternalCandidateSkin && !::HTMLStringFtbWnd.empty())
{
const size_t htmlTag = ::HTMLStringFtbWnd.find(L"', htmlTag);
if (htmlTagEnd != std::wstring::npos)
{
::HTMLStringFtbWnd.insert(htmlTagEnd,
fmt::format(L" data-candidate-skin=\"{}\" data-candidate-base=\"{}\" "
L"data-candidate-theme=\"{}\"",
string_to_wstring(candidateSkin),
string_to_wstring(baseCandidateSkin), ftbLight ? L"light" : L"dark"));
}
InjectExternalToolbarSkin(::HTMLStringFtbWnd, *activeExternalCandidateSkin, ftbLight);
}
// The small windows navigate from strings. Inline the pinned local runtime
// before navigation instead of blocking first paint on two virtual-host loads.
// Immutable for this process, like the executable's protocol contract.
static const std::wstring protocolSchema = ReadHtmlFile(assetPath + L"/html/webview2/shared/schema.js");
static const std::wstring protocolRuntime = ReadHtmlFile(assetPath + L"/html/webview2/shared/runtime.js");
InlineWebViewProtocolScripts(::HTMLStringCandWnd, protocolSchema, protocolRuntime);
InlineWebViewProtocolScripts(::HTMLStringMenuWnd, protocolSchema, protocolRuntime);
InlineWebViewProtocolScripts(::HTMLStringFtbWnd, protocolSchema, protocolRuntime);
preparedCandidateSkin = candidateSkin;
return 0;
}
bool ApplyConfiguredCandidateWindowLayout()
{
PrepareHtmlForWnds();
if (CandidatePresenter::Instance().IsBound() && !webviewCandWnd)
{
return true;
}
if (!webviewCandWnd || HTMLStringCandWnd.empty())
{
return false;
}
const bool ok = SUCCEEDED(webviewCandWnd->NavigateToString(HTMLStringCandWnd.c_str()));
if (ok)
{
loadedCandidateSkin = preparedCandidateSkin;
}
return ok;
}
bool ApplyConfiguredUiThemes()
{
if (FloatingToolbarPresenter::Instance().IsBound())
{
FloatingToolbarPresenter::Instance().ApplyTheme();
}
const std::string candidateSkin = GetConfiguredCandidateSkin();
PrepareHtmlForWnds();
bool ok = true;
if (webviewCandWnd && !HTMLStringCandWnd.empty())
{
const bool candidateOk = SUCCEEDED(webviewCandWnd->NavigateToString(HTMLStringCandWnd.c_str()));
if (candidateOk)
{
loadedCandidateSkin = candidateSkin;
}
ok = candidateOk && ok;
}
if (webviewFtbWnd && !HTMLStringFtbWnd.empty())
{
ClearFloatingToolbarNavigationState();
const bool floatingToolbarOk = SUCCEEDED(webviewFtbWnd->NavigateToString(HTMLStringFtbWnd.c_str()));
if (floatingToolbarOk)
{
loadedFloatingToolbarSkin = candidateSkin;
}
ok = floatingToolbarOk && ok;
}
if (webviewMenuWnd && !HTMLStringMenuWnd.empty())
{
ok = SUCCEEDED(webviewMenuWnd->NavigateToString(HTMLStringMenuWnd.c_str())) && ok;
}
return ok;
}
bool ApplyConfiguredCandidateSkinIfChanged()
{
const std::string &candidateSkin = GetConfiguredCandidateSkin();
const bool candidateCurrent = !webviewCandWnd || loadedCandidateSkin == candidateSkin;
const bool floatingToolbarCurrent = !webviewFtbWnd || loadedFloatingToolbarSkin == candidateSkin;
if (candidateCurrent && floatingToolbarCurrent)
{
return true;
}
return ApplyConfiguredUiThemes();
}
uint64_t GetCandidateSkinReloadRevision()
{
return candidateSkinReloadRevision;
}
bool ForceReloadConfiguredCandidateSkin()
{
++candidateSkinReloadRevision;
loadedCandidateSkin.clear();
loadedFloatingToolbarSkin.clear();
const bool cloakCandidate = ::is_global_wnd_cand_shown && ::global_hwnd && IsWindow(::global_hwnd) &&
!CandidatePresenter::Instance().IsBound();
if (cloakCandidate)
SetCandidateHostCloaked(true);
const bool ok = ApplyConfiguredUiThemes();
if (!ok && cloakCandidate)
SetCandidateHostCloaked(false);
return ok;
}
bool ApplyConfiguredCandidateAppearance()
{
if (CandidatePresenter::Instance().IsBound() && !webviewCandWnd)
{
return true;
}
if (!webviewCandWnd)
{
return false;
}
nlohmann::json cfg = {{"font", ResolveSystemFontFamilyForCss(GetConfiguredCandidateFont())},
{"english_font", ResolveSystemFontFamilyForCss(GetConfiguredCandidateEnglishFont())},
{"fallback_fonts", GetConfiguredCandidateFallbackFontFamilies()},
{"font_size", GetConfiguredCandidateFontSize()},
{"preedit_font_size", GetConfiguredCandidateWindowPreeditFontSize()},
{"cand_text_color", GetConfiguredCandidateTextColor()}};
std::string family;
auto appendFont = [&](const std::string &font) {
// JSON quoting also escapes CSS quotes/backslashes; font names exclude control characters.
if (!family.empty())
family += ", ";
family += nlohmann::json(font).dump(-1, ' ', false);
};
// 皮肤的 font_family 排在最前,用户配置的字体整体退为回退,与 D2D 端同序。
if (activeExternalCandidateSkin && !activeExternalCandidateSkin->fontFamily.empty())
appendFont(ResolveSystemFontFamilyForCss(activeExternalCandidateSkin->fontFamily));
appendFont(ResolveSystemFontFamilyForCss(GetConfiguredCandidateEnglishFont()));
for (const auto &font : GetConfiguredCandidateFallbackFontFamilies())
appendFont(font);
cfg["font_family"] = family + ", sans-serif";
const std::wstring script =
L"(function(c){"
L"const root=document.documentElement;"
L"const family=c.font_family;"
L"root.style.setProperty('--cand-font-family', family);"
L"root.style.setProperty('--cand-font-size', String(c.font_size||16)+'px');"
L"root.style.setProperty('--preedit-font-size', String(c.preedit_font_size||c.font_size||16)+'px');"
L"const color=(c.cand_text_color||'auto');"
L"if(color&&color!=='auto'){"
L"root.style.setProperty('--cand-text', color);"
L"root.style.setProperty('--cand-num', color.length===7?color+'9d':color);"
// 外部皮肤的 candidate_text / preedit_text 写成 var(--msime-user-text, 皮肤色),设置页的文字色照样压过它们。
L"root.style.setProperty('--msime-user-text', color);"
L"}else{"
L"root.style.removeProperty('--cand-text');"
L"root.style.removeProperty('--cand-num');"
L"root.style.removeProperty('--msime-user-text');"
L"}"
// Drop any stale nowrap fast-layout sheet so the skin's wrap-at-max-width
// rules apply; forcing a single line makes the card overflow the cap and
// get clipped by .container{overflow-x:hidden}.
L"document.getElementById('msime-fast-layout')?.remove();"
L"})(" +
string_to_wstring(cfg.dump()) + L");";
return SUCCEEDED(webviewCandWnd->ExecuteScript(script.c_str(), nullptr));
}
bool ApplyConfiguredFloatingToolbarAppearance()
{
return ApplyConfiguredFloatingToolbarAppearance(nullptr);
}
bool ApplyConfiguredFloatingToolbarAppearance(std::function onComplete)
{
if (FloatingToolbarPresenter::Instance().IsBound())
{
FloatingToolbarPresenter::Instance().ApplyAppearance();
if (onComplete)
{
onComplete();
}
return true;
}
if (!webviewFtbWnd)
{
if (onComplete)
{
onComplete();
}
return false;
}
nlohmann::json cfg = {{"scale", GetConfiguredFloatingToolbarScale()},
{"font_size", GetConfiguredFloatingToolbarFontSize()}};
const std::wstring script = L"(function(c){"
L"const root=document.documentElement;"
L"const scale=(typeof c.scale==='number'&&c.scale>0)?c.scale:1;"
L"const icon=(typeof c.font_size==='number'&&c.font_size>0)?c.font_size:24;"
L"root.style.setProperty('--ftb-scale', String(scale));"
L"root.style.setProperty('--ftb-icon-size', String(icon)+'px');"
L"return true;"
L"})(" +
string_to_wstring(cfg.dump()) + L");";
if (!onComplete)
{
return SUCCEEDED(webviewFtbWnd->ExecuteScript(script.c_str(), nullptr));
}
return SUCCEEDED(webviewFtbWnd->ExecuteScript(
script.c_str(), Callback([onComplete](HRESULT, LPCWSTR) -> HRESULT {
onComplete();
return S_OK;
}).Get()));
}