fix(D.2b): behavioral widgets are leaf — ConsumesDatChildren (chat menu open)
The generalized channel menu wouldn't open: the factory recursed the Type-6 menu element's dat children, building its invisible Type-12 label child as a UiText. Hit-testing is children-first and UiText consumes MouseDown (selection), so the label child swallowed the menu button click and the dropdown never opened. The transcript similarly gained an invisible Ghosted-button child (a 16x16 selection dead-zone). The old hand-made build never had these — it skipped Type 12 and hand-placed the widgets with no children. Fix: behavioral widgets (Meter/Menu/Button/Scrollbar/Text/Field) draw their full appearance and reproduce their dat sub-elements procedurally, so they are LEAF — the importer must not build their dat children as separate (click-stealing) widgets. Add UiElement.ConsumesDatChildren (default false; the 6 behavioral widgets override true) and gate LayoutImporter recursion on it (replacing the UiMeter-only special case). Only generic containers (UiDatElement, panels) recurse. Visually confirmed in the live client (channel menu opens; General/Trade selected and sent). Vitals unchanged (UiMeter was already leaf). Full suite: 404 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
83076cdbb6
commit
d7002552bc
8 changed files with 45 additions and 5 deletions
|
|
@ -106,11 +106,14 @@ public static class LayoutImporter
|
||||||
|
|
||||||
if (info.Id != 0) byId[info.Id] = w;
|
if (info.Id != 0) byId[info.Id] = w;
|
||||||
|
|
||||||
// Meters consume their own children: DatWidgetFactory already extracted the
|
// Behavioral widgets that draw their full appearance + reproduce their dat
|
||||||
// slice-sprite ids from the grandchild image elements during UiMeter construction.
|
// sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps,
|
||||||
// Adding those children as separate UiElement nodes would produce duplicate
|
// Button labels, Scrollbar arrows) CONSUME their dat children — building those as
|
||||||
// geometry and wrong widget semantics. Every other element type recurses normally.
|
// separate widgets double-draws and lets an invisible child steal pointer/focus
|
||||||
if (w is not UiMeter)
|
// 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)
|
foreach (var child in info.Children)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ public sealed class UiButton : UiElement
|
||||||
// else ActiveState stays "" (DirectState)
|
// else ActiveState stays "" (DirectState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the File id for the current <see cref="ActiveState"/>, falling back to
|
/// Returns the File id for the current <see cref="ActiveState"/>, falling back to
|
||||||
/// the DirectState ("" key) if the named state is absent.
|
/// the DirectState ("" key) if the named state is absent.
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,19 @@ public abstract class UiElement
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="AcDream.App.UI.Layout.LayoutImporter"/> 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 <c>true</c>; the generic container
|
||||||
|
/// (<see cref="AcDream.App.UI.Layout.UiDatElement"/>) and panels return <c>false</c>
|
||||||
|
/// and recurse their children normally. Mirrors retail, where each
|
||||||
|
/// <c>UIElement_X::DrawSelf</c> owns its internal structure.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool ConsumesDatChildren => false;
|
||||||
|
|
||||||
// ── Virtual overrides ───────────────────────────────────────────────
|
// ── Virtual overrides ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ public sealed class UiField : UiElement
|
||||||
CapturesPointerDrag = true; // interior drag selects, doesn't move the window
|
CapturesPointerDrag = true; // interior drag selects, doesn't move the window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
// ── Editing primitives ──────────────────────────────────────────────
|
// ── Editing primitives ──────────────────────────────────────────────
|
||||||
|
|
||||||
public void InsertChar(char c)
|
public void InsertChar(char c)
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,10 @@ public sealed class UiMenu : UiElement
|
||||||
|
|
||||||
public UiMenu() { CapturesPointerDrag = true; }
|
public UiMenu() { CapturesPointerDrag = true; }
|
||||||
|
|
||||||
|
/// <summary>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).</summary>
|
||||||
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
protected override void OnDraw(UiRenderContext ctx)
|
protected override void OnDraw(UiRenderContext ctx)
|
||||||
{
|
{
|
||||||
var resolve = SpriteResolve;
|
var resolve = SpriteResolve;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ public sealed class UiMeter : UiElement
|
||||||
|
|
||||||
public UiMeter() { ClickThrough = true; }
|
public UiMeter() { ClickThrough = true; }
|
||||||
|
|
||||||
|
/// <summary>The meter draws its own 3-slice bars; the importer must not build its
|
||||||
|
/// grandchild slice/text elements as separate widgets.</summary>
|
||||||
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
|
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
|
||||||
/// (local px) for a bar of <paramref name="w"/> x <paramref name="h"/>.</summary>
|
/// (local px) for a bar of <paramref name="w"/> x <paramref name="h"/>.</summary>
|
||||||
public static (float x, float y, float w, float h) ComputeFillRect(
|
public static (float x, float y, float w, float h) ComputeFillRect(
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,10 @@ public sealed class UiScrollbar : UiElement
|
||||||
|
|
||||||
public UiScrollbar() { CapturesPointerDrag = true; }
|
public UiScrollbar() { CapturesPointerDrag = true; }
|
||||||
|
|
||||||
|
/// <summary>The scrollbar draws its own track/thumb/arrows; its dat up/down button
|
||||||
|
/// children are reproduced procedurally, so the importer must not build them.</summary>
|
||||||
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the thumb rectangle (local y origin and height) within the track area
|
/// Computes the thumb rectangle (local y origin and height) within the track area
|
||||||
/// between the two end buttons. Ports retail <c>UIElement_Scrollbar::UpdateLayout
|
/// between the two end buttons. Ports retail <c>UIElement_Scrollbar::UpdateLayout
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@ public sealed class UiText : UiElement
|
||||||
CapturesPointerDrag = true; // interior drag selects, doesn't move the window
|
CapturesPointerDrag = true; // interior drag selects, doesn't move the window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The text view draws its own lines + background; any dat sub-elements
|
||||||
|
/// (scroll indicators, caps) are not built as separate widgets by the importer.</summary>
|
||||||
|
public override bool ConsumesDatChildren => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a scroll offset to [0, max] where max = content-height - view-height
|
/// 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.
|
/// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue