From ea9218ceff3c82fdcc37fe1bc316856b85d9d3d2 Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Wed, 2 Sep 2026 16:04:29 +0800 Subject: [PATCH] Replace the tray menu WebView2 with a D2D presenter TrayMenuPresenter renders the language-bar right-click menu through Direct2D on the existing menu HWND, matching the HTML version 1:1: floating toolbar toggle, emoji/handwriting/keyboard/voice launchers, settings, and about. The WebView2 menu path is kept as a fallback but no longer created by default. Preedit caret reflow now uses an inline object slot instead of letter-spacing, and cloud/AI candidate badges are restored on D2D items. --- server/CMakeLists.txt | 2 + server/msimeui | 2 +- server/src/webview2/windows_webview2.cpp | 63 +++- server/src/window/ime_windows.cpp | 25 ++ server/src/window/tray_menu_presenter.cpp | 417 ++++++++++++++++++++++ server/src/window/tray_menu_presenter.h | 35 ++ 6 files changed, 537 insertions(+), 7 deletions(-) create mode 100644 server/src/window/tray_menu_presenter.cpp create mode 100644 server/src/window/tray_menu_presenter.h diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index d409604d..cbc65e08 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -171,6 +171,8 @@ set(SRC_FILES "./src/webview2/windows_webview2.h" "./src/window/candidate_presenter.cpp" "./src/window/candidate_presenter.h" + "./src/window/tray_menu_presenter.cpp" + "./src/window/tray_menu_presenter.h" "./src/window/ime_windows.cpp" "./src/window/ime_windows.h" "./src/window/window_hook.cpp" diff --git a/server/msimeui b/server/msimeui index 1c12182a..547bb3ee 160000 --- a/server/msimeui +++ b/server/msimeui @@ -1 +1 @@ -Subproject commit 1c12182ab64ca5df694915cb24813397d2a906fd +Subproject commit 547bb3ee9f4fae96db32ec1b5da544f6a6a70177 diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index bdd0581a..5fc98fa7 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -6,6 +6,7 @@ #include "utils/ime_utils.h" #include "utils/webview_utils.h" #include "window/candidate_presenter.h" +#include "window/tray_menu_presenter.h" #include "window/floating_toolbar_visibility_policy.h" #include #include @@ -478,6 +479,16 @@ void OnSmallWindowWebviewInitFailed(HRESULT hr) void MaybeFlushPendingTrayMenuShow() { + if (!pendingTrayMenuShow || !::global_hwnd_menu) + { + return; + } + if (TrayMenuPresenter::Instance().IsBound()) + { + pendingTrayMenuShow = false; + PostMessage(::global_hwnd_menu, WM_LANGBAR_RIGHTCLICK, 0, 0); + return; + } if (!pendingTrayMenuShow || !webviewControllerMenuWnd || !menuNavigationReady || !::global_hwnd_menu) { return; @@ -521,14 +532,12 @@ void RequestNextSmallWindowController() HRESULT (*request)(HWND, HRESULT, ICoreWebView2Environment *); }; const Host hosts[] = { - {L"menu", smallWindowMenuHwnd, webviewControllerMenuWnd != nullptr, &OnMenuWindowEnvironmentCreated}, {L"ftb", smallWindowFtbHwnd, webviewControllerFtbWnd != nullptr, &OnFtbWindowEnvironmentCreated}, }; int chosen = -1; - for (int step = 1; step <= 2; ++step) + for (int i = 0; i < 1; ++i) { - const int i = (lastFailedSmallWindowHostIndex + step) % 2; if (!hosts[i].hasController && hosts[i].hwnd) { chosen = i; @@ -658,8 +667,10 @@ FloatingToolbarState floatingToolbarState; bool AreSmallWindowWebviewsReadyUnlocked() { - return menuNavigationReady && floatingToolbarNavigationReady && webviewMenuWnd != nullptr && - webviewFtbWnd != nullptr && webviewControllerMenuWnd != nullptr && webviewControllerFtbWnd != nullptr; + const bool menuReady = TrayMenuPresenter::Instance().IsBound() || + (menuNavigationReady && webviewMenuWnd != nullptr && webviewControllerMenuWnd != nullptr); + return menuReady && floatingToolbarNavigationReady && webviewFtbWnd != nullptr && + webviewControllerFtbWnd != nullptr; } void CancelStaggeredTopmost() @@ -726,6 +737,10 @@ void RenotifyControllerAfterPin(ICoreWebView2Controller *controller, HWND hwnd) // host into the topmost band mid-initialisation and leaves it permanently blank. bool TrayMenuIsOpenToUser() { + if (TrayMenuPresenter::Instance().IsBound()) + { + return TrayMenuPresenter::Instance().IsOpenToUser(); + } if (!::global_hwnd_menu || !webviewControllerMenuWnd || !menuNavigationReady) { return false; @@ -767,7 +782,17 @@ void ApplySmallWindowTopmostStep(SmallWindowTopmostStep step) case SmallWindowTopmostStep::TrayMenu: PinHostTopmost(::global_hwnd_menu); - RenotifyControllerAfterPin(webviewControllerMenuWnd.Get(), ::global_hwnd_menu); + if (TrayMenuPresenter::Instance().IsBound()) + { + if (TrayMenuPresenter::Instance().IsOpenToUser()) + { + TrayMenuPresenter::Instance().Present(); + } + } + else + { + RenotifyControllerAfterPin(webviewControllerMenuWnd.Get(), ::global_hwnd_menu); + } break; } @@ -1048,6 +1073,16 @@ void RaiseTrayMenuAboveSmallWindows(const wchar_t *reason) { return; } + if (TrayMenuPresenter::Instance().IsBound()) + { + constexpr UINT flag = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE; + SetWindowPos(::global_hwnd_menu, HWND_TOPMOST, 0, 0, 0, 0, flag); + if (TrayMenuPresenter::Instance().IsOpenToUser()) + { + TrayMenuPresenter::Instance().Present(); + } + return; + } // Backstop for the callers above: making the host TOPMOST before a // controller exists is fatal in a uiAccess=true process, because UIPI then // blocks WebView2's cross-process SetParent and every @@ -4161,6 +4196,18 @@ HRESULT OnSettingsWindowEnvironmentCreated(HWND hwnd, HRESULT result, ICoreWebVi */ void SyncMenuFloatingToolbarToggle() { + if (TrayMenuPresenter::Instance().IsBound()) + { + if (TrayMenuPresenter::Instance().IsOpenToUser()) + { + TrayMenuPresenter::Instance().ShowFromLangBar(); + } + else + { + TrayMenuPresenter::Instance().ApplyTheme(); + } + return; + } if (!::webviewMenuWnd) { return; @@ -4692,6 +4739,10 @@ void InitSmallWindowWebviews(HWND candHwnd, HWND menuHwnd, HWND ftbHwnd) bool PrepareTrayMenuWebviewForShow() { + if (TrayMenuPresenter::Instance().IsBound()) + { + return true; + } if (webviewControllerMenuWnd && menuNavigationReady) { return true; diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index 836babec..98f85783 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -3,6 +3,7 @@ #include "ipc/ipc.h" #include "ime_windows.h" #include "window/candidate_presenter.h" +#include "window/tray_menu_presenter.h" #include "defines/defines.h" #include "defines/globals.h" #include @@ -1696,6 +1697,7 @@ int CreateCandidateWindow(HINSTANCE hInstance) } PrepareLayeredHostWindow(hwnd_menu); ::global_hwnd_menu = hwnd_menu; + TrayMenuPresenter::Instance().Bind(hwnd_menu); // // floating toolbar 窗口 @@ -2304,6 +2306,11 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA LRESULT CALLBACK WndProcMenuWindow(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (TrayMenuPresenter::Instance().HandleMessage(message, wParam, lParam)) + { + return message == WM_ERASEBKGND ? 1 : 0; + } + switch (message) { case WM_MOUSEACTIVATE: @@ -2322,6 +2329,16 @@ LRESULT CALLBACK WndProcMenuWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA } case WM_LANGBAR_RIGHTCLICK: { + if (TrayMenuPresenter::Instance().IsBound()) + { + TrayMenuPresenter::Instance().ShowFromLangBar(); + SetHostWindowCloaked(::global_hwnd_menu, false); + if (!g_mouseHook) + { + g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, nullptr, 0); + } + break; + } // Host HWND alone is an invisible click-blocker. Do not show it until the // menu WebView controller exists (watchdog relaunch / early logon can // leave init pending; Prepare queues a retry and re-posts this message). @@ -2410,6 +2427,14 @@ LRESULT CALLBACK WndProcMenuWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA } case WM_DPICHANGED: { + if (TrayMenuPresenter::Instance().IsBound()) + { + if (TrayMenuPresenter::Instance().IsOpenToUser()) + { + TrayMenuPresenter::Instance().ShowFromLangBar(); + } + return 0; + } // Prefer dip * newScale over the suggested rect alone: the menu host is // content-sized, and a stale/oversized create-time rect would otherwise // keep the wrong physical size across display-scale changes. diff --git a/server/src/window/tray_menu_presenter.cpp b/server/src/window/tray_menu_presenter.cpp new file mode 100644 index 00000000..37921c71 --- /dev/null +++ b/server/src/window/tray_menu_presenter.cpp @@ -0,0 +1,417 @@ +#include "window/tray_menu_presenter.h" + +#include "config/ime_config.h" +#include "defines/globals.h" +#include "global/globals.h" +#include "ipc/ipc.h" +#include "log/ftb_diag_log.h" +#include "settings/settings_launcher.h" +#include "utils/window_utils.h" +#include "voice-input/voice_input_service.h" +#include "webview2/windows_webview2.h" +#include "window/ime_windows.h" + +#include "msimeui/Controls.h" +#include "msimeui/DeviceResources.h" +#include "msimeui/Layout.h" +#include "msimeui/Scene.h" +#include "msimeui/Window.h" + +#include +#include +#include +#include +#include +#include + +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); +} + +constexpr float kShadowPadLeft = 16.0f; +constexpr float kShadowPadTop = 12.0f; +constexpr float kShadowPadRight = 16.0f; +constexpr float kShadowPadBottom = 20.0f; + +const char kSvgToolbar[] = + R"( + + + + )"; + +const char kSvgEmoji[] = + R"( + + + + + )"; + +const char kSvgHandwriting[] = + R"( + + )"; + +const char kSvgKeyboard[] = + R"( + + )"; + +const char kSvgVoice[] = + R"( + + )"; + +const char kSvgSettings[] = + R"( + + + )"; + +const char kSvgAbout[] = + R"( + + )"; +} // namespace + +struct TrayMenuPresenter::Impl +{ + msimeui::DeviceResources resources; + std::unique_ptr window; + std::shared_ptr root; + std::shared_ptr card; + std::shared_ptr frame; + std::shared_ptr items; + std::shared_ptr floatingToggle; + std::vector> rows; + D2D1_COLOR_F fill = ColorFromRgb(0x2B2B2B); + D2D1_COLOR_F border = ColorFromRgb(0x3A3A3A); + D2D1_COLOR_F text = ColorFromRgb(0xE0E0E0); + D2D1_COLOR_F hover = ColorFromRgb(0x3B3B3B); +}; + +TrayMenuPresenter::TrayMenuPresenter() = default; + +TrayMenuPresenter &TrayMenuPresenter::Instance() +{ + static TrayMenuPresenter instance; + return instance; +} + +bool TrayMenuPresenter::IsBound() const +{ + return bound_; +} + +bool TrayMenuPresenter::IsOpenToUser() const +{ + if (!bound_ || !hwnd_ || !openToUser_ || !IsWindowVisible(hwnd_)) + { + return false; + } + DWORD cloaked = 0; + DwmGetWindowAttribute(hwnd_, DWMWA_CLOAKED, &cloaked, sizeof(cloaked)); + return cloaked == 0; +} + +bool TrayMenuPresenter::Bind(HWND hwnd) +{ + hwnd_ = hwnd; + if (!hwnd) + { + bound_ = false; + impl_.reset(); + return false; + } + impl_ = std::make_unique(); + impl_->window = std::make_unique(L"metasequoiaime_windows", L"menu", 1, 1); + impl_->window->AdoptExistingHwnd(hwnd); + impl_->window->SetStealFocusOnClick(false); + bound_ = impl_->resources.EnsureForComposition(hwnd); + ApplyTheme(); + return bound_; +} + +void TrayMenuPresenter::RebuildScene() +{ + if (!impl_ || !impl_->window) + { + return; + } + impl_->items = std::make_shared(1.0f); + impl_->items->SetPadding({6.0f, 2.8f, 6.0f, 2.8f}); + impl_->rows.clear(); + + auto addItem = [this](const std::wstring &label, const char *svg, bool toggle) { + auto item = std::make_shared(label, false); + item->SetLeadingSvg(svg); + item->SetTrailingToggle(toggle); + impl_->items->AddChild(item); + impl_->rows.push_back(item); + return item; + }; + + impl_->floatingToggle = addItem(L"悬浮工具栏", kSvgToolbar, true); + impl_->floatingToggle->SetOnClick([this]() { + if (!impl_->floatingToggle) + { + return; + } + const bool on = impl_->floatingToggle->IsToggleOn(); + if (SetConfiguredFloatingToolbarEnabled(on)) + { + ApplyConfiguredFloatingToolbarVisibility(L"tray-menu-toggle"); + PostSettingsConfig(); + } + }); + + auto launch = [this](auto fn) { + return [this, fn]() { + Hide(); + fn(); + }; + }; + addItem(L"表情/符号面板", kSvgEmoji, false)->SetOnClick(launch(OpenEmojiPanelApplication)); + addItem(L"手写识别板", kSvgHandwriting, false)->SetOnClick(launch(OpenHandwritingPanelApplication)); + addItem(L"屏幕键盘", kSvgKeyboard, false)->SetOnClick(launch(OpenKeyboardPanelApplication)); + addItem(L"语音输入", kSvgVoice, false)->SetOnClick([this]() { + Hide(); + VoiceInput::ToggleRecording(); + }); + addItem(L"设置", kSvgSettings, false)->SetOnClick(launch(OpenSettingsApplication)); + addItem(L"关于", kSvgAbout, false)->SetOnClick(launch(OpenSettingsAboutApplication)); + + msimeui::Brush brush; + brush.fill = impl_->fill; + brush.stroke = impl_->border; + brush.strokeWidth = 1.0f; + brush.radiusX = 8.0f; + brush.radiusY = 8.0f; + impl_->card = std::make_shared(brush, 0.0f); + impl_->card->AddChild(impl_->items); + impl_->frame = std::make_shared(); + impl_->frame->SetPadding({kShadowPadLeft, kShadowPadTop, kShadowPadRight, kShadowPadBottom}); + impl_->frame->SetChild(impl_->card); + impl_->root = std::make_shared(0.0f); + impl_->root->AddChild(impl_->frame); + auto scene = std::make_unique(); + scene->SetRoot(impl_->root); + impl_->window->SetScene(std::move(scene)); +} + +void TrayMenuPresenter::ApplyTheme() +{ + if (!impl_) + { + return; + } + const bool light = ResolveConfiguredTheme(GetConfiguredThemeMenu()) == "light"; + if (light) + { + impl_->fill = ColorFromRgb(0xFFFFFF); + impl_->border = D2D1::ColorF(0, 0.10f); + impl_->text = ColorFromRgb(0x1A1A1A); + impl_->hover = ColorFromRgb(0xF0F0F0); + } + else + { + impl_->fill = ColorFromRgb(0x2B2B2B); + impl_->border = ColorFromRgb(0x3A3A3A); + impl_->text = ColorFromRgb(0xE0E0E0); + impl_->hover = ColorFromRgb(0x3B3B3B); + } + RebuildScene(); + for (const auto &row : impl_->rows) + { + row->SetColors(impl_->text, impl_->hover); + } + if (impl_->floatingToggle) + { + impl_->floatingToggle->SetToggleOn(GetConfiguredFloatingToolbarEnabled()); + } +} + +void TrayMenuPresenter::SyncFloatingToolbarToggle() +{ + if (impl_ && impl_->floatingToggle) + { + impl_->floatingToggle->SetToggleOn(GetConfiguredFloatingToolbarEnabled()); + if (openToUser_) + { + Present(); + } + } +} + +void TrayMenuPresenter::PlaceAndShow(float widthDip, float heightDip) +{ + const int left = Global::Point[0]; + const int top = Global::Point[1]; + const int right = Global::Keycode; + const int bottom = Global::ModifiersDown; + (void)bottom; + FLOAT scale = GetWindowScale(hwnd_); + if (scale <= 0.0f) + { + scale = GetScaleForPoint(POINT{left, top}); + } + if (scale <= 0.0f) + { + scale = 1.0f; + } + const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(POINT{left, top}); + widthDip = static_cast(ClampWidthDipToHalfScreen(widthDip, limits)); + heightDip = static_cast(ClampHeightDipToHalfScreen(heightDip, limits)); + const int widthPx = (std::max)(1, static_cast(std::ceil(widthDip * scale))); + const int heightPx = (std::max)(1, static_cast(std::ceil(heightDip * scale))); + const int iconWidth = static_cast((right - left) * scale); + const int iconMiddleX = left + iconWidth / 2; + float cardLeftDip = kShadowPadLeft; + float cardTopDip = kShadowPadTop; + if (impl_->card) + { + const msimeui::RectF card = impl_->card->GetBounds(); + if (card.width > 1.0f) + { + cardLeftDip = card.x; + cardTopDip = card.y; + } + } + const int cardLeftPx = static_cast(std::lround(cardLeftDip * scale)); + const int cardTopPx = static_cast(std::lround(cardTopDip * scale)); + const int cardWidthPx = widthPx - cardLeftPx - static_cast(std::lround(kShadowPadRight * scale)); + int menuX = iconMiddleX - cardWidthPx / 2 - cardLeftPx; + int menuY = top - (heightPx - static_cast(std::lround(kShadowPadBottom * scale))); + ::MENU_CONTENT_WIDTH_DIP = widthDip; + ::MENU_CONTENT_HEIGHT_DIP = heightDip; + ::MENU_WINDOW_WIDTH = widthPx; + ::MENU_WINDOW_HEIGHT = heightPx; + EnsureSmallWindowsTopmost(L"show-menu"); + const HWND zorder = AreSmallWindowsTopmostApplied() ? HWND_TOPMOST : HWND_TOP; + SetWindowPos(hwnd_, zorder, menuX, menuY, widthPx, heightPx, SWP_SHOWWINDOW | SWP_NOACTIVATE); + RaiseTrayMenuAboveSmallWindows(L"show-menu"); + impl_->resources.EnsureForComposition(hwnd_); + Present(); + openToUser_ = true; + FTB_DIAG_LOGF(L"menu d2d show pos=({},{}) size={}x{}", menuX, menuY, widthPx, heightPx); +} + +void TrayMenuPresenter::ShowFromLangBar() +{ + if (!bound_ || !hwnd_ || !impl_ || !impl_->root) + { + return; + } + ApplyTheme(); + impl_->root->InvalidateMeasure(); + const msimeui::SizeF measured = impl_->root->MeasureInLayout({320.0f, 640.0f}); + impl_->root->InvalidateArrange(); + impl_->root->ArrangeInLayout({0.0f, 0.0f, measured.width, measured.height}); + PlaceAndShow((std::max)(measured.width, 1.0f), (std::max)(measured.height, 80.0f)); +} + +void TrayMenuPresenter::Hide() +{ + openToUser_ = false; + if (!hwnd_) + { + return; + } + ShowWindow(hwnd_, SW_HIDE); +} + +void TrayMenuPresenter::Present() +{ + if (!bound_ || !hwnd_ || !impl_ || !impl_->window) + { + return; + } + RECT rc{}; + GetClientRect(hwnd_, &rc); + const UINT width = static_cast((std::max)(1L, rc.right)); + const UINT height = static_cast((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(width), dpi), + msimeui::PixelsToDips(static_cast(height), dpi)}); + scene->Render(impl_->resources); + } + const HRESULT hr = target->EndDraw(); + if (hr == D2DERR_RECREATE_TARGET) + { + impl_->resources.DiscardTarget(); + return; + } + impl_->resources.Present(); +} + +bool TrayMenuPresenter::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 (openToUser_) + { + impl_->resources.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + Present(); + } + return true; + case WM_MOUSEMOVE: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_MOUSELEAVE: + impl_->window->DispatchImportedMessage(message, wParam, lParam); + Present(); + return true; + default: + return false; + } +} + +TrayMenuPresenter::~TrayMenuPresenter() = default; diff --git a/server/src/window/tray_menu_presenter.h b/server/src/window/tray_menu_presenter.h new file mode 100644 index 00000000..18e64bca --- /dev/null +++ b/server/src/window/tray_menu_presenter.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +class TrayMenuPresenter +{ + public: + static TrayMenuPresenter &Instance(); + + bool Bind(HWND hwnd); + bool IsBound() const; + bool IsOpenToUser() const; + void ShowFromLangBar(); + void Hide(); + void Present(); + void SyncFloatingToolbarToggle(); + void ApplyTheme(); + bool HandleMessage(UINT message, WPARAM wParam, LPARAM lParam); + + private: + TrayMenuPresenter(); + ~TrayMenuPresenter(); + TrayMenuPresenter(const TrayMenuPresenter &) = delete; + TrayMenuPresenter &operator=(const TrayMenuPresenter &) = delete; + + void RebuildScene(); + void PlaceAndShow(float widthDip, float heightDip); + + struct Impl; + std::unique_ptr impl_; + HWND hwnd_ = nullptr; + bool bound_ = false; + bool openToUser_ = false; +};