fix #210: stabilize mouse-up capture ownership

Pin each mouse-up to its original captured target before callbacks can hide a window or transfer capture. Classify double-clicks before dispatch, preserve replacement capture owners, and cover both re-entrant paths so jump/combat visibility changes cannot crash the UI root.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 10:27:27 +02:00
parent db03b4bda8
commit e74efc5c06
3 changed files with 106 additions and 19 deletions

View file

@ -20,11 +20,18 @@ public class UiRootInputTests
public bool Clicked;
public int Clicks;
public int DoubleClicks;
public Action? ClickAction;
public ClickRecorder(bool handlesClick) => _handlesClick = handlesClick;
public override bool HandlesClick => _handlesClick;
public override bool OnEvent(in UiEvent e)
{
if (e.Type == UiEventType.Click) { Clicked = true; Clicks++; return true; }
if (e.Type == UiEventType.Click)
{
Clicked = true;
Clicks++;
ClickAction?.Invoke();
return true;
}
if (e.Type == UiEventType.DoubleClick) { DoubleClicks++; return true; }
return false;
}
@ -66,6 +73,47 @@ public class UiRootInputTests
Assert.Equal(1, btn.DoubleClicks);
}
[Fact]
public void DoubleClick_WhenClickHidesCapturedWindow_KeepsOriginalEventTarget()
{
var root = new UiRoot { Width = 800, Height = 600 };
var window = new UiPanel { Width = 200, Height = 100 };
var btn = new ClickRecorder(handlesClick: true) { Width = 120, Height = 20 };
window.AddChild(btn);
root.AddChild(window);
root.RegisterWindow("test", window);
root.Tick(0, nowMs: 1_000);
root.OnMouseDown(UiMouseButton.Left, 10, 10);
root.OnMouseUp(UiMouseButton.Left, 10, 10);
btn.ClickAction = () => root.HideWindow("test");
root.Tick(0, nowMs: 1_300);
root.OnMouseDown(UiMouseButton.Left, 10, 10);
root.OnMouseUp(UiMouseButton.Left, 10, 10);
Assert.False(window.Visible);
Assert.Null(root.Captured);
Assert.Equal(2, btn.Clicks);
Assert.Equal(1, btn.DoubleClicks);
}
[Fact]
public void MouseUp_WhenClickTransfersCapture_PreservesNewCaptureOwner()
{
var root = new UiRoot { Width = 800, Height = 600 };
var btn = new ClickRecorder(handlesClick: true) { Width = 120, Height = 20 };
var modal = new UiPanel { Left = 200, Width = 100, Height = 100 };
root.AddChild(btn);
root.AddChild(modal);
btn.ClickAction = () => root.SetCapture(modal);
root.OnMouseDown(UiMouseButton.Left, 10, 10);
root.OnMouseUp(UiMouseButton.Left, 10, 10);
Assert.Same(modal, root.Captured);
}
[Fact]
public void ItemDragReleasedOutsideUi_raisesOutsideReleaseEvent()
{