feat(ui): port retained widget foundations
This commit is contained in:
parent
44f9ec13d9
commit
d825572e31
44 changed files with 84813 additions and 292 deletions
|
|
@ -203,9 +203,19 @@ public static class LayoutImporter
|
|||
tops.Add(Resolve(dats, d, new HashSet<(uint, uint)>()));
|
||||
}
|
||||
|
||||
return tops.Count == 1
|
||||
? tops[0]
|
||||
: new ElementInfo { Id = 0, Type = 3, Children = tops };
|
||||
if (tops.Count == 1)
|
||||
return tops[0];
|
||||
|
||||
foreach (var top in tops)
|
||||
SetOriginalParentSize(top, ld.Width, ld.Height);
|
||||
return new ElementInfo
|
||||
{
|
||||
Id = 0,
|
||||
Type = 3,
|
||||
Width = ld.Width,
|
||||
Height = ld.Height,
|
||||
Children = tops,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -290,7 +300,11 @@ public static class LayoutImporter
|
|||
// Resolve + attach children. Each child gets a FRESH base-chain set:
|
||||
// the cycle guard is per-element, not shared across siblings.
|
||||
foreach (var kv in d.Children)
|
||||
result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
|
||||
{
|
||||
var child = Resolve(dats, kv.Value, new HashSet<(uint, uint)>());
|
||||
SetOriginalParentSize(child, result.Width, result.Height);
|
||||
result.Children.Add(child);
|
||||
}
|
||||
|
||||
// Sub-window mount: a pure-container leaf (no own children, no own media) that inherits
|
||||
// from a base WITH content attaches the base's subtree. Targets the gmInventoryUI panels
|
||||
|
|
@ -313,42 +327,9 @@ public static class LayoutImporter
|
|||
// FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.)
|
||||
result.ZLevel = self.ZLevel;
|
||||
|
||||
// Sub-layout slot sizing: when a sub-layout is mounted into a slot that is TALLER
|
||||
// (or wider) than the sub-layout's own design height, the cascade of Bottom/Right
|
||||
// anchored elements must be resized to fill the slot. Without this step, the
|
||||
// background element (0x10000226 for the Attributes tab, designed at 337px) captures
|
||||
// _amB = slotH - designH = 238 and permanently stays at 337px, and every element
|
||||
// within it with Bottom anchor stays at its design size too (list box at 160px
|
||||
// instead of the correct 398px).
|
||||
//
|
||||
// Retail reference: UIElement::UpdateForParentSizeChange (C++ runtime) propagates a
|
||||
// new parent height down the whole tree, updating each Bottom-anchored child's
|
||||
// rect by maintaining its bottom margin. We replicate that cascade here at import
|
||||
// time on the base-children subtree so the anchor-capture during the first render
|
||||
// frame sees zero (or unchanged) margins — not the spurious "slot bigger than design"
|
||||
// margin.
|
||||
//
|
||||
// Safe guard: only fire when the slot has explicit size AND the base children are
|
||||
// smaller (i.e., this is the "slot bigger than design" case).
|
||||
// Inventory/paperdoll slots are unaffected because their slot H already matches the
|
||||
// sub-layout design H, so no cascade occurs.
|
||||
if (result.Width > 0 && result.Height > 0)
|
||||
{
|
||||
foreach (var child in baseChildren)
|
||||
{
|
||||
var childAnchors = ElementReader.ToAnchors(child.Left, child.Top, child.Right, child.Bottom);
|
||||
const AnchorEdges StretchAll = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
||||
if (child.X == 0 && child.Y == 0
|
||||
&& (childAnchors & StretchAll) == StretchAll // full-stretch background
|
||||
&& child.Width <= result.Width
|
||||
&& child.Height > 0 && child.Height < result.Height) // smaller than slot
|
||||
{
|
||||
// Cascade UpdateForParentSizeChange down the subtree: maintain each
|
||||
// Bottom-anchored element's bottom margin while growing the parent height.
|
||||
CascadeHeight(child, child.Height, result.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mounted descendants retain the base layout's design parent size. The runtime
|
||||
// UiLayoutPolicy therefore performs the retail parent-size update from the correct
|
||||
// baseline when this slot is larger than the mounted layout.
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -378,17 +359,19 @@ public static class LayoutImporter
|
|||
Bottom = d.BottomEdge,
|
||||
ReadOrder = d.ReadOrder,
|
||||
ZLevel = d.ZLevel,
|
||||
DefaultStateId = (uint)d.DefaultState,
|
||||
DefaultStateName = (defState is "Undef" or "Undefined" or "0") ? "" : defState,
|
||||
};
|
||||
|
||||
// DirectState (unnamed, key "").
|
||||
if (d.StateDesc is not null)
|
||||
ReadState(d.StateDesc, "", info);
|
||||
ReadState(d.StateDesc, UiStateInfo.DirectStateId, "", info);
|
||||
|
||||
// Named states (e.g. UIStateId.HideDetail → "HideDetail").
|
||||
foreach (var s in d.States)
|
||||
ReadState(s.Value, s.Key.ToString(), info);
|
||||
ReadState(s.Value, (uint)s.Key, s.Key.ToString(), info);
|
||||
|
||||
ElementReader.ApplyCanonicalLegacyProjection(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
|
|
@ -398,14 +381,23 @@ public static class LayoutImporter
|
|||
/// <c>info.StateCursors[name]</c>, and extract the font DID from property 0x1A
|
||||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>) if not yet set.
|
||||
/// </summary>
|
||||
private static void ReadState(StateDesc sd, string name, ElementInfo info)
|
||||
private static void ReadState(StateDesc sd, uint stateId, string name, ElementInfo info)
|
||||
{
|
||||
var state = new UiStateInfo
|
||||
{
|
||||
Id = stateId,
|
||||
Name = name,
|
||||
PassToChildren = sd.PassToChildren,
|
||||
IncorporationFlags = (uint)sd.IncorporationFlags,
|
||||
};
|
||||
|
||||
bool imageRead = false;
|
||||
foreach (var m in sd.Media)
|
||||
{
|
||||
if (!imageRead && m is MediaDescImage img && img.File != 0)
|
||||
{
|
||||
info.StateMedia[name] = (img.File, (int)img.DrawMode);
|
||||
state.Image = new UiImageMedia(img.File, (int)img.DrawMode);
|
||||
imageRead = true;
|
||||
}
|
||||
|
||||
|
|
@ -415,9 +407,17 @@ public static class LayoutImporter
|
|||
cursor.File,
|
||||
checked((int)cursor.XHotspot),
|
||||
checked((int)cursor.YHotspot));
|
||||
state.Cursor = info.StateCursors[name];
|
||||
}
|
||||
}
|
||||
|
||||
if (sd.Properties is not null)
|
||||
{
|
||||
foreach (var (propertyId, property) in sd.Properties)
|
||||
state.Properties.Values[propertyId] = ConvertProperty(property);
|
||||
}
|
||||
info.States[stateId] = state;
|
||||
|
||||
// Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }.
|
||||
// Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
|
||||
if (info.FontDid == 0 && sd.Properties is not null
|
||||
|
|
@ -476,6 +476,82 @@ public static class LayoutImporter
|
|||
}
|
||||
}
|
||||
|
||||
internal static UiPropertyValue ConvertProperty(BaseProperty property)
|
||||
{
|
||||
var value = new UiPropertyValue { MasterPropertyId = property.MasterPropertyId };
|
||||
switch (property)
|
||||
{
|
||||
case EnumBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Enum;
|
||||
value.UnsignedValue = p.Value;
|
||||
break;
|
||||
case BoolBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Bool;
|
||||
value.BoolValue = p.Value;
|
||||
break;
|
||||
case DataIdBaseProperty p:
|
||||
value.Kind = UiPropertyKind.DataId;
|
||||
value.UnsignedValue = p.Value;
|
||||
break;
|
||||
case FloatBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Float;
|
||||
value.FloatValue = p.Value;
|
||||
break;
|
||||
case IntegerBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Integer;
|
||||
value.IntegerValue = p.Value;
|
||||
break;
|
||||
case StringInfoBaseProperty p:
|
||||
value.Kind = UiPropertyKind.StringInfo;
|
||||
value.StringInfoValue = new UiStringInfoValue(
|
||||
p.Value.Token,
|
||||
p.Value.StringId,
|
||||
p.Value.TableId.DataId,
|
||||
(byte)p.Value.Override,
|
||||
p.Value.English,
|
||||
p.Value.Comment);
|
||||
break;
|
||||
case ColorBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Color;
|
||||
value.ColorValue = new UiColorValue(
|
||||
p.Value.Blue,
|
||||
p.Value.Green,
|
||||
p.Value.Red,
|
||||
p.Value.Alpha);
|
||||
break;
|
||||
case ArrayBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Array;
|
||||
foreach (var item in p.Value)
|
||||
value.ArrayValue.Add(ConvertProperty(item));
|
||||
break;
|
||||
case StructBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Struct;
|
||||
foreach (var (key, item) in p.Value)
|
||||
value.StructValue[key] = ConvertProperty(item);
|
||||
break;
|
||||
case VectorBaseProperty p:
|
||||
value.Kind = UiPropertyKind.Vector;
|
||||
value.VectorValue = p.Value;
|
||||
break;
|
||||
case Bitfield32BaseProperty p:
|
||||
value.Kind = UiPropertyKind.Bitfield32;
|
||||
value.UnsignedValue = p.Value;
|
||||
break;
|
||||
case Bitfield64BaseProperty p:
|
||||
value.Kind = UiPropertyKind.Bitfield64;
|
||||
value.UnsignedValue = p.Value;
|
||||
break;
|
||||
case InstanceIdBaseProperty p:
|
||||
value.Kind = UiPropertyKind.InstanceId;
|
||||
value.UnsignedValue = p.Value;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported UI base-property type {property.GetType().FullName}.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── Prototype detection helpers ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -534,59 +610,12 @@ public static class LayoutImporter
|
|||
return null;
|
||||
}
|
||||
|
||||
// ── Sub-layout slot sizing helpers ────────────────────────────────────────
|
||||
// ── Raw-edge layout provenance ────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Recursively propagates a parent-height change from <paramref name="oldParentH"/>
|
||||
/// to <paramref name="newParentH"/> through <paramref name="el"/> and all its
|
||||
/// descendants, replicating <c>UIElement::UpdateForParentSizeChange</c> from the
|
||||
/// retail runtime.
|
||||
///
|
||||
/// <para>Three anchor cases for the vertical axis:
|
||||
/// <list type="bullet">
|
||||
/// <item>Top+Bottom (stretch): maintain top margin AND bottom margin; height grows.</item>
|
||||
/// <item>Bottom only (pin-to-bottom, fixed height): maintain bottom margin; Y moves, H unchanged.</item>
|
||||
/// <item>Top only or None: Y and H unchanged (pinned to top with fixed height).</item>
|
||||
/// </list>
|
||||
/// Recurse with the element's new height so grandchildren see the correct parent size.</para>
|
||||
///
|
||||
/// <para>This is called once at import time on base-children that are mounted into a
|
||||
/// slot taller than the sub-layout's design height.</para>
|
||||
/// </summary>
|
||||
private static void CascadeHeight(ElementInfo el, float oldParentH, float newParentH)
|
||||
private static void SetOriginalParentSize(ElementInfo child, float width, float height)
|
||||
{
|
||||
float oldH = el.Height;
|
||||
float newH = el.Height;
|
||||
float oldY = el.Y;
|
||||
|
||||
var anchors = ElementReader.ToAnchors(el.Left, el.Top, el.Right, el.Bottom);
|
||||
|
||||
bool hasTop = (anchors & AnchorEdges.Top) != 0;
|
||||
bool hasBottom = (anchors & AnchorEdges.Bottom) != 0;
|
||||
|
||||
if (hasTop && hasBottom)
|
||||
{
|
||||
// Stretch: maintain both top and bottom margins.
|
||||
// amT = el.Y (pinned to top, unchanged)
|
||||
// amB = oldParentH - (el.Y + el.H)
|
||||
float amB = oldParentH - (el.Y + el.Height);
|
||||
newH = newParentH - el.Y - amB;
|
||||
if (newH < 0f) newH = 0f;
|
||||
el.Height = newH;
|
||||
}
|
||||
else if (hasBottom && !hasTop)
|
||||
{
|
||||
// Pin to bottom: maintain bottom margin, height fixed, Y moves.
|
||||
// amB = oldParentH - (el.Y + el.H)
|
||||
float amB = oldParentH - (el.Y + el.Height);
|
||||
float newY = newParentH - amB - el.Height;
|
||||
el.Y = newY;
|
||||
// Height unchanged; newH = oldH for recursion.
|
||||
}
|
||||
// else: Top-only or None — element is top-pinned with fixed height; nothing moves.
|
||||
|
||||
// Recurse into children with the element's new height as their parent.
|
||||
foreach (var child in el.Children)
|
||||
CascadeHeight(child, oldH, newH);
|
||||
child.OriginalParentWidth = width;
|
||||
child.OriginalParentHeight = height;
|
||||
child.HasOriginalParentSize = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue