Grid Inventory¶
a 4x3 grid of selectable item slots.
▶ Run in browserTags: ui inventory game-ui
What it demonstrates¶
A
GridContainerlaying out item slots in a fixed-column grid.Each slot is a
Buttoncarrying an item name and colour swatch.Clicking a slot selects it and updates a detail label below the grid.
The grid is wrapped in a centred panel anchored with
AnchorPreset.CENTER.
Run: uv run python examples/features/ui/inventory.py Headless self-check: uv run python examples/features/ui/inventory.py –test
Source¶
1"""Grid Inventory: a 4x3 grid of selectable item slots.
2
3# /// simvx
4# tags = ["ui", "inventory", "game-ui"]
5# web = { root = "InventoryDemo", width = 800, height = 600, responsive = true }
6# ///
7
8## What it demonstrates
9- A `GridContainer` laying out item slots in a fixed-column grid.
10- Each slot is a `Button` carrying an item name and colour swatch.
11- Clicking a slot selects it and updates a detail label below the grid.
12- The grid is wrapped in a centred panel anchored with `AnchorPreset.CENTER`.
13
14Run: uv run python examples/features/ui/inventory.py
15Headless self-check: uv run python examples/features/ui/inventory.py --test
16"""
17
18from simvx.core import AnchorPreset, Colour, Label, Node, Panel, Vec2
19from simvx.core.ui import Button, GridContainer, VBoxContainer
20from simvx.graphics import App
21
22# (name, swatch colour). Empty slots have a None colour.
23ITEMS = [
24 ("Sword", "#C0392B"),
25 ("Shield", "#2980B9"),
26 ("Potion", "#27AE60"),
27 ("Key", "#F1C40F"),
28 ("Bow", "#8E44AD"),
29 ("Arrow", "#7F8C8D"),
30 ("Gem", "#16A085"),
31 ("Map", "#E67E22"),
32 ("Bread", "#D35400"),
33 (None, None),
34 ("Torch", "#E74C3C"),
35 (None, None),
36]
37COLUMNS = 4
38
39
40class InventoryDemo(Node):
41 """Root node: a centred grid inventory with click-to-select slots."""
42
43 def on_ready(self):
44 # Centred panel hosting the grid and detail line.
45 panel = Panel(name="InventoryPanel")
46 panel.set_anchor_preset(AnchorPreset.CENTER)
47 panel.margin_left = -240
48 panel.margin_right = 240
49 panel.margin_top = -200
50 panel.margin_bottom = 200
51 panel.bg_colour = Colour.hex("#1B1B26")
52 self.add_child(panel)
53
54 # FULL_RECT stretches both axes, so all four margins are positive insets.
55 column = VBoxContainer(name="Column")
56 column.separation = 16.0
57 column.set_anchor_preset(AnchorPreset.FULL_RECT)
58 column.margin_left = 24
59 column.margin_right = 24
60 column.margin_top = 20
61 column.margin_bottom = 20
62 panel.add_child(column)
63
64 title = Label("Inventory")
65 title.font_size = 22.0
66 title.alignment = "center"
67 column.add_child(title)
68
69 # The grid: GridContainer sets .position on its children, which is the
70 # sanctioned container layout policy.
71 grid = GridContainer(columns=COLUMNS, name="ItemGrid")
72 grid.separation = 8.0
73 for i, (name, swatch) in enumerate(ITEMS):
74 label = name or "(empty)"
75 slot = Button(label, on_press=self._make_select(i))
76 slot.name = f"Slot{i}"
77 slot.size = Vec2(96, 64)
78 if swatch is not None:
79 slot.bg_colour = Colour.hex(swatch)
80 grid.add_child(slot)
81 column.add_child(grid)
82
83 # Detail line: shows the selected item's name.
84 self._detail = Label("Select an item.")
85 self._detail.font_size = 15.0
86 self._detail.text_colour = Colour.LIGHT_GRAY
87 self._detail.alignment = "center"
88 column.add_child(self._detail)
89
90 def _make_select(self, index: int):
91 """Return a click handler bound to the slot at index."""
92
93 def handler():
94 name = ITEMS[index][0]
95 if name is None:
96 self._detail.text = f"Slot {index + 1} is empty."
97 else:
98 self._detail.text = f"Selected: {name} (slot {index + 1})"
99
100 return handler
101
102
103def _selftest() -> bool:
104 """Headless: click the slots the way a player does and read the panel back.
105
106 ``UITestHarness`` lays the real widgets out at the real screen size and sends
107 real clicks at each slot's own centre, so what is asserted is what a pointer
108 lands on rather than a handler called by name.
109 """
110 from simvx.core.ui.testing import UITestHarness
111
112 harness = UITestHarness(InventoryDemo(name="InventoryDemo"), screen_size=(800, 600))
113 scene = harness.tree.root
114 ok = True
115
116 def check(label: str, passed: bool, detail: str) -> None:
117 nonlocal ok
118 ok = ok and passed
119 print(f"{'ok ' if passed else 'FAIL'} {label}: {detail}")
120
121 harness.tick()
122 slots = [harness.find_by_name(f"Slot{i}") for i in range(len(ITEMS))]
123
124 # The GridContainer is what places the slots, so the layout is read back off
125 # the widgets: COLUMNS per row, and every row at one y.
126 rows: dict[float, list[float]] = {}
127 for slot in slots:
128 rows.setdefault(round(float(slot.position.y), 1), []).append(float(slot.position.x))
129 widths = sorted({len(xs) for xs in rows.values()})
130 check(
131 f"the grid lays {len(ITEMS)} slots out {COLUMNS} to a row",
132 len(rows) == len(ITEMS) // COLUMNS and widths == [COLUMNS],
133 f"{len(rows)} rows of {widths} slots",
134 )
135
136 # Every slot's label reaches the screen, so a mis-sized panel that clipped
137 # the grid would be caught here rather than looking fine in the node tree.
138 drawn = harness.draw_log.texts()
139 named = [name for name, _ in ITEMS if name]
140 check(
141 "every slot's label is drawn",
142 all(name in drawn for name in named) and "(empty)" in drawn,
143 f"{len(named)} named items plus the empty slots, {len(drawn)} strings drawn",
144 )
145
146 check("nothing is selected to start with", scene._detail.text == "Select an item.", scene._detail.text)
147
148 harness.click(slots[0])
149 harness.tick()
150 first = scene._detail.text
151 check("clicking a slot selects its item", first == "Selected: Sword (slot 1)", first)
152
153 harness.click(slots[9])
154 harness.tick()
155 empty = scene._detail.text
156 check("clicking an empty slot says so", empty == "Slot 10 is empty.", empty)
157
158 # A click on the panel between the slots must not select anything: the
159 # buttons own their own hit areas.
160 harness.click((20.0, 20.0))
161 harness.tick()
162 check("a click off the grid changes nothing", scene._detail.text == empty, scene._detail.text)
163
164 harness.teardown()
165 print("SELFTEST:", "PASS" if ok else "FAIL")
166 return ok
167
168
169if __name__ == "__main__":
170 import sys
171
172 if "--test" in sys.argv:
173 sys.exit(0 if _selftest() else 1)
174 App(title="SimVX Inventory", width=800, height=600).run(InventoryDemo())