Files
msime-windows/server/src/settings/native/native_page_dictionary.cpp
T
Lu Fan(SonnyCalcr) 1b875267db refactor: 移除 WebView2,界面只保留 Direct2D (#1013)
* refactor(engine): 语音独立工具改用原生托盘菜单,移除 WebView2

托盘菜单改用 Win32 弹出菜单,设置项改为用关联编辑器打开 config.toml,关于页改为消息框。删除 window_webview2 与两份 HTML 资源,windows-app 特性不再依赖 webview2 端口。

* refactor(server): 移除 WebView2,候选窗、工具栏、托盘菜单与设置窗口只保留 Direct2D

删除 src/webview2 与 webview_utils,小窗口的置顶、皮肤重载和工具栏状态缓存移到 window/small_windows,presenter 成为唯一路径,D2D 下的置顶时序与 cloak 行为保持不变。设置进程去掉 WebView2 宿主、鼠标转发与启动图,只构建原生窗口;旧快捷方式传入的 --d2d / --webview2 被忽略。删除 appearance.ui_backend 与 appearance.settings_ui_backend(读取、写入、设置页下拉框与两份配置模板),升级时由模板合并丢弃旧值。设置窗口的图片资源移到 server/assets/settings,安装到数据目录的 assets\settings。不再链接 WebView2Loader,vcpkg 去掉 webview2 端口。

* build(installer): 打包与 CI 不再处理 WebView2 页面和运行时

Prepare-PackageFiles 去掉 -UiHtmlDirectory 与 html 暂存,改为暂存 server/assets/settings;安装器不再检查 WebView2 Runtime、不再创建 ProgramData 下的 WebView2 用户数据目录,升级和卸载时清理旧版留下的 webview2 / webview2-settings 与数据目录里的 html。CI 删除设置页构建、契约同步检查和候选窗 DOM 测试,release 的打包任务不再依赖网页产物;dependabot 去掉 npm。第三方声明去掉 WebView2Loader。

* refactor(ui-html): 删除 WebView2 页面与设置页前端工程

候选窗、工具栏、托盘菜单、设置页、主题和契约副本都已没有使用者;设置窗口用到的图片资源已移到 server/assets/settings。sync-contracts.py 与 pnpm/Vite/TypeScript 工程一并删除,契约仍以 engine/contracts/webview 为唯一来源。

* docs(server): 文档改为只描述 Direct2D 界面

仓库地图去掉 ui-html,组件边界、uiAccess 与置顶时序、设置窗口和配置读写的说明改写为 Direct2D 唯一界面;安装说明去掉 WebView2 Runtime 检查,打包命令去掉 -UiHtmlDirectory;engine/contracts/webview 的说明改为设置窗口是唯一使用者,名字是历史遗留。

* docs(skins): 皮肤说明不再区分 WebView2 与 D2D 渲染器

v2 字段去掉「仅 WebView2」,改为说明由 Direct2D 原生渲染、部分字段仍在补全;颜色必须带 # 改为清单格式约定。随包的 skins/niya-demo/skin.toml 不改,免得用户机器上的同名皮肤在升级时被当成已修改而改名备份。

* docs(ui): demo 说明文字不再提 WebView2
2026-10-17 02:48:02 +08:00

250 lines
9.0 KiB
C++

#include "settings/native/native_page_dictionary_panel.h"
#include "msimeui/DeviceResources.h"
#include "msimeui/Window.h"
#include <algorithm>
// partials/dict.html + modules/dict.ts + styles/modules/dict.css
namespace native_settings
{
namespace
{
// `.dict-tabs`: padding 14px 18px 0, gap 4px, bottom border; `.dict-tab`: padding 11px 20px,
// 15px system font (a <button>), 2px accent underline when active.
class DictTabs : public msimeui::Visual
{
public:
using SelectHandler = std::function<void(size_t index)>;
explicit DictTabs(std::vector<std::wstring> labels) : labels_(std::move(labels))
{
}
void SetOnSelect(SelectHandler handler)
{
onSelect_ = std::move(handler);
}
SizeF Measure(const SizeF &availableSize) override
{
return {availableSize.width, 14.0f + 11.0f + TabLineHeight() + 11.0f + 2.0f + CssBorder()};
}
void Arrange(const RectF &finalRect) override
{
bounds_ = finalRect;
}
void Render(msimeui::DeviceResources &dr) override
{
const Palette &p = CurrentPalette();
const float border = CssBorder();
FillRect(dr, {bounds_.x, bounds_.y + bounds_.height - border, bounds_.width, border}, p.border);
for (size_t i = 0; i < labels_.size(); ++i)
{
const RectF tab = TabRect(i);
const bool active = i == selected_;
// Chrome sits the system font lower in its line box than DirectWrite does.
DrawTextInRect(dr, labels_[i], TabStyle(), {tab.x, tab.y + 1.33f, tab.width, tab.height - 2.0f},
active ? p.text : p.textMuted, true);
if (active)
FillRect(dr, {tab.x, tab.y + tab.height - 2.0f, tab.width, 2.0f}, p.accent);
}
}
bool OnMouseDown(const POINT &point, WPARAM) override
{
pressed_ = HitTab(point);
return pressed_ != labels_.size();
}
bool OnMouseUp(const POINT &point, WPARAM) override
{
const size_t hit = HitTab(point);
const bool clicked = hit == pressed_ && hit < labels_.size();
pressed_ = labels_.size();
if (clicked && hit != selected_)
{
selected_ = hit;
InvalidateVisual();
if (onSelect_)
onSelect_(hit);
}
return true;
}
HCURSOR GetCursor() const override
{
return LoadCursor(nullptr, IDC_HAND);
}
private:
// Chrome lays the 15px system font out in a ~21.3px line, a little taller than
// DirectWrite's natural line (measured against the former web page).
static float TabLineHeight()
{
return std::max(LineHeight(TabStyle()), 21.33f);
}
static TextStyle TabStyle()
{
TextStyle style = ControlStyle();
style.size = kFontSectionTitle;
return style;
}
RectF TabRect(size_t index) const
{
float x = bounds_.x + 18.0f;
for (size_t i = 0; i < index; ++i)
x += MeasureTextWidth(labels_[i], TabStyle()) + 40.0f + 4.0f;
const float width = MeasureTextWidth(labels_[index], TabStyle()) + 40.0f;
return {x, bounds_.y + 14.0f, width, bounds_.height - 14.0f - CssBorder()};
}
size_t HitTab(const POINT &point) const
{
if (!window_)
return labels_.size();
const PointF dip = window_->ClientPixelsToDips(point);
for (size_t i = 0; i < labels_.size(); ++i)
{
if (Contains(TabRect(i), dip))
return i;
}
return labels_.size();
}
std::vector<std::wstring> labels_;
size_t selected_ = 0;
size_t pressed_ = 3;
SelectHandler onSelect_;
};
// `.dict-export-row`: label and button, min-height 62px, divider on top.
class ExportRow : public msimeui::Container
{
public:
ExportRow(VisualPtr content, float minHeight, bool topBorder)
: msimeui::Container(std::move(content)), minHeight_(minHeight), topBorder_(topBorder)
{
}
SizeF Measure(const SizeF &availableSize) override
{
const float border = topBorder_ ? CssBorder() : 0.0f;
const SizeF size = child_ ? child_->MeasureInLayout(availableSize) : SizeF{};
return {availableSize.width, std::max(minHeight_, size.height + border)};
}
void Arrange(const RectF &finalRect) override
{
bounds_ = finalRect;
const float border = topBorder_ ? CssBorder() : 0.0f;
if (!child_)
return;
// align-items: center inside the min-height.
const float height = child_->MeasureInLayout({finalRect.width, finalRect.height - border}).height;
const float top = finalRect.y + border + std::max(0.0f, (finalRect.height - border - height) * 0.5f);
child_->ArrangeInLayout({finalRect.x, top, finalRect.width, height});
}
void Render(msimeui::DeviceResources &dr) override
{
if (topBorder_)
FillRect(dr, {bounds_.x, bounds_.y, bounds_.width, CssBorder()}, CurrentPalette().divider);
msimeui::Container::Render(dr);
}
private:
float minHeight_ = 62.0f;
bool topBorder_ = true;
};
} // namespace
VisualPtr MakeExportRow(const std::wstring &label, std::function<void()> onExport, float minHeight, bool topBorder)
{
auto row = std::make_shared<Row>(18.0f);
row->SetJustify(Row::Justify::SpaceBetween);
TextStyle style;
style.size = kFontDefault;
row->Add(std::make_shared<Label>(label, style), 1.0f);
auto button = std::make_shared<Button>(L"导出", ButtonKind::Primary);
button->SetMinWidth(68.0f);
button->SetOnClick(std::move(onExport));
row->Add(button);
return std::make_shared<ExportRow>(row, minHeight, topBorder);
}
VisualPtr BuildDictionaryPage(PageContext &context)
{
PageBuilder page(L"词库");
auto panel = std::make_shared<DictionaryPanel>(context, DictionaryPanel::Kind::Dictionary);
// .dict-card
auto cardBody = std::make_shared<Column>(0.0f);
auto tabs = std::make_shared<DictTabs>(std::vector<std::wstring>{L"全拼词库", L"五笔词库", L"英文词库"});
cardBody->Add(tabs);
panel->AddTo(*cardBody);
auto card = std::make_shared<PageCard>(cardBody, PageCard::Style::DictCard, Thickness{});
page.AddRaw(card, 18.0f);
// .dict-hans-import-section
TextStyle hansStyle = SectionTitleStyle();
hansStyle.lineHeight = 1.45f;
hansStyle.wrap = true;
auto hansTitle = std::make_shared<Label>(L"批量导入纯汉字词组(e.g.人名)到拼音词库", hansStyle);
auto hansButton = std::make_shared<Button>(L"导入", ButtonKind::Primary);
hansButton->SetMinWidth(68.0f);
hansButton->SetOnClick([panel]() { panel->Import("importHans"); });
auto hansRow = std::make_shared<Row>(10.0f);
hansRow->Add(hansTitle, 1.0f);
hansRow->Add(hansButton);
auto hansCard =
std::make_shared<PageCard>(hansRow, PageCard::Style::Section, Thickness{24.0f, 20.0f, 24.0f, 20.0f});
hansCard->SetMargin({0.0f, 0.0f, 0.0f, 18.0f});
auto hans = std::make_shared<Hideable>(hansCard);
page.AddRaw(hans, 0.0f);
tabs->SetOnSelect([panel](size_t index) {
static const char *const kDictionaries[] = {"quanpin", "wubi", "english"};
panel->SetDictionary(kDictionaries[std::min<size_t>(index, 2)]);
});
std::weak_ptr<DictionaryPanel> weakPanel = panel;
panel->SetOnModeChanged([weakPanel, hans, hansTitle]() {
auto locked = weakPanel.lock();
if (!locked)
return;
// 纯汉字导入按当前标签页出码;英文词库收不了纯汉字,整块隐藏。
hans->SetVisible(locked->Dictionary() != "english");
hansTitle->SetText(locked->Dictionary() == "quanpin" ? L"批量导入纯汉字词组(e.g.人名)到拼音词库"
: L"批量导入纯汉字词组(e.g.人名)到五笔词库");
});
// .dict-export-section: padding-top 18px, padding-bottom 0.
auto exportBody = std::make_shared<Column>(0.0f);
auto exportTitle = MakeTitle(L"导出词库");
exportTitle->SetMargin({0.0f, 0.0f, 0.0f, 14.0f});
exportBody->Add(exportTitle);
exportBody->Add(MakeExportRow(L"导出拼音用户词库", [panel]() { panel->Export("quanpin"); }, 62.0f, true));
exportBody->Add(MakeExportRow(L"导出五笔用户词库", [panel]() { panel->Export("wubi"); }, 62.0f, true));
exportBody->Add(MakeExportRow(L"导出英文用户词库", [panel]() { panel->Export("english"); }, 62.0f, true));
page.AddRaw(std::make_shared<PageCard>(exportBody, PageCard::Style::Section, Thickness{24.0f, 18.0f, 24.0f, 0.0f}),
18.0f);
// The panel lives as long as the page; the page visual keeps it alive through the
// callbacks above, which hold it by value.
auto content = page.Build();
struct Holder : msimeui::Container
{
Holder(VisualPtr child, std::shared_ptr<DictionaryPanel> panel)
: msimeui::Container(std::move(child)), panel_(std::move(panel))
{
}
std::shared_ptr<DictionaryPanel> panel_;
};
return std::make_shared<Holder>(content, panel);
}
} // namespace native_settings