feat(server): 新增仿微软拼音的内置皮肤 microsoft

- 候选窗:深色 #2C2C2C 底、#383838 选中底、#E183D9 紫色选中条,序号与候选同字号,
  横排每项最小 4.35em、间距 0.28em(换行的行间同样隔 0.28em);尺寸均按字号缩放
- 翻页:默认开启,实心圆角三角,与候选行等高,横排前加分隔线
- 悬浮工具栏沿用默认页面,拖拽条改为紫色
- ui:PagerArrows 新增 Glyph::Triangle 与分隔线,三角用单一路径填充,半透明时不出现描边重叠;
  CandidateList 新增 minItemWidth 与 selectedBarInside
- 设置页新增皮肤卡片与预览;更新内置皮肤列表、文档与配置注释,补充测试
This commit is contained in:
fanlusky committed 2026-09-30 17:35:38 +08:00
1 parent 9b6c369a29
commit ec00892121
29 files changed
+1270 -35

No files matched your search

+101 -7
View File
@@ -111,6 +111,19 @@ struct CandSkinTokens
float itemPadRight = 0.0f;
// 横排时把卡片多出的宽度均分给同一行各项,铺满高亮的皮肤(杨柳青)末项才能贴到卡片右边。
bool justifyRows = false;
// 序号字号相对候选字号的比例、序号与候选之间的空、横排每项的最小宽度(0 为不限)。
float labelScale = 0.8f;
float labelGap = 1.5f;
float minItemWidth = 0.0f;
// 选中条的尺寸;selectedBarInside 让它落在高亮左缘之内,而不是骑在左缘上。
float selectedBarWidth = 3.0f;
float selectedBarHeightEm = 0.85f;
bool selectedBarInside = false;
// 翻页箭头画成候选文字色的实心三角(微软拼音那样),横排时前面再加一条分隔线。
bool trianglePager = false;
// 预编辑行隐藏时抵掉它与列表之间的 2px 行距,让卡片上下内边距一致。贴角皮肤总是这样做。
bool trimHiddenPreeditGap = false;
D2D1_COLOR_F pagerDivider = D2D1::ColorF(0, 0.0f);
msimeui::Thickness preeditMargin{};
D2D1_COLOR_F rowTextSelected = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F rowLabelSelected = D2D1::ColorF(0, 0.0f);
@@ -468,6 +481,7 @@ void CandidatePresenter::ApplySkin()
RebuildScene();
lastSkinFingerprint_ = skinKey;
}
const float fontSize = static_cast<float>((std::max)(12, GetConfiguredCandidateFontSize()));
if (baseSkin == "fluent")
{
if (horizontalLayout)
@@ -627,6 +641,53 @@ void CandidatePresenter::ApplySkin()
tokens.menuHover = ColorFromRgb(0x637885);
}
}
else if (baseSkin == "microsoft")
{
// 几何随字号缩放,对应 CSS 里以 em 写的尺寸(ui-html/webview2/candwnd/skins/microsoft/)。
tokens.borderWidth = 1.0f;
tokens.radius = 8.0f;
tokens.containerPad = std::round(fontSize * 0.3f);
tokens.itemRadius = 4.0f;
tokens.itemExtraHeight = std::round(fontSize * 0.47f);
// 序号左端距高亮左缘 0.76em,其中 5 DIP 是列表固定的 textPadLeft。
tokens.itemPadLeft = (std::max)(0.0f, std::round(fontSize * 0.76f) - 5.0f);
tokens.itemPadRight = std::round(fontSize * 0.9f);
if (horizontalLayout)
{
tokens.itemGap = std::round(fontSize * 0.28f);
tokens.minItemWidth = std::round(fontSize * 4.35f);
}
tokens.labelScale = 1.0f;
tokens.labelGap = std::round(fontSize * 0.2f);
tokens.selectedBarWidth = (std::max)(3.0f, fontSize * 0.24f);
tokens.selectedBarHeightEm = 1.1f;
tokens.selectedBarInside = true;
tokens.trianglePager = true;
tokens.trimHiddenPreeditGap = true;
tokens.accent = ColorFromRgb(0xE183D9);
if (candLight)
{
tokens.selected = ColorFromRgb(0xEAEAEA);
tokens.hover = ColorFromRgb(0xF0F0F0);
tokens.number = ColorFromRgb(0x5F5F5F);
tokens.pagerDivider = ColorFromRgb(0xE0E0E0);
tokens.menuFill = ColorFromRgb(0xF9F9F9);
tokens.menuBorder = D2D1::ColorF(0, 0.1f);
tokens.menuText = ColorFromRgb(0x1A1A1A);
tokens.menuHover = ColorFromRgb(0xEAEAEA);
}
else
{
tokens.selected = ColorFromRgb(0x383838);
tokens.hover = ColorFromRgb(0x353535);
tokens.number = ColorFromRgb(0xCFCFCF);
tokens.pagerDivider = ColorFromRgb(0x3D3D3D);
tokens.menuFill = ColorFromRgb(0x2C2C2C);
tokens.menuBorder = ColorFromRgb(0x1C1C1C);
tokens.menuText = ColorFromRgb(0xFFFFFF);
tokens.menuHover = ColorFromRgb(0x383838);
}
}
if (package)
{
@@ -693,7 +754,6 @@ void CandidatePresenter::ApplySkin()
theme.windowBackground = D2D1::ColorF(0, 0.0f);
msimeui::ThemeManager::SetCurrent(theme);
const float fontSize = static_cast<float>((std::max)(12, GetConfiguredCandidateFontSize()));
const float preeditSize = static_cast<float>((std::max)(12, GetConfiguredCandidateWindowPreeditFontSize()));
msimeui::CandidateList::Appearance appearance;
appearance.fontFamily = string_to_wstring(ResolveSystemFontFamilyForCss(GetConfiguredCandidateEnglishFont()));
@@ -708,7 +768,7 @@ void CandidatePresenter::ApplySkin()
appearance.itemHeight = fontSize * 1.35f + tokens.itemExtraHeight;
appearance.itemGap = tokens.itemGap;
appearance.fontSize = fontSize;
appearance.labelFontSize = fontSize * 0.8f;
appearance.labelFontSize = fontSize * tokens.labelScale;
appearance.annotationFontSize = fontSize;
appearance.contentPadLeft = tokens.itemPadLeft;
appearance.contentPadRight = tokens.itemPadRight;
@@ -716,9 +776,11 @@ void CandidatePresenter::ApplySkin()
// 对应 CSS 里 .row.cand 的 padding 包住整个 .text(杨柳青横排上下各 6px)。
appearance.contentPadBottom = tokens.itemExtraHeight * 0.5f;
appearance.textPadLeft = 5.0f;
appearance.labelGap = 1.5f;
appearance.selectedBarWidth = 3.0f;
appearance.selectedBarHeight = fontSize * 0.85f;
appearance.labelGap = tokens.labelGap;
appearance.minItemWidth = tokens.minItemWidth;
appearance.selectedBarWidth = tokens.selectedBarWidth;
appearance.selectedBarHeight = fontSize * tokens.selectedBarHeightEm;
appearance.selectedBarInside = tokens.selectedBarInside;
appearance.showSelectedBar = tokens.showSelectedBar;
appearance.selectedBarColor = tokens.selectedBar.a > 0.001f ? tokens.selectedBar : tokens.accent;
appearance.cornerRadius = tokens.itemRadius;
@@ -730,7 +792,8 @@ void CandidatePresenter::ApplySkin()
// 隐藏的预编辑行高度为 0,但 body 仍在它和列表之间留 2px 行距;贴角皮肤把它抵掉,
// 让首项贴着卡片的内容顶边(秋桂四边内边距一致,杨柳青高亮直抵卡片顶边)。
if (preeditHidden)
impl_->preedit->SetMargin({0.0f, 0.0f, 0.0f, tokens.outerItemRadius > 0.0f ? -2.0f : 0.0f});
impl_->preedit->SetMargin(
{0.0f, 0.0f, 0.0f, tokens.outerItemRadius > 0.0f || tokens.trimHiddenPreeditGap ? -2.0f : 0.0f});
else
impl_->preedit->SetMargin(tokens.preeditMargin);
appearance.textColor = palette.candidateText;
@@ -765,8 +828,39 @@ void CandidatePresenter::ApplySkin()
pagerAppearance.disabledGlyphColor = WithAlpha(tokens.number, 0.3f);
pagerAppearance.hoverFill = tokens.hover;
pagerAppearance.pressedFill = tokens.selected.a > 0.001f ? tokens.selected : tokens.hover;
if (tokens.trianglePager)
{
// 微软拼音式:候选文字色的实心三角,按钮与候选行等高。横排时按钮更宽,前面隔一条分隔线,
// 与 CSS 的 .page-arrow 同一组 em 尺寸。
pagerAppearance.glyph = msimeui::PagerArrows::Glyph::Triangle;
// 高 0.56em、宽 0.47em,三个角各圆掉高度的四分之一,尖端是钝的。
pagerAppearance.glyphSize = fontSize * 0.56f;
pagerAppearance.strokeWidth = pagerAppearance.glyphSize * 0.25f;
pagerAppearance.cornerRadius = tokens.itemRadius;
pagerAppearance.gap = 0.0f;
pagerAppearance.glyphColor = palette.candidateText;
pagerAppearance.disabledGlyphColor = WithAlpha(palette.candidateText, 0.25f);
pagerAppearance.buttonHeight = std::round(appearance.itemHeight);
if (horizontalLayout)
{
pagerAppearance.buttonWidth = std::round(fontSize * 1.73f);
pagerAppearance.dividerWidth = 1.0f;
pagerAppearance.dividerGap = std::round(fontSize * 0.12f);
pagerAppearance.dividerColor = tokens.pagerDivider;
}
else
{
pagerAppearance.buttonWidth = std::round(fontSize * 1.2f);
pagerAppearance.buttonHeight = std::round(fontSize * 1.2f);
}
}
impl_->pager->SetAppearance(pagerAppearance);
if (horizontalLayout)
if (horizontalLayout && tokens.trianglePager)
{
// 与最后一项隔 0.3em,按钮与行等高,不用再抬。
impl_->pager->SetMargin({std::round(fontSize * 0.3f), 0.0f, 0.0f, 0.0f});
}
else if (horizontalLayout)
{
// 横排:窄列与候选之间留一点空,箭头竖直居中在最后一行上。
const float lift = (std::max)(0.0f, (appearance.itemHeight - pagerAppearance.buttonHeight) * 0.5f);