From 3a7beb18abe39d6f1277ffbce52dbefd1ee77d0f Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Sun, 27 Sep 2026 09:00:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(server):=20=E5=90=8C=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E5=86=85=E5=80=99=E9=80=89=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=8F=AA=E5=A2=9E=E4=B8=8D=E5=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 译文、云/AI 候选、混输英文等内容是陆续到达的,每一批都让卡片在两帧之间改变 宽高。每一帧本身画得完整,但尺寸来回变化看起来就是闪烁。 同一次组合内卡片只增大;自然尺寸低于保留尺寸的 70% 时(页面确实变小了)才跟着 收回,候选窗隐藏后重新开始。D2D 给 card 设置 MinWidth/MinHeight 后重新排版; WebView2 在更新内容的同一个 JS 任务里给 #realContainer 和 #measureContainer 设置最小宽高,绘制前就是最终尺寸,原生区域测量的也是同一个盒子。两端的收回比例 常量互相注明了对应关系。 --- server/src/webview2/windows_webview2.cpp | 40 +++++++++++++++++ server/src/window/candidate_presenter.cpp | 53 ++++++++++++++++++++--- server/src/window/candidate_presenter.h | 3 ++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index 4ca9864d..25429d6f 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -1825,6 +1825,43 @@ html:not(.msime-hover-armed) #realContainer.hover-active .cand:not(.first):hover })(); )"; +// Late additions to the page (translations, cloud/AI/English merges) resize the +// card between two frames of one composition, which reads as flicker even +// though each frame is complete. Within a composition the card only grows, and +// shrinks back only when its natural size drops below kStickyShrinkRatio of the +// kept one (the page really got smaller). Runs in the same task as the content +// update, so the frame is laid out once with the final size; the native region +// measures the same box and follows. An empty update (hide) ends the +// composition. Mirrors kStickyCardShrinkRatio in the D2D candidate presenter. +constexpr wchar_t kStickyCandidateCardScript[] = LR"( +window.MsimeStickyCandidateCard = function (reset) { + const kStickyShrinkRatio = 0.7; + const box = document.getElementById('realContainer'); + if (!box) return; + const measure = document.getElementById('measureContainer'); + const boxes = measure ? [box, measure] : [box]; + boxes.forEach(function (b) { b.style.minWidth = ''; b.style.minHeight = ''; }); + if (reset) { window.__msimeStickyCard = null; return; } + box.style.boxSizing = 'border-box'; + const rect = box.getBoundingClientRect(); + const kept = window.__msimeStickyCard || {width: 0, height: 0}; + const follow = function (keptValue, natural) { + return natural >= keptValue || natural < keptValue * kStickyShrinkRatio ? natural : keptValue; + }; + let width = follow(kept.width, rect.width); + const maxWidth = Number.parseFloat(box.style.maxWidth); + if (maxWidth > 0) width = Math.min(width, maxWidth); + const height = follow(kept.height, rect.height); + window.__msimeStickyCard = {width: width, height: height}; + if (width > rect.width + 0.5 || height > rect.height + 0.5) { + boxes.forEach(function (b) { + b.style.minWidth = width + 'px'; + b.style.minHeight = height + 'px'; + }); + } +}; +)"; + VOID CALLBACK CandidateHoverArmTimerProc(HWND, UINT, UINT_PTR, DWORD) { MaybeArmCandidatePointerHover(); @@ -2163,6 +2200,9 @@ 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(kStickyCandidateCardScript); + script.append(newContent.empty() ? L"window.MsimeStickyCandidateCard(true);\n" + : L"window.MsimeStickyCandidateCard(false);\n"); script.append(L"if (window.CheckContentTruncation) { window.CheckContentTruncation(); }\n"); if (diagnosticsEnabled) { diff --git a/server/src/window/candidate_presenter.cpp b/server/src/window/candidate_presenter.cpp index a8c5e124..bcd564da 100644 --- a/server/src/window/candidate_presenter.cpp +++ b/server/src/window/candidate_presenter.cpp @@ -156,6 +156,16 @@ constexpr float kShadowPadRight = 32.0f; constexpr float kShadowPadBottom = 40.0f; constexpr float kCandidateMinWidthDip = 160.0f; constexpr float kDecorationCardOverlap = 5.0f; +// Below this fraction of the kept extent, the page really got smaller (fewer or +// shorter candidates) and the card follows it; above it, the difference is a +// late addition coming or going and the card keeps its size. Mirrors +// kStickyShrinkRatio in the WebView2 candidate script. +constexpr float kStickyCardShrinkRatio = 0.7f; + +float StickyCardExtent(float kept, float natural) +{ + return natural >= kept || natural < kept * kStickyCardShrinkRatio ? natural : kept; +} } // namespace @@ -1076,12 +1086,41 @@ void CandidatePresenter::ShowFromGlobalState(POINT caret) const HalfScreenDipLimits limits = ApplyScaleToHalfScreenLimits(caret, scale.scale); const float maxW = limits.maxWidthDip > 1.0 ? static_cast(limits.maxWidthDip) : 480.0f; const float maxH = limits.maxHeightDip > 1.0 ? static_cast(limits.maxHeightDip) : 640.0f; - impl_->root->InvalidateMeasure(); - const msimeui::SizeF measured = impl_->root->MeasureInLayout({maxW, maxH}); - float widthDip = (std::max)(measured.width, kCandidateMinWidthDip + kShadowPadLeft + kShadowPadRight); - float heightDip = (std::max)(measured.height, 36.0f); - impl_->root->InvalidateArrange(); - impl_->root->ArrangeInLayout({0.0f, 0.0f, widthDip, heightDip}); + float widthDip = 0.0f; + float heightDip = 0.0f; + auto layoutRoot = [&]() { + impl_->root->InvalidateMeasure(); + const msimeui::SizeF measured = impl_->root->MeasureInLayout({maxW, maxH}); + widthDip = (std::max)(measured.width, kCandidateMinWidthDip + kShadowPadLeft + kShadowPadRight); + heightDip = (std::max)(measured.height, 36.0f); + impl_->root->InvalidateArrange(); + impl_->root->ArrangeInLayout({0.0f, 0.0f, widthDip, heightDip}); + }; + // Late additions to the page (translations, cloud/AI/English merges) change + // the card's size between two frames of the same composition, and the eye + // reads that as flicker even though every frame is complete. Within one + // composition the card therefore only grows; it shrinks back only when the + // natural size falls well below the kept one (a page that really got + // smaller). Hide() starts the next composition from the natural size. + if (impl_->card) + { + impl_->card->SetMinWidth(kCandidateMinWidthDip); + impl_->card->ClearMinHeight(); + } + layoutRoot(); + if (impl_->card) + { + const msimeui::RectF natural = impl_->card->GetBounds(); + const float maxCardWidthDip = (std::max)(kCandidateMinWidthDip, maxW - kShadowPadLeft - kShadowPadRight); + stickyCardWidthDip_ = (std::min)(StickyCardExtent(stickyCardWidthDip_, natural.width), maxCardWidthDip); + stickyCardHeightDip_ = StickyCardExtent(stickyCardHeightDip_, natural.height); + if (stickyCardWidthDip_ > natural.width + 0.5f || stickyCardHeightDip_ > natural.height + 0.5f) + { + impl_->card->SetMinWidth((std::max)(kCandidateMinWidthDip, stickyCardWidthDip_)); + impl_->card->SetMinHeight(stickyCardHeightDip_); + layoutRoot(); + } + } hoverArmed_ = false; if (!GetCursorPos(&hoverBaseline_)) { @@ -1122,6 +1161,8 @@ void CandidatePresenter::Hide() SetCandidateHostCloaked(true); lastHostWidthPx_ = 0; lastHostHeightPx_ = 0; + stickyCardWidthDip_ = 0.0f; + stickyCardHeightDip_ = 0.0f; SetWindowPos(hwnd_, nullptr, 0, Global::INVALID_Y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW); } diff --git a/server/src/window/candidate_presenter.h b/server/src/window/candidate_presenter.h index b3140121..418f33f1 100644 --- a/server/src/window/candidate_presenter.h +++ b/server/src/window/candidate_presenter.h @@ -59,5 +59,8 @@ class CandidatePresenter int lastHostHeightPx_ = 0; float lastLayoutWidthDip_ = 0.0f; float lastLayoutHeightDip_ = 0.0f; + // Largest card size kept for the current composition; reset by Hide(). + float stickyCardWidthDip_ = 0.0f; + float stickyCardHeightDip_ = 0.0f; int wheelDeltaAccumulator_ = 0; };