From ac8d47ef095db9ba929c1b3cb948eec52817f2ea Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Sat, 8 Aug 2026 21:37:09 +0800 Subject: [PATCH] Change the candidate window to a stable transparent host window occupying about 1/4 of the screen (updating with the cursor monitor/DPI), with content wrapping according to the host width and scrolling according to the host height, and fix the issues of high-DPI edge cropping and inconsistency between measurement and CSS limits. --- server/src/utils/webview_utils.cpp | 45 +++- server/src/utils/webview_utils.h | 8 +- server/src/utils/window_utils.cpp | 66 ++++++ server/src/utils/window_utils.h | 16 ++ server/src/webview2/windows_webview2.cpp | 231 ++++++++++++++++++ server/src/webview2/windows_webview2.h | 2 + server/src/window/ime_windows.cpp | 287 +++++++++++++++++------ 7 files changed, 580 insertions(+), 75 deletions(-) diff --git a/server/src/utils/webview_utils.cpp b/server/src/utils/webview_utils.cpp index 57017862..e6eed4f3 100644 --- a/server/src/utils/webview_utils.cpp +++ b/server/src/utils/webview_utils.cpp @@ -57,6 +57,8 @@ void GetCandidateCardSize( ComPtr webview, const wchar_t *boxId, const wchar_t *parentId, + double maxWidthDip, + double maxHeightDip, std::function)> callback) { if (!webview) @@ -64,6 +66,15 @@ void GetCandidateCardSize( callback({0.0, 0.0}); return; } + if (maxWidthDip < 1.0) + { + maxWidthDip = static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); + } + if (maxHeightDip < 1.0) + { + // Same order of magnitude as the stable quarter-screen host height. + maxHeightDip = static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); + } std::wstring script = fmt::format( LR"( (function() {{ @@ -74,11 +85,17 @@ void GetCandidateCardSize( return JSON.stringify({{width: 0, height: 0}}); }} var maxW = {2}; + var maxH = {3}; if (box) {{ box.style.maxWidth = maxW + "px"; + box.style.maxHeight = maxH + "px"; box.style.width = "fit-content"; box.style.boxSizing = "border-box"; box.style.whiteSpace = "normal"; + // Cap height to the host so large fonts scroll inside the card + // instead of being clipped by SetWindowRgn / HWND. + box.style.overflowX = "hidden"; + box.style.overflowY = "auto"; box.querySelectorAll(".row-wrapper").forEach(function (node) {{ node.style.minWidth = "0"; node.style.maxWidth = "100%"; @@ -96,16 +113,26 @@ void GetCandidateCardSize( }} if (el) {{ el.style.maxWidth = maxW + "px"; + el.style.maxHeight = maxH + "px"; el.style.width = "fit-content"; }} void target.offsetWidth; + void target.offsetHeight; var rect = target.getBoundingClientRect(); - var width = Math.min(maxW, Math.max(rect.width, target.offsetWidth || 0)); - var height = Math.max(rect.height, target.scrollHeight || 0, target.offsetHeight || 0); + // scrollWidth catches glyph overflow that rect/offsetWidth miss at + // high DPI; +1 DIP avoids ceil→floor undersize that clips the last + // candidate against SetWindowRgn / screen edges. + var width = Math.min( + maxW, + Math.max(rect.width, target.offsetWidth || 0, target.scrollWidth || 0) + 1); + // Visible (capped) height for host region — NOT full scrollHeight. + var height = Math.min( + maxH, + Math.max(rect.height, target.offsetHeight || 0) + 1); return JSON.stringify({{width: width, height: height}}); }})(); )", - boxId, parentId, ::CANDIDATE_WINDOW_MAX_WIDTH_DIP); + boxId, parentId, maxWidthDip, maxHeightDip); const HRESULT submitHr = webview->ExecuteScript( script.c_str(), Callback([callback](HRESULT errorCode, LPCWSTR result) -> HRESULT { @@ -121,14 +148,18 @@ void GetCandidateCardSize( } } // namespace -void GetContainerSizeCand(ComPtr webview, std::function)> callback) +void GetContainerSizeCand(ComPtr webview, std::function)> callback, + double maxWidthDip, double maxHeightDip) { - GetCandidateCardSize(webview, L"measureContainer", L"measureContainerParent", std::move(callback)); + GetCandidateCardSize(webview, L"measureContainer", L"measureContainerParent", maxWidthDip, maxHeightDip, + std::move(callback)); } -void GetRealCandidateCardSize(ComPtr webview, std::function)> callback) +void GetRealCandidateCardSize(ComPtr webview, std::function)> callback, + double maxWidthDip, double maxHeightDip) { - GetCandidateCardSize(webview, L"realContainer", L"realContainerParent", std::move(callback)); + GetCandidateCardSize(webview, L"realContainer", L"realContainerParent", maxWidthDip, maxHeightDip, + std::move(callback)); } void GetContainerSizeFtb(ComPtr webview, std::function)> callback) diff --git a/server/src/utils/webview_utils.h b/server/src/utils/webview_utils.h index 427e0903..010384a2 100644 --- a/server/src/utils/webview_utils.h +++ b/server/src/utils/webview_utils.h @@ -5,9 +5,13 @@ void MeasureDomUpdateTime(ComPtr); -void GetContainerSizeCand(ComPtr webview, std::function)> callback); +void GetContainerSizeCand(ComPtr webview, std::function)> callback, + double maxWidthDip = 0.0, double maxHeightDip = 0.0); // Measure the painted candidate card after margins/content are applied. -void GetRealCandidateCardSize(ComPtr webview, std::function)> callback); +// maxWidth/HeightDip: host wrap/scroll budget in CSS DIP (half-screen). +// <=0 falls back to CANDIDATE_WINDOW_MAX_WIDTH_DIP. +void GetRealCandidateCardSize(ComPtr webview, std::function)> callback, + double maxWidthDip = 0.0, double maxHeightDip = 0.0); void GetContainerSizeFtb(ComPtr webview, std::function)> callback); void GetContainerSizeMenu(ComPtr webview, std::function)> callback); void MoveContainerBottom(ComPtr webview, int marginTop); diff --git a/server/src/utils/window_utils.cpp b/server/src/utils/window_utils.cpp index 40310577..d908f76e 100644 --- a/server/src/utils/window_utils.cpp +++ b/server/src/utils/window_utils.cpp @@ -3,6 +3,7 @@ #include "defines/globals.h" #include "ipc/ipc.h" #include "webview_utils.h" +#include #include #include #include @@ -83,6 +84,68 @@ FLOAT GetScaleForPoint(POINT pt) return ScaleFromMonitor(MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST)); } +HalfScreenDipLimits QueryHalfScreenDipLimitsForPoint(POINT pt) +{ + HalfScreenDipLimits limits{}; + limits.scale = GetScaleForPoint(pt); + if (limits.scale <= 0.0f) + { + limits.scale = 1.0f; + } + limits.monitor = GetMonitorCoordinatesFromPoint(pt); + const double monitorWidthPx = + static_cast((std::max)(1, limits.monitor.right - limits.monitor.left)); + const double monitorHeightPx = + static_cast((std::max)(1, limits.monitor.bottom - limits.monitor.top)); + limits.maxWidthDip = (monitorWidthPx * 0.5) / static_cast(limits.scale); + limits.maxHeightDip = (monitorHeightPx * 0.5) / static_cast(limits.scale); + return limits; +} + +HalfScreenDipLimits QueryHalfScreenDipLimitsForHwnd(HWND hwnd) +{ + RECT windowRect{}; + if (hwnd && GetWindowRect(hwnd, &windowRect)) + { + POINT anchor{windowRect.left + (windowRect.right - windowRect.left) / 2, + windowRect.top + (windowRect.bottom - windowRect.top) / 2}; + HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(anchor); + // Prefer the HWND's own DPI once it has landed on a monitor. + const FLOAT hwndScale = GetWindowScale(hwnd); + if (hwndScale > 0.0f) + { + const double monitorWidthPx = + static_cast((std::max)(1, limits.monitor.right - limits.monitor.left)); + const double monitorHeightPx = + static_cast((std::max)(1, limits.monitor.bottom - limits.monitor.top)); + limits.scale = hwndScale; + limits.maxWidthDip = (monitorWidthPx * 0.5) / static_cast(limits.scale); + limits.maxHeightDip = (monitorHeightPx * 0.5) / static_cast(limits.scale); + } + return limits; + } + POINT origin{0, 0}; + return QueryHalfScreenDipLimitsForPoint(origin); +} + +double ClampWidthDipToHalfScreen(double widthDip, const HalfScreenDipLimits &limits) +{ + if (limits.maxWidthDip > 0.0) + { + return (std::min)(widthDip, limits.maxWidthDip); + } + return widthDip; +} + +double ClampHeightDipToHalfScreen(double heightDip, const HalfScreenDipLimits &limits) +{ + if (limits.maxHeightDip > 0.0) + { + return (std::min)(heightDip, limits.maxHeightDip); + } + return heightDip; +} + MonitorCoordinates GetMonitorCoordinatesFromPoint(POINT pt) { return MonitorCoordinatesFromHandle(MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST)); @@ -189,6 +252,9 @@ int AdjustCandidateWindowPosition( // properPos->first = point->x; properPos->second = point->y + caretGapPx; + // Clamp to the card itself so a right-edge push leaves the opaque card flush + // with the monitor (shadow may clip). Reserving SHADOW here opened a large + // visible gap at high DPI. int width = static_cast(std::ceil(containerSize.first * scale)); const double boundary_height_dip = is_vertical ? max_vertical_container_height : containerSize.second; const int boundary_height = static_cast(std::ceil(boundary_height_dip * scale)); diff --git a/server/src/utils/window_utils.h b/server/src/utils/window_utils.h index 7b245d24..0bd1465b 100644 --- a/server/src/utils/window_utils.h +++ b/server/src/utils/window_utils.h @@ -1,3 +1,5 @@ +#pragma once + #include "defines/base_structures.h" #include #include @@ -14,6 +16,20 @@ MonitorCoordinates GetMonitorCoordinates(); MonitorCoordinates GetMainMonitorCoordinates(); int GetTaskbarHeight(); +// Half of the target monitor in CSS DIPs (physical/2 / dpiScale). Single source +// of truth for FTB / menu / candidate max content size. +struct HalfScreenDipLimits +{ + FLOAT scale = 1.0f; + double maxWidthDip = 0.0; + double maxHeightDip = 0.0; + MonitorCoordinates monitor{}; +}; +HalfScreenDipLimits QueryHalfScreenDipLimitsForHwnd(HWND hwnd); +HalfScreenDipLimits QueryHalfScreenDipLimitsForPoint(POINT pt); +double ClampWidthDipToHalfScreen(double widthDip, const HalfScreenDipLimits &limits); +double ClampHeightDipToHalfScreen(double heightDip, const HalfScreenDipLimits &limits); + int AdjustCandidateWindowPosition( // const POINT *point, // const std::pair &, // diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index 465a515a..868f059e 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -82,6 +82,178 @@ ULONGLONG smallWindowControllerRequestStartTick = 0; constexpr ULONGLONG kSmallWindowControllerRequestTimeoutMs = 15000; constexpr int kMaxSmallWindowInitAttempts = 12; constexpr UINT_PTR kRetrySmallWindowWebviewTimerId = 9001; +// Avoid remasure storms when HTML keeps reporting contentTruncated after DPI. +constexpr ULONGLONG kContentTruncationCooldownMs = 400; +ULONGLONG g_last_content_truncation_ftb_ms = 0; +ULONGLONG g_last_content_truncation_menu_ms = 0; +ULONGLONG g_last_content_truncation_cand_ms = 0; + +bool AllowContentTruncationRemeasure(ULONGLONG &last_ms) +{ + const ULONGLONG now = GetTickCount64(); + if (last_ms != 0 && now - last_ms < kContentTruncationCooldownMs) + { + return false; + } + last_ms = now; + return true; +} + +constexpr double kTruncationSizeFactor = 1.2; + +double JsonNumberAsDouble(const json::value &value) +{ + if (value.is_double()) + { + return value.as_double(); + } + if (value.is_int64()) + { + return static_cast(value.as_int64()); + } + if (value.is_uint64()) + { + return static_cast(value.as_uint64()); + } + return 0.0; +} + +void InjectSurfaceViewportLimitsImpl(ICoreWebView2 *webview, HWND hwnd) +{ + if (!webview || !hwnd) + { + return; + } + const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + nlohmann::json cfg = {{"maxWidthDip", limits.maxWidthDip}, + {"maxHeightDip", limits.maxHeightDip}, + {"scale", limits.scale}}; + const std::wstring script = + L"(function(c){" + L"const root=document.documentElement;" + L"if(!root)return;" + L"root.style.setProperty('--msime-max-width-dip',String(c.maxWidthDip||0)+'px');" + L"root.style.setProperty('--msime-max-height-dip',String(c.maxHeightDip||0)+'px');" + L"root.style.setProperty('--msime-dpi-scale',String(c.scale||1));" + L"if(window.CheckContentTruncation)window.CheckContentTruncation();" + L"})(" + + string_to_wstring(cfg.dump()) + L");"; + webview->ExecuteScript(script.c_str(), nullptr); +} + +// Fallback only: grow the host to 1.2x HTML content (DIP), capped at half the +// monitor. Main layout paths should already apply half-screen limits. +bool ApplyContentTruncationResize( // + HWND hwnd, // + ICoreWebView2 *webview, // + ICoreWebView2Controller *controller, // + double contentWidthDip, // + double contentHeightDip, // + int extraShadowDip // +) +{ + if (!hwnd || contentWidthDip < 1.0 || contentHeightDip < 1.0) + { + return false; + } + const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + const double cappedContentW = ClampWidthDipToHalfScreen(contentWidthDip, limits); + const double cappedContentH = ClampHeightDipToHalfScreen(contentHeightDip, limits); + double hostWidthDip = + ClampWidthDipToHalfScreen(cappedContentW * kTruncationSizeFactor, limits) + + static_cast((std::max)(0, extraShadowDip)); + double hostHeightDip = + ClampHeightDipToHalfScreen(cappedContentH * kTruncationSizeFactor, limits) + + static_cast((std::max)(0, extraShadowDip)); + + int physWidth = + static_cast(std::ceil(hostWidthDip * static_cast(limits.scale))); + int physHeight = + static_cast(std::ceil(hostHeightDip * static_cast(limits.scale))); + const int monitorWidth = (std::max)(1, limits.monitor.right - limits.monitor.left); + const int monitorHeight = (std::max)(1, limits.monitor.bottom - limits.monitor.top); + physWidth = (std::min)(physWidth, monitorWidth); + physHeight = (std::min)(physHeight, monitorHeight); + + RECT current{}; + GetWindowRect(hwnd, ¤t); + const int curW = current.right - current.left; + const int curH = current.bottom - current.top; + int posX = current.left; + int posY = current.top; + if (posX + physWidth > limits.monitor.right) + { + posX = limits.monitor.right - physWidth; + } + if (posY + physHeight > limits.monitor.bottom) + { + posY = limits.monitor.bottom - physHeight; + } + if (posX < limits.monitor.left) + { + posX = limits.monitor.left; + } + if (posY < limits.monitor.top) + { + posY = limits.monitor.top; + } + + const bool sizeUnchanged = std::abs(curW - physWidth) <= 1 && std::abs(curH - physHeight) <= 1; + if (!sizeUnchanged) + { + SetWindowPos(hwnd, nullptr, posX, posY, physWidth, physHeight, SWP_NOZORDER | SWP_NOACTIVATE); + } + if (controller) + { + RECT bounds{}; + GetClientRect(hwnd, &bounds); + controller->put_Bounds(bounds); + controller->NotifyParentWindowPositionChanged(); + } + InjectSurfaceViewportLimitsImpl(webview, hwnd); + FTB_DIAG_LOGF( + L"trunc-fallback dip=({:.1f},{:.1f})→host=({:.1f},{:.1f}) px=({},{}) half=({:.1f},{:.1f}) scale={:.3f}", + contentWidthDip, contentHeightDip, hostWidthDip, hostHeightDip, physWidth, physHeight, + limits.maxWidthDip, limits.maxHeightDip, static_cast(limits.scale)); + return !sizeUnchanged; +} + +bool HandleContentTruncatedMessage( // + HWND hwnd, // + ICoreWebView2 *webview, // + ICoreWebView2Controller *controller, // + const json::value &val, // + ULONGLONG &cooldownSlot, // + int extraShadowDip // +) +{ + if (!AllowContentTruncationRemeasure(cooldownSlot)) + { + return false; + } + double widthDip = 0.0; + double heightDip = 0.0; + if (const auto *data = val.as_object().if_contains("data")) + { + if (data->is_object()) + { + const auto &obj = data->as_object(); + if (const auto *w = obj.if_contains("width")) + { + widthDip = JsonNumberAsDouble(*w); + } + if (const auto *h = obj.if_contains("height")) + { + heightDip = JsonNumberAsDouble(*h); + } + } + } + if (widthDip < 1.0 || heightDip < 1.0) + { + return false; + } + return ApplyContentTruncationResize(hwnd, webview, controller, widthDip, heightDip, extraShadowDip); +} bool candidateNavigationReady = false; bool menuNavigationReady = false; @@ -643,6 +815,7 @@ void RenderFloatingToolbarState(ICoreWebView2 *webview) : L"window.setToolbarItem('screen-keyboard',false);"); script.append(items.settings ? L"window.setToolbarItem('settings',true);" : L"window.setToolbarItem('settings',false);"); + script.append(L"if(window.CheckContentTruncation)window.CheckContentTruncation();"); webview->ExecuteScript(script.c_str(), nullptr); } @@ -660,6 +833,11 @@ void SetWebviewMemoryUsageTarget(ComPtr webview, COREWEBVIEW2_MEM } } // namespace +void InjectSurfaceViewportLimits(ICoreWebView2 *webview, HWND hwnd) +{ + InjectSurfaceViewportLimitsImpl(webview, hwnd); +} + bool EnsureSmallWindowsTopmost(const wchar_t *reason) { smallWindowTopmostRequested = true; @@ -927,6 +1105,7 @@ void UpdateHtmlContentWithJavaScript(ComPtr webview, const std::w script.append(GetConfiguredCandidateWindowPreeditStyle() == "empty" ? L"false" : L"true"); script.append(L"); }\n"); script.append(L"if (window.SetPreeditCaret) { window.SetPreeditCaret(); }\n"); + script.append(L"if (window.CheckContentTruncation) { window.CheckContentTruncation(); }\n"); if (!onComplete) { @@ -1480,6 +1659,20 @@ HRESULT OnControllerCreatedCandWnd( // PostMessage(::global_hwnd, WM_COMMIT_CANDIDATE, idx, 0); } } + else if (type == "contentTruncated") + { + // Fallback only: FineTune already sizes to content with a + // half-screen cap. Clear a stale region if we still had to grow. + if (::is_global_wnd_cand_shown && + HandleContentTruncatedMessage(hwnd, webviewCandWnd.Get(), + webviewControllerCandWnd.Get(), val, + g_last_content_truncation_cand_ms, 0)) + { + SetWindowRgn(hwnd, nullptr, TRUE); + Global::MarginLeft = 0; + Global::MarginTop = 0; + } + } } catch (const std::exception &e) { @@ -1506,6 +1699,7 @@ HRESULT OnControllerCreatedCandWnd( // { NotifySmallWindowNavigationReady(candidateNavigationReady, L"candidate"); ApplyConfiguredCandidateAppearance(); + InjectSurfaceViewportLimits(webviewCandWnd.Get(), hwnd); } else { @@ -1701,6 +1895,7 @@ HRESULT OnControllerCreatedMenuWnd( // })())", nullptr); SyncMenuFloatingToolbarToggle(); + InjectSurfaceViewportLimits(sender, hwnd); PostMessage(hwnd, WM_REFRESH_MENU_SIZE, 0, 0); return S_OK; }) @@ -1764,6 +1959,23 @@ HRESULT OnControllerCreatedMenuWnd( // VoiceInput::ToggleRecording(); ShowWindow(::global_hwnd_menu, SW_HIDE); } + else if (type == "contentTruncated") + { + if (HandleContentTruncatedMessage(hwnd, webviewMenuWnd.Get(), + webviewControllerMenuWnd.Get(), val, + g_last_content_truncation_menu_ms, 0)) + { + ::MENU_CONTENT_WIDTH_DIP = + (std::max)(::MENU_CONTENT_WIDTH_DIP, JsonNumberAsDouble(val.at("data").at("width"))); + ::MENU_CONTENT_HEIGHT_DIP = + (std::max)(::MENU_CONTENT_HEIGHT_DIP, JsonNumberAsDouble(val.at("data").at("height"))); + const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + ::MENU_CONTENT_WIDTH_DIP = + ClampWidthDipToHalfScreen(::MENU_CONTENT_WIDTH_DIP, limits); + ::MENU_CONTENT_HEIGHT_DIP = + ClampHeightDipToHalfScreen(::MENU_CONTENT_HEIGHT_DIP, limits); + } + } } return S_OK; }) @@ -2848,6 +3060,7 @@ HRESULT OnControllerCreatedFtbWnd( // ApplyConfiguredFloatingToolbarAppearance(); RenderFloatingToolbarState(sender); ApplyConfiguredFloatingToolbarSize(); + InjectSurfaceViewportLimits(sender, ::global_hwnd_ftb); // Show first so a cold user-data folder can finish painting; // a short timer then hides or keeps it based on real state. ApplyConfiguredFloatingToolbarVisibility(L"ftb-navigation-completed"); @@ -3007,6 +3220,24 @@ HRESULT OnControllerCreatedFtbWnd( // { OpenKeyboardPanelApplication(); } + else if (type == "contentTruncated") + { + if (HandleContentTruncatedMessage(hwnd, webviewFtbWnd.Get(), + webviewControllerFtbWnd.Get(), val, + g_last_content_truncation_ftb_ms, + ::FTB_WND_SHADOW_WIDTH)) + { + const double widthDip = JsonNumberAsDouble(val.at("data").at("width")); + const double heightDip = JsonNumberAsDouble(val.at("data").at("height")); + const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + ::FTB_CONTENT_WIDTH_DIP = ClampWidthDipToHalfScreen(widthDip, limits); + ::FTB_CONTENT_HEIGHT_DIP = ClampHeightDipToHalfScreen(heightDip, limits); + ::FTB_WND_WIDTH = + static_cast(std::ceil(::FTB_CONTENT_WIDTH_DIP * kTruncationSizeFactor)); + ::FTB_WND_HEIGHT = + static_cast(std::ceil(::FTB_CONTENT_HEIGHT_DIP * kTruncationSizeFactor)); + } + } } return S_OK; diff --git a/server/src/webview2/windows_webview2.h b/server/src/webview2/windows_webview2.h index c3cd4c04..c705f158 100644 --- a/server/src/webview2/windows_webview2.h +++ b/server/src/webview2/windows_webview2.h @@ -67,6 +67,8 @@ bool ApplyConfiguredUiThemes(); bool ApplyConfiguredCandidateAppearance(); bool ApplyConfiguredFloatingToolbarAppearance(); bool ApplyConfiguredFloatingToolbarAppearance(std::function onComplete); +// Push half-monitor CSS max width/height (DIP) into a small-window page. +void InjectSurfaceViewportLimits(ICoreWebView2 *webview, HWND hwnd); void ResetContainerHoverCandWnd(ComPtr webview); void DisableMouseForAWhileWhenShownCandWnd(ComPtr webview); void InflateCandWnd(std::wstring &str); diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index ed92a122..a95a2567 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -162,14 +162,17 @@ void ClipCandidateWindowToContent( return; } - // Keep the large, stable WebView host used to avoid resize flashes, but - // remove its transparent reserve from the native window region. Child + // Keep a large, stable WebView host (quarter-screen) to avoid resize flashes, + // but remove its transparent reserve from the native window region. Child // windows (including WebView2) are clipped by the parent region, so points // outside the real candidate card fall through to the editor and preserve // its I-beam cursor. // // Reserve left/top shadow the same way as the right/bottom edges so a // box-shadow painted slightly outside the measured card is not clipped. + // +2 DIP safety: high-DPI fractional glyph bounds otherwise lose the last + // character against the region edge. + constexpr double kRegionSafetyDip = 2.0; const int left = (std::max)( static_cast(client.left), static_cast(std::floor( @@ -181,12 +184,12 @@ void ClipCandidateWindowToContent( const int right = (std::min)( static_cast(client.right), static_cast(std::ceil( - (Global::MarginLeft + containerSize.first + ::SHADOW_WIDTH) * + (Global::MarginLeft + containerSize.first + ::SHADOW_WIDTH + kRegionSafetyDip) * static_cast(clipScale)))); const int bottom = (std::min)( static_cast(client.bottom), static_cast(std::ceil( - (Global::MarginTop + containerSize.second + ::SHADOW_HEIGHT) * + (Global::MarginTop + containerSize.second + ::SHADOW_HEIGHT + kRegionSafetyDip) * static_cast(clipScale)))); if (right <= left || bottom <= top) @@ -207,6 +210,99 @@ void ClipCandidateWindowToContent( } } +// After host clamp + MarginLeft/Top, keep the painted card inside both the +// quarter-screen HWND and the caret's monitor. Underestimated measure widths +// otherwise leave the last candidate hanging past the screen edge. +void KeepCandidateCardInsideHostAndMonitor( // + int hostX, // + int hostY, // + int hostWidthPx, // + int hostHeightPx, // + double contentWidthDip, // + double contentHeightDip, // + FLOAT layoutScale, // + const MonitorCoordinates &coordinates // +) +{ + if (layoutScale <= 0.0f) + { + layoutScale = 1.0f; + } + const double hostWidthDip = static_cast(hostWidthPx) / static_cast(layoutScale); + const double hostHeightDip = static_cast(hostHeightPx) / static_cast(layoutScale); + + // Allow the opaque card to reach the host's right/bottom edge. Box-shadow may + // clip at the HWND; pulling MarginLeft back by SHADOW left a visible gap on + // the monitor's right edge. + if (contentWidthDip < hostWidthDip) + { + const double maxMarginLeft = hostWidthDip - contentWidthDip; + if (Global::MarginLeft > maxMarginLeft) + { + Global::MarginLeft = static_cast(std::floor(maxMarginLeft)); + } + } + else + { + Global::MarginLeft = 0; + } + if (contentHeightDip < hostHeightDip) + { + const double maxMarginTop = hostHeightDip - contentHeightDip; + if (Global::MarginTop > maxMarginTop) + { + Global::MarginTop = static_cast(std::floor(maxMarginTop)); + } + } + else if (Global::MarginTop > 0 && contentHeightDip >= hostHeightDip) + { + Global::MarginTop = 0; + } + + const int edgePadPx = + static_cast(std::lround(2.0 * static_cast(layoutScale))); + const int cardLeft = + hostX + static_cast(std::lround(Global::MarginLeft * static_cast(layoutScale))); + const int cardTop = + hostY + static_cast(std::lround(Global::MarginTop * static_cast(layoutScale))); + const int cardRight = + cardLeft + static_cast(std::ceil(contentWidthDip * static_cast(layoutScale))); + const int cardBottom = + cardTop + static_cast(std::ceil(contentHeightDip * static_cast(layoutScale))); + + if (cardRight > coordinates.right - edgePadPx) + { + const int overflowPx = cardRight - (coordinates.right - edgePadPx); + const int reduceDip = + static_cast(std::ceil(static_cast(overflowPx) / static_cast(layoutScale))); + Global::MarginLeft = (std::max)(0, Global::MarginLeft - reduceDip); + } + if (cardLeft < coordinates.left + edgePadPx) + { + const int deficitPx = (coordinates.left + edgePadPx) - cardLeft; + const int addDip = + static_cast(std::ceil(static_cast(deficitPx) / static_cast(layoutScale))); + Global::MarginLeft += addDip; + if (contentWidthDip < hostWidthDip) + { + const double maxMarginLeft = hostWidthDip - contentWidthDip; + if (Global::MarginLeft > maxMarginLeft) + { + Global::MarginLeft = static_cast(std::floor(maxMarginLeft)); + } + } + } + if (cardBottom > coordinates.bottom - edgePadPx) + { + const int overflowPx = cardBottom - (coordinates.bottom - edgePadPx); + const int reduceDip = + static_cast(std::ceil(static_cast(overflowPx) / static_cast(layoutScale))); + Global::MarginTop = (std::max)(0, Global::MarginTop - reduceDip); + } + (void)hostY; + (void)hostHeightPx; +} + // Recompute FTB outer HWND from design DIPs * current DPI. Placement used to be // SWP_NOSIZE-only, so a live display-scale change left the host stuck at the // create-time physical size while WebView content grew. @@ -241,6 +337,22 @@ void LayoutFloatingToolbar(HWND hwnd, bool reset_to_default_corner, FLOAT scaleO { scale = 1.0f; } + // Main-path half-screen cap so HTML wrap/scroll and HWND agree before show. + HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + if (scaleOverride > 0.0f) + { + const double monitorWidthPx = + static_cast((std::max)(1, limits.monitor.right - limits.monitor.left)); + const double monitorHeightPx = + static_cast((std::max)(1, limits.monitor.bottom - limits.monitor.top)); + limits.scale = scale; + limits.maxWidthDip = (monitorWidthPx * 0.5) / static_cast(scale); + limits.maxHeightDip = (monitorHeightPx * 0.5) / static_cast(scale); + } + ::FTB_WND_WIDTH = static_cast( + std::ceil(ClampWidthDipToHalfScreen(static_cast(::FTB_WND_WIDTH), limits))); + ::FTB_WND_HEIGHT = static_cast( + std::ceil(ClampHeightDipToHalfScreen(static_cast(::FTB_WND_HEIGHT), limits))); const int width = static_cast(std::ceil((::FTB_WND_WIDTH + ::FTB_WND_SHADOW_WIDTH) * static_cast(scale))); const int height = @@ -283,6 +395,7 @@ void LayoutFloatingToolbar(HWND hwnd, bool reset_to_default_corner, FLOAT scaleO const BOOL ok = SetWindowPos(hwnd, nullptr, posX, posY, width, height, SWP_NOZORDER | SWP_NOACTIVATE); SyncHostWebViewBounds(::webviewControllerFtbWnd.Get(), hwnd); + InjectSurfaceViewportLimits(::webviewFtbWnd.Get(), hwnd); (void)ok; } @@ -312,6 +425,9 @@ void ApplyMenuPhysicalSizeFromDips(HWND hwnd, FLOAT scale, UINT flags) { scale = 1.0f; } + const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + ::MENU_CONTENT_WIDTH_DIP = ClampWidthDipToHalfScreen(::MENU_CONTENT_WIDTH_DIP, limits); + ::MENU_CONTENT_HEIGHT_DIP = ClampHeightDipToHalfScreen(::MENU_CONTENT_HEIGHT_DIP, limits); const int newWidth = static_cast(std::ceil(::MENU_CONTENT_WIDTH_DIP * scale)); const int newHeight = static_cast(std::ceil(::MENU_CONTENT_HEIGHT_DIP * scale)); ::SCALE = scale; @@ -1532,6 +1648,7 @@ LRESULT CALLBACK WndProcMenuWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA flag |= wasVisible ? SWP_SHOWWINDOW : SWP_HIDEWINDOW; FLOAT scale = GetWindowScale(hwnd); ApplyMenuPhysicalSizeFromDips(hwnd, scale, flag); + InjectSurfaceViewportLimits(::webviewMenuWnd.Get(), hwnd); (void)0; } }); @@ -2172,16 +2289,31 @@ int FineTuneWindow(HWND hwnd) return 0; } const uint64_t generation = ++g_candidate_finetune_generation; + // Wrap/scroll budget = stable host size (half-screen DIP). Width wraps; + // height scrolls inside the card when fonts make the list taller than host. + const HalfScreenDipLimits measureLimits = QueryHalfScreenDipLimitsForPoint(caretPt); + const double wrapMaxDip = + measureLimits.maxWidthDip > 1.0 + ? measureLimits.maxWidthDip + : static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); + const double wrapMaxHeightDip = + measureLimits.maxHeightDip > 1.0 + ? measureLimits.maxHeightDip + : static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); // Give horizontal measure an unconstrained viewport before reading DOM size. PrepareCandidateWebViewBoundsForMeasure(hwnd); + InjectSurfaceViewportLimits(::webviewCandWnd.Get(), hwnd); std::shared_ptr> properPos = std::make_shared>(); - GetContainerSizeCand(webviewCandWnd, [flag, // - scale, // - caretX, // - caretY, // - properPos, // - generation, // - hwnd](std::pair containerSize) { + GetContainerSizeCand( + webviewCandWnd, + [flag, // + scale, // + caretX, // + caretY, // + properPos, // + generation, // + wrapMaxDip, // + hwnd](std::pair containerSize) { // Commit/ClearState may have hidden the window while this WebView2 // measure callback was still pending — do not resurrect it. if (!::is_global_wnd_cand_shown || caretY == Global::INVALID_Y) @@ -2195,14 +2327,22 @@ int FineTuneWindow(HWND hwnd) return; } - // Parentheses keep Windows min() macro from eating std::min. - containerSize.first = - (std::min)(containerSize.first, static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP)); - POINT pt = {caretX, caretY}; - // properPos here is the desired top-left of the opaque card (content), not - // necessarily the host HWND — host may stay clamped while the card slides - // via MarginLeft/MarginTop inside a stable larger window. + HalfScreenDipLimits halfLimits = QueryHalfScreenDipLimitsForPoint(pt); + auto capCandWidthDip = [&](double widthDip) { + return ClampWidthDipToHalfScreen(widthDip, halfLimits); + }; + auto capCandHeightDip = [&](double heightDip) { + return ClampHeightDipToHalfScreen(heightDip, halfLimits); + }; + + // Parentheses keep Windows min() macro from eating std::min. + containerSize.first = capCandWidthDip(containerSize.first); + containerSize.second = capCandHeightDip(containerSize.second); + + // properPos is the desired top-left of the opaque card (content), not + // necessarily the host HWND — host stays at a stable quarter-screen size + // while the card slides via MarginLeft/MarginTop. AdjustCandidateWindowPosition(&pt, containerSize, properPos); std::wstring preedit = GetConfiguredCandidateWindowPreeditStyle() == "empty" @@ -2216,29 +2356,19 @@ int FineTuneWindow(HWND hwnd) return; } - // Stable host: keep the transparent HWND at wrap-cap width and at least - // the default height so right-edge growth does not SetWindowPos-move the - // host (which flashes through layered transparency). The card is offset - // inside with MarginLeft / MarginTop instead. - auto computeHostPixels = [](double heightDip, FLOAT hostScale) { - // Include left and right shadow so ClipCandidateWindowToContent can - // reserve SHADOW_* on both sides without clamping the card away. - const double hostContentWidthDip = static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); - const int hostWidthPx = static_cast(std::lround( - (hostContentWidthDip + (2.0 * ::SHADOW_WIDTH) + ::POP_UP_WND_WIDTH) * hostScale)); - const int hostHeightPx = static_cast(std::lround( - (heightDip + (2.0 * ::SHADOW_HEIGHT) + ::POP_UP_WND_HEIGHT) * hostScale)); + // Stable host = half monitor width × half height (≈ 1/4 screen area). + // Avoids SetWindowPos flashes when the card grows; transparent pixels + // outside the card are clipped out so input still passes through. + auto computeHostPixels = [&](const HalfScreenDipLimits &limits) { + const int hostWidthPx = + (std::max)(1, (limits.monitor.right - limits.monitor.left) / 2); + const int hostHeightPx = + (std::max)(1, (limits.monitor.bottom - limits.monitor.top) / 2); return std::pair{hostWidthPx, hostHeightPx}; }; - double heightDip = (std::max)(containerSize.second, static_cast(::CANDIDATE_WINDOW_HEIGHT)); - if (Global::MarginTop > 0) - { - heightDip = containerSize.second + static_cast(Global::MarginTop); - } - FLOAT layoutScale = scale; - auto hostPx = computeHostPixels(heightDip, layoutScale); + auto hostPx = computeHostPixels(halfLimits); int hostWidthPx = hostPx.first; int hostHeightPx = hostPx.second; @@ -2277,6 +2407,8 @@ int FineTuneWindow(HWND hwnd) Global::MarginTop = packingMarginTop + (std::max)(0, offsetYDip); }; applyCardMargins(layoutScale); + KeepCandidateCardInsideHostAndMonitor(hostX, hostY, hostWidthPx, hostHeightPx, containerSize.first, + containerSize.second, layoutScale, coordinates); int newWidth = hostWidthPx; int newHeight = hostHeightPx; @@ -2303,8 +2435,8 @@ int FineTuneWindow(HWND hwnd) return; } - // Geometry changes are rare now (stable 720 DIP host). Do not cloak — - // card motion is CSS margin inside the already-placed transparent host. + // Geometry size is stable (quarter-screen). Do not cloak — card motion + // is CSS margin inside the already-placed transparent host. BOOL positioned = FALSE; { SuppressCandidateDpiChange suppressDpi; @@ -2326,7 +2458,8 @@ int FineTuneWindow(HWND hwnd) if (hwndScale > 0.0f && std::fabs(hwndScale - layoutScale) > 0.001f) { layoutScale = hwndScale; - hostPx = computeHostPixels(heightDip, layoutScale); + halfLimits = QueryHalfScreenDipLimitsForHwnd(hwnd); + hostPx = computeHostPixels(halfLimits); hostWidthPx = hostPx.first; hostHeightPx = hostPx.second; @@ -2359,6 +2492,8 @@ int FineTuneWindow(HWND hwnd) std::lround((properPos->second - hostY) / static_cast(layoutScale))); Global::MarginLeft = (std::max)(0, offsetXDip); Global::MarginTop = resyncPackingMarginTop + (std::max)(0, offsetYDip); + KeepCandidateCardInsideHostAndMonitor(hostX, hostY, hostWidthPx, hostHeightPx, containerSize.first, + containerSize.second, layoutScale, coordinates); if (!::is_global_wnd_cand_shown || generation != g_candidate_finetune_generation.load()) { @@ -2381,6 +2516,7 @@ int FineTuneWindow(HWND hwnd) { webviewControllerCandWnd->NotifyParentWindowPositionChanged(); } + InjectSurfaceViewportLimits(::webviewCandWnd.Get(), hwnd); InflateCandWnd(str, [hwnd, positioned, generation, containerSize, layoutScale, caretX, caretY, packingMarginTop]() { @@ -2394,8 +2530,9 @@ int FineTuneWindow(HWND hwnd) { return; } - finalSize.first = - (std::min)(finalSize.first, static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP)); + const HalfScreenDipLimits clipLimits = QueryHalfScreenDipLimitsForHwnd(hwnd); + finalSize.first = ClampWidthDipToHalfScreen(finalSize.first, clipLimits); + finalSize.second = ClampHeightDipToHalfScreen(finalSize.second, clipLimits); ClipCandidateWindowToContent(hwnd, finalSize, layoutScale); FTB_DIAG_LOGF( L"cand-clip scale={:.3f} margin=({},{}) size=({:.1f},{:.1f})", @@ -2409,11 +2546,21 @@ int FineTuneWindow(HWND hwnd) (void)0; }; - // Pass 2: remeasure the painted card after margins so a wrap from a - // tight viewport updates host height / region instead of clipping. - GetRealCandidateCardSize(webviewCandWnd, [hwnd, generation, layoutScale, caretX, caretY, - packingMarginTop, containerSize, - finalizeClip](std::pair paintedSize) { + // Pass 2: remeasure the painted card after margins. Keep the stable + // quarter-screen host; only re-clamp position / margins / region. + const HalfScreenDipLimits pass2LimitsForWrap = QueryHalfScreenDipLimitsForHwnd(hwnd); + const double pass2WrapMaxDip = + pass2LimitsForWrap.maxWidthDip > 1.0 + ? pass2LimitsForWrap.maxWidthDip + : static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); + const double pass2WrapMaxHeightDip = + pass2LimitsForWrap.maxHeightDip > 1.0 + ? pass2LimitsForWrap.maxHeightDip + : static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); + GetRealCandidateCardSize( + webviewCandWnd, + [hwnd, generation, layoutScale, caretX, caretY, packingMarginTop, containerSize, + finalizeClip](std::pair paintedSize) { if (!::is_global_wnd_cand_shown || generation != g_candidate_finetune_generation.load()) { return; @@ -2423,8 +2570,9 @@ int FineTuneWindow(HWND hwnd) finalizeClip(containerSize); return; } - paintedSize.first = - (std::min)(paintedSize.first, static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP)); + HalfScreenDipLimits pass2Limits = QueryHalfScreenDipLimitsForHwnd(hwnd); + paintedSize.first = ClampWidthDipToHalfScreen(paintedSize.first, pass2Limits); + paintedSize.second = ClampHeightDipToHalfScreen(paintedSize.second, pass2Limits); const bool sizeChanged = std::fabs(paintedSize.first - containerSize.first) > 0.75 || @@ -2444,21 +2592,13 @@ int FineTuneWindow(HWND hwnd) if (hwndScale > 0.0f) { pass2Scale = hwndScale; + pass2Limits = QueryHalfScreenDipLimitsForHwnd(hwnd); } - double heightDip = - (std::max)(paintedSize.second, static_cast(::CANDIDATE_WINDOW_HEIGHT)); - const int packingTop = (std::max)(0, Global::MarginTop); - if (packingTop > 0) - { - heightDip = paintedSize.second + static_cast(packingTop); - } - - const double hostContentWidthDip = static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP); - const int hostWidthPx = static_cast(std::lround( - (hostContentWidthDip + (2.0 * ::SHADOW_WIDTH) + ::POP_UP_WND_WIDTH) * pass2Scale)); - const int hostHeightPx = static_cast(std::lround( - (heightDip + (2.0 * ::SHADOW_HEIGHT) + ::POP_UP_WND_HEIGHT) * pass2Scale)); + const int hostWidthPx = + (std::max)(1, (pass2Limits.monitor.right - pass2Limits.monitor.left) / 2); + const int hostHeightPx = + (std::max)(1, (pass2Limits.monitor.bottom - pass2Limits.monitor.top) / 2); MonitorCoordinates coordinates = GetMonitorCoordinatesFromPoint(pt); int hostX = properPos->first; @@ -2482,12 +2622,15 @@ int FineTuneWindow(HWND hwnd) hostY = coordinates.top + edgePadPx; } + const int packingTop = (std::max)(0, Global::MarginTop); const int offsetXDip = static_cast( std::lround((properPos->first - hostX) / static_cast(pass2Scale))); const int offsetYDip = static_cast( std::lround((properPos->second - hostY) / static_cast(pass2Scale))); Global::MarginLeft = (std::max)(0, offsetXDip); Global::MarginTop = packingTop + (std::max)(0, offsetYDip); + KeepCandidateCardInsideHostAndMonitor(hostX, hostY, hostWidthPx, hostHeightPx, paintedSize.first, + paintedSize.second, pass2Scale, coordinates); if (!::is_global_wnd_cand_shown || generation != g_candidate_finetune_generation.load()) { @@ -2495,10 +2638,20 @@ int FineTuneWindow(HWND hwnd) } { SuppressCandidateDpiChange suppressDpi; - SetWindowPos(hwnd, nullptr, hostX, hostY, hostWidthPx, hostHeightPx, - SWP_NOZORDER | SWP_SHOWWINDOW); + // 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; + RECT cur{}; + if (GetWindowRect(hwnd, &cur) && + (cur.right - cur.left) == hostWidthPx && + (cur.bottom - cur.top) == hostHeightPx) + { + pass2Flag |= SWP_NOSIZE; + } + SetWindowPos(hwnd, nullptr, hostX, hostY, hostWidthPx, hostHeightPx, pass2Flag); } SyncCandidateWebViewBoundsToHost(hwnd); + InjectSurfaceViewportLimits(::webviewCandWnd.Get(), hwnd); // Re-apply margins into the live DOM before clipping. std::wstring marginScript = L"(function(){var el=document.getElementById('realContainerParent');" @@ -2521,8 +2674,10 @@ int FineTuneWindow(HWND hwnd) finalizeClip(paintedSize); } (void)packingMarginTop; - }); + }, + pass2WrapMaxDip, pass2WrapMaxHeightDip); }); - }); + }, + wrapMaxDip, wrapMaxHeightDip); return 0; }