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:
parent
472525b99e
commit
67b0815c79
5 changed files with 142 additions and 10 deletions
|
|
@ -178,6 +178,89 @@ public class UiScrollbarTests
|
|||
Assert.Equal(1, completedCount);
|
||||
}
|
||||
|
||||
// ── OP5 re-check R1/R2 (2026-08-11, coordinator pass) ───────────────────
|
||||
|
||||
[Fact]
|
||||
public void CaptureLossMidDrag_EndsTheGesture_CompletesOnce_AndUnlatchesIsDragging()
|
||||
{
|
||||
// R1: UiRoot can drop capture WITHOUT a MouseUp (panel hidden by a
|
||||
// keybind mid-drag; a second button re-targeting capture). The
|
||||
// WM_CAPTURECHANGED delivery must end the drag, fire ONE completion
|
||||
// (persisting the user's last-seen value), and unlatch IsDragging —
|
||||
// otherwise every later Reset/Defaults flush is silently suppressed.
|
||||
int completedCount = 0;
|
||||
var bar = new UiScrollbar
|
||||
{
|
||||
Width = 90f,
|
||||
Height = 14f,
|
||||
Horizontal = true,
|
||||
ScalarChanged = _ => { },
|
||||
DragCompleted = () => completedCount++,
|
||||
};
|
||||
bar.SetScalarPosition(0f);
|
||||
|
||||
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseDown, Data1: 5)));
|
||||
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseMove, Data1: 30)));
|
||||
Assert.True(bar.IsDragging);
|
||||
|
||||
bar.OnEvent(new UiEvent(0u, bar, UiEventType.CaptureChanged));
|
||||
|
||||
Assert.False(bar.IsDragging);
|
||||
Assert.Equal(1, completedCount);
|
||||
|
||||
// A later stray MouseUp (capture already gone) must not double-fire.
|
||||
bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseUp, Data1: 30));
|
||||
Assert.Equal(1, completedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaptureChange_WithNoActiveDrag_IsANoOp()
|
||||
{
|
||||
int completedCount = 0;
|
||||
var bar = new UiScrollbar
|
||||
{
|
||||
Width = 90f,
|
||||
Height = 14f,
|
||||
Horizontal = true,
|
||||
DragCompleted = () => completedCount++,
|
||||
};
|
||||
|
||||
bar.OnEvent(new UiEvent(0u, bar, UiEventType.CaptureChanged));
|
||||
|
||||
Assert.False(bar.IsDragging);
|
||||
Assert.Equal(0, completedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalScalar_BareTrackClickJump_DefersItsTickAndCompletesExactlyOnce()
|
||||
{
|
||||
// R2: a bare track click (outside the thumb) jumps the scalar. The
|
||||
// latch now arms BEFORE the jump applies, so the jump's own
|
||||
// ScalarChanged tick observes IsDragging=true (a consumer defers its
|
||||
// flush) and the MouseUp's single DragCompleted carries the gesture's
|
||||
// one flush — never the inline-then-completed double.
|
||||
int completedCount = 0;
|
||||
bool draggingDuringTick = false;
|
||||
UiScrollbar bar = null!;
|
||||
bar = new UiScrollbar
|
||||
{
|
||||
Width = 90f,
|
||||
Height = 14f,
|
||||
Horizontal = true,
|
||||
ScalarChanged = _ => draggingDuringTick = bar.IsDragging,
|
||||
DragCompleted = () => completedCount++,
|
||||
};
|
||||
bar.SetScalarPosition(0f); // thumb spans [0, 16]
|
||||
|
||||
// Click far outside the thumb — the jump branch.
|
||||
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseDown, Data1: 70)));
|
||||
Assert.True(draggingDuringTick); // the jump tick saw the latch armed
|
||||
Assert.Equal(0, completedCount);
|
||||
|
||||
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseUp, Data1: 70)));
|
||||
Assert.Equal(1, completedCount); // one gesture, one completion
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalModel_DragCompleted_FiresOnceAtMouseUp()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue