diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 6a3cdd1e..aa6264c9 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -191,6 +191,14 @@ public static class LayoutImporter // ── Inheritance resolution ──────────────────────────────────────────────── + /// True when a pure-container leaf should inherit its base's subtree (the + /// gmInventoryUI sub-window mount): the derived element has no own children, no own + /// state media, and the base resolved to ≥1 child. Inert for media-bearing inheritors + /// (close button / title), elements with their own children, and childless style + /// prototypes (vitals/chat/toolbar text — the base prototype has no children). + internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount) + => derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0; + /// /// Converts an to a resolved : /// reads own fields + media, applies the BaseElement / BaseLayoutId chain @@ -204,6 +212,7 @@ public static class LayoutImporter // Read this element's own fields + media (no inheritance, no children yet). var self = ToInfo(d); var result = self; + List? baseChildren = null; // Apply BaseElement / BaseLayoutId inheritance if present. if (d.BaseElement != 0 && d.BaseLayoutId != 0 @@ -215,9 +224,9 @@ public static class LayoutImporter { // Recurse the base chain (already guarded by the HashSet add above). var baseInfo = Resolve(dats, baseDesc, baseChain); - // Derived fields override the base; result.Children is still empty here - // — children are attached below from the DERIVED element's own tree. + // Derived fields override the base; children are attached below. result = ElementReader.Merge(baseInfo, self); + baseChildren = baseInfo.Children; // capture for the sub-window mount } } @@ -226,6 +235,15 @@ public static class LayoutImporter foreach (var kv in d.Children) result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>())); + // 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 + // (0x100001CD/CE/CF — Type-0, media-less, childless, BaseLayoutId → a gm*UI window); + // inert for media-bearing inheritors (close button/title) and childless style prototypes + // (vitals/chat/toolbar). See ShouldMountBaseChildren + the B-Grid spec. + if (baseChildren is not null + && ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren.Count)) + result.Children.AddRange(baseChildren); + return result; } diff --git a/tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs b/tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs new file mode 100644 index 00000000..eca936e9 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/LayoutImporterMountTests.cs @@ -0,0 +1,22 @@ +using AcDream.App.UI.Layout; + +namespace AcDream.App.Tests.UI; + +public class LayoutImporterMountTests +{ + [Fact] + public void Mounts_ChildlessMediaLessInheritor_WithContentfulBase() + => Assert.True(LayoutImporter.ShouldMountBaseChildren(derivedChildCount: 0, derivedMediaCount: 0, baseChildCount: 5)); + + [Fact] + public void DoesNotMount_WhenDerivedHasOwnMedia() // close button / title + => Assert.False(LayoutImporter.ShouldMountBaseChildren(0, 1, 5)); + + [Fact] + public void DoesNotMount_WhenDerivedHasOwnChildren() + => Assert.False(LayoutImporter.ShouldMountBaseChildren(2, 0, 5)); + + [Fact] + public void DoesNotMount_WhenBaseIsChildless() // vitals/chat/toolbar style prototypes + => Assert.False(LayoutImporter.ShouldMountBaseChildren(0, 0, 0)); +}