Settings Screen¶
centred panel with slider, checkbox, and dropdown controls.
▶ Run in browserTags: ui settings game-ui
What it demonstrates¶
A centred dialog panel via
AnchorPreset.CENTERwith symmetric margins.A
Slider(volume),CheckBox(toggle), andDropDown(quality preset).A
FormLayoutto align labels with their controls.Apply / Back buttons that read the live control state into a status line.
Run: uv run python examples/features/ui/settings.py Headless self-check: uv run python examples/features/ui/settings.py –test
Source¶
1"""Settings Screen: centred panel with slider, checkbox, and dropdown controls.
2
3# /// simvx
4# tags = ["ui", "settings", "game-ui"]
5# web = { root = "SettingsDemo", width = 800, height = 600, responsive = true }
6# ///
7
8## What it demonstrates
9- A centred dialog panel via `AnchorPreset.CENTER` with symmetric margins.
10- A `Slider` (volume), `CheckBox` (toggle), and `DropDown` (quality preset).
11- A `FormLayout` to align labels with their controls.
12- Apply / Back buttons that read the live control state into a status line.
13
14Run: uv run python examples/features/ui/settings.py
15Headless self-check: uv run python examples/features/ui/settings.py --test
16"""
17
18from simvx.core import AnchorPreset, Colour, Label, Node, Panel, Vec2
19from simvx.core.ui import Button, CheckBox, DropDown, FormLayout, HBoxContainer, Slider, VBoxContainer
20from simvx.graphics import App
21
22QUALITY_PRESETS = ["Low", "Medium", "High", "Ultra"]
23
24
25class SettingsDemo(Node):
26 """Root node: a centred settings panel with a few controls."""
27
28 def on_ready(self):
29 # Centred panel: anchored to screen centre with symmetric margins so it
30 # stays centred at any window size.
31 panel = Panel(name="SettingsPanel")
32 panel.set_anchor_preset(AnchorPreset.CENTER)
33 panel.margin_left = -250
34 panel.margin_right = 250
35 panel.margin_top = -145
36 panel.margin_bottom = 145
37 panel.bg_colour = Colour.hex("#1A1A2E")
38 self._panel = self.add_child(panel)
39
40 # Vertical stack filling the panel with a small inset. FULL_RECT
41 # stretches both axes, so all four margins are positive insets.
42 column = VBoxContainer(name="Column")
43 column.separation = 14.0
44 column.set_anchor_preset(AnchorPreset.FULL_RECT)
45 column.margin_left = 24
46 column.margin_right = 24
47 column.margin_top = 20
48 column.margin_bottom = 20
49 panel.add_child(column)
50
51 title = Label("Settings")
52 title.font_size = 22.0
53 title.alignment = "center"
54 column.add_child(title)
55
56 # Form aligns "Label:" against each control.
57 form = FormLayout(name="SettingsForm")
58 form.separation = 12.0
59
60 # Volume slider.
61 self._volume = Slider(0, 100, 70)
62 self._volume.size = Vec2(240, 24)
63 self._volume.value_changed.connect(self._on_volume)
64 form.add_field("Volume:", self._volume)
65
66 # Fullscreen toggle.
67 self._fullscreen = CheckBox("Enabled", checked=True)
68 self._fullscreen.toggled.connect(self._on_fullscreen)
69 form.add_field("Fullscreen:", self._fullscreen)
70
71 # Quality preset dropdown.
72 self._quality = DropDown(items=QUALITY_PRESETS, selected_index=2)
73 self._quality.item_selected.connect(self._on_quality)
74 form.add_field("Quality:", self._quality)
75
76 column.add_child(form)
77
78 # Live status line echoing the current values.
79 self._status = Label("")
80 self._status.font_size = 13.0
81 self._status.text_colour = Colour.LIGHT_GRAY
82 column.add_child(self._status)
83
84 # Apply / Back buttons in a bottom row.
85 buttons = HBoxContainer(name="Buttons")
86 buttons.separation = 12.0
87 self._apply = Button("Apply", on_press=self._on_apply)
88 self._apply.size = Vec2(120, 34)
89 buttons.add_child(self._apply)
90 self._back = Button("Back", on_press=self._on_back)
91 self._back.size = Vec2(120, 34)
92 buttons.add_child(self._back)
93 column.add_child(buttons)
94
95 self._refresh_status()
96
97 def _on_volume(self, value):
98 self._refresh_status()
99
100 def _on_fullscreen(self, checked):
101 self._refresh_status()
102
103 def _on_quality(self, index):
104 self._refresh_status()
105
106 def _summary(self) -> str:
107 """Read the live control state into one line of text."""
108 fs = "on" if self._fullscreen.checked else "off"
109 return f"Volume {int(self._volume.value)} | Fullscreen {fs} | Quality {self._quality.selected_text}"
110
111 def _refresh_status(self):
112 self._status.text = self._summary()
113
114 def _on_apply(self):
115 self._status.text = f"Applied: {self._summary()}"
116
117 def _on_back(self):
118 self._status.text = "Back pressed (no changes saved)"
119
120
121def _selftest() -> bool:
122 """Headless: drive each control by pointer and read the status line back.
123
124 The status line is the demo's one output and is rebuilt from the live control
125 state, so driving the slider, the checkbox and the dropdown by pointer and then
126 reading that one string checks the whole chain from click to signal to label.
127 Nothing is called directly: even the dropdown's preset is chosen by clicking the
128 row where the open list draws it.
129 """
130 from simvx.core.ui.testing import UITestHarness
131
132 harness = UITestHarness(SettingsDemo(name="SettingsDemo"), screen_size=(800, 600))
133 scene = harness.tree.root
134 ok = True
135
136 def check(label: str, passed: bool, detail: str) -> None:
137 nonlocal ok
138 ok = ok and passed
139 print(f"{'ok ' if passed else 'FAIL'} {label}: {detail}")
140
141 harness.tick()
142
143 # CENTER with symmetric margins: a 500x290 panel in the middle of the screen.
144 px, py, pw, ph = (round(v) for v in scene._panel.get_global_rect())
145 check(
146 "the panel is centred at the screen's own size",
147 (pw, ph) == (500, 290) and (px + pw / 2, py + ph / 2) == (400, 300),
148 f"{pw}x{ph} centred on ({px + pw / 2:.0f}, {py + ph / 2:.0f})",
149 )
150
151 check(
152 "the status line starts from the controls' own values",
153 scene._status.text == scene._summary(),
154 scene._status.text,
155 )
156
157 # The slider is driven by pointer position on its own track, so the check is
158 # that the value tracks WHERE the click lands rather than that some handler ran.
159 sx, sy, sw, sh = scene._volume.get_global_rect()
160 landed = []
161 for fraction in (0.25, 0.75):
162 harness.click((sx + sw * fraction, sy + sh / 2))
163 harness.tick()
164 landed.append(float(scene._volume.value))
165 span = scene._volume.max_value - scene._volume.min_value
166 check(
167 "the slider's value tracks where the pointer lands on its track",
168 all(abs(v - f * span) <= 2.0 for v, f in zip(landed, (0.25, 0.75), strict=True)),
169 f"clicks at 25% and 75% gave {landed[0]:.0f} and {landed[1]:.0f} of {span:.0f}",
170 )
171 check(
172 "and the status line followed the signal", f"Volume {landed[-1]:.0f}" in scene._status.text, scene._status.text
173 )
174
175 harness.click(scene._fullscreen)
176 harness.tick()
177 check(
178 "clicking the checkbox toggles it",
179 not scene._fullscreen.checked and "Fullscreen off" in scene._status.text,
180 scene._status.text,
181 )
182
183 # The dropdown's list only exists while it is open, so a preset takes two
184 # clicks: one on the button, one on the row. Rows are the button's own height
185 # and stack directly under it, so the row centre is arithmetic on the button's
186 # rect -- and ``is_point_inside`` covers that rect only while the list is open,
187 # which is how the two clicks are told apart.
188 quality = scene._quality
189 dx, dy, dw, dh = quality.get_global_rect()
190 target = (quality.selected_index + 1) % len(quality.items)
191 row_centre = Vec2(dx + dw / 2, dy + dh * (target + 1.5))
192 check(
193 "the dropdown claims no screen below itself while closed",
194 not quality.is_point_inside(row_centre),
195 f"{row_centre.y - dy:.0f}px below the button is outside it",
196 )
197
198 harness.click(quality)
199 harness.tick()
200 opened = quality.is_point_inside(row_centre)
201 harness.click(row_centre)
202 harness.tick()
203 check(
204 "clicking the dropdown opens its list, and clicking a row selects that preset",
205 opened and not quality.is_point_inside(row_centre) and quality.selected_index == target,
206 f"opened, picked row {target} = {quality.selected_text!r}, then closed",
207 )
208 check(
209 "and the status line followed the dropdown too",
210 f"Quality {quality.selected_text}" in scene._status.text,
211 scene._status.text,
212 )
213
214 harness.click(scene._apply)
215 harness.tick()
216 check("Apply echoes the live state", scene._status.text == f"Applied: {scene._summary()}", scene._status.text)
217
218 harness.click(scene._back)
219 harness.tick()
220 check(
221 "Back reports that nothing was saved",
222 scene._status.text == "Back pressed (no changes saved)",
223 scene._status.text,
224 )
225
226 harness.teardown()
227 print("SELFTEST:", "PASS" if ok else "FAIL")
228 return ok
229
230
231if __name__ == "__main__":
232 import sys
233
234 if "--test" in sys.argv:
235 sys.exit(0 if _selftest() else 1)
236 App(title="SimVX Settings", width=800, height=600).run(SettingsDemo())