fix(ui): OP5 re-check residuals R1/R2 (coordinator pass) — OP5 CLOSED

R1: UiRoot now delivers WM_CAPTURECHANGED (0x215 — retail's own Win32
event-id space) to the element losing pointer capture on BOTH release
and re-target; UiScrollbar terminates a mid-drag gesture there,
completing it (one DragCompleted flush persisting the user's last-seen
value) and unlatching IsDragging — a panel-close keybind mid-drag or a
second-button re-target can no longer latch the drag flag forever and
silently suppress every later settings flush. Normal MouseUp paths
no-op (the latch is already clear when capture releases).

R2: the scalar latch arms BEFORE the track-click jump applies, so the
jump's own ScalarChanged tick defers its flush to the MouseUp's single
DragCompleted — one flush per press gesture, never the
inline-then-completed double; the DragCompleted doc now states the real
contract (fires once per value-capable gesture incl. capture loss)
instead of the refuted never-on-jump claim.

Tests: capture-loss mid-drag (ends + completes once + stray-MouseUp
no-double), no-drag capture-change no-op, bare-track-click
single-completion with the latch observed armed during the jump tick.
Also reconciles the research doc's U4 row to its closure (the six
caption pairs, the BN zero-fold post-mortem) per the OP6 rework's flag.

Full Release suite: 13,128 passed / 4 skipped / 0 failed (one
documented #250-class allocation flake on first run, green in
isolation and on full-suite rerun).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 08:13:24 +02:00
parent 472525b99e
commit 67b0815c79
5 changed files with 142 additions and 10 deletions

View file

@ -787,6 +787,7 @@ public sealed class UiRoot : UiElement
if (ReferenceEquals(Captured, e)) return;
UiElement? previous = Captured;
Captured = e;
NotifyCaptureLost(previous);
PointerCaptureChanged?.Invoke(previous, e);
}
@ -797,10 +798,27 @@ public sealed class UiRoot : UiElement
// Retail restarts the tooltip idle deadline when capture is released.
_hoverStartedMs = _nowMs;
_tooltipFired = false;
NotifyCaptureLost(previous);
if (previous is not null)
PointerCaptureChanged?.Invoke(previous, null);
}
/// <summary>OP5 re-check R1 (2026-08-11): WM_CAPTURECHANGED to the element
/// losing capture — a capture drop WITHOUT a MouseUp (panel hidden by a
/// keybind mid-drag; a second button re-targeting capture) must let the
/// element terminate any capture-keyed gesture (the scrollbar's drag
/// latch, which otherwise reads IsDragging=true forever and silently
/// suppresses every later settings flush). A normal MouseUp path is
/// unaffected: the gesture state is already cleared by the time capture
/// releases, so the handler no-ops.</summary>
private static void NotifyCaptureLost(UiElement? previous)
{
if (previous is null) return;
var lost = new UiEvent(
previous.EventId, previous, UiEventType.CaptureChanged);
previous.OnEvent(in lost);
}
// ── Window manager (named top-level windows: Show / Hide / Toggle) ───
// Registry state lives in RetailWindowManager; methods below are compatibility forwarders.