diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
index 287971fe..2fb75977 100644
--- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
+++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
@@ -77,8 +77,11 @@ public static class DatWidgetFactory
e.Width = info.Width;
e.Height = info.Height;
- // Honor the dat's draw order so overlapping pieces (grip overlay over bevel chrome) layer correctly.
- e.ZOrder = (int)info.ReadOrder;
+ // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the
+ // gmInventoryUI full-window backdrop at ZLevel 100 sits behind the ZLevel-0 panels, #145);
+ // ReadOrder is the within-layer tiebreaker (higher = on top). K=10000 exceeds any window's
+ // element count so ZLevel always dominates. Vitals (all ZLevel 0) keep ZOrder == ReadOrder.
+ e.ZOrder = (int)info.ReadOrder - (int)info.ZLevel * 10000;
// Map the four raw edge-anchor values to the AnchorEdges bit-flag that the
// UI layout engine uses for reflow.
diff --git a/src/AcDream.App/UI/Layout/ElementReader.cs b/src/AcDream.App/UI/Layout/ElementReader.cs
index 93a4eb30..bbafcd05 100644
--- a/src/AcDream.App/UI/Layout/ElementReader.cs
+++ b/src/AcDream.App/UI/Layout/ElementReader.cs
@@ -39,6 +39,12 @@ public sealed class ElementInfo
/// 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.
@@ -152,6 +158,7 @@ public static class ElementReader
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,
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs
index aa6264c9..26c63465 100644
--- a/src/AcDream.App/UI/Layout/LayoutImporter.cs
+++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs
@@ -270,6 +270,7 @@ public static class LayoutImporter
Right = d.RightEdge,
Bottom = d.BottomEdge,
ReadOrder = d.ReadOrder,
+ ZLevel = d.ZLevel,
DefaultStateName = (defState is "Undef" or "Undefined" or "0") ? "" : defState,
};
diff --git a/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs b/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs
new file mode 100644
index 00000000..52ba5332
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs
@@ -0,0 +1,43 @@
+using AcDream.App.UI.Layout;
+
+namespace AcDream.App.Tests.UI;
+
+public class DatWidgetFactoryZOrderTests
+{
+ private static (uint, int, int) NoChrome(uint id) => (0u, 0, 0);
+
+ [Fact]
+ public void ZOrder_HigherZLevel_DrawsBehind()
+ {
+ // The gmInventoryUI backdrop (ZLevel 100, ReadOrder 4) must draw BEHIND the panels
+ // (ZLevel 0, ReadOrder 1): lower ZOrder = drawn first = behind. Before the fix, ZOrder
+ // came from ReadOrder only, so the backdrop (4) painted over the panels (1).
+ var backdrop = new ElementInfo { Type = 3, ReadOrder = 4, ZLevel = 100 };
+ var panel = new ElementInfo { Type = 3, ReadOrder = 1, ZLevel = 0 };
+
+ var b = DatWidgetFactory.Create(backdrop, NoChrome, null);
+ var p = DatWidgetFactory.Create(panel, NoChrome, null);
+
+ Assert.True(b!.ZOrder < p!.ZOrder, $"backdrop ZOrder {b.ZOrder} should be < panel ZOrder {p.ZOrder}");
+ }
+
+ [Fact]
+ public void ZOrder_AllZeroZLevel_EqualsReadOrder()
+ {
+ // Regression guard: a vitals-style window (every element ZLevel 0) keeps ZOrder == ReadOrder.
+ var e = new ElementInfo { Type = 3, ReadOrder = 5, ZLevel = 0 };
+ var w = DatWidgetFactory.Create(e, NoChrome, null);
+ Assert.Equal(5, w!.ZOrder);
+ }
+
+ [Fact]
+ public void ZOrder_SameZLevel_OrderedByReadOrder()
+ {
+ // Within one ZLevel, ReadOrder is the tiebreaker (higher ReadOrder draws on top).
+ var lo = new ElementInfo { Type = 3, ReadOrder = 2, ZLevel = 2 };
+ var hi = new ElementInfo { Type = 3, ReadOrder = 7, ZLevel = 2 };
+ var l = DatWidgetFactory.Create(lo, NoChrome, null);
+ var h = DatWidgetFactory.Create(hi, NoChrome, null);
+ Assert.True(l!.ZOrder < h!.ZOrder);
+ }
+}