diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 758271da..d1d6b01c 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -86,6 +86,9 @@ linear_step = 1 [appearance] # 主题模式,可选:"dark", "light", "system" theme_mode = "system" +# 候选窗 / 悬浮工具栏 / 托盘菜单渲染,可选:"d2d", "webview2" +# 更改后需重启输入法进程生效 +ui_backend = "d2d" # 分表面主题,可选:"follow", "dark", "light"(follow = 跟随 theme_mode) theme_settings = "follow" theme_cand = "follow" diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index 547bb916..73a582ad 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -103,6 +103,8 @@ bool g_paired_punctuation_enabled = true; std::string g_punctuation_lock = "follow"; std::string g_candidate_window_layout = "vertical"; bool g_candidate_window_follow_cursor = true; +std::string g_ui_backend = "d2d"; +std::string g_ui_backend_active = "d2d"; std::string g_candidate_skin = "fluent"; std::string g_candidate_window_preedit_style = "pinyin"; std::string g_theme_mode = "system"; @@ -124,6 +126,13 @@ FrequencyAdjustmentConfig g_frequency_adjustment; std::filesystem::path g_config_path; std::optional g_config_last_write_time; +std::string NormalizeSmallWindowUiBackend(const std::string &value) +{ + if (value == "webview2" || value == "webview" || value == "web") + return "webview2"; + return "d2d"; +} + bool IsValidCandidateSkinId(const std::string &skin) { if (skin.empty() || skin.size() > 64 || !std::isalnum(static_cast(skin.front()))) @@ -786,6 +795,8 @@ bool LoadImeConfig() const std::string layout = tbl["appearance"]["candidate_window_layout"].value_or(std::string("vertical")); g_candidate_window_layout = layout == "horizontal" ? "horizontal" : "vertical"; g_candidate_window_follow_cursor = tbl["appearance"]["candidate_window_follow_cursor"].value_or(true); + g_ui_backend = NormalizeSmallWindowUiBackend( + tbl["appearance"]["ui_backend"].value_or(std::string("d2d"))); const std::string skin = tbl["appearance"]["candidate_skin"].value_or(std::string("fluent")); g_candidate_skin = IsValidCandidateSkinId(skin) ? skin : "fluent"; { @@ -1239,6 +1250,7 @@ void InitImeConfig() (void)0; #endif } + g_ui_backend_active = g_ui_backend; } bool ReloadImeConfigIfChanged() @@ -2347,6 +2359,27 @@ bool SetConfiguredCandidateWindowFollowCursor(bool enabled) return true; } +const std::string &GetConfiguredUiBackend() +{ + return g_ui_backend; +} + +bool SetConfiguredUiBackend(const std::string &backend) +{ + const std::string normalized = NormalizeSmallWindowUiBackend(backend); + if (!WriteConfiguredValue("appearance", "ui_backend", EscapeTomlBasicString(normalized))) + { + return false; + } + g_ui_backend = normalized; + return true; +} + +bool UseD2dSmallWindowUi() +{ + return g_ui_backend_active != "webview2"; +} + const std::string &GetConfiguredCandidateSkin() { return g_candidate_skin; diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index 0efb020b..b6cd1ce7 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -280,6 +280,13 @@ const std::string &GetConfiguredCandidateWindowLayout(); bool SetConfiguredCandidateWindowLayout(const std::string &layout); bool GetConfiguredCandidateWindowFollowCursor(); bool SetConfiguredCandidateWindowFollowCursor(bool enabled); +// "d2d" | "webview2" — candidate, floating toolbar, and tray menu renderer. +// Changing this writes config immediately; the process snapshot does not switch +// until the IME server is restarted. +const std::string &GetConfiguredUiBackend(); +bool SetConfiguredUiBackend(const std::string &backend); +// Process-lifetime renderer chosen at InitImeConfig. Independent of later reloads. +bool UseD2dSmallWindowUi(); // "fluent" | "wechat" | "graphite" | "willow_green" — candidate-window and floating-toolbar skin. const std::string &GetConfiguredCandidateSkin(); bool SetConfiguredCandidateSkin(const std::string &skin); diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index 4dc485dc..4395f7b4 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -350,7 +350,9 @@ void PostConfig(bool refresh_skin_catalog = false) {"r_mode", GetConfiguredRModeEnabled()}, {"clipboard_history", GetConfiguredClipboardHistoryEnabled()}}}, {"appearance", - {{"candidate_window_layout", GetConfiguredCandidateWindowLayout()}, + {{"ui_backend", GetConfiguredUiBackend()}, + {"candidate_window_layout", GetConfiguredCandidateWindowLayout()}, + {"candidate_window_follow_cursor", GetConfiguredCandidateWindowFollowCursor()}, {"candidate_skin", GetConfiguredCandidateSkin()}, {"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()}, {"tsf_preedit_style", GetConfiguredTsfPreeditStyle()}, @@ -477,8 +479,12 @@ bool ApplyConfigUpdate(const json::object &data) return SetConfiguredPunctuationLock(json::value_to(data.at("value"))); if (path == "appearance.tsf_preedit_style") return SetConfiguredTsfPreeditStyle(json::value_to(data.at("value"))); + if (path == "appearance.ui_backend") + return SetConfiguredUiBackend(json::value_to(data.at("value"))); if (path == "appearance.candidate_window_layout") return SetConfiguredCandidateWindowLayout(json::value_to(data.at("value"))); + if (path == "appearance.candidate_window_follow_cursor") + return SetConfiguredCandidateWindowFollowCursor(json::value_to(data.at("value"))); if (path == "appearance.candidate_skin") return SetConfiguredCandidateSkin(json::value_to(data.at("value"))); if (path == "appearance.candidate_window_preedit_style") diff --git a/server/src/utils/webview_utils.cpp b/server/src/utils/webview_utils.cpp index cc33e33d..00632a59 100644 --- a/server/src/utils/webview_utils.cpp +++ b/server/src/utils/webview_utils.cpp @@ -141,6 +141,10 @@ void GetCandidateCardSize( }).Get()); DIAG_LOGF(L"ui-measure submit box={} parent={} max_dip=({:.1f},{:.1f}) hr={:#x}", boxId, parentId, maxWidthDip, maxHeightDip, static_cast(submitHr)); + if (FAILED(submitHr)) + { + callback({0.0, 0.0}); + } } } // namespace diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index ac100d9a..b3338be4 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -101,6 +101,7 @@ enum class SmallWindowInitState SmallWindowInitState smallWindowInitState = SmallWindowInitState::Idle; int smallWindowInitAttempts = 0; bool pendingTrayMenuShow = false; +bool pendingCandidateShow = false; bool floatingToolbarNavigationReady = false; // After NavigationCompleted, keep the host shown briefly so a cold WebView2 // user-data folder can finish first paint; then reconcile hide/show for real. @@ -533,12 +534,16 @@ void RequestNextSmallWindowController() HRESULT (*request)(HWND, HRESULT, ICoreWebView2Environment *); }; const Host hosts[] = { + {L"cand", smallWindowCandHwnd, webviewControllerCandWnd != nullptr, &OnEnvironmentCreated}, + {L"menu", smallWindowMenuHwnd, webviewControllerMenuWnd != nullptr, &OnMenuWindowEnvironmentCreated}, {L"ftb", smallWindowFtbHwnd, webviewControllerFtbWnd != nullptr, &OnFtbWindowEnvironmentCreated}, }; + constexpr int kHostCount = 3; int chosen = -1; - for (int i = 0; i < 1; ++i) + for (int step = 1; step <= kHostCount; ++step) { + const int i = (lastFailedSmallWindowHostIndex + step) % kHostCount; if (!hosts[i].hasController && hosts[i].hwnd) { chosen = i; @@ -548,6 +553,7 @@ void RequestNextSmallWindowController() if (chosen < 0) { MaybeFlushPendingTrayMenuShow(); + MaybeFlushPendingCandidateShow(); return; } const Host &host = hosts[chosen]; @@ -594,7 +600,7 @@ void OnSmallWindowControllerSettled(HRESULT hr) void BeginSmallWindowWebviewEnvironmentCreate() { - if (!smallWindowMenuHwnd || !smallWindowFtbHwnd) + if (!smallWindowCandHwnd || !smallWindowMenuHwnd || !smallWindowFtbHwnd) { return; } @@ -668,11 +674,14 @@ FloatingToolbarState floatingToolbarState; bool AreSmallWindowWebviewsReadyUnlocked() { + const bool candReady = + CandidatePresenter::Instance().IsBound() || + (candidateNavigationReady && webviewCandWnd != nullptr && webviewControllerCandWnd != nullptr); const bool menuReady = TrayMenuPresenter::Instance().IsBound() || (menuNavigationReady && webviewMenuWnd != nullptr && webviewControllerMenuWnd != nullptr); const bool ftbReady = FloatingToolbarPresenter::Instance().IsBound() || (floatingToolbarNavigationReady && webviewFtbWnd != nullptr && webviewControllerFtbWnd != nullptr); - return menuReady && ftbReady; + return candReady && menuReady && ftbReady; } void CancelStaggeredTopmost() @@ -765,9 +774,20 @@ void ApplySmallWindowTopmostStep(SmallWindowTopmostStep step) { case SmallWindowTopmostStep::Candidate: PinHostTopmost(::global_hwnd); - if (::is_global_wnd_cand_shown && ::global_hwnd && CandidatePresenter::Instance().IsBound()) + if (CandidatePresenter::Instance().IsBound()) { - CandidatePresenter::Instance().Present(); + if (::is_global_wnd_cand_shown && ::global_hwnd) + { + CandidatePresenter::Instance().Present(); + } + } + else + { + RenotifyControllerAfterPin(webviewControllerCandWnd.Get(), ::global_hwnd); + if (::is_global_wnd_cand_shown && ::global_hwnd) + { + FineTuneWindow(::global_hwnd); + } } break; @@ -897,6 +917,7 @@ void NotifySmallWindowNavigationReady(bool &readyFlag, const wchar_t *which) LogSmallWindowReadyGateUnlocked(L"after-nav-ready"); TryApplyPendingLazyTopmost(L"pending-after-nav-ready"); MaybeFlushPendingTrayMenuShow(); + MaybeFlushPendingCandidateShow(); } bool UpdateBinaryState(int value, int &state) @@ -1128,6 +1149,56 @@ void RaiseTrayMenuAboveSmallWindows(const wchar_t *reason) RenotifyControllerAfterPin(webviewControllerMenuWnd.Get(), ::global_hwnd_menu); } +void DeferCandidateShowUntilWebviewReady() +{ + pendingCandidateShow = true; + CAND_DIAG_LOGF(L"candidate show deferred until webview ready {}", DescribeCandidateHostState()); +} + +void MaybeFlushPendingCandidateShow() +{ + if (!pendingCandidateShow || !::global_hwnd || !IsWindow(::global_hwnd)) + { + return; + } + if (!::is_global_wnd_cand_shown) + { + pendingCandidateShow = false; + return; + } + if (!IsCandidateWebviewReady()) + { + return; + } + pendingCandidateShow = false; + g_candidate_show_msg_pending.store(false); + CAND_DIAG_LOGF(L"candidate replaying show that was deferred until the webview was ready"); + PostMessage(::global_hwnd, WM_SHOW_MAIN_WINDOW, 0, 0); +} + +void RaiseCandidateHostForShow(const wchar_t *reason) +{ + if (!::global_hwnd) + { + return; + } + if (CandidatePresenter::Instance().IsBound()) + { + constexpr UINT flag = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE; + SetWindowPos(::global_hwnd, HWND_TOPMOST, 0, 0, 0, 0, flag); + return; + } + if (!webviewControllerCandWnd) + { + CAND_DIAG_LOGF(L"candidate raise reason={} skipped: no controller yet", reason); + return; + } + CAND_DIAG_LOGF(L"candidate raise reason={} nav_ready={} {}", reason, candidateNavigationReady, + DescribeCandidateHostState()); + PinHostTopmost(::global_hwnd); + RenotifyControllerAfterPin(webviewControllerCandWnd.Get(), ::global_hwnd); +} + bool AreSmallWindowsTopmostApplied() { return smallWindowTopmostApplied; @@ -1140,7 +1211,8 @@ bool AreSmallWindowWebviewsReady() bool IsCandidateWebviewReady() { - return CandidatePresenter::Instance().IsBound(); + return CandidatePresenter::Instance().IsBound() || + (candidateNavigationReady && webviewCandWnd != nullptr && webviewControllerCandWnd != nullptr); } bool IsFloatingToolbarWebviewReady() @@ -1583,6 +1655,58 @@ bool InjectExternalSkinCssFile( return true; } +void AppendExternalCandidateColorCss(std::wstring &css, const CandidateSkinCatalog::CandidateColors &colors) +{ + auto add = [&](const std::string &value, const wchar_t *selector, const wchar_t *property) { + if (value.empty()) + { + return; + } + css.append(selector); + css.append(L" { "); + css.append(property); + css.append(L": "); + css.append(string_to_wstring(value)); + css.append(L"; }\n"); + }; + add(colors.accent, L".cursor, .first::before", L"background"); + add(colors.selected, L".first, .cand.first", L"background-color"); + add(colors.hover, L".hover-active .cand:not(.first):hover", L"background-color"); + add(colors.surface, L".container", L"background"); + add(colors.border, L".container", L"border-color"); + add(colors.text, L".container", L"color"); + add(colors.number, L".num, .cand-no", L"color"); + if (colors.showSelectedBar.has_value() && !*colors.showSelectedBar) + { + css.append(L".first::before { display: none; }\n"); + } +} + +std::wstring BuildExternalCandidateSkinCss(const CandidateSkinCatalog::Package &skin, const std::wstring &skinsRoot) +{ + std::wstring css; + if (skin.decorationTopDip > 0.0) + { + std::wstring preview = L"none"; + if (!skin.preview.empty()) + { + preview = EmbedSkinCssUrl(skinsRoot, skin.id, string_to_wstring(skin.preview)); + } + css.append(L".containerParent { padding-top: var(--msime-skin-decoration-top, 0px); " + L"position: relative; box-sizing: border-box; }\n" + L".containerParent:not(:empty)::before { content: \"\"; position: absolute; " + L"z-index: 0; top: 0; right: 0; width: var(--msime-skin-decoration-width, 0px); " + L"height: var(--msime-skin-decoration-top, 0px); background: "); + css.append(preview); + css.append(L" center / contain no-repeat; pointer-events: none; }\n" + L".container { position: relative; z-index: 1; " + L"min-width: max(7em, var(--msime-skin-min-width, 0px)); }\n"); + } + const bool light = ResolveConfiguredTheme(GetConfiguredThemeCand()) == "light"; + AppendExternalCandidateColorCss(css, light ? skin.light : skin.dark); + return css; +} + bool InjectExternalCandidateSkin( std::wstring &html, const CandidateSkinCatalog::Package &skin, @@ -1596,12 +1720,19 @@ bool InjectExternalCandidateSkin( L"", skin.minWidthDip, skin.decorationTopDip, skin.decorationWidthDip); + std::wstring css = BuildExternalCandidateSkinCss(skin, skinsRoot); + NeutralizeEmbeddedStyleClosers(css); + std::wstring generated; + if (!css.empty()) + { + generated = L""; + } const size_t headEnd = html.find(L""); if (headEnd == std::wstring::npos) { return false; } - html.insert(headEnd, vars); + html.insert(headEnd, vars + generated); return true; } @@ -1959,12 +2090,21 @@ std::wstring BuildCandidateSlotPayloadJson(const std::wstring &commaSeparated, b nlohmann::json payload; payload["preedit"] = words.empty() ? std::string{} : wstring_to_string(words[0]); payload["items"] = nlohmann::json::array(); + // Preserve slot indices: do not compress out empty tokens. + // The WebView DOM renderer expects wrapper<->slot index alignment. + uint16_t nonEmptyMask = 0; for (size_t i = 1; i < words.size(); ++i) { - if (!words[i].empty()) + if (i <= 9 && !words[i].empty()) { - payload["items"].push_back(wstring_to_string(words[i])); + nonEmptyMask |= static_cast(1u) << static_cast(i - 1); } + payload["items"].push_back(wstring_to_string(words[i])); + } + if (nonEmptyMask == 0 && !words.empty()) + { + CAND_DIAG_LOGF(L"candidate-slot payload all-empty words_sz={} content_only={} preedit_len={}", + words.size(), contentOnly ? 1 : 0, words[0].size()); } payload["selected"] = Global::candidate_ui.selected_index_in_page; payload["preeditVisible"] = GetConfiguredCandidateWindowPreeditStyle() != "empty"; @@ -2525,18 +2665,20 @@ window.mouseBlockTimeout = setTimeout(() => { void InflateCandWnd(std::wstring &str) { - InflateCandWnd(str, nullptr, false); + InflateCandWnd(str, nullptr); } void InflateCandWnd(std::wstring &str, std::function onComplete) { - InflateCandWnd(str, std::move(onComplete), false); + std::wstring result = InflateCandidateTemplate(BodyStringCandWnd, str); + UpdateHtmlContentWithJavaScript(webviewCandWnd, result, std::move(onComplete)); } void InflateCandWnd(std::wstring &str, std::function onComplete, bool contentOnly) { - UpdateCandidateSlotsWithJavaScript(webviewCandWnd, BuildCandidateSlotPayloadJson(str, contentOnly), - std::move(onComplete), contentOnly); + (void)contentOnly; + std::wstring result = InflateCandidateTemplate(BodyStringCandWnd, str); + UpdateHtmlContentWithJavaScript(webviewCandWnd, result, std::move(onComplete)); } void InflateMeasureDivCandWnd(std::wstring &str) @@ -2702,6 +2844,10 @@ HRESULT OnControllerCreatedCandWnd( // (void)0; // Navigate to HTML + if (HTMLStringCandWnd.empty()) + { + PrepareHtmlForWnds(); + } HRESULT hr = webviewCandWnd->NavigateToString(HTMLStringCandWnd.c_str()); CAND_DIAG_LOGF(L"webview NavigateToString hr={:#x} html_chars={} skin={}", static_cast(hr), HTMLStringCandWnd.size(), string_to_wstring(preparedCandidateSkin)); @@ -3618,6 +3764,14 @@ HRESULT OnControllerCreatedSettingsWnd( // PostSettingsConfig(); } } + else if (path == "appearance.ui_backend") + { + const std::string value = json::value_to(data.at("value")); + if (SetConfiguredUiBackend(value)) + { + PostSettingsConfig(); + } + } else if (path == "appearance.candidate_window_layout") { const std::string value = json::value_to(data.at("value")); @@ -4375,7 +4529,8 @@ void PostSettingsConfig() {"y_mode", GetConfiguredYModeEnabled()}, {"r_mode", GetConfiguredRModeEnabled()}}}, {"appearance", - {{"candidate_window_layout", GetConfiguredCandidateWindowLayout()}, + {{"ui_backend", GetConfiguredUiBackend()}, + {"candidate_window_layout", GetConfiguredCandidateWindowLayout()}, {"candidate_window_follow_cursor", GetConfiguredCandidateWindowFollowCursor()}, {"candidate_skin", GetConfiguredCandidateSkin()}, {"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()}, @@ -4780,8 +4935,8 @@ HRESULT OnFtbWindowEnvironmentCreated(HWND hwnd, HRESULT result, ICoreWebView2En } /** - * @brief Initialize the tray menu and floating toolbar WebViews in one - * environment. The candidate window is Direct2D and does not use WebView2. + * @brief Initialize candidate, tray menu, and floating toolbar WebViews in one + * environment when appearance.ui_backend is webview2. */ void InitSmallWindowWebviews(HWND candHwnd, HWND menuHwnd, HWND ftbHwnd) { @@ -4794,9 +4949,10 @@ void InitSmallWindowWebviews(HWND candHwnd, HWND menuHwnd, HWND ftbHwnd) currentSmallWindowHostIndex = -1; lastFailedSmallWindowHostIndex = -1; pendingTrayMenuShow = false; - if (FloatingToolbarPresenter::Instance().IsBound() && TrayMenuPresenter::Instance().IsBound()) + pendingCandidateShow = false; + if (UseD2dSmallWindowUi()) { - FTB_DIAG_LOGF(L"skip small-window webview init: candidate/menu/ftb are d2d"); + FTB_DIAG_LOGF(L"skip small-window webview init: ui_backend=d2d"); return; } BeginSmallWindowWebviewEnvironmentCreate(); @@ -4851,6 +5007,7 @@ void ShutdownWebviews() // interface on the UI STA before WinMain balances CoInitializeEx. ResetSmallWindowTopmostGate(); pendingTrayMenuShow = false; + pendingCandidateShow = false; smallWindowInitState = SmallWindowInitState::Idle; smallWindowControllerRequestInFlight = false; if (smallWindowCandHwnd) diff --git a/server/src/webview2/windows_webview2.h b/server/src/webview2/windows_webview2.h index 8a171875..589707af 100644 --- a/server/src/webview2/windows_webview2.h +++ b/server/src/webview2/windows_webview2.h @@ -105,6 +105,13 @@ void UpdateSmallWindowWebviewVisibility(HWND hwnd, bool visible); // Kick (or retry) shared small-window WebView2 init. Returns true when the menu // controller already exists and the tray menu may be shown. bool PrepareTrayMenuWebviewForShow(); +// Remember a candidate show that arrived before this host's WebView2 was +// ready, and replay WM_SHOW_MAIN_WINDOW once navigation completes. +void DeferCandidateShowUntilWebviewReady(); +void MaybeFlushPendingCandidateShow(); +// After the candidate controller exists, pin TOPMOST and renotify WebView2 +// (same uiAccess rule as the tray menu: never pin before CreateCoreWebView2Controller). +void RaiseCandidateHostForShow(const wchar_t *reason); // Expand WebView bounds beyond the host client so horizontal measure/layout // is not constrained by a still-narrow HWND. void PrepareCandidateWebViewBoundsForMeasure(HWND hwnd); diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index cfd51c54..b22ca8ff 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -46,6 +46,10 @@ constexpr UINT_PTR TIMER_ID_FTB_VISIBILITY_RECONCILE = 9; constexpr UINT_PTR TIMER_ID_FTB_DPI_REMEASURE = 10; constexpr UINT_PTR TIMER_ID_CANDIDATE_MOVE_SETTLE = 11; constexpr UINT kCandidateMoveSettleMs = 50; +// WebView2 backend: TSF may briefly toggle "should_show" while composing, +// which can hide the candidate host right after it is un-cloaked. +// Keep it visible for a short grace window so it can actually be perceived. +constexpr UINT kCandidateHideGraceMs = 150; // Long enough fallback when the page never posts ready (old HTML / failed JS). // Page-ready normally ends the grace earlier; until then the toolbar stays shown. constexpr UINT kFloatingToolbarPaintGraceMs = 6000; @@ -69,6 +73,7 @@ std::atomic g_candidate_force_layout{false}; std::atomic g_candidate_content_generation{0}; int g_last_placed_caret_x = Global::INVALID_Y; int g_last_placed_caret_y = Global::INVALID_Y; +ULONGLONG g_candidate_hide_grace_until_tick = 0; bool g_candidate_session_anchor_valid = false; POINT g_candidate_session_anchor{}; bool g_has_last_candidate_clip = false; @@ -419,7 +424,7 @@ void PlaceCandidateHostNearCaret(HWND hwnd, std::pair requestedC KeepCandidateCardInsideHostAndMonitor(hostX, hostY, hostWidthPx, hostHeightPx, decoratedSize.first, decoratedSize.second, layoutScale, coordinates, cardSize.first); - UINT flag = SWP_NOZORDER | SWP_SHOWWINDOW; + UINT flag = SWP_SHOWWINDOW; RECT currentRect{}; if (GetWindowRect(hwnd, ¤tRect)) { @@ -436,7 +441,7 @@ void PlaceCandidateHostNearCaret(HWND hwnd, std::pair requestedC { SuppressCandidateDpiChange suppressDpi; - SetWindowPos(hwnd, nullptr, hostX, hostY, hostWidthPx, hostHeightPx, flag); + SetWindowPos(hwnd, HWND_TOPMOST, hostX, hostY, hostWidthPx, hostHeightPx, flag); } if ((flag & SWP_NOSIZE) == 0) { @@ -1637,25 +1642,31 @@ int CreateCandidateWindow(HINSTANCE hInstance) #ifndef WS_EX_NOREDIRECTIONBITMAP #define WS_EX_NOREDIRECTIONBITMAP 0x00200000L #endif - DWORD dwExStyle = WS_EX_NOREDIRECTIONBITMAP | // - WS_EX_TOOLWINDOW | // - WS_EX_NOACTIVATE; // + const bool d2dUi = UseD2dSmallWindowUi(); + DWORD dwExStyle = d2dUi ? (WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) + : (WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE); FLOAT scale = GetForegroundWindowScale(); + const int candWidth = d2dUi ? (std::max)(8, static_cast(32 * scale)) + : static_cast((::CANDIDATE_WINDOW_WIDTH + ::SHADOW_WIDTH + ::POP_UP_WND_WIDTH) * + scale); + const int candHeight = d2dUi ? (std::max)(8, static_cast(32 * scale)) + : static_cast( + (::CANDIDATE_WINDOW_HEIGHT + ::SHADOW_HEIGHT + ::POP_UP_WND_HEIGHT) * scale); - HWND hwnd_cand = CreateWindowEx( // - dwExStyle, // - szWindowClass, // - lpWindowNameCand, // - WS_POPUP, // - 100, // - 100, // - (std::max)(8, static_cast(32 * scale)), // - (std::max)(8, static_cast(32 * scale)), // - nullptr, // - nullptr, // - hInstance, // - nullptr // - ); // + HWND hwnd_cand = CreateWindowEx( // + dwExStyle, // + szWindowClass, // + lpWindowNameCand, // + WS_POPUP, // + 100, // + 100, // + candWidth, // + candHeight, // + nullptr, // + nullptr, // + hInstance, // + nullptr // + ); // if (!hwnd_cand) { @@ -1672,8 +1683,16 @@ int CreateCandidateWindow(HINSTANCE hInstance) } ::global_hwnd = hwnd_cand; - CandidatePresenter::Instance().Bind(hwnd_cand); - CAND_DIAG_LOGF(L"candidate host created {}", DescribeCandidateHostState()); + if (d2dUi) + { + CandidatePresenter::Instance().Bind(hwnd_cand); + } + else + { + PrepareLayeredHostWindow(hwnd_cand); + } + CAND_DIAG_LOGF(L"candidate host created backend={} {}", d2dUi ? L"d2d" : L"webview2", + DescribeCandidateHostState()); // A client can activate while the pipe server is already listening but this // window does not exist yet, which is the race that leaves the toolbar @@ -1713,7 +1732,10 @@ int CreateCandidateWindow(HINSTANCE hInstance) } PrepareLayeredHostWindow(hwnd_menu); ::global_hwnd_menu = hwnd_menu; - TrayMenuPresenter::Instance().Bind(hwnd_menu); + if (UseD2dSmallWindowUi()) + { + TrayMenuPresenter::Instance().Bind(hwnd_menu); + } // // floating toolbar 窗口 @@ -1755,7 +1777,10 @@ int CreateCandidateWindow(HINSTANCE hInstance) PrepareLayeredHostWindow(hwnd_ftb); ::global_hwnd_ftb = hwnd_ftb; FanyNamedPipe::RegisterStatusSnapshotWindow(hwnd_ftb); - FloatingToolbarPresenter::Instance().Bind(hwnd_ftb); + if (UseD2dSmallWindowUi()) + { + FloatingToolbarPresenter::Instance().Bind(hwnd_ftb); + } // Cloaked show: WebView2 sees a visible on-monitor host; the user does not. WarmupHostWindowCloaked(hwnd_cand); @@ -1770,14 +1795,17 @@ int CreateCandidateWindow(HINSTANCE hInstance) UpdateWindow(hwnd_ftb); // - // Preparing webview2 env + // Preparing webview2 env (skipped when appearance.ui_backend is d2d) // - PrepareHtmlForWnds(); - /* 候选框、托盘语言区右键菜单和 floating toolbar 共用一个 WebView2 environment */ - InitSmallWindowWebviews(hwnd_cand, hwnd_menu, hwnd_ftb); + if (!UseD2dSmallWindowUi()) + { + PrepareHtmlForWnds(); + /* 候选框、托盘语言区右键菜单和 floating toolbar 共用一个 WebView2 environment */ + InitSmallWindowWebviews(hwnd_cand, hwnd_menu, hwnd_ftb); - /* 菜单窗口:首屏导航完成后量一次尺寸(暂不 TOPMOST) */ - SetTimer(hwnd_menu, TIMER_ID_INIT_WEBVIEW_MENU, 200, nullptr); + /* 菜单窗口:首屏导航完成后量一次尺寸(暂不 TOPMOST) */ + SetTimer(hwnd_menu, TIMER_ID_INIT_WEBVIEW_MENU, 200, nullptr); + } /* 监听文本配置文件变化,并同步到运行中的候选框。Settings 已是独立进程。 */ SetTimer(hwnd_cand, TIMER_ID_CONFIG_SYNC, 300, nullptr); @@ -1908,11 +1936,94 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA (void)0; } const POINT layoutCaret = GetCandidateLayoutCaret(); - CandidatePresenter::Instance().ShowFromGlobalState(layoutCaret); - g_last_placed_caret_x = layoutCaret.x; - g_last_placed_caret_y = layoutCaret.y; - CAND_DIAG_LOGF(L"candidate-frame path=d2d content_gen={} elapsed_ms={}", contentGeneration, - GetTickCount64() - updateStartedTick); + if (CandidatePresenter::Instance().IsBound()) + { + CandidatePresenter::Instance().ShowFromGlobalState(layoutCaret); + g_last_placed_caret_x = layoutCaret.x; + g_last_placed_caret_y = layoutCaret.y; + CAND_DIAG_LOGF(L"candidate-frame path=d2d content_gen={} elapsed_ms={}", contentGeneration, + GetTickCount64() - updateStartedTick); + return 0; + } + + // Do not SetWindowPos / ExecuteScript the candidate host while its + // WebView2 controller is still being created. That is what makes + // CreateCoreWebView2Controller fail for this HWND while menu/FTB succeed. + ::is_global_wnd_cand_shown = true; + if (!IsCandidateWebviewReady()) + { + DeferCandidateShowUntilWebviewReady(); + CAND_DIAG_LOGF(L"candidate-frame path=deferred-webview content_gen={} {}", contentGeneration, + DescribeCandidateHostState()); + return 0; + } + RaiseCandidateHostForShow(L"show-candidate"); + + std::wstring preedit = + GetConfiguredCandidateWindowPreeditStyle() == "empty" ? std::wstring{} : GetPreeditWithCaretMarker(); + std::wstring str = preedit + L"," + Global::CandidateString; + const bool sameCaret = + g_last_placed_caret_x == layoutCaret.x && g_last_placed_caret_y == layoutCaret.y; + const bool alreadyVisible = IsCandidateHostPaintedVisible(hwnd); + const bool layoutInflight = g_candidate_layout_inflight.load(); + CAND_DIAG_LOGF(L"candidate-layout-decision content_gen={} last_caret=({},{}) caret=({},{}) " + L"delta=({},{}) same={} visible={} inflight={}", + contentGeneration, g_last_placed_caret_x, g_last_placed_caret_y, layoutCaret.x, + layoutCaret.y, static_cast(layoutCaret.x) - g_last_placed_caret_x, + static_cast(layoutCaret.y) - g_last_placed_caret_y, sameCaret, alreadyVisible, + layoutInflight); + if (!alreadyVisible) + { + PlaceCandidateHostNearCaret(hwnd); + } + if (alreadyVisible) + { + CAND_DIAG_LOGF(L"candidate-frame path=content-only content_gen={} inflight={}", contentGeneration, + layoutInflight); + InflateCandWnd(str, [hwnd, contentGeneration, updateStartedTick]() { + const ULONGLONG callbackTick = GetTickCount64(); + CAND_DIAG_LOGF(L"candidate-frame dom-callback content_gen={} current_gen={} elapsed_ms={}", + contentGeneration, g_candidate_content_generation.load(), + callbackTick - updateStartedTick); + if (!::is_global_wnd_cand_shown || contentGeneration != g_candidate_content_generation.load()) + { + return; + } + RefreshCandidateClipAfterPaint(hwnd, contentGeneration, updateStartedTick); + }); + if (!sameCaret) + { + if (CandidateCaretNeedsStableFollow(hwnd, layoutCaret)) + { + KillTimer(hwnd, TIMER_ID_CANDIDATE_MOVE_SETTLE); + SetTimer(hwnd, TIMER_ID_CANDIDATE_MOVE_SETTLE, kCandidateMoveSettleMs, nullptr); + } + else + { + CAND_DIAG_LOGF(L"candidate-position same-line-follow-suppressed anchor=({},{}) reported=({},{})", + g_last_placed_caret_x, g_last_placed_caret_y, layoutCaret.x, layoutCaret.y); + } + } + return 0; + } + if (layoutInflight) + { + CAND_DIAG_LOGF(L"show folded into in-flight fine-tune caret=({},{})", Global::Point[0], + Global::Point[1]); + CAND_DIAG_LOGF(L"candidate-frame path=folded-inflight content_gen={}", contentGeneration); + InflateCandWnd(str); + KillTimer(hwnd, TIMER_ID_CANDIDATE_MOVE_SETTLE); + SetTimer(hwnd, TIMER_ID_CANDIDATE_MOVE_SETTLE, kCandidateMoveSettleMs, nullptr); + return 0; + } + CAND_DIAG_LOGF(L"candidate-frame path=full-layout content_gen={}", contentGeneration); + InflateMeasureDivCandWnd(str, [hwnd]() { + if (!::is_global_wnd_cand_shown) + { + return; + } + FineTuneWindow(hwnd); + }); return 0; } @@ -1931,7 +2042,36 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA ResetCandidatePlacementMemory(); { SuppressCandidateDpiChange suppressDpi; - CandidatePresenter::Instance().Hide(); + if (CandidatePresenter::Instance().IsBound()) + { + CandidatePresenter::Instance().Hide(); + } + else if (!webviewControllerCandWnd) + { + // Moving this HWND while CreateCoreWebView2Controller is in + // flight makes the candidate controller fail (menu/FTB still work). + SetHostWindowCloaked(hwnd, true); + } + else + { + ClearCandidateWindowRegion(hwnd); + FLOAT scale = GetForegroundWindowScale(); + if (scale < 1.5) + { + scale = 1.5; + } + SetWindowPos( // + hwnd, // + HWND_TOP, // + 0, // + Global::INVALID_Y, // + (::CANDIDATE_WINDOW_WIDTH + ::SHADOW_WIDTH + ::POP_UP_WND_WIDTH) * scale, // + (::CANDIDATE_WINDOW_HEIGHT + ::SHADOW_HEIGHT + ::POP_UP_WND_HEIGHT) * scale, // + SWP_SHOWWINDOW // + ); + SetHostWindowCloaked(hwnd, true); + UpdateHtmlContentWithJavaScript(webviewCandWnd, L""); + } } CAND_DIAG_LOGF(L"hide message end {}", DescribeCandidateHostState()); return 0; @@ -2051,7 +2191,14 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA ForceReloadConfiguredCandidateSkin(); if (::is_global_wnd_cand_shown) { - CandidatePresenter::Instance().ShowFromGlobalState(GetCandidateLayoutCaret()); + if (CandidatePresenter::Instance().IsBound()) + { + CandidatePresenter::Instance().ShowFromGlobalState(GetCandidateLayoutCaret()); + } + else + { + FineTuneWindow(hwnd); + } } return 0; } @@ -2170,7 +2317,14 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA previous_preedit_font_size != GetConfiguredCandidateWindowPreeditFontSize() || previous_cand_text_color != GetConfiguredCandidateTextColor())) { - CandidatePresenter::Instance().ShowFromGlobalState(GetCandidateLayoutCaret()); + if (CandidatePresenter::Instance().IsBound()) + { + CandidatePresenter::Instance().ShowFromGlobalState(GetCandidateLayoutCaret()); + } + else + { + FineTuneWindow(hwnd); + } } if (previous_floating_toolbar != GetConfiguredFloatingToolbarEnabled()) { @@ -2263,6 +2417,14 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA break; } + case WM_SIZE: { + if (::webviewControllerCandWnd && wParam != SIZE_MINIMIZED) + { + SyncCandidateWebViewBoundsToHost(hwnd); + } + break; + } + /* Clear dictionary buffer cache */ case WM_CLS_DICT_CACHE: { FanyNamedPipe::EnqueueResetInputSessionCacheTask(); @@ -2505,6 +2667,10 @@ LRESULT CALLBACK WndProcMenuWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA if (wParam == TIMER_ID_INIT_WEBVIEW_MENU) { KillTimer(hwnd, TIMER_ID_INIT_WEBVIEW_MENU); + if (TrayMenuPresenter::Instance().IsBound() || UseD2dSmallWindowUi()) + { + break; + } if (::webviewMenuWnd) // 确保 webview 已初始化 { GetContainerSizeMenu(webviewMenuWnd, [hwnd](std::pair containerSize) { @@ -3210,7 +3376,11 @@ LRESULT CALLBACK WndProcFtbWindow(HWND hwnd, UINT message, WPARAM wParam, LPARAM if (wParam == TIMER_ID_FTB_DPI_REMEASURE) { KillTimer(hwnd, TIMER_ID_FTB_DPI_REMEASURE); - if (::webviewFtbWnd) + if (FloatingToolbarPresenter::Instance().IsBound()) + { + FloatingToolbarPresenter::Instance().RelayoutHost(); + } + else if (::webviewFtbWnd) { ApplyConfiguredFloatingToolbarSize(); } @@ -3240,7 +3410,7 @@ int FineTuneWindow(HWND hwnd) return 0; } - UINT flag = SWP_NOZORDER | SWP_SHOWWINDOW; + UINT flag = SWP_SHOWWINDOW; int caretX = layoutCaret.x; int caretY = layoutCaret.y; @@ -3250,7 +3420,7 @@ int FineTuneWindow(HWND hwnd) FLOAT scale = QueryCandidateHalfScreenDipLimitsForPoint(hwnd, caretPt).scale; (void)0; - if (!webviewCandWnd) + if (!webviewCandWnd || !IsCandidateWebviewReady()) { (void)0; LogSmallWindowReadyGate(L"fine-tune-no-webview"); @@ -3446,7 +3616,7 @@ int FineTuneWindow(HWND hwnd) EndCandidateLayoutIfCurrent(generation); return; } - + RaiseCandidateHostForShow(L"fine-tune"); // Geometry size is stable (quarter-screen). Do not cloak — card motion // is CSS margin inside the already-placed transparent host. BOOL positioned = FALSE; @@ -3456,7 +3626,7 @@ int FineTuneWindow(HWND hwnd) SetLastError(0); positioned = SetWindowPos( // hwnd, // - nullptr, // + HWND_TOPMOST, // hostX, // hostY, // newWidth, // @@ -3525,7 +3695,7 @@ int FineTuneWindow(HWND hwnd) { SuppressCandidateDpiChange suppressDpi; SetLastError(0); - const BOOL resyncResult = SetWindowPos(hwnd, nullptr, hostX, hostY, hostWidthPx, hostHeightPx, flag); + const BOOL resyncResult = SetWindowPos(hwnd, HWND_TOPMOST, hostX, hostY, hostWidthPx, hostHeightPx, flag); WEBVIEW_DIAG_LOGF(L"ui-layout generation={} pass=dpi-resync scale={:.3f}->{:.3f} " L"desired=({},{},{}x{}) result={} gle={} half_dip=({:.2f},{:.2f}) margin=({},{})", generation, static_cast(oldLayoutScale), static_cast(layoutScale), hostX, @@ -3691,7 +3861,7 @@ int FineTuneWindow(HWND hwnd) SuppressCandidateDpiChange suppressDpi; // Prefer SWP_NOSIZE when already on the quarter-screen size so // only the clamped top-left moves with the card. - UINT pass2Flag = SWP_NOZORDER | SWP_SHOWWINDOW; + UINT pass2Flag = SWP_SHOWWINDOW; RECT cur{}; if (GetWindowRect(hwnd, &cur) && (cur.right - cur.left) == hostWidthPx && (cur.bottom - cur.top) == hostHeightPx) @@ -3699,7 +3869,7 @@ int FineTuneWindow(HWND hwnd) pass2Flag |= SWP_NOSIZE; } const BOOL pass2Result = - SetWindowPos(hwnd, nullptr, hostX, hostY, hostWidthPx, hostHeightPx, pass2Flag); + SetWindowPos(hwnd, HWND_TOPMOST, hostX, hostY, hostWidthPx, hostHeightPx, pass2Flag); CAND_DIAG_LOGF(L"candidate-layout painted-pass layout_gen={} caret=({},{}) " L"current=({},{},{}x{}) desired=({},{},{}x{}) flags={:#x} " L"result={} gle={} margin=({},{}) size_dip=({:.1f},{:.1f})",