diff --git a/README.md b/README.md index 86aea134..cfa70ed0 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整 - 输入统计(默认关闭):只在本机记录字符数量与活跃时长,可在设置 → 统计中查看与清理。 - 快捷短语(K,可选混入候选)、日期时间(T)、Unicode(U)、Emoji(E)、颜文字(M)、超级简拼(J)、临时英文(Y)、临时日语(R)。 - 智能标点(中文标点后按空格转英文标点,可撤回)、成对标点补全、以词定字、简繁转换。 -- 多种皮肤:Fluent、微信绿、石墨 Graphite、杨柳青 Willow green、秋桂 Autumn osmanthus,均支持深色 / 浅色。 +- 多种皮肤:Fluent、微信绿、石墨 Graphite、杨柳青 Willow green、秋桂 Autumn osmanthus、微软 Microsoft,均支持深色 / 浅色。 ## 核心功能指南 @@ -276,7 +276,7 @@ ASR 提供商可选豆包(流式)、OpenAI、SiliconFlow、Groq。豆包支 - **手写识别板**:鼠标或触控书写后显示候选汉字。若没有结果,请确认 Windows 已安装中文手写包。 - **屏幕键盘**:用鼠标或触控输入文字和快捷按键。 - **悬浮工具栏**:可调整缩放和图标尺寸;中英文切换始终显示,其余组件(全角、标点、简繁、表情、屏幕键盘、设置)可按需勾选。 -- **皮肤**:Fluent 默认、微信绿、石墨 Graphite、杨柳青 Willow green、秋桂 Autumn osmanthus。候选窗、工具栏等界面可分别跟随全局主题,或单独指定深色 / 浅色。 +- **皮肤**:Fluent 默认、微信绿、石墨 Graphite、杨柳青 Willow green、秋桂 Autumn osmanthus、微软 Microsoft。候选窗、工具栏等界面可分别跟随全局主题,或单独指定深色 / 浅色。 ### 词库 diff --git a/examples/skin-examples/schema/README.md b/examples/skin-examples/schema/README.md index 4507ae74..f2de4f48 100644 --- a/examples/skin-examples/schema/README.md +++ b/examples/skin-examples/schema/README.md @@ -6,7 +6,7 @@ ## 顶层字段 -- 必填:`schema_version`、`id`、`name`、`version`、`base`(继承的内置皮肤:`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`) +- 必填:`schema_version`、`id`、`name`、`version`、`base`(继承的内置皮肤:`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`、`microsoft`) - 可选:`author`、`description` - `[supports]`:`layouts`(`horizontal` / `vertical`)、`themes`(`dark` / `light`) - `[license]`:`code`、`assets` diff --git a/examples/skin-examples/tests/test_manifests.py b/examples/skin-examples/tests/test_manifests.py index a5c504cd..c9b1297f 100644 --- a/examples/skin-examples/tests/test_manifests.py +++ b/examples/skin-examples/tests/test_manifests.py @@ -8,7 +8,7 @@ import unittest ROOT = Path(__file__).resolve().parents[1] # MSIME_SKINS_DIR points the same checks at another skin collection, e.g. the repository's skins/. SKINS_DIR = Path(os.environ.get("MSIME_SKINS_DIR", ROOT / "skins")).resolve() -BUILTIN_SKINS = ("fluent", "wechat", "graphite", "willow_green", "autumn_osmanthus") +BUILTIN_SKINS = ("fluent", "wechat", "graphite", "willow_green", "autumn_osmanthus", "microsoft") class ManifestTests(unittest.TestCase): diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index 9bd66bbe..7239a33c 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -164,7 +164,7 @@ page_size = 6 candidate_window_follow_cursor = true # 候选窗口排列方式,可选:"vertical", "horizontal" candidate_window_layout = "vertical" -# 候选窗口与悬浮工具栏皮肤,可选:"fluent", "wechat" +# 候选窗口与悬浮工具栏皮肤,可选:"fluent", "wechat", "graphite", "willow_green", "autumn_osmanthus", "microsoft",或外部皮肤的 id candidate_skin = "willow_green" # 候选窗口顶部预编辑,可选:"pinyin", "empty" # pinyin -> 显示分词预编辑(与候选框当前行为一致) diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 60fadaa1..b82208a8 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -163,7 +163,7 @@ page_size = 8 candidate_window_follow_cursor = true # 候选窗口排列方式,可选:"vertical", "horizontal" candidate_window_layout = "vertical" -# 候选窗口与悬浮工具栏皮肤,可选:"fluent", "wechat" +# 候选窗口与悬浮工具栏皮肤,可选:"fluent", "wechat", "graphite", "willow_green", "autumn_osmanthus", "microsoft",或外部皮肤的 id candidate_skin = "fluent" # 候选窗口顶部预编辑,可选:"pinyin", "empty" # pinyin -> 显示分词预编辑(与候选框当前行为一致) diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index dfae2697..5e0c7ccd 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -436,7 +436,8 @@ const std::string &GetConfiguredUiBackend(); bool SetConfiguredUiBackend(const std::string &backend); // Process-lifetime renderer chosen at InitImeConfig. Independent of later reloads. bool UseD2dSmallWindowUi(); -// "fluent" | "wechat" | "graphite" | "willow_green" | "autumn_osmanthus" — candidate-window and floating-toolbar skin. +// "fluent" | "wechat" | "graphite" | "willow_green" | "autumn_osmanthus" | "microsoft" +// — candidate-window and floating-toolbar skin. const std::string &GetConfiguredCandidateSkin(); bool SetConfiguredCandidateSkin(const std::string &skin); const std::string &GetConfiguredCandidateWindowPreeditStyle(); diff --git a/server/src/skin/candidate_skin_catalog.cpp b/server/src/skin/candidate_skin_catalog.cpp index 71e4c098..9c58ad58 100644 --- a/server/src/skin/candidate_skin_catalog.cpp +++ b/server/src/skin/candidate_skin_catalog.cpp @@ -269,7 +269,8 @@ bool ReadToolbarColors(const toml::node *node, ToolbarColors &out) const std::vector &BuiltInIds() { - static const std::vector ids = {"fluent", "wechat", "graphite", "willow_green", "autumn_osmanthus"}; + static const std::vector ids = {"fluent", "wechat", "graphite", + "willow_green", "autumn_osmanthus", "microsoft"}; return ids; } diff --git a/server/src/webview2/windows_webview2_skin.cpp b/server/src/webview2/windows_webview2_skin.cpp index 1c81b4b5..9beb21af 100644 --- a/server/src/webview2/windows_webview2_skin.cpp +++ b/server/src/webview2/windows_webview2_skin.cpp @@ -607,6 +607,15 @@ int PrepareHtmlForWnds() } std::wstring entireHtmlPathFtbWnd = assetPath + htmlFtbWnd; ::HTMLStringFtbWnd = ReadHtmlFileWithFallback(entireHtmlPathFtbWnd, assetPath + L"/html/webview2/ftb/default.html"); + if (baseCandidateSkin == "microsoft") + { + // 微软皮肤沿用默认工具栏页面,只把拖拽条换成候选框选中条的紫色;与 ResolveFloatingToolbarSkin 同值。 + // 插在外部皮肤的覆盖之前,外部皮肤写了 handle 时仍以它为准。 + const size_t headEnd = ::HTMLStringFtbWnd.find(L""); + if (headEnd != std::wstring::npos) + ::HTMLStringFtbWnd.insert( + headEnd, L""); + } if (activeExternalCandidateSkin && !::HTMLStringFtbWnd.empty()) { const size_t htmlTag = ::HTMLStringFtbWnd.find(L"((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((std::max)(12, GetConfiguredCandidateFontSize())); const float preeditSize = static_cast((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); diff --git a/server/src/window/candidate_skin_palette.cpp b/server/src/window/candidate_skin_palette.cpp index dd96ade1..c37b4797 100644 --- a/server/src/window/candidate_skin_palette.cpp +++ b/server/src/window/candidate_skin_palette.cpp @@ -121,6 +121,13 @@ CandidateSkinPalette ResolveCandidateSkinPalette(const std::string &skinId, bool : CandidateSkinPalette{CandidateColorFromRgb(0x1C1F23), CandidateColorFromRgb(0x30353B), CandidateColorFromRgb(0xAEB6C2)}; } + else if (paletteSkinId == "microsoft") + { + palette = light ? CandidateSkinPalette{CandidateColorFromRgb(0xF9F9F9), D2D1::ColorF(0, 0.1f), + CandidateColorFromRgb(0x1A1A1A)} + : CandidateSkinPalette{CandidateColorFromRgb(0x2C2C2C), CandidateColorFromRgb(0x1C1C1C), + CandidateColorFromRgb(0xFFFFFF)}; + } if (packageColors) { if (!packageColors->surface.empty()) diff --git a/server/src/window/floating_toolbar_skin.cpp b/server/src/window/floating_toolbar_skin.cpp index cee0f76f..7e6b6fdc 100644 --- a/server/src/window/floating_toolbar_skin.cpp +++ b/server/src/window/floating_toolbar_skin.cpp @@ -56,6 +56,11 @@ FloatingToolbarSkin ResolveFloatingToolbarSkin(const std::string &skinId, bool l skin.hover = light ? CandidateColorFromRgb(0xFFE399, 0.70f) : CandidateColorFromRgb(0xF97D0A, 0.28f); skin.radius = 10.0f; } + else if (skinId == "microsoft") + { + // 其余沿用默认(Fluent)工具栏,只有拖拽条跟候选框选中条同为紫色。 + skin.handle = CandidateColorFromRgb(0xE183D9); + } return skin; } diff --git a/server/tests/src/test_candidate_skin_palette.cpp b/server/tests/src/test_candidate_skin_palette.cpp index 0ffde391..29f40a04 100644 --- a/server/tests/src/test_candidate_skin_palette.cpp +++ b/server/tests/src/test_candidate_skin_palette.cpp @@ -42,6 +42,19 @@ TEST_CASE(candidate_skin_palette_matches_autumn_osmanthus_light_and_dark_preview REQUIRE_EQ(FlattenCandidateColor(light.text, light.surface), RGB(31, 49, 56)); } +TEST_CASE(candidate_skin_palette_matches_microsoft_light_and_dark_preview) +{ + const CandidateSkinPalette dark = ResolveCandidateSkinPalette("microsoft", false, "auto"); + REQUIRE_EQ(FlattenCandidateColor(dark.surface, dark.surface), RGB(44, 44, 44)); + REQUIRE_EQ(FlattenCandidateColor(dark.border, dark.surface), RGB(28, 28, 28)); + REQUIRE_EQ(FlattenCandidateColor(dark.text, dark.surface), RGB(255, 255, 255)); + + const CandidateSkinPalette light = ResolveCandidateSkinPalette("microsoft", true, "auto"); + REQUIRE_EQ(FlattenCandidateColor(light.surface, light.surface), RGB(249, 249, 249)); + REQUIRE(std::abs(light.border.a - 0.1f) < 0.001f); + REQUIRE_EQ(FlattenCandidateColor(light.text, light.surface), RGB(26, 26, 26)); +} + TEST_CASE(candidate_skin_palette_custom_missing_colors_inherit_package_base) { CandidateSkinCatalog::CandidateColors colors; diff --git a/server/tests/src/test_floating_toolbar_skin.cpp b/server/tests/src/test_floating_toolbar_skin.cpp index f951a9b4..26c2bdbe 100644 --- a/server/tests/src/test_floating_toolbar_skin.cpp +++ b/server/tests/src/test_floating_toolbar_skin.cpp @@ -40,6 +40,17 @@ TEST_CASE(floating_toolbar_skin_follows_builtin_skin_pages) const FloatingToolbarSkin autumnLight = ResolveFloatingToolbarSkin("autumn_osmanthus", true); REQUIRE_EQ(FlattenCandidateColor(autumnLight.fill, autumnLight.fill), RGB(214, 236, 240)); REQUIRE_EQ(FlattenCandidateColor(autumnLight.glyph, autumnLight.fill), RGB(26, 26, 26)); + + // Microsoft keeps the default toolbar and only takes its purple selection bar colour for the handle. + for (const bool light : {false, true}) + { + const FloatingToolbarSkin microsoft = ResolveFloatingToolbarSkin("microsoft", light); + const FloatingToolbarSkin fluent = ResolveFloatingToolbarSkin("fluent", light); + REQUIRE_EQ(FlattenCandidateColor(microsoft.handle, microsoft.fill), RGB(225, 131, 217)); + REQUIRE_EQ(FlattenCandidateColor(microsoft.fill, microsoft.fill), + FlattenCandidateColor(fluent.fill, fluent.fill)); + REQUIRE_EQ(microsoft.radius, fluent.radius); + } } // An external skin's [toolbar] tables reach the D2D toolbar too, not only the WebView2 page. diff --git a/skins/README.md b/skins/README.md index f92e165b..c724faac 100644 --- a/skins/README.md +++ b/skins/README.md @@ -18,7 +18,7 @@ ## 默认皮肤设置(`default/`) -[`default/`](default/) 不是外部皮肤,而是五个内置皮肤(`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`)各自的设置清单。安装包会把它们放到数据目录的 `skins\default\\skin.toml`,升级时只补缺失的文件、不覆盖已有的,所以可以直接在那里改。 +[`default/`](default/) 不是外部皮肤,而是六个内置皮肤(`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`、`microsoft`)各自的设置清单。安装包会把它们放到数据目录的 `skins\default\\skin.toml`,升级时只补缺失的文件、不覆盖已有的,所以可以直接在那里改。 内置皮肤的外观仍由输入法内置样式决定,这份清单只承载皮肤级的开关,目前支持: diff --git a/skins/default/microsoft/skin.toml b/skins/default/microsoft/skin.toml new file mode 100644 index 00000000..7ab338e4 --- /dev/null +++ b/skins/default/microsoft/skin.toml @@ -0,0 +1,9 @@ +# 内置皮肤「微软」的设置清单。外观由输入法内置的样式决定,这里只放皮肤级的开关; +# 基于 microsoft 的外部皮肤没写的开关沿用这里的值。 +schema_version = 1 +id = "microsoft" +name = "微软 Microsoft" + +[candidate_window] +# 候选框里的翻页箭头:与微软拼音一样默认打开。横排在候选右侧、隔一条分隔线,竖排在最后一行下方、与序号左对齐 +page_arrows = true diff --git a/ui-html/webview2/candwnd/README.md b/ui-html/webview2/candwnd/README.md index 24acd0a8..1ebf82e5 100644 --- a/ui-html/webview2/candwnd/README.md +++ b/ui-html/webview2/candwnd/README.md @@ -13,14 +13,15 @@ skins/ ├─ wechat/ ├─ graphite/ ├─ willow_green/ -└─ autumn_osmanthus/ +├─ autumn_osmanthus/ +└─ microsoft/ ``` 每个皮肤目录包含 `horizontal_dark.css`、`horizontal_light.css`、`vertical_dark.css` 和 `vertical_light.css`。Server 根据基础皮肤、候选框布局和明暗模式组合资源路径,并以内联 `