diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 018cbb07..0db0f61d 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -106,11 +106,14 @@ public static class LayoutImporter if (info.Id != 0) byId[info.Id] = w; - // Meters consume their own children: DatWidgetFactory already extracted the - // slice-sprite ids from the grandchild image elements during UiMeter construction. - // Adding those children as separate UiElement nodes would produce duplicate - // geometry and wrong widget semantics. Every other element type recurses normally. - if (w is not UiMeter) + // Behavioral widgets that draw their full appearance + reproduce their dat + // sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps, + // Button labels, Scrollbar arrows) CONSUME their dat children — building those as + // separate widgets double-draws and lets an invisible child steal pointer/focus + // from the behavioral widget (e.g. the channel Menu's label child intercepting the + // button click). Only generic containers (UiDatElement, panels) recurse. See + // UiElement.ConsumesDatChildren. + if (!w.ConsumesDatChildren) { foreach (var child in info.Children) { diff --git a/src/AcDream.App/UI/UiButton.cs b/src/AcDream.App/UI/UiButton.cs index c6c5be26..6c31797d 100644 --- a/src/AcDream.App/UI/UiButton.cs +++ b/src/AcDream.App/UI/UiButton.cs @@ -71,6 +71,10 @@ public sealed class UiButton : UiElement // else ActiveState stays "" (DirectState) } + /// The button draws its own face + label; any dat label child is reproduced + /// procedurally, so the importer must not build the button's children as widgets. + public override bool ConsumesDatChildren => true; + /// /// Returns the File id for the current , falling back to /// the DirectState ("" key) if the named state is absent. diff --git a/src/AcDream.App/UI/UiElement.cs b/src/AcDream.App/UI/UiElement.cs index a65a573b..7e1df4ad 100644 --- a/src/AcDream.App/UI/UiElement.cs +++ b/src/AcDream.App/UI/UiElement.cs @@ -146,6 +146,19 @@ public abstract class UiElement return true; } + /// + /// True if this widget draws its full appearance itself and REPRODUCES its dat + /// sub-elements procedurally (3-slice caps, button labels, scroll arrows, popup + /// rows…) — so the must NOT build + /// those dat child elements as separate widgets (they would double-draw and, worse, + /// steal pointer/focus from the behavioral widget). All registered behavioral widgets + /// (Meter/Menu/Button/Scrollbar/Text/Field) return true; the generic container + /// () and panels return false + /// and recurse their children normally. Mirrors retail, where each + /// UIElement_X::DrawSelf owns its internal structure. + /// + public virtual bool ConsumesDatChildren => false; + // ── Virtual overrides ─────────────────────────────────────────────── /// diff --git a/src/AcDream.App/UI/UiField.cs b/src/AcDream.App/UI/UiField.cs index ab9b8750..9bc7ef32 100644 --- a/src/AcDream.App/UI/UiField.cs +++ b/src/AcDream.App/UI/UiField.cs @@ -71,6 +71,10 @@ public sealed class UiField : UiElement CapturesPointerDrag = true; // interior drag selects, doesn't move the window } + /// The field draws its own background + caret + caps; its dat cap sub-elements + /// are reproduced procedurally, so the importer must not build them as widgets. + public override bool ConsumesDatChildren => true; + // ── Editing primitives ────────────────────────────────────────────── public void InsertChar(char c) diff --git a/src/AcDream.App/UI/UiMenu.cs b/src/AcDream.App/UI/UiMenu.cs index 85241a68..c10bd419 100644 --- a/src/AcDream.App/UI/UiMenu.cs +++ b/src/AcDream.App/UI/UiMenu.cs @@ -80,6 +80,10 @@ public sealed class UiMenu : UiElement public UiMenu() { CapturesPointerDrag = true; } + /// The menu draws its own button face + popup; its dat label/row children + /// must NOT be built (an invisible label child would intercept the button click). + public override bool ConsumesDatChildren => true; + protected override void OnDraw(UiRenderContext ctx) { var resolve = SpriteResolve; diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs index f93737a3..b5ee4a40 100644 --- a/src/AcDream.App/UI/UiMeter.cs +++ b/src/AcDream.App/UI/UiMeter.cs @@ -57,6 +57,10 @@ public sealed class UiMeter : UiElement public UiMeter() { ClickThrough = true; } + /// The meter draws its own 3-slice bars; the importer must not build its + /// grandchild slice/text elements as separate widgets. + public override bool ConsumesDatChildren => true; + /// Clamp to [0,1] and return the fill rect /// (local px) for a bar of x . public static (float x, float y, float w, float h) ComputeFillRect( diff --git a/src/AcDream.App/UI/UiScrollbar.cs b/src/AcDream.App/UI/UiScrollbar.cs index 99e4dcdc..d574b597 100644 --- a/src/AcDream.App/UI/UiScrollbar.cs +++ b/src/AcDream.App/UI/UiScrollbar.cs @@ -61,6 +61,10 @@ public sealed class UiScrollbar : UiElement public UiScrollbar() { CapturesPointerDrag = true; } + /// The scrollbar draws its own track/thumb/arrows; its dat up/down button + /// children are reproduced procedurally, so the importer must not build them. + public override bool ConsumesDatChildren => true; + /// /// Computes the thumb rectangle (local y origin and height) within the track area /// between the two end buttons. Ports retail UIElement_Scrollbar::UpdateLayout diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index 439350db..b5aa838a 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -91,6 +91,10 @@ public sealed class UiText : UiElement CapturesPointerDrag = true; // interior drag selects, doesn't move the window } + /// The text view draws its own lines + background; any dat sub-elements + /// (scroll indicators, caps) are not built as separate widgets by the importer. + public override bool ConsumesDatChildren => true; + /// /// Clamp a scroll offset to [0, max] where max = content-height - view-height /// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests.