Revert the WebView2 changes and temporarily make it switchable between rendering backends.

This commit is contained in:
fanlusky committed 2026-09-04 01:42:10 +08:00
1 parent cf547ac6ed
commit b0156540c0
8 files changed
+451 -64

No files matched your search

+3
View File
@@ -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"
+33
View File
@@ -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<std::filesystem::file_time_type> 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<unsigned char>(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;
+7
View File
@@ -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);
+7 -1
View File
@@ -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<std::string>(data.at("value")));
if (path == "appearance.tsf_preedit_style")
return SetConfiguredTsfPreeditStyle(json::value_to<std::string>(data.at("value")));
if (path == "appearance.ui_backend")
return SetConfiguredUiBackend(json::value_to<std::string>(data.at("value")));
if (path == "appearance.candidate_window_layout")
return SetConfiguredCandidateWindowLayout(json::value_to<std::string>(data.at("value")));
if (path == "appearance.candidate_window_follow_cursor")
return SetConfiguredCandidateWindowFollowCursor(json::value_to<bool>(data.at("value")));
if (path == "appearance.candidate_skin")
return SetConfiguredCandidateSkin(json::value_to<std::string>(data.at("value")));
if (path == "appearance.candidate_window_preedit_style")
+4
View File
@@ -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<unsigned>(submitHr));
if (FAILED(submitHr))
{
callback({0.0, 0.0});
}
}
} // namespace
+175 -18
View File
@@ -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"<style id=\"external-candidate-skin-vars\">:root{{--msime-skin-min-width:{}px;"
L"--msime-skin-decoration-top:{}px;--msime-skin-decoration-width:{}px;}}</style>",
skin.minWidthDip, skin.decorationTopDip, skin.decorationWidthDip);
std::wstring css = BuildExternalCandidateSkinCss(skin, skinsRoot);
NeutralizeEmbeddedStyleClosers(css);
std::wstring generated;
if (!css.empty())
{
generated = L"<style id=\"external-candidate-skin\">" + css + L"</style>";
}
const size_t headEnd = html.find(L"</head>");
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<uint16_t>(1u) << static_cast<uint16_t>(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<void()> 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<void()> 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<unsigned>(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<std::string>(data.at("value"));
if (SetConfiguredUiBackend(value))
{
PostSettingsConfig();
}
}
else if (path == "appearance.candidate_window_layout")
{
const std::string value = json::value_to<std::string>(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)
+7
View File
@@ -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);
+215 -45
View File
@@ -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<bool> g_candidate_force_layout{false};
std::atomic<uint64_t> 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<double, double> 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, &currentRect))
{
@@ -436,7 +441,7 @@ void PlaceCandidateHostNearCaret(HWND hwnd, std::pair<double, double> 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<int>(32 * scale))
: static_cast<int>((::CANDIDATE_WINDOW_WIDTH + ::SHADOW_WIDTH + ::POP_UP_WND_WIDTH) *
scale);
const int candHeight = d2dUi ? (std::max)(8, static_cast<int>(32 * scale))
: static_cast<int>(
(::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<int>(32 * scale)), //
(std::max)(8, static_cast<int>(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<long long>(layoutCaret.x) - g_last_placed_caret_x,
static_cast<long long>(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<double, double> 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<double>(oldLayoutScale), static_cast<double>(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})",