`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是 product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次 搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、 miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。 Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、 macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。 词库仍按 product-lock.json 在构建时取发布产物。 引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游 commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。 verify-contracts / verify-checkout 随之移除,verify-published 保留。 CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除 列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job; engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily 改为 monthly。 README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录 文档同步说明新的位置与边界。
1177 lines
43 KiB
C++
1177 lines
43 KiB
C++
#include "window/candidate_presenter.h"
|
|
|
|
#include "config/ime_config.h"
|
|
#include "defines/defines.h"
|
|
#include "defines/globals.h"
|
|
#include "global/globals.h"
|
|
#include "ipc/ipc.h"
|
|
#include "log/candidate_diag_log.h"
|
|
#include "skin/candidate_skin_catalog.h"
|
|
#include "utils/common_utils.h"
|
|
#include "utils/ime_utils.h"
|
|
#include "utils/window_utils.h"
|
|
#include "window/ime_windows.h"
|
|
|
|
#include "msimeui/Controls.h"
|
|
#include "msimeui/DeviceResources.h"
|
|
#include "msimeui/Layout.h"
|
|
#include "msimeui/Scene.h"
|
|
#include "msimeui/Theme.h"
|
|
#include "msimeui/Window.h"
|
|
|
|
#include "engine/common/helpcode_utils.h"
|
|
#include "engine/core/scheme_type.h"
|
|
#include "engine/core/word_item.h"
|
|
|
|
#include <d2d1.h>
|
|
#include <windowsx.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cctype>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
D2D1_COLOR_F ColorFromRgb(UINT rgb, float alpha = 1.0f)
|
|
{
|
|
return D2D1::ColorF(((rgb >> 16) & 0xFF) / 255.0f, ((rgb >> 8) & 0xFF) / 255.0f, (rgb & 0xFF) / 255.0f, alpha);
|
|
}
|
|
|
|
std::string TrimCopy(std::string text)
|
|
{
|
|
while (!text.empty() && std::isspace(static_cast<unsigned char>(text.front())))
|
|
{
|
|
text.erase(text.begin());
|
|
}
|
|
while (!text.empty() && std::isspace(static_cast<unsigned char>(text.back())))
|
|
{
|
|
text.pop_back();
|
|
}
|
|
return text;
|
|
}
|
|
|
|
int AlignHostPixels(int value)
|
|
{
|
|
constexpr int kBucket = 64;
|
|
if (value < 1)
|
|
{
|
|
value = 1;
|
|
}
|
|
return ((value + kBucket - 1) / kBucket) * kBucket;
|
|
}
|
|
|
|
// Recompute half-screen DIP limits against `scale` when it differs from the
|
|
// monitor-derived scale, so clamping and window sizing stay on one scale
|
|
// (mirrors the webview path's rasterization-scale composition).
|
|
HalfScreenDipLimits ApplyScaleToHalfScreenLimits(POINT pt, FLOAT scale)
|
|
{
|
|
HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(pt);
|
|
if (scale > 0.0f)
|
|
{
|
|
const double monitorWidthPx = static_cast<double>((std::max)(1, limits.monitor.right - limits.monitor.left));
|
|
const double monitorHeightPx = static_cast<double>((std::max)(1, limits.monitor.bottom - limits.monitor.top));
|
|
limits.scale = scale;
|
|
limits.maxWidthDip = (monitorWidthPx * 0.5) / static_cast<double>(scale);
|
|
limits.maxHeightDip = (monitorHeightPx * 0.5) / static_cast<double>(scale);
|
|
}
|
|
return limits;
|
|
}
|
|
|
|
D2D1_COLOR_F ParseCssColor(const std::string &text, D2D1_COLOR_F fallback)
|
|
{
|
|
std::string value = TrimCopy(text);
|
|
if (value.empty() || value == "auto" || value == "none" || value == "transparent")
|
|
{
|
|
return value == "transparent" ? D2D1::ColorF(0, 0.0f) : fallback;
|
|
}
|
|
if (value.rfind("rgba(", 0) == 0 || value.rfind("rgb(", 0) == 0)
|
|
{
|
|
const auto open = value.find('(');
|
|
const auto close = value.rfind(')');
|
|
if (open != std::string::npos && close != std::string::npos && close > open)
|
|
{
|
|
std::string inner = value.substr(open + 1, close - open - 1);
|
|
for (char &ch : inner)
|
|
{
|
|
if (ch == ',')
|
|
{
|
|
ch = ' ';
|
|
}
|
|
}
|
|
std::istringstream stream(inner);
|
|
float r = 0;
|
|
float g = 0;
|
|
float b = 0;
|
|
float a = 1.0f;
|
|
if (stream >> r >> g >> b)
|
|
{
|
|
stream >> a;
|
|
return D2D1::ColorF(r / 255.0f, g / 255.0f, b / 255.0f, a);
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
if (value[0] == '#')
|
|
{
|
|
value.erase(value.begin());
|
|
}
|
|
auto hexByte = [](const std::string &hex) { return static_cast<int>(std::stoul(hex, nullptr, 16)); };
|
|
try
|
|
{
|
|
if (value.size() == 3)
|
|
{
|
|
const int r = hexByte(std::string(2, value[0]));
|
|
const int g = hexByte(std::string(2, value[1]));
|
|
const int b = hexByte(std::string(2, value[2]));
|
|
return D2D1::ColorF(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f);
|
|
}
|
|
if (value.size() == 6)
|
|
{
|
|
return ColorFromRgb(static_cast<UINT>(std::stoul(value, nullptr, 16)));
|
|
}
|
|
if (value.size() == 8)
|
|
{
|
|
const unsigned long packed = std::stoul(value, nullptr, 16);
|
|
const UINT rgb = static_cast<UINT>((packed >> 8) & 0xFFFFFFu);
|
|
const float alpha = static_cast<float>(packed & 0xFFu) / 255.0f;
|
|
return ColorFromRgb(rgb, alpha);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
struct CandSkinTokens
|
|
{
|
|
D2D1_COLOR_F surface = ColorFromRgb(0x202020);
|
|
D2D1_COLOR_F border = ParseCssColor("#9b9b9b2e", ColorFromRgb(0x3A3A3A, 0.18f));
|
|
D2D1_COLOR_F text = ParseCssColor("#e9e8e8", ColorFromRgb(0xE9E8E8));
|
|
D2D1_COLOR_F number = ParseCssColor("#e9e8e89d", ColorFromRgb(0xE9E8E8, 0.616f));
|
|
D2D1_COLOR_F selected = ParseCssColor("#3e3e3eb9", ColorFromRgb(0x3E3E3E, 0.725f));
|
|
D2D1_COLOR_F hover = ColorFromRgb(0x414141);
|
|
D2D1_COLOR_F accent = ColorFromRgb(0x6B69D6);
|
|
float radius = 6.0f;
|
|
float borderWidth = 1.5f;
|
|
float containerPad = 5.0f;
|
|
bool showSelectedBar = true;
|
|
// Row corner radius, selected-row text colors and context-menu palette.
|
|
// Defaults are fluent dark; the fluent light branch and per-skin branches
|
|
// override. rowTextSelected/rowLabelSelected alpha 0 keeps the normal
|
|
// colors (bar-style selection, like the fluent CSS).
|
|
float itemRadius = 4.0f;
|
|
D2D1_COLOR_F rowTextSelected = D2D1::ColorF(0, 0.0f);
|
|
D2D1_COLOR_F rowLabelSelected = D2D1::ColorF(0, 0.0f);
|
|
D2D1_COLOR_F menuFill = D2D1::ColorF(0x2D2D2D);
|
|
D2D1_COLOR_F menuBorder = ParseCssColor("#9b9b9b2e", D2D1::ColorF(0x3A3A3A, 0.18f));
|
|
D2D1_COLOR_F menuText = D2D1::ColorF(0xE9E8E8);
|
|
D2D1_COLOR_F menuHover = ColorFromRgb(0x414141);
|
|
};
|
|
|
|
void ApplyPackageColors(const CandidateSkinCatalog::CandidateColors &colors, CandSkinTokens &tokens)
|
|
{
|
|
if (!colors.accent.empty())
|
|
{
|
|
tokens.accent = ParseCssColor(colors.accent, tokens.accent);
|
|
}
|
|
if (!colors.selected.empty())
|
|
{
|
|
tokens.selected = ParseCssColor(colors.selected, tokens.selected);
|
|
}
|
|
if (!colors.hover.empty())
|
|
{
|
|
tokens.hover = ParseCssColor(colors.hover, tokens.hover);
|
|
}
|
|
if (!colors.surface.empty())
|
|
{
|
|
tokens.surface = ParseCssColor(colors.surface, tokens.surface);
|
|
}
|
|
if (!colors.border.empty())
|
|
{
|
|
tokens.border = ParseCssColor(colors.border, tokens.border);
|
|
}
|
|
if (!colors.text.empty())
|
|
{
|
|
tokens.text = ParseCssColor(colors.text, tokens.text);
|
|
}
|
|
if (!colors.number.empty())
|
|
{
|
|
tokens.number = ParseCssColor(colors.number, tokens.number);
|
|
}
|
|
if (colors.showSelectedBar.has_value())
|
|
{
|
|
tokens.showSelectedBar = *colors.showSelectedBar;
|
|
}
|
|
}
|
|
|
|
std::wstring AssetRoot()
|
|
{
|
|
return CommonUtils::get_ime_data_path_w();
|
|
}
|
|
|
|
constexpr float kShadowPadLeft = 32.0f;
|
|
constexpr float kShadowPadTop = 20.0f;
|
|
constexpr float kShadowPadRight = 32.0f;
|
|
constexpr float kShadowPadBottom = 40.0f;
|
|
constexpr float kCandidateMinWidthDip = 160.0f;
|
|
constexpr float kDecorationCardOverlap = 5.0f;
|
|
|
|
} // namespace
|
|
|
|
struct CandidatePresenter::Impl
|
|
{
|
|
msimeui::DeviceResources resources;
|
|
std::unique_ptr<msimeui::Window> window;
|
|
std::shared_ptr<msimeui::StackPanel> root;
|
|
std::shared_ptr<msimeui::Image> decoration;
|
|
std::shared_ptr<msimeui::Card> card;
|
|
std::shared_ptr<msimeui::Container> frame;
|
|
std::shared_ptr<msimeui::StackPanel> body;
|
|
std::shared_ptr<msimeui::TextBlock> preedit;
|
|
std::shared_ptr<msimeui::CandidateList> list;
|
|
std::shared_ptr<msimeui::Popup> contextMenu;
|
|
std::shared_ptr<msimeui::Popup> contextSubmenu;
|
|
std::shared_ptr<msimeui::MenuFlyoutItem> fixPositionItem;
|
|
D2D1_COLOR_F menuFill = D2D1::ColorF(0x2D2D2D);
|
|
D2D1_COLOR_F menuBorder = ParseCssColor("#9b9b9b2e", D2D1::ColorF(0x3A3A3A, 0.18f));
|
|
D2D1_COLOR_F menuText = D2D1::ColorF(0xE9E8E8);
|
|
D2D1_COLOR_F menuHover = D2D1::ColorF(0x414141);
|
|
RECT hostRectBeforeMenu{};
|
|
bool contextMenuOpen = false;
|
|
bool contextSubmenuOpen = false;
|
|
bool hostExpandedForMenu = false;
|
|
size_t contextMenuPageIndex = 0;
|
|
};
|
|
|
|
CandidatePresenter::CandidatePresenter() = default;
|
|
|
|
CandidatePresenter &CandidatePresenter::Instance()
|
|
{
|
|
static CandidatePresenter instance;
|
|
return instance;
|
|
}
|
|
|
|
bool CandidatePresenter::IsBound() const
|
|
{
|
|
return bound_;
|
|
}
|
|
|
|
bool CandidatePresenter::Bind(HWND hwnd)
|
|
{
|
|
hwnd_ = hwnd;
|
|
if (!hwnd)
|
|
{
|
|
bound_ = false;
|
|
impl_.reset();
|
|
return false;
|
|
}
|
|
impl_ = std::make_unique<Impl>();
|
|
impl_->window = std::make_unique<msimeui::Window>(L"metasequoiaime_windows", L"cand", 1, 1);
|
|
impl_->window->AdoptExistingHwnd(hwnd);
|
|
impl_->window->SetStealFocusOnClick(false);
|
|
bound_ = impl_->resources.EnsureForComposition(hwnd);
|
|
RebuildScene();
|
|
CAND_DIAG_LOGF(L"candidate-d2d bind hwnd={:#x} composition={}", reinterpret_cast<uintptr_t>(hwnd), bound_ ? 1 : 0);
|
|
return bound_;
|
|
}
|
|
|
|
void CandidatePresenter::RebuildScene()
|
|
{
|
|
if (!impl_ || !impl_->window)
|
|
{
|
|
return;
|
|
}
|
|
lastSkinFingerprint_.clear();
|
|
impl_->root = std::make_shared<msimeui::StackPanel>(0.0f);
|
|
impl_->decoration = std::make_shared<msimeui::Image>(L"");
|
|
impl_->decoration->SetHorizontalAlignment(msimeui::HorizontalAlignment::Trailing);
|
|
impl_->preedit = std::make_shared<msimeui::TextBlock>(L"", 14.0f, D2D1::ColorF(0xF5F5F5));
|
|
impl_->preedit->SetTextLayoutPadding({0.0f, 1.0f, 0.0f, 1.0f});
|
|
impl_->preedit->SetHorizontalAlignment(msimeui::HorizontalAlignment::Leading);
|
|
impl_->list = std::make_shared<msimeui::CandidateList>(28.0f);
|
|
impl_->list->SetOnItemActivated([this](size_t index) { CommitItem(index); });
|
|
impl_->list->SetOnContextMenu(
|
|
[this](size_t index, const POINT &clientPoint) { ShowItemContextMenu(index, clientPoint); });
|
|
impl_->body = std::make_shared<msimeui::StackPanel>(2.0f);
|
|
impl_->body->SetPadding({0.0f, 0.0f, 0.0f, 0.0f});
|
|
impl_->body->AddChild(impl_->preedit);
|
|
impl_->body->AddChild(impl_->list);
|
|
msimeui::Brush brush;
|
|
brush.fill = D2D1::ColorF(0x202020);
|
|
brush.stroke = ParseCssColor("#9b9b9b2e", D2D1::ColorF(0x3A3A3A, 0.18f));
|
|
brush.strokeWidth = 1.5f;
|
|
brush.radiusX = 6.0f;
|
|
brush.radiusY = 6.0f;
|
|
impl_->card = std::make_shared<msimeui::Card>(brush, 5.0f);
|
|
impl_->card->SetHorizontalAlignment(msimeui::HorizontalAlignment::Leading);
|
|
impl_->card->AddChild(impl_->body);
|
|
impl_->root->SetHorizontalContentAlignment(msimeui::HorizontalAlignment::Leading);
|
|
impl_->frame = std::make_shared<msimeui::Container>();
|
|
impl_->frame->SetPadding({kShadowPadLeft, kShadowPadTop, kShadowPadRight, kShadowPadBottom});
|
|
impl_->frame->SetChild(impl_->card);
|
|
impl_->root->AddChild(impl_->decoration);
|
|
impl_->root->AddChild(impl_->frame);
|
|
auto scene = std::make_unique<msimeui::Scene>();
|
|
scene->SetRoot(impl_->root);
|
|
impl_->window->SetScene(std::move(scene));
|
|
}
|
|
|
|
void CandidatePresenter::ApplySkin()
|
|
{
|
|
if (!impl_ || !impl_->list || !impl_->preedit || !impl_->window)
|
|
{
|
|
return;
|
|
}
|
|
const std::string skinId = GetConfiguredCandidateSkin();
|
|
// Resolve the effective light/dark mode before the cache check: with
|
|
// theme_cand = "follow" the raw setting is unchanged when theme_mode or
|
|
// the Windows theme flips, but the resolved colors are what we render
|
|
// with, so the resolved mode must participate in the fingerprint.
|
|
const bool candLight = ResolveConfiguredTheme(GetConfiguredThemeCand()) == "light";
|
|
std::ostringstream fingerprint;
|
|
fingerprint << skinId << '|' << GetConfiguredThemeCand() << '|' << (candLight ? 'L' : 'D') << '|'
|
|
<< GetConfiguredCandidateFont() << '|' << GetConfiguredCandidateFontSize() << '|'
|
|
<< GetConfiguredCandidateWindowPreeditFontSize() << '|' << GetConfiguredCandidateWindowLayout() << '|'
|
|
<< GetConfiguredCandidateTextColor();
|
|
fingerprint << '|' << GetConfiguredCandidateEnglishFont();
|
|
for (const auto &font : GetConfiguredCandidateFallbackFonts())
|
|
fingerprint << '|' << font.size() << ':' << font;
|
|
const std::string skinKey = fingerprint.str();
|
|
if (skinKey == lastSkinFingerprint_ && impl_->card)
|
|
{
|
|
return;
|
|
}
|
|
lastSkinFingerprint_ = skinKey;
|
|
CandSkinTokens tokens;
|
|
if (candLight)
|
|
{
|
|
tokens.surface = ColorFromRgb(0xFFFFFF);
|
|
tokens.border = D2D1::ColorF(0, 0.12f);
|
|
tokens.text = ColorFromRgb(0x1A1A1A);
|
|
tokens.number = D2D1::ColorF(26.0f / 255.0f, 26.0f / 255.0f, 26.0f / 255.0f, 0.55f);
|
|
tokens.selected = ColorFromRgb(0xE8E8E8);
|
|
tokens.hover = ColorFromRgb(0xECECEC);
|
|
tokens.menuFill = ColorFromRgb(0xFFFFFF);
|
|
tokens.menuBorder = D2D1::ColorF(0, 0.12f);
|
|
tokens.menuText = ColorFromRgb(0x1A1A1A);
|
|
tokens.menuHover = ColorFromRgb(0xECECEC);
|
|
}
|
|
// Built-in skin palettes mirror ui-html/webview2/candwnd/skins/<skin>/;
|
|
// the WebView2 CSS is the reference for both the light and dark values.
|
|
if (skinId == "wechat")
|
|
{
|
|
tokens.borderWidth = 1.0f;
|
|
tokens.radius = 5.0f;
|
|
tokens.containerPad = 2.0f;
|
|
tokens.accent = ColorFromRgb(0x07C160);
|
|
tokens.selected = ColorFromRgb(0x07C160);
|
|
tokens.showSelectedBar = false;
|
|
tokens.rowTextSelected = ColorFromRgb(0xFFFFFF);
|
|
tokens.rowLabelSelected = ColorFromRgb(0xFFFFFF);
|
|
if (candLight)
|
|
{
|
|
tokens.surface = ColorFromRgb(0xF7F7F7);
|
|
tokens.border = ColorFromRgb(0xDEDEDE);
|
|
tokens.hover = D2D1::ColorF(7.0f / 255.0f, 193.0f / 255.0f, 96.0f / 255.0f, 0.14f);
|
|
tokens.text = ColorFromRgb(0x333333);
|
|
tokens.number = ColorFromRgb(0x757575);
|
|
tokens.menuFill = ColorFromRgb(0xFFFFFF);
|
|
tokens.menuBorder = ColorFromRgb(0xD9D9D9);
|
|
tokens.menuText = ColorFromRgb(0x333333);
|
|
tokens.menuHover = ColorFromRgb(0xEEEEEE);
|
|
}
|
|
else
|
|
{
|
|
tokens.surface = ColorFromRgb(0x151515);
|
|
tokens.border = ColorFromRgb(0x292929);
|
|
tokens.hover = D2D1::ColorF(7.0f / 255.0f, 193.0f / 255.0f, 96.0f / 255.0f, 0.32f);
|
|
tokens.text = ColorFromRgb(0xB7B7B7);
|
|
tokens.number = ColorFromRgb(0x858585);
|
|
tokens.menuFill = ColorFromRgb(0x1F1F1F);
|
|
tokens.menuBorder = ColorFromRgb(0x343434);
|
|
tokens.menuText = ColorFromRgb(0xD0D0D0);
|
|
tokens.menuHover = ColorFromRgb(0x2A2A2A);
|
|
}
|
|
}
|
|
else if (skinId == "willow_green")
|
|
{
|
|
tokens.borderWidth = 0.0f;
|
|
tokens.radius = 9.0f;
|
|
tokens.containerPad = 0.0f;
|
|
// CSS 把行圆角设为 0、靠容器 clip-path 裁出窗口圆角,但 D2D 端 Card
|
|
// 不裁剪子控件,行圆角 0 会让绿色选中块盖掉窗口圆角。有意偏离 CSS:
|
|
// 沿用对齐前的 4px 行圆角(视觉上等效圆角窗口,用户拍板的选择)。
|
|
tokens.itemRadius = 4.0f;
|
|
tokens.showSelectedBar = false;
|
|
tokens.rowTextSelected = ColorFromRgb(0xFFFFFF);
|
|
tokens.rowLabelSelected = ColorFromRgb(0xFFFFFF);
|
|
if (candLight)
|
|
{
|
|
tokens.surface = ColorFromRgb(0xF4F5F3);
|
|
tokens.accent = ColorFromRgb(0x58B980);
|
|
tokens.selected = ColorFromRgb(0x58B980);
|
|
tokens.hover = D2D1::ColorF(88.0f / 255.0f, 185.0f / 255.0f, 128.0f / 255.0f, 0.16f);
|
|
tokens.text = ColorFromRgb(0x343936);
|
|
tokens.number = ColorFromRgb(0x686F6A);
|
|
tokens.menuFill = ColorFromRgb(0xFBFCFA);
|
|
tokens.menuBorder = ColorFromRgb(0xD8DED9);
|
|
tokens.menuText = ColorFromRgb(0x343936);
|
|
tokens.menuHover = ColorFromRgb(0xE9EEEA);
|
|
}
|
|
else
|
|
{
|
|
tokens.surface = ColorFromRgb(0x2D2F2E);
|
|
tokens.accent = ColorFromRgb(0x65C98D);
|
|
tokens.selected = ColorFromRgb(0x65C98D);
|
|
tokens.hover = D2D1::ColorF(101.0f / 255.0f, 201.0f / 255.0f, 141.0f / 255.0f, 0.22f);
|
|
tokens.text = ColorFromRgb(0xD8DBD8);
|
|
tokens.number = ColorFromRgb(0xA6ABA7);
|
|
tokens.menuFill = ColorFromRgb(0x343635);
|
|
tokens.menuBorder = ColorFromRgb(0x454845);
|
|
tokens.menuText = ColorFromRgb(0xE0E2DF);
|
|
tokens.menuHover = ColorFromRgb(0x414441);
|
|
}
|
|
}
|
|
else if (skinId == "graphite")
|
|
{
|
|
tokens.borderWidth = 1.0f;
|
|
tokens.radius = 3.0f;
|
|
tokens.containerPad = 5.0f;
|
|
tokens.itemRadius = 2.0f;
|
|
tokens.selected = D2D1::ColorF(0, 0.0f);
|
|
tokens.showSelectedBar = false;
|
|
if (candLight)
|
|
{
|
|
tokens.surface = ColorFromRgb(0xFBFBFC);
|
|
tokens.border = ColorFromRgb(0xE2E5E9);
|
|
tokens.accent = ColorFromRgb(0x5F6B7A);
|
|
tokens.hover = D2D1::ColorF(31.0f / 255.0f, 41.0f / 255.0f, 55.0f / 255.0f, 0.055f);
|
|
tokens.text = ColorFromRgb(0x586476);
|
|
tokens.number = ColorFromRgb(0x8993A1);
|
|
tokens.rowTextSelected = ColorFromRgb(0x111827);
|
|
tokens.rowLabelSelected = ColorFromRgb(0x111827);
|
|
tokens.menuFill = ColorFromRgb(0xFFFFFF);
|
|
tokens.menuBorder = ColorFromRgb(0xDFE3E8);
|
|
tokens.menuText = ColorFromRgb(0x374151);
|
|
tokens.menuHover = ColorFromRgb(0xF1F3F5);
|
|
}
|
|
else
|
|
{
|
|
tokens.surface = ColorFromRgb(0x1C1F23);
|
|
tokens.border = ColorFromRgb(0x30353B);
|
|
tokens.accent = ColorFromRgb(0x8993A0);
|
|
tokens.hover = D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.055f);
|
|
tokens.text = ColorFromRgb(0xAEB6C2);
|
|
tokens.number = ColorFromRgb(0x707987);
|
|
tokens.rowTextSelected = ColorFromRgb(0xF1F3F5);
|
|
tokens.rowLabelSelected = ColorFromRgb(0xF1F3F5);
|
|
tokens.menuFill = ColorFromRgb(0x23272C);
|
|
tokens.menuBorder = ColorFromRgb(0x3A4047);
|
|
tokens.menuText = ColorFromRgb(0xC7CDD5);
|
|
tokens.menuHover = ColorFromRgb(0x30353B);
|
|
}
|
|
}
|
|
|
|
const std::wstring skinsRoot = AssetRoot() + L"\\skins";
|
|
std::optional<CandidateSkinCatalog::Package> package;
|
|
if (!CandidateSkinCatalog::IsBuiltIn(skinId))
|
|
{
|
|
package = CandidateSkinCatalog::Load(std::filesystem::path(skinsRoot), skinId);
|
|
if (package)
|
|
{
|
|
ApplyPackageColors(candLight ? package->light : package->dark, tokens);
|
|
}
|
|
}
|
|
|
|
msimeui::Theme theme = msimeui::ThemeManager::GetCurrent();
|
|
theme.textInputFontFamily = string_to_wstring(GetConfiguredCandidateFont());
|
|
if (theme.textInputFontFamily.empty())
|
|
{
|
|
theme.textInputFontFamily = L"Microsoft YaHei UI";
|
|
}
|
|
theme.uiFontFamily = theme.textInputFontFamily;
|
|
theme.surface = tokens.surface;
|
|
theme.border = tokens.border;
|
|
theme.textPrimary = ParseCssColor(GetConfiguredCandidateTextColor(), tokens.text);
|
|
theme.textSecondary = tokens.number;
|
|
theme.primary = tokens.accent;
|
|
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()));
|
|
for (const auto &font : GetConfiguredCandidateFallbackFontFamilies())
|
|
appearance.fallbackFontFamilies.push_back(string_to_wstring(font));
|
|
appearance.itemHeight = fontSize * 1.35f + 2.0f;
|
|
appearance.itemGap = 2.0f;
|
|
appearance.fontSize = fontSize;
|
|
appearance.labelFontSize = fontSize * 0.8f;
|
|
appearance.annotationFontSize = fontSize;
|
|
appearance.contentPadLeft = 0.0f;
|
|
appearance.contentPadRight = 0.0f;
|
|
appearance.textPadLeft = 5.0f;
|
|
appearance.labelGap = 1.5f;
|
|
appearance.selectedBarWidth = 3.0f;
|
|
appearance.selectedBarHeight = fontSize * 0.85f;
|
|
appearance.showSelectedBar = tokens.showSelectedBar;
|
|
appearance.selectedBarColor = tokens.accent;
|
|
appearance.cornerRadius = tokens.itemRadius;
|
|
appearance.textColor = theme.textPrimary;
|
|
appearance.labelColor = tokens.number;
|
|
appearance.annotationColor = theme.textPrimary;
|
|
appearance.rowFillSelected = tokens.selected;
|
|
appearance.rowFillHover = tokens.hover;
|
|
appearance.rowFillPressed = tokens.selected;
|
|
appearance.rowTextSelected = tokens.rowTextSelected;
|
|
appearance.rowLabelSelected = tokens.rowLabelSelected;
|
|
impl_->menuFill = tokens.menuFill;
|
|
impl_->menuBorder = tokens.menuBorder;
|
|
impl_->menuText = tokens.menuText;
|
|
impl_->menuHover = tokens.menuHover;
|
|
impl_->list->SetAppearance(appearance);
|
|
impl_->list->SetOrientation(GetConfiguredCandidateWindowLayout() == "horizontal"
|
|
? msimeui::CandidateList::Orientation::Horizontal
|
|
: msimeui::CandidateList::Orientation::Vertical);
|
|
impl_->preedit->SetFontFamily(appearance.fontFamily);
|
|
impl_->preedit->SetFallbackFontFamilies(appearance.fallbackFontFamilies);
|
|
impl_->preedit->SetFontSize(preeditSize);
|
|
impl_->preedit->SetColor(theme.textPrimary);
|
|
impl_->preedit->SetCaretColor(tokens.accent);
|
|
impl_->preedit->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
|
|
impl_->preedit->SetTextLayoutPadding({5.0f, 0.0f, 5.0f, 0.0f});
|
|
|
|
decorationTopDip_ = 0.0f;
|
|
decorationWidthDip_ = 0.0f;
|
|
std::wstring decorationPath;
|
|
if (package)
|
|
{
|
|
decorationTopDip_ = static_cast<float>(package->decorationTopDip);
|
|
decorationWidthDip_ = static_cast<float>(package->decorationWidthDip);
|
|
if (!package->preview.empty())
|
|
{
|
|
decorationPath =
|
|
skinsRoot + L"\\" + string_to_wstring(package->id) + L"\\" + string_to_wstring(package->preview);
|
|
}
|
|
}
|
|
if (decorationPath.empty() || decorationTopDip_ <= 0.0f)
|
|
{
|
|
impl_->decoration->SetHeight(0.0f);
|
|
impl_->decoration->ClearWidth();
|
|
impl_->decoration->SetSource(L"");
|
|
impl_->decoration->SetMargin({0.0f, 0.0f, 0.0f, 0.0f});
|
|
}
|
|
else
|
|
{
|
|
impl_->decoration->SetSource(decorationPath);
|
|
impl_->decoration->SetHeight(decorationTopDip_);
|
|
if (decorationWidthDip_ > 0.0f)
|
|
{
|
|
impl_->decoration->SetWidth(decorationWidthDip_);
|
|
}
|
|
impl_->decoration->SetStretch(msimeui::ImageStretch::Uniform);
|
|
impl_->decoration->SetMargin({0.0f, 0.0f, 0.0f, -(kShadowPadTop + kDecorationCardOverlap)});
|
|
}
|
|
|
|
msimeui::Brush brush;
|
|
brush.fill = tokens.surface;
|
|
brush.stroke = tokens.border;
|
|
brush.strokeWidth = tokens.borderWidth;
|
|
brush.radiusX = tokens.radius;
|
|
brush.radiusY = tokens.radius;
|
|
if (impl_->card)
|
|
{
|
|
impl_->card->SetBrush(brush);
|
|
impl_->card->SetPadding(tokens.containerPad);
|
|
impl_->card->SetMinWidth(kCandidateMinWidthDip);
|
|
}
|
|
}
|
|
|
|
void CandidatePresenter::FillItemsFromUi()
|
|
{
|
|
const Global::CandidatePageSnapshotPtr page = Global::LoadCandidatePageSnapshot();
|
|
std::vector<msimeui::CandidateList::Item> items;
|
|
for (size_t i = 0; i < page->page_views.size(); ++i)
|
|
{
|
|
const auto &view = page->page_views[i];
|
|
msimeui::CandidateList::Item item;
|
|
item.label = std::to_wstring(i + 1);
|
|
item.text = string_to_wstring(view.text + view.badge);
|
|
item.annotation = string_to_wstring(view.annotation);
|
|
if (GetConfiguredCandidateTranslationsEnabled())
|
|
item.translation = string_to_wstring(view.translation);
|
|
items.push_back(std::move(item));
|
|
}
|
|
ignoreSelectionCallback_ = true;
|
|
impl_->list->SetItems(std::move(items));
|
|
impl_->list->SetSelectedIndex(static_cast<size_t>((std::max)(0, page->selected_index_in_page)));
|
|
ignoreSelectionCallback_ = false;
|
|
}
|
|
|
|
void CandidatePresenter::CommitItem(size_t pageIndex)
|
|
{
|
|
if (!hwnd_)
|
|
{
|
|
return;
|
|
}
|
|
PostMessageW(hwnd_, WM_COMMIT_CANDIDATE, static_cast<WPARAM>(pageIndex + 1), 0);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
constexpr float kContextMenuWidthDip = 108.0f;
|
|
constexpr float kContextSubmenuWidthDip = 96.0f;
|
|
|
|
void StyleContextPopup(msimeui::Popup &popup, const D2D1_COLOR_F &fill, const D2D1_COLOR_F &border, float width)
|
|
{
|
|
popup.SetMatchAnchorWidth(false);
|
|
popup.SetWidth(width);
|
|
popup.SetPadding({2.0f, 2.0f, 2.0f, 2.0f});
|
|
popup.SetBackgroundFill(fill);
|
|
popup.SetBorderColor(border);
|
|
popup.SetCornerRadius(6.0f);
|
|
popup.SetShadowEnabled(true);
|
|
popup.SetOffset(0.0f, 0.0f);
|
|
popup.SetConstrainToViewport(true);
|
|
}
|
|
} // namespace
|
|
|
|
void CandidatePresenter::ShowItemContextMenu(size_t pageIndex, POINT clientPoint)
|
|
{
|
|
if (!hwnd_ || !impl_ || !impl_->list || !impl_->window)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CloseContextMenu(true);
|
|
impl_->contextMenuPageIndex = pageIndex;
|
|
|
|
// A mouse callback has no posted-message handoff at all, so it must not walk the worker's live page either.
|
|
const Global::CandidatePageSnapshotPtr page = Global::LoadCandidatePageSnapshot();
|
|
std::wstring word;
|
|
if (pageIndex < page->page_words.size())
|
|
{
|
|
word = page->page_words[pageIndex];
|
|
}
|
|
size_t codePoints = 0;
|
|
for (size_t i = 0; i < word.size(); ++i)
|
|
{
|
|
++codePoints;
|
|
if (i + 1 < word.size() && IS_HIGH_SURROGATE(word[i]) && IS_LOW_SURROGATE(word[i + 1]))
|
|
{
|
|
++i;
|
|
}
|
|
}
|
|
const bool showDelete = codePoints != 1;
|
|
const WPARAM oneBased = static_cast<WPARAM>(pageIndex + 1);
|
|
|
|
auto makeItem = [this](const std::wstring &text, bool hasSubmenu, std::function<void()> onClick) {
|
|
auto item = std::make_shared<msimeui::MenuFlyoutItem>(text, hasSubmenu);
|
|
item->SetColors(impl_->menuText, impl_->menuHover);
|
|
if (onClick)
|
|
{
|
|
item->SetOnClick(std::move(onClick));
|
|
}
|
|
return item;
|
|
};
|
|
|
|
auto stack = std::make_shared<msimeui::StackPanel>(0.0f);
|
|
auto pin = makeItem(L"置顶", false, [this, oneBased]() {
|
|
CloseContextMenu(true);
|
|
PostMessageW(hwnd_, WM_PIN_TO_TOP_CANDIDATE, oneBased, 0);
|
|
});
|
|
pin->SetOnHover([this](bool hovered) {
|
|
if (hovered)
|
|
{
|
|
CloseFixSubmenu();
|
|
}
|
|
});
|
|
impl_->fixPositionItem = makeItem(L"固定排位", true, {});
|
|
impl_->fixPositionItem->SetOnHover([this](bool hovered) {
|
|
if (hovered)
|
|
{
|
|
OpenFixSubmenu();
|
|
}
|
|
});
|
|
stack->AddChild(pin);
|
|
stack->AddChild(impl_->fixPositionItem);
|
|
if (showDelete)
|
|
{
|
|
auto del = makeItem(L"删除", false, [this, oneBased]() {
|
|
CloseContextMenu(true);
|
|
PostMessageW(hwnd_, WM_DELETE_CANDIDATE, oneBased, 0);
|
|
});
|
|
del->SetOnHover([this](bool hovered) {
|
|
if (hovered)
|
|
{
|
|
CloseFixSubmenu();
|
|
}
|
|
});
|
|
stack->AddChild(del);
|
|
}
|
|
|
|
auto subStack = std::make_shared<msimeui::StackPanel>(0.0f);
|
|
for (int position = 1; position <= 5; ++position)
|
|
{
|
|
const std::wstring label = L"第 " + std::to_wstring(position) + L" 位";
|
|
subStack->AddChild(makeItem(label, false, [this, oneBased, position]() {
|
|
CloseContextMenu(true);
|
|
PostMessageW(hwnd_, WM_FIX_CANDIDATE_POSITION, oneBased, position);
|
|
}));
|
|
}
|
|
auto separator = std::make_shared<msimeui::MenuSeparator>();
|
|
separator->SetColor(D2D1::ColorF(impl_->menuText.r, impl_->menuText.g, impl_->menuText.b, 0.125f));
|
|
subStack->AddChild(separator);
|
|
subStack->AddChild(makeItem(L"取消固定", false, [this, oneBased]() {
|
|
CloseContextMenu(true);
|
|
PostMessageW(hwnd_, WM_CLEAR_CANDIDATE_POSITION, oneBased, 0);
|
|
}));
|
|
|
|
impl_->contextMenu = std::make_shared<msimeui::Popup>(stack);
|
|
StyleContextPopup(*impl_->contextMenu, impl_->menuFill, impl_->menuBorder, kContextMenuWidthDip);
|
|
impl_->contextSubmenu = std::make_shared<msimeui::Popup>(subStack);
|
|
StyleContextPopup(*impl_->contextSubmenu, impl_->menuFill, impl_->menuBorder, kContextSubmenuWidthDip);
|
|
|
|
const msimeui::PointF anchor = impl_->window->ClientPixelsToDips(clientPoint);
|
|
impl_->contextMenu->SetAnchorRect({anchor.x, anchor.y, 1.0f, 1.0f});
|
|
ExpandHostForMenu(clientPoint);
|
|
|
|
if (msimeui::Scene *scene = impl_->window->GetScene())
|
|
{
|
|
scene->AddPopup(impl_->contextMenu, [this]() { CloseContextMenu(true); });
|
|
impl_->contextMenuOpen = true;
|
|
}
|
|
Present();
|
|
}
|
|
|
|
void CandidatePresenter::CloseContextMenu(bool restoreHost)
|
|
{
|
|
if (!impl_ || !impl_->window)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (msimeui::Scene *scene = impl_->window->GetScene())
|
|
{
|
|
if (impl_->contextSubmenu)
|
|
{
|
|
scene->RemovePopup(impl_->contextSubmenu.get(), false);
|
|
}
|
|
if (impl_->contextMenu)
|
|
{
|
|
scene->RemovePopup(impl_->contextMenu.get(), false);
|
|
}
|
|
}
|
|
impl_->contextMenuOpen = false;
|
|
impl_->contextSubmenuOpen = false;
|
|
impl_->fixPositionItem.reset();
|
|
impl_->contextMenu.reset();
|
|
impl_->contextSubmenu.reset();
|
|
if (restoreHost)
|
|
{
|
|
RestoreHostAfterMenu();
|
|
}
|
|
else
|
|
{
|
|
impl_->hostExpandedForMenu = false;
|
|
}
|
|
}
|
|
|
|
void CandidatePresenter::OpenFixSubmenu()
|
|
{
|
|
if (!impl_ || !impl_->window || !impl_->contextSubmenu || !impl_->fixPositionItem || impl_->contextSubmenuOpen)
|
|
{
|
|
return;
|
|
}
|
|
const msimeui::RectF anchor = impl_->fixPositionItem->GetBounds();
|
|
if (anchor.width < 1.0f || anchor.height < 1.0f)
|
|
{
|
|
return;
|
|
}
|
|
impl_->contextSubmenu->SetAnchorRect(anchor);
|
|
impl_->contextSubmenu->SetOffset(anchor.width - 2.0f, -anchor.height);
|
|
if (msimeui::Scene *scene = impl_->window->GetScene())
|
|
{
|
|
scene->AddPopup(impl_->contextSubmenu, [this]() { impl_->contextSubmenuOpen = false; });
|
|
impl_->contextSubmenuOpen = true;
|
|
}
|
|
}
|
|
|
|
void CandidatePresenter::CloseFixSubmenu()
|
|
{
|
|
if (!impl_ || !impl_->window || !impl_->contextSubmenu || !impl_->contextSubmenuOpen)
|
|
{
|
|
return;
|
|
}
|
|
if (msimeui::Scene *scene = impl_->window->GetScene())
|
|
{
|
|
scene->RemovePopup(impl_->contextSubmenu.get(), false);
|
|
}
|
|
impl_->contextSubmenuOpen = false;
|
|
}
|
|
|
|
void CandidatePresenter::ExpandHostForMenu(POINT clientPoint)
|
|
{
|
|
if (!hwnd_ || !impl_ || !impl_->window)
|
|
{
|
|
return;
|
|
}
|
|
RECT windowRect{};
|
|
RECT clientRect{};
|
|
GetWindowRect(hwnd_, &windowRect);
|
|
GetClientRect(hwnd_, &clientRect);
|
|
if (!impl_->hostExpandedForMenu)
|
|
{
|
|
impl_->hostRectBeforeMenu = windowRect;
|
|
impl_->hostExpandedForMenu = true;
|
|
}
|
|
|
|
const float dpi = impl_->window->GetDpi();
|
|
const msimeui::PointF anchor = impl_->window->ClientPixelsToDips(clientPoint);
|
|
const msimeui::SizeF clientDip = impl_->window->ClientPixelsToDips(SIZE{clientRect.right, clientRect.bottom});
|
|
const float needWidth =
|
|
(std::max)(clientDip.width, anchor.x + kContextMenuWidthDip + kContextSubmenuWidthDip + 16.0f);
|
|
const float needHeight = (std::max)(clientDip.height, (std::max)(anchor.y + 168.0f, 176.0f));
|
|
const int widthPx = (std::max)(static_cast<int>(windowRect.right - windowRect.left),
|
|
static_cast<int>(std::ceil(needWidth * dpi / 96.0f)));
|
|
const int heightPx = (std::max)(static_cast<int>(windowRect.bottom - windowRect.top),
|
|
static_cast<int>(std::ceil(needHeight * dpi / 96.0f)));
|
|
if (widthPx == windowRect.right - windowRect.left && heightPx == windowRect.bottom - windowRect.top)
|
|
{
|
|
return;
|
|
}
|
|
SetWindowPos(hwnd_, HWND_TOPMOST, windowRect.left, windowRect.top, widthPx, heightPx,
|
|
SWP_NOACTIVATE | SWP_NOZORDER);
|
|
}
|
|
|
|
void CandidatePresenter::RestoreHostAfterMenu()
|
|
{
|
|
if (!hwnd_ || !impl_ || !impl_->hostExpandedForMenu)
|
|
{
|
|
return;
|
|
}
|
|
const RECT &rc = impl_->hostRectBeforeMenu;
|
|
SetWindowPos(hwnd_, nullptr, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
|
|
SWP_NOACTIVATE | SWP_NOZORDER);
|
|
impl_->hostExpandedForMenu = false;
|
|
}
|
|
|
|
void CandidatePresenter::PlaceAndShow(POINT caret, float widthDip, float heightDip, float cardLeftDip, float cardTopDip,
|
|
const ResolvedCandidateScale &resolved)
|
|
{
|
|
FLOAT scale = resolved.scale;
|
|
CandidateScaleSource scaleSource = resolved.source;
|
|
if (scale <= 0.0f)
|
|
{
|
|
const ResolvedCandidateScale now = ResolveCandidateScaleForCaret(caret);
|
|
scale = now.scale;
|
|
scaleSource = now.source;
|
|
}
|
|
if (scale <= 0.0f)
|
|
{
|
|
scale = 1.0f;
|
|
scaleSource = CandidateScaleSource::Monitor;
|
|
}
|
|
const HalfScreenDipLimits limits = ApplyScaleToHalfScreenLimits(caret, scale);
|
|
widthDip = static_cast<float>(ClampWidthDipToHalfScreen(widthDip, limits));
|
|
heightDip = static_cast<float>(ClampHeightDipToHalfScreen(heightDip, limits));
|
|
lastLayoutWidthDip_ = widthDip;
|
|
lastLayoutHeightDip_ = heightDip;
|
|
cardLeftDip = (std::max)(0.0f, cardLeftDip);
|
|
cardTopDip = (std::max)(0.0f, (std::min)(cardTopDip, heightDip));
|
|
float cardWidthDip = (std::max)(widthDip - cardLeftDip - kShadowPadRight, 1.0f);
|
|
float cardHeightDip = (std::max)(heightDip - cardTopDip - kShadowPadBottom, 1.0f);
|
|
if (impl_->card)
|
|
{
|
|
const msimeui::RectF card = impl_->card->GetBounds();
|
|
if (card.width > 1.0f)
|
|
{
|
|
cardWidthDip = card.width;
|
|
}
|
|
if (card.height > 1.0f)
|
|
{
|
|
cardHeightDip = card.height;
|
|
}
|
|
}
|
|
auto properPos = std::make_shared<std::pair<int, int>>();
|
|
// Position the opaque card, not the decoration or drop shadow around it.
|
|
AdjustCandidateWindowPosition(&caret, {cardWidthDip, cardHeightDip}, properPos, scale, cardWidthDip);
|
|
int widthPx = AlignHostPixels((std::max)(1, static_cast<int>(std::ceil(widthDip * scale))));
|
|
int heightPx = AlignHostPixels((std::max)(1, static_cast<int>(std::ceil(heightDip * scale))));
|
|
widthPx = (std::max)(widthPx, lastHostWidthPx_);
|
|
heightPx = (std::max)(heightPx, lastHostHeightPx_);
|
|
lastHostWidthPx_ = widthPx;
|
|
lastHostHeightPx_ = heightPx;
|
|
const int cardLeftPx = static_cast<int>(std::lround(cardLeftDip * scale));
|
|
const int cardTopPx = static_cast<int>(std::lround(cardTopDip * scale));
|
|
const int cardHeightPx = static_cast<int>(std::lround(cardHeightDip * scale));
|
|
int x = properPos->first - cardLeftPx;
|
|
int y = properPos->second - cardTopPx;
|
|
const MonitorCoordinates monitor = GetMonitorCoordinatesFromPoint(caret);
|
|
if (x + widthPx > monitor.right)
|
|
{
|
|
x = monitor.right - widthPx - 2;
|
|
}
|
|
if (x < monitor.left)
|
|
{
|
|
x = monitor.left + 2;
|
|
}
|
|
const int cardBottom = y + cardTopPx + cardHeightPx;
|
|
if (cardBottom > monitor.bottom)
|
|
{
|
|
y = monitor.bottom - cardTopPx - cardHeightPx - 2;
|
|
}
|
|
// Allow the mascot to clip above the work area so the card stays on the caret.
|
|
if (y + cardTopPx < monitor.top)
|
|
{
|
|
y = monitor.top + 2 - cardTopPx;
|
|
}
|
|
SetWindowPos(hwnd_, HWND_TOPMOST, x, y, widthPx, heightPx, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
// Rendering must use the same scale the window was just sized with: the ui
|
|
// override replaces GetDpiForWindow (144 inside RDP) so DIPs map exactly
|
|
// onto the pixels computed above. Must land before EnsureForComposition —
|
|
// a freshly created target reads it through DpiForHwnd().
|
|
const FLOAT dpiOverride = scale * 96.0f;
|
|
impl_->window->SetDpiOverride(dpiOverride);
|
|
impl_->resources.SetDpiOverride(dpiOverride);
|
|
impl_->resources.EnsureForComposition(hwnd_);
|
|
Present();
|
|
SetCandidateHostCloaked(false);
|
|
// Acceptance trace for the RDP candidate-scale fix: which scale authority
|
|
// won, and the system's own (potentially diverging) DPI values. Coordinates
|
|
// and DPI only — never user input, per the diagnostic logging red lines.
|
|
CAND_DIAG_LOGF(L"candidate-d2d place source={} scale={:.3f} hwnd_dpi={} system_dpi={} remote={} caret=({},{}) "
|
|
L"size_px=({},{})",
|
|
scaleSource == CandidateScaleSource::RdpForeground ? L"rdp-foreground" : L"monitor", scale,
|
|
GetDpiForWindow(hwnd_), GetDpiForSystem(), GetSystemMetrics(SM_REMOTESESSION) ? 1 : 0, caret.x,
|
|
caret.y, widthPx, heightPx);
|
|
}
|
|
|
|
void CandidatePresenter::ShowFromGlobalState()
|
|
{
|
|
ShowFromGlobalState(POINT{Global::Point[0], Global::Point[1]});
|
|
}
|
|
|
|
void CandidatePresenter::ShowFromGlobalState(POINT caret)
|
|
{
|
|
if (!bound_ || !hwnd_ || !impl_ || !impl_->root)
|
|
{
|
|
return;
|
|
}
|
|
// A composition can start before the host reports a usable text extent
|
|
// (first focus, or resuming after a long idle). The caret then arrives as
|
|
// (0, INVALID_Y), which placement would clamp into the monitor's work area.
|
|
// Hide even an already-visible host until a real anchor arrives. Preserve
|
|
// the show request so MoveCandidate / the settle timer can place it later.
|
|
if (caret.y == Global::INVALID_Y)
|
|
{
|
|
CloseContextMenu(false);
|
|
SetCandidateHostCloaked(true);
|
|
::is_global_wnd_cand_shown = true;
|
|
CAND_DIAG_LOGF(L"candidate-d2d show deferred: no usable caret anchor ({},{})", caret.x, caret.y);
|
|
return;
|
|
}
|
|
CloseContextMenu(false);
|
|
ApplySkin();
|
|
std::wstring preedit;
|
|
size_t caretIndex = 0;
|
|
if (GetConfiguredCandidateWindowPreeditStyle() != "empty")
|
|
{
|
|
preedit = GetPreeditWithCaretMarker();
|
|
const size_t marker = preedit.find(L'\uE000');
|
|
if (marker != std::wstring::npos)
|
|
{
|
|
caretIndex = marker;
|
|
preedit.erase(marker, 1);
|
|
}
|
|
else
|
|
{
|
|
caretIndex = preedit.size();
|
|
}
|
|
}
|
|
impl_->preedit->SetText(preedit);
|
|
if (preedit.empty() && GetConfiguredCandidateWindowPreeditStyle() == "empty")
|
|
{
|
|
impl_->preedit->ClearCaret();
|
|
impl_->preedit->SetHeight(0.0f);
|
|
}
|
|
else
|
|
{
|
|
impl_->preedit->ClearHeight();
|
|
impl_->preedit->SetCaretIndex(caretIndex);
|
|
}
|
|
FillItemsFromUi();
|
|
impl_->list->SetHoverEnabled(hoverArmed_);
|
|
// Resolve the scale once per show and thread it through measure, clamping,
|
|
// window sizing and rendering DPI — a foreground window changing mid-show
|
|
// must not split measure (clamped at one scale) from placement (another).
|
|
const ResolvedCandidateScale scale = ResolveCandidateScaleForCaret(caret);
|
|
const HalfScreenDipLimits limits = ApplyScaleToHalfScreenLimits(caret, scale.scale);
|
|
const float maxW = limits.maxWidthDip > 1.0 ? static_cast<float>(limits.maxWidthDip) : 480.0f;
|
|
const float maxH = limits.maxHeightDip > 1.0 ? static_cast<float>(limits.maxHeightDip) : 640.0f;
|
|
impl_->root->InvalidateMeasure();
|
|
const msimeui::SizeF measured = impl_->root->MeasureInLayout({maxW, maxH});
|
|
float widthDip = (std::max)(measured.width, kCandidateMinWidthDip + kShadowPadLeft + kShadowPadRight);
|
|
float heightDip = (std::max)(measured.height, 36.0f);
|
|
impl_->root->InvalidateArrange();
|
|
impl_->root->ArrangeInLayout({0.0f, 0.0f, widthDip, heightDip});
|
|
hoverArmed_ = false;
|
|
if (!GetCursorPos(&hoverBaseline_))
|
|
{
|
|
hoverBaseline_ = {};
|
|
}
|
|
float cardLeftDip = 0.0f;
|
|
float cardTopDip = 0.0f;
|
|
if (impl_->card)
|
|
{
|
|
const msimeui::RectF card = impl_->card->GetBounds();
|
|
cardLeftDip = card.x;
|
|
cardTopDip = card.y;
|
|
}
|
|
PlaceAndShow(caret, widthDip, heightDip, cardLeftDip, cardTopDip, scale);
|
|
::is_global_wnd_cand_shown = true;
|
|
}
|
|
|
|
void CandidatePresenter::Hide()
|
|
{
|
|
if (!hwnd_)
|
|
{
|
|
return;
|
|
}
|
|
CloseContextMenu(false);
|
|
::is_global_wnd_cand_shown = false;
|
|
hoverArmed_ = false;
|
|
if (impl_ && impl_->list)
|
|
{
|
|
impl_->list->SetHoverEnabled(false);
|
|
}
|
|
SetCandidateHostCloaked(true);
|
|
lastHostWidthPx_ = 0;
|
|
lastHostHeightPx_ = 0;
|
|
SetWindowPos(hwnd_, nullptr, 0, Global::INVALID_Y, 0, 0,
|
|
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
}
|
|
|
|
void CandidatePresenter::Present()
|
|
{
|
|
if (!bound_ || !hwnd_ || !impl_ || !impl_->window)
|
|
{
|
|
return;
|
|
}
|
|
RECT rc{};
|
|
GetClientRect(hwnd_, &rc);
|
|
const UINT width = static_cast<UINT>((std::max)(1L, rc.right));
|
|
const UINT height = static_cast<UINT>((std::max)(1L, rc.bottom));
|
|
if (!impl_->resources.EnsureForComposition(hwnd_))
|
|
{
|
|
return;
|
|
}
|
|
impl_->resources.Resize(width, height);
|
|
ID2D1RenderTarget *target = impl_->resources.GetRenderTarget();
|
|
if (!target)
|
|
{
|
|
return;
|
|
}
|
|
target->BeginDraw();
|
|
target->Clear(D2D1::ColorF(0, 0.0f));
|
|
if (msimeui::Scene *scene = impl_->window->GetScene())
|
|
{
|
|
const float dpi = impl_->window->GetDpi();
|
|
float layoutW = msimeui::PixelsToDips(static_cast<float>(width), dpi);
|
|
float layoutH = msimeui::PixelsToDips(static_cast<float>(height), dpi);
|
|
// Host HWND is larger than the card (shadow padding, 64px buckets, and a
|
|
// grow-only size while typing). Stretching the scene to that HWND fills
|
|
// the rounded card with empty space. Keep layout at the measured DIP size.
|
|
if (!impl_->hostExpandedForMenu && lastLayoutWidthDip_ > 0.0f && lastLayoutHeightDip_ > 0.0f)
|
|
{
|
|
layoutW = lastLayoutWidthDip_;
|
|
layoutH = lastLayoutHeightDip_;
|
|
}
|
|
scene->EnsureLayout({layoutW, layoutH});
|
|
scene->Render(impl_->resources);
|
|
}
|
|
const HRESULT hr = target->EndDraw();
|
|
if (hr == D2DERR_RECREATE_TARGET)
|
|
{
|
|
impl_->resources.DiscardTarget();
|
|
return;
|
|
}
|
|
impl_->resources.Present();
|
|
}
|
|
|
|
void CandidatePresenter::ArmHoverIfPointerMoved()
|
|
{
|
|
POINT now{};
|
|
if (!GetCursorPos(&now))
|
|
{
|
|
return;
|
|
}
|
|
const int dx = now.x - hoverBaseline_.x;
|
|
const int dy = now.y - hoverBaseline_.y;
|
|
if (dx * dx + dy * dy < 4)
|
|
{
|
|
return;
|
|
}
|
|
hoverArmed_ = true;
|
|
if (impl_ && impl_->list)
|
|
{
|
|
impl_->list->SetHoverEnabled(true);
|
|
}
|
|
}
|
|
|
|
bool CandidatePresenter::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
if (!bound_ || !impl_ || !impl_->window)
|
|
{
|
|
return false;
|
|
}
|
|
switch (message)
|
|
{
|
|
case WM_ERASEBKGND:
|
|
return true;
|
|
case WM_PAINT: {
|
|
PAINTSTRUCT ps{};
|
|
BeginPaint(hwnd_, &ps);
|
|
Present();
|
|
EndPaint(hwnd_, &ps);
|
|
return true;
|
|
}
|
|
case WM_SIZE:
|
|
if (::is_global_wnd_cand_shown)
|
|
{
|
|
impl_->resources.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
|
Present();
|
|
}
|
|
return true;
|
|
case WM_MOUSEMOVE:
|
|
ArmHoverIfPointerMoved();
|
|
if (!hoverArmed_ && !(impl_ && impl_->contextMenuOpen))
|
|
{
|
|
return true;
|
|
}
|
|
impl_->window->DispatchImportedMessage(message, wParam, lParam);
|
|
Present();
|
|
return true;
|
|
case WM_LBUTTONDOWN:
|
|
case WM_LBUTTONUP:
|
|
case WM_RBUTTONUP:
|
|
case WM_MOUSELEAVE:
|
|
impl_->window->DispatchImportedMessage(message, wParam, lParam);
|
|
Present();
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
CandidatePresenter::~CandidatePresenter() = default;
|