using System.Collections.Generic;
namespace AcDream.App.UI.Layout;
///
/// GL-free, dat-free snapshot of a resolved layout element.
/// Populated by the LayoutDesc importer from DatReaderWriter.ElementDesc
/// after inheritance is applied. The pure transforms on
/// operate on this type so they can be unit-tested without the dats or OpenGL.
///
/// IMPORTANT: Tasks 3–6 depend on this shape exactly. Do not add members without
/// updating the plan spec and downstream consumers.
///
public sealed class ElementInfo
{
/// Dat element id (e.g. 0x100000E6).
public uint Id;
///
/// Raw element class id as a uint.
/// Game-specific ids like 0x1000004D (gmVitalsUI root) and 0x10000009
/// overflow int when treated as signed, so this stays uint.
/// Known values: 0=text, 2=dragbar, 3=container/chrome, 7=meter,
/// 9=resize-grip, 12=style-prototype (skip), 0x10000009/0x1000004D=window root.
///
public uint Type;
/// Position and size within the parent, in pixels (cast from dat uint fields).
public float X, Y, Width, Height;
///
/// Raw edge-anchor flag values from the dat (LeftEdge, TopEdge,
/// RightEdge, BottomEdge fields of ElementDesc).
/// Values 0–4; map to bit-flags via
/// .
///
public uint Left, Top, Right, Bottom;
/// Draw order within the parent (lower = drawn first / behind).
public uint ReadOrder;
///
/// Font dat object id inherited from the base element's Properties[0x1A]
/// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited.
///
public uint FontDid;
///
/// Sprite per state: state name → (RenderSurface file id, DrawMode int).
/// The "" key represents the unnamed DirectState (ElementDesc.StateDesc).
/// Named states use the UIStateId.ToString() value as the key
/// (e.g. "HideDetail", "ShowDetail").
///
public Dictionary StateMedia = new();
///
/// The element's initial active state name, taken from ElementDesc.DefaultState.ToString().
/// Normalized to "" when the dat carries Undef/Undefined/0 (no default set).
/// Used by to pick which state's sprite to render initially.
/// Examples: "Normal" (Send button), "Minimized" (max/min button), "" (DirectState).
///
public string DefaultStateName = "";
///
/// Resolved child elements (populated by the importer in Task 5).
/// Children come from the derived element's own tree, not the base element's.
///
public List Children = new();
}
///
/// Pure, GL-free, dat-free transforms for the LayoutDesc importer.
/// All methods are static and operate on POCOs.
/// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond
/// the bit-flag enum from AcDream.App.UI.
///
public static class ElementReader
{
/// Edge-anchor flags → AnchorEdges, per retail UIElement::UpdateForParentSizeChange
/// @0x00462640. The far-axis fields drive stretch: RightEdge==1 ⇒ the right edge tracks the
/// parent's right edge (stretch); LeftEdge==2 ⇒ a fixed-width element's left tracks the right
/// edge (it moves right). ==4 (not present in the vitals layout) = both-sides stretch; ==3 =
/// centered (no edge anchor → falls back to pin-top-left). This is the INVERSE of the earlier
/// format-doc §4 reading, which was wrong (it made every piece fixed-width).
/// LeftEdge dat field value (0–4).
/// TopEdge dat field value (0–4).
/// RightEdge dat field value (0–4).
/// BottomEdge dat field value (0–4).
public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom)
{
var a = AnchorEdges.None;
if (left == 1 || left == 4) a |= AnchorEdges.Left;
if (right == 1 || right == 4 || left == 2) a |= AnchorEdges.Right;
if (top == 1 || top == 4) a |= AnchorEdges.Top;
if (bottom == 1 || bottom == 4 || top == 2) a |= AnchorEdges.Bottom;
if (a == AnchorEdges.None) a = AnchorEdges.Left | AnchorEdges.Top; // default: pin top-left
return a;
}
///
/// Merges a base element snapshot with a derived element snapshot, mirroring
/// the BaseElement / BaseLayoutId inheritance chain in the dat.
///
///
/// Rules:
///
/// -
/// Scalar fields (, ,
/// , ,
/// ): derived wins if non-zero; otherwise
/// inherited from base.
///
/// -
/// Position (, ) and
/// edge flags ( etc.) and
/// : always taken from the derived element
/// (derived placement, not the base prototype's geometry).
///
/// -
/// : base entries are the default; derived
/// entries override (or add) per state name key.
///
/// -
/// : come from the derived element's own tree only.
///
///
///
///
public static ElementInfo Merge(ElementInfo base_, ElementInfo derived)
{
var m = new ElementInfo
{
Id = derived.Id != 0 ? derived.Id : base_.Id,
// Type: derived wins if non-zero; Type 0 (text element per format §8) inherits the base's Type.
// For a text element whose base prototype is Type 12 (style prototype), this yields Type 12 —
// which DatWidgetFactory skips (returns null). That is intentional for Plan 1: vitals text
// numbers render via UiMeter.Label bound by VitalsController, not a dat text node.
// A Plan-2 standalone text element would need a type-preserving path (e.g. float? nullable
// Width/Height, or explicit handling of Type 0 before the merge).
Type = derived.Type != 0 ? derived.Type : base_.Type,
X = derived.X,
Y = derived.Y,
// NOTE: 0 is the "not set, inherit from base" sentinel for Width/Height. This
// diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is
// indistinguishable for Plan 1 (all base elements are zero-size Type-12 prototypes).
// If a real zero-size derived element ever needs to override a non-zero base in
// Plan 2, switch Width/Height to float? + null-coalescing (and update Tasks 3-5).
Width = derived.Width != 0 ? derived.Width : base_.Width,
Height = derived.Height != 0 ? derived.Height : base_.Height,
Left = derived.Left,
Top = derived.Top,
Right = derived.Right,
Bottom = derived.Bottom,
ReadOrder = derived.ReadOrder,
FontDid = derived.FontDid != 0 ? derived.FontDid : base_.FontDid,
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
// Children come from the derived element's own tree, not the base prototype's.
// Defensive copy: prevent a later mutation of either the merged result or the input
// from corrupting the other. Safe for the Task-5 flow (derived.Children is fully
// populated by the recursive importer BEFORE Merge is called and never mutated after).
Children = new List(derived.Children),
};
// Start with base StateMedia as defaults, then let derived entries override.
m.StateMedia = new Dictionary(base_.StateMedia);
foreach (var kv in derived.StateMedia)
m.StateMedia[kv.Key] = kv.Value;
return m;
}
}