diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs
index 35973c09..a3c95895 100644
--- a/src/AcDream.App/UI/UiItemSlot.cs
+++ b/src/AcDream.App/UI/UiItemSlot.cs
@@ -38,6 +38,23 @@ public sealed class UiItemSlot : UiElement
/// state id 0x10000040).
public uint DragRejectSprite { get; set; } = 0x060011F8u;
+ /// True when this cell is the OPEN container (its contents fill the grid). Draws the
+ /// open-container triangle. Port of UIElement_ItemList::UpdateOpenContainerIndicator
+ /// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId.
+ public bool IsOpenContainer { get; set; }
+ /// Open-container triangle sprite (element 0x10000450 on the container prototype
+ /// 0x1000033F). Configurable; guard id != 0 before resolving.
+ public uint OpenContainerSprite { get; set; } = 0x06005D9Cu;
+
+ /// True when this cell is the SELECTED item. Draws the green/yellow selection square.
+ /// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState
+ /// (0x004e1240): shown when item.itemID == selectedItemId. Uniform across item + container cells.
+ public bool Selected { get; set; }
+ /// Selected-item square sprite (element 0x10000342 on the 32×32 item prototype
+ /// 0x10000341; pixel-confirmed green/yellow frame). Drawn as a procedural overlay so it renders
+ /// on the 36×36 container cell too (whose prototype lacks the square child).
+ public uint SelectedSprite { get; set; } = 0x06004D21u;
+
/// Accept/reject overlay state while a drag hovers this cell.
public enum DragAcceptState { None, Accept, Reject }
private DragAcceptState _dragAccept = DragAcceptState.None;
@@ -220,6 +237,23 @@ public sealed class UiItemSlot : UiElement
}
}
+ // Open-container triangle (retail SetOpenContainerState 0x004e1200) + selected-item square
+ // (retail SetSelectedState 0x004e1240). Drawn procedurally like the digit/drag overlays;
+ // guard id != 0 BEFORE resolving (feedback_ui_resolve_zero_magenta). Triangle under the
+ // square; both under the transient drag overlay below.
+ if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0)
+ {
+ var (tex, _, _) = SpriteResolve(OpenContainerSprite);
+ if (tex != 0)
+ ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
+ }
+ if (Selected && SpriteResolve is not null && SelectedSprite != 0)
+ {
+ var (tex, _, _) = SpriteResolve(SelectedSprite);
+ if (tex != 0)
+ ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
+ }
+
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).
diff --git a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
index f99e9dc5..c0e91753 100644
--- a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
+++ b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
@@ -141,4 +141,20 @@ public class UiItemSlotTests
s.SetShortcutNum(0, peace: true);
Assert.Null(s.ActiveDigitArray());
}
+
+ // ── Container-switching indicator overlays (decomp SetOpenContainerState 0x004e1200 /
+ // SetSelectedState 0x004e1240) ───────────────────────────────────────────────────────
+ [Fact]
+ public void Selected_defaultsFalse() => Assert.False(new UiItemSlot().Selected);
+
+ [Fact]
+ public void IsOpenContainer_defaultsFalse() => Assert.False(new UiItemSlot().IsOpenContainer);
+
+ [Fact]
+ public void SelectedSprite_default_isGreenYellowSquare()
+ => Assert.Equal(0x06004D21u, new UiItemSlot().SelectedSprite);
+
+ [Fact]
+ public void OpenContainerSprite_default_isTriangle()
+ => Assert.Equal(0x06005D9Cu, new UiItemSlot().OpenContainerSprite);
}