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));
+}