Files
msime-windows/server/src/window/candidate_presenter.cpp
T
fanlusky 5c02503139 Render the candidate window with D2D instead of WebView2
Drive the existing candidate HWND through CandidatePresenter and msimeui,
keep WebView2 for the menu, floating toolbar, and settings, and load
external skins from skin.toml color tokens. Restore cloud/AI badges on
D2D items, and keep click, pin, fix-position, and delete on the same
window messages as before.
2026-09-02 15:30:03 +08:00

1020 lines
34 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 "MetasequoiaImeEngine/common/helpcode_utils.h"
#include "MetasequoiaImeEngine/core/scheme_type.h"
#include "MetasequoiaImeEngine/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;
}
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;
};
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 string_to_wstring(CommonUtils::get_local_appdata_path()) + L"\\" + GlobalIme::AppName;
}
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;
bool ShouldShowHelpcode()
{
switch (GetConfiguredActiveInputScheme())
{
case SchemeType::Shuangpin:
return GetConfiguredShowShuangpinHelpcodeInCandidateWindow();
case SchemeType::Quanpin:
return GetConfiguredShowQuanpinHelpcodeInCandidateWindow();
default:
return false;
}
}
} // 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;
}
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->AddChild(impl_->body);
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 bool candLight = ResolveConfiguredTheme(GetConfiguredThemeCand()) == "light";
const std::string skinId = GetConfiguredCandidateSkin();
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);
}
if (skinId == "wechat")
{
tokens.surface = candLight ? ColorFromRgb(0xF7F7F7) : ColorFromRgb(0x151515);
tokens.border = ColorFromRgb(0x292929);
tokens.borderWidth = 1.0f;
tokens.radius = 5.0f;
tokens.accent = ColorFromRgb(0x07C160);
tokens.selected = ColorFromRgb(0x07C160);
tokens.hover = D2D1::ColorF(7.0f / 255.0f, 193.0f / 255.0f, 96.0f / 255.0f, 0.32f);
tokens.showSelectedBar = false;
tokens.text = ColorFromRgb(0xB7B7B7);
tokens.number = ColorFromRgb(0x858585);
}
else if (skinId == "willow_green")
{
tokens.surface = ColorFromRgb(0x2D2F2E);
tokens.borderWidth = 0.0f;
tokens.radius = 9.0f;
tokens.containerPad = 0.0f;
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.showSelectedBar = false;
}
else if (skinId == "graphite")
{
tokens.surface = ColorFromRgb(0x1C1F23);
tokens.border = ColorFromRgb(0x30353B);
tokens.borderWidth = 1.0f;
tokens.radius = 3.0f;
tokens.containerPad = 5.0f;
tokens.accent = ColorFromRgb(0x8993A0);
tokens.selected = D2D1::ColorF(0, 0.0f);
tokens.hover = D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.055f);
tokens.text = ColorFromRgb(0xAEB6C2);
tokens.number = ColorFromRgb(0x707987);
tokens.showSelectedBar = false;
}
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.itemHeight = fontSize * 1.35f + 2.0f;
appearance.itemGap = 2.0f;
appearance.fontSize = fontSize;
appearance.labelFontSize = fontSize * 0.8f;
appearance.annotationFontSize = fontSize;
appearance.cornerRadius = 4.0f;
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.textColor = theme.textPrimary;
appearance.labelColor = tokens.number;
appearance.annotationColor = theme.textPrimary;
appearance.rowFillSelected = tokens.selected;
appearance.rowFillHover = tokens.hover;
appearance.rowFillPressed = tokens.selected;
impl_->menuFill = candLight ? ColorFromRgb(0xFFFFFF) : ColorFromRgb(0x2D2D2D);
impl_->menuBorder = tokens.border;
impl_->menuText = theme.textPrimary;
impl_->menuHover = tokens.hover;
impl_->list->SetAppearance(appearance);
impl_->list->SetOrientation(GetConfiguredCandidateWindowLayout() == "horizontal"
? msimeui::CandidateList::Orientation::Horizontal
: msimeui::CandidateList::Orientation::Vertical);
impl_->preedit->SetFontFamily(theme.textInputFontFamily);
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;
impl_->card = std::make_shared<msimeui::Card>(brush, tokens.containerPad);
impl_->card->SetMinWidth(kCandidateMinWidthDip);
impl_->card->AddChild(impl_->body);
impl_->frame = std::make_shared<msimeui::Container>();
impl_->frame->SetPadding({kShadowPadLeft, kShadowPadTop, kShadowPadRight, kShadowPadBottom});
impl_->frame->SetChild(impl_->card);
impl_->root = std::make_shared<msimeui::StackPanel>(0.0f);
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::FillItemsFromUi()
{
auto &ui = Global::candidate_ui;
std::vector<msimeui::CandidateList::Item> items;
const bool help = ShouldShowHelpcode();
const int start = ui.current_page_start();
for (size_t i = 0; i < ui.page_words.size(); ++i)
{
msimeui::CandidateList::Item item;
item.label = std::to_wstring(i + 1);
item.text = ui.page_words[i];
const size_t absIndex = static_cast<size_t>(start) + i;
if (absIndex < ui.items.size())
{
switch (ui.items[absIndex].source)
{
case CandidateSource::CloudSuggestion:
item.text += L" ☁️";
break;
case CandidateSource::AiSuggestion:
item.text += L" 🤖";
break;
default:
break;
}
}
if (help && absIndex < ui.items.size())
{
item.annotation = string_to_wstring(HelpcodeUtils::compute_helpcodes(ui.items[absIndex].word));
}
if (GetConfiguredCandidateTranslationsEnabled() && GetConfiguredCandidateWindowLayout() == "vertical" &&
i < ui.page_glosses.size())
{
item.translation = ui.page_glosses[i];
}
items.push_back(std::move(item));
}
ignoreSelectionCallback_ = true;
impl_->list->SetItems(std::move(items));
impl_->list->SetSelectedIndex(static_cast<size_t>((std::max)(0, ui.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;
auto &ui = Global::candidate_ui;
std::wstring word;
if (pageIndex < ui.page_words.size())
{
word = ui.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 HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(caret);
FLOAT scale = limits.scale > 0.0f ? limits.scale : GetScaleForPoint(caret);
if (scale <= 0.0f)
{
scale = 1.0f;
}
widthDip = static_cast<float>(ClampWidthDipToHalfScreen(widthDip, limits));
heightDip = static_cast<float>(ClampHeightDipToHalfScreen(heightDip, limits));
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);
const int widthPx = (std::max)(1, static_cast<int>(std::ceil(widthDip * scale)));
const int heightPx = (std::max)(1, static_cast<int>(std::ceil(heightDip * scale)));
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);
impl_->resources.EnsureForComposition(hwnd_);
Present();
SetCandidateHostCloaked(false);
}
void CandidatePresenter::ShowFromGlobalState()
{
ShowFromGlobalState(POINT{Global::Point[0], Global::Point[1]});
}
void CandidatePresenter::ShowFromGlobalState(POINT caret)
{
if (!bound_ || !hwnd_ || !impl_ || !impl_->root)
{
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_);
const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(caret);
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);
::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);
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();
scene->EnsureLayout({msimeui::PixelsToDips(static_cast<float>(width), dpi),
msimeui::PixelsToDips(static_cast<float>(height), dpi)});
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;