fix(D.2b): Slice 2 — buttons inside a whole-window-Draggable frame get their Click
Visual gate 2 (user): the "Slots" toggle caption was visible but unclickable. Root cause (UiRoot.OnMouseDown/OnMouseUp): a left-press on a non-drag-source widget inside a whole-window-Draggable frame (the inventory window's IA-12 drag) set _windowDragTarget; OnMouseUp then early-returned before emitting the Click. So the paperdoll Slots button (the first plain button inside the draggable inventory frame) never received its click. Chat/toolbar buttons escape this — their frames aren't whole-window-draggable. Fix (toolkit, root cause not band-aid): add UiElement.HandlesClick (a virtual opt-out parallel to IsDragSource); UiButton overrides it true; OnMouseDown routes a HandlesClick press to the widget (like CapturesPointerDrag) instead of the window-drag, so OnMouseUp emits the Click. 2 regression tests lock it (HandlesClick widget in a Draggable frame emits Click; a plain one doesn't). Build + full App suite green (596, +2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe319bd2aa
commit
8ee3d89feb
4 changed files with 67 additions and 5 deletions
|
|
@ -75,6 +75,11 @@ public sealed class UiButton : UiElement
|
|||
/// procedurally, so the importer must not build the button's children as widgets.</summary>
|
||||
public override bool ConsumesDatChildren => true;
|
||||
|
||||
/// <summary>A button is interactive — it must receive its Click even inside a whole-window-Draggable
|
||||
/// frame (e.g. the paperdoll "Slots" toggle in the inventory window), so it opts out of the
|
||||
/// IA-12 whole-window-drag that would otherwise swallow the press.</summary>
|
||||
public override bool HandlesClick => true;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the File id for the current <see cref="ActiveState"/>, falling back to
|
||||
/// the DirectState ("" key) if the named state is absent.
|
||||
|
|
|
|||
|
|
@ -121,6 +121,15 @@ public abstract class UiElement
|
|||
/// false; overridden by drag sources (e.g. an occupied <see cref="UiItemSlot"/>).</summary>
|
||||
public virtual bool IsDragSource => false;
|
||||
|
||||
/// <summary>If true, a left-press on this element is handled BY the element (it receives the Click
|
||||
/// on release) instead of being captured as a whole-window move on a Draggable ancestor. Set by
|
||||
/// interactive leaf widgets (e.g. <see cref="UiButton"/>) so they stay clickable inside a
|
||||
/// whole-window-Draggable frame like the inventory window — where, without this, the IA-12
|
||||
/// whole-window-drag swallows the press and the Click is never emitted. Distinct from
|
||||
/// <see cref="IsDragSource"/> (starts a drag-drop) and <see cref="CapturesPointerDrag"/> (a
|
||||
/// self-driven interior drag such as text selection). Default false.</summary>
|
||||
public virtual bool HandlesClick => false;
|
||||
|
||||
/// <summary>Minimum size enforced while resizing.</summary>
|
||||
public float MinWidth { get; set; } = 40f;
|
||||
public float MinHeight { get; set; } = 40f;
|
||||
|
|
|
|||
|
|
@ -275,12 +275,13 @@ public sealed class UiRoot : UiElement
|
|||
// the bar movable by its empty cells / chrome.
|
||||
_dragCandidate = true;
|
||||
}
|
||||
else if (target.CapturesPointerDrag)
|
||||
else if (target.CapturesPointerDrag || target.HandlesClick)
|
||||
{
|
||||
// The pressed widget owns interior drags (e.g. text selection):
|
||||
// do NOT move the ancestor window. The already-dispatched MouseDown
|
||||
// event + SetCapture(target) let the target drive its own drag via
|
||||
// the MouseMove events it receives while captured.
|
||||
// The pressed widget owns its pointer interaction — either an interior drag (e.g. text
|
||||
// selection, CapturesPointerDrag) or a click it must receive (e.g. a UiButton,
|
||||
// HandlesClick). Either way do NOT move the ancestor window. The already-dispatched
|
||||
// MouseDown + SetCapture(target) let the target handle it; on release OnMouseUp emits
|
||||
// the Click over the same element. (A HandlesClick widget is not a drag candidate.)
|
||||
_dragCandidate = false;
|
||||
}
|
||||
else if (window.Draggable)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,53 @@ public class UiRootInputTests
|
|||
Assert.Equal(AnchorEdges.None, panel.Anchors);
|
||||
}
|
||||
|
||||
private sealed class ClickRecorder : UiElement
|
||||
{
|
||||
private readonly bool _handlesClick;
|
||||
public bool Clicked;
|
||||
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; return true; }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesClickWidget_insideDraggableWindow_stillEmitsClick()
|
||||
{
|
||||
// Regression (paperdoll "Slots" toggle): a HandlesClick widget (e.g. a UiButton) inside a
|
||||
// whole-window-Draggable frame (the inventory window, IA-12 whole-window-drag) must still
|
||||
// receive its Click. Before the fix the window-move branch captured the press and OnMouseUp
|
||||
// returned before emitting Click, so the toggle button did nothing.
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var frame = new UiPanel { Left = 10, Top = 300, Width = 200, Height = 60, Draggable = true };
|
||||
var btn = new ClickRecorder(handlesClick: true) { Left = 5, Top = 5, Width = 120, Height = 14 };
|
||||
frame.AddChild(btn);
|
||||
root.AddChild(frame);
|
||||
|
||||
root.OnMouseDown(UiMouseButton.Left, 20, 310); // press over the button (screen rect 15,305..135,319)
|
||||
root.OnMouseUp(UiMouseButton.Left, 20, 310); // release same spot → Click
|
||||
Assert.True(btn.Clicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlainWidget_insideDraggableWindow_doesNotEmitClick()
|
||||
{
|
||||
// Contrast that locks the distinction: a NON-HandlesClick child inside the Draggable frame is
|
||||
// captured as a whole-window-drag, so no Click is emitted (the frame is draggable by it).
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var frame = new UiPanel { Left = 10, Top = 300, Width = 200, Height = 60, Draggable = true };
|
||||
var plain = new ClickRecorder(handlesClick: false) { Left = 5, Top = 5, Width = 120, Height = 14 };
|
||||
frame.AddChild(plain);
|
||||
root.AddChild(frame);
|
||||
|
||||
root.OnMouseDown(UiMouseButton.Left, 20, 310);
|
||||
root.OnMouseUp(UiMouseButton.Left, 20, 310);
|
||||
Assert.False(plain.Clicked);
|
||||
}
|
||||
|
||||
private sealed class CoordRecorder : UiElement
|
||||
{
|
||||
public (int x, int y)? Down, Move;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue