Settings Screen

centred panel with slider, checkbox, and dropdown controls.

▶ Run in browser

Tags: ui settings game-ui

What it demonstrates

  • A centred dialog panel via AnchorPreset.CENTER with symmetric margins.

  • A Slider (volume), CheckBox (toggle), and DropDown (quality preset).

  • A FormLayout to 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())