fix(server): 同一次输入内候选卡片只增不减

译文、云/AI 候选、混输英文等内容是陆续到达的,每一批都让卡片在两帧之间改变
宽高。每一帧本身画得完整,但尺寸来回变化看起来就是闪烁。

同一次组合内卡片只增大;自然尺寸低于保留尺寸的 70% 时(页面确实变小了)才跟着
收回,候选窗隐藏后重新开始。D2D 给 card 设置 MinWidth/MinHeight 后重新排版;
WebView2 在更新内容的同一个 JS 任务里给 #realContainer 和 #measureContainer
设置最小宽高,绘制前就是最终尺寸,原生区域测量的也是同一个盒子。两端的收回比例
常量互相注明了对应关系。
This commit is contained in:
fanlusky committed 2026-09-27 09:00:47 +08:00
1 parent 444b015a02
commit 3a7beb18ab
3 files changed
+90 -6

No files matched your search

+40
View File
@@ -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<ICoreWebView2> 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)
{
+47 -6
View File
@@ -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<float>(limits.maxWidthDip) : 480.0f;
const float maxH = limits.maxHeightDip > 1.0 ? static_cast<float>(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);
}
+3
View File
@@ -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;
};