using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.UI.Layout;
///
/// Horizontal text justification read from dat property 0x14 (UIElement HorizontalJustification).
/// Values match the retail enum: 0=Left, 1=Center, 3/5=Right.
///
public enum HJustify : byte { Left = 0, Center = 1, Right = 2 }
///
/// Vertical text justification read from dat property 0x15 (UIElement VerticalJustification).
/// Values: 2=Top, 4=Bottom; absent/other = Center.
///
public enum VJustify : byte { Top = 0, Center = 1, Bottom = 2 }
///
/// 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;
/// Layer level from the dat (ElementDesc.ZLevel). Higher = drawn further
/// BACK (a full-window backdrop at ZLevel 100 sits behind ZLevel-0 panels). The factory
/// folds it into so ZLevel dominates and ReadOrder is the
/// within-layer tiebreaker. Issue #145 (vitals are all ZLevel 0, so they're unaffected).
public uint ZLevel;
///
/// Font dat object id inherited from the base element's Properties[0x1A]
/// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited.
///
public uint FontDid;
///
/// Horizontal text justification from dat Properties[0x14]
/// (EnumBaseProperty: 0=Left, 1=Center, 3/5=Right).
/// Default is to preserve existing behavior where
/// controllers set Centered=true and no property was read.
///
public HJustify HJustify = HJustify.Center;
///
/// Vertical text justification from dat Properties[0x15]
/// (EnumBaseProperty: 2=Top, 4=Bottom; absent/other = Center).
/// Default is to preserve existing behavior.
///
public VJustify VJustify = VJustify.Center;
///
/// Font color from dat Properties[0x1B] (ColorBaseProperty, ARGB bytes).
/// Null when the dat carries no color for this element; the factory then leaves the
/// widget at its default white ().
/// Propagated in with the same "non-null derived wins"
/// rule used for and .
///
public Vector4? FontColor;
///
/// 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,
ZLevel = derived.ZLevel != 0 ? derived.ZLevel : base_.ZLevel,
FontDid = derived.FontDid != 0 ? derived.FontDid : base_.FontDid,
// HJustify/VJustify: derived wins when it carries an explicit non-Center value
// (the dat property was present and read); otherwise inherit the base prototype's value.
// Center is the default (= "not set by this element") so Center-derived never overrides
// a non-Center base — matching the FontDid "non-zero wins" convention.
HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify,
VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify,
// FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base.
// Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base.
FontColor = derived.FontColor ?? base_.FontColor,
// 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;
}
}