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.