Widget Showcase¶
a tour of the built-in UI widgets, one family per tab.
â–¶ Run in browserTags: ui
Everything in simvx.core.ui is a Control, and every control here is built
the same way: construct it with its settings as keywords, add it to a
container, and connect its signal. A TabContainer takes each child as a page
and titles its tab with the child’s name. A MenuBar sits above the tabs and
a status Label below them, where the widgets report what the player did.
Controls:
Button(on_press=, and adisabledone),CheckBox(toggled),SpinBox, aSliderdriving aProgressBarthroughvalue_changed, aDropDown(item_selected) and a single-lineTextEdit(text_changed), lined up by aFormLayoutwhoseadd_fieldreturns the control it adds.Text: a
MultiLineTextEditwith line numbers, aCodeTextEditwith Python highlighting, and aRichTextLabelreading ANSI colour codes, split by twoSplitContainers (vertical=Falsestacks the second pair).Layout:
GridContainer(columns=4),FormLayout, aSplitContainerwhose right pane is aScrollContainerstacking a column of buttons, and aMarginContainer. The wheel scrolls the list wherever the pointer rests on it, buttons included.Trees: a
TreeViewbuilt from nestedTreeItems, and aVirtualScrollContainerover 1000 rows:set_data_source(count, make_row)builds rows only for what is on screen, handingmake_rowa scrolled-off row to reuse.Menus: a second
MenuBar, aToolbarwhose Brush, Eraser and Fill share aButtonGroup(checking one unchecks the rest, andchecked_changedreports it), and aPopupMenuopened under its button withshow(x, y).Canvas: 2D nodes inside a UI page: an animated
Line2D, a spinningPolygon2D, a marker riding aPath2Dcurve on aPathFollow2D, a starMeshInstance2Dand aFastNoiseLitegrid drawn inon_draw.Advanced: a
GraphEditof connectedGraphNodes, aColourPickerand aTerminalEmulator.Theme:
set_theme(AppTheme.light())restyles every control at once; the buttons switch between the built-in themes live.
Hover any control with a tooltip= to see its hint.
Source¶
1# /// simvx
2# web = { width = 1280, height = 720 }
3# ///
4"""Widget Showcase: a tour of the built-in UI widgets, one family per tab.
5
6Everything in `simvx.core.ui` is a `Control`, and every control here is built
7the same way: construct it with its settings as keywords, add it to a
8container, and connect its signal. A `TabContainer` takes each child as a page
9and titles its tab with the child's `name`. A `MenuBar` sits above the tabs and
10a status `Label` below them, where the widgets report what the player did.
11
12- **Controls**: `Button` (`on_press=`, and a `disabled` one), `CheckBox`
13 (`toggled`), `SpinBox`, a `Slider` driving a `ProgressBar` through
14 `value_changed`, a `DropDown` (`item_selected`) and a single-line `TextEdit`
15 (`text_changed`), lined up by a `FormLayout` whose `add_field` returns the
16 control it adds.
17- **Text**: a `MultiLineTextEdit` with line numbers, a `CodeTextEdit` with
18 Python highlighting, and a `RichTextLabel` reading ANSI colour codes, split
19 by two `SplitContainer`s (`vertical=False` stacks the second pair).
20- **Layout**: `GridContainer(columns=4)`, `FormLayout`, a `SplitContainer`
21 whose right pane is a `ScrollContainer` stacking a column of buttons, and a
22 `MarginContainer`. The wheel scrolls the list wherever the pointer rests on
23 it, buttons included.
24- **Trees**: a `TreeView` built from nested `TreeItem`s, and a
25 `VirtualScrollContainer` over 1000 rows: `set_data_source(count, make_row)`
26 builds rows only for what is on screen, handing `make_row` a scrolled-off
27 row to reuse.
28- **Menus**: a second `MenuBar`, a `Toolbar` whose Brush, Eraser and Fill share
29 a `ButtonGroup` (checking one unchecks the rest, and `checked_changed`
30 reports it), and a `PopupMenu` opened under its button with `show(x, y)`.
31- **Canvas**: 2D nodes inside a UI page: an animated `Line2D`, a spinning
32 `Polygon2D`, a marker riding a `Path2D` curve on a `PathFollow2D`, a star
33 `MeshInstance2D` and a `FastNoiseLite` grid drawn in `on_draw`.
34- **Advanced**: a `GraphEdit` of connected `GraphNode`s, a `ColourPicker`
35 and a `TerminalEmulator`.
36- **Theme**: `set_theme(AppTheme.light())` restyles every control at once;
37 the buttons switch between the built-in themes live.
38
39Hover any control with a `tooltip=` to see its hint.
40"""
41
42import math
43from functools import partial
44
45from simvx.core import (
46 Colour,
47 FastNoiseLite,
48 Line2D,
49 Mesh2D,
50 MeshInstance2D,
51 Node,
52 Node2D,
53 NoiseType,
54 Path2D,
55 PathFollow2D,
56 Polygon2D,
57)
58from simvx.core.ui import (
59 Anchor,
60 AppTheme,
61 Button,
62 ButtonGroup,
63 CheckBox,
64 CodeTextEdit,
65 ColourPicker,
66 DropDown,
67 FormLayout,
68 GraphEdit,
69 GraphNode,
70 GridContainer,
71 HBoxContainer,
72 Label,
73 MarginContainer,
74 MenuBar,
75 MenuItem,
76 MultiLineTextEdit,
77 Panel,
78 PopupMenu,
79 ProgressBar,
80 RichTextLabel,
81 ScrollContainer,
82 SizingMode,
83 Slider,
84 SpinBox,
85 SplitContainer,
86 TabContainer,
87 TerminalEmulator,
88 TextEdit,
89 Toolbar,
90 TreeItem,
91 TreeView,
92 VBoxContainer,
93 VirtualScrollContainer,
94 set_theme,
95)
96from simvx.graphics import App
97
98THEMES = {
99 "Dark": AppTheme.dark,
100 "Abyss": AppTheme.abyss,
101 "Midnight": AppTheme.midnight,
102 "Light": AppTheme.light,
103 "Monokai": AppTheme.monokai,
104 "Solarised": AppTheme.solarised_dark,
105 "Nord": AppTheme.nord,
106}
107GRID_COLOURS = ["#E63946", "#457B9D", "#1D3557", "#F4A261", "#2A9D8F", "#E9C46A", "#264653", "#A8DADC"]
108CODE = 'def hello():\n """Greet the world."""\n print("Hello, SimVX!")\n\nfor i in range(10):\n hello()'
109ANSI = (
110 "\033[1;36mSimVX\033[0m \033[32mEngine\033[0m\n"
111 "\033[1;33mWarning:\033[0m This is \033[1;31mcolourful\033[0m text\n"
112 "\033[34mBlue\033[0m \033[35mMagenta\033[0m \033[36mCyan\033[0m"
113)
114
115
116def heading(text: str) -> Label:
117 return Label(text, font_size=13, stretch_ratio=0)
118
119
120def titled(title: str, widget) -> VBoxContainer:
121 """A heading over a widget that takes the rest of the column's height."""
122 column = VBoxContainer(separation=4)
123 column.sizing = SizingMode.EXPAND
124 column.add_child(heading(title))
125 column.add_child(widget)
126 return column
127
128
129class CanvasDemo(Node2D):
130 """2D nodes animated in on_update, plus a noise grid drawn in on_draw."""
131
132 dynamic = True # on_draw reads a plain float clock, so redraw every frame
133
134 def on_ready(self):
135 self.time = 0.0
136 self.wave = self.add_child(Line2D(position=(20, 60), colour=(0.0, 0.9, 0.9, 1.0), width=2.0))
137 hexagon = [(40 * math.cos(i * math.pi / 3), 40 * math.sin(i * math.pi / 3)) for i in range(6)]
138 self.hexagon = self.add_child(Polygon2D(position=(500, 120), polygon=hexagon, colour=(0.85, 0.2, 0.85, 0.8)))
139 self.star = self.add_child(
140 MeshInstance2D(mesh=star_mesh(5, 45, 20), modulate=(1.0, 0.6, 0.1, 0.9), position=(680, 120))
141 )
142
143 self.track = self.add_child(Path2D(position=(20, 200)))
144 self.track.curve.add_point((0, 0), handle_out=(80, -80))
145 self.track.curve.add_point((200, 0), handle_in=(-80, -80), handle_out=(80, 80))
146 self.track.curve.add_point((400, 0), handle_in=(-80, 80))
147 # The marker is the follower's child, so it rides the curve with no code of its own.
148 self.follower = self.track.add_child(PathFollow2D(loop=True))
149 square = [(-6, -6), (6, -6), (6, 6), (-6, 6)]
150 self.follower.add_child(Polygon2D(polygon=square, colour=(1.0, 0.9, 0.2, 1.0)))
151
152 self.noise = FastNoiseLite(seed=42, noise_type=NoiseType.SIMPLEX, frequency=0.06)
153
154 def on_update(self, dt):
155 self.time += dt
156 self.wave.points = [(i * 5.0, 40.0 * math.sin(self.time * 2.0 + i * 0.15)) for i in range(80)]
157 self.hexagon.rotation = self.time * 0.8
158 self.star.rotation = -self.time * 0.5
159 self.follower.progress += dt * 80.0
160
161 def on_draw(self, renderer):
162 t, cell = self.time * 0.5, 14
163 for row in range(8):
164 for col in range(16):
165 b = (self.noise.get_noise_2d(col + t, row + t * 0.7) + 1.0) * 0.5
166 renderer.rect((20 + col * cell, 300 + row * cell), (cell - 1, cell - 1), fill=(0.3 * b, 0.7 * b, b, 1))
167 origin = self.track.position
168 curve = [origin + p for p in self.track.curve.get_baked_points()]
169 renderer.line(*curve, stroke=(1.0, 0.8, 0.0, 0.6), width=2.0)
170
171
172def star_mesh(points: int, outer: float, inner: float) -> Mesh2D:
173 """A star as a triangle fan around its centre; a fan from an outline vertex would fill the notches."""
174 vertices = [(0.0, 0.0)]
175 for i in range(points * 2):
176 angle, radius = math.pi / 2 + i * math.pi / points, outer if i % 2 == 0 else inner
177 vertices.append((radius * math.cos(angle), radius * math.sin(angle)))
178 indices = [index for i in range(1, points * 2 + 1) for index in (0, i, i % (points * 2) + 1)]
179 return Mesh2D(vertices=vertices, indices=indices)
180
181
182class WidgetShowcase(Node):
183 def on_ready(self):
184 panel = self.add_child(Panel(anchor=Anchor.FULL_RECT))
185 title = Label("SimVX UI Showcase", font_size=20, alignment="centre", anchor=Anchor.TOP_WIDE, size=(0, 36))
186 panel.add_child(title)
187 menubar = panel.add_child(MenuBar(anchor=Anchor.TOP_WIDE, inset=(0, 36), size=(0, 28)))
188 menubar.add_menu("File", self.menu_items("File", "New", "Open", None, "Exit"))
189 menubar.add_menu("Edit", self.menu_items("Edit", "Undo", "Redo"))
190 menubar.add_menu("View", self.menu_items("View", "Fullscreen"))
191
192 self.tabs = panel.add_child(TabContainer(anchor=Anchor.FULL_RECT, inset=(10, 68, 10, 32)))
193 self.status = Label("Ready.", font_size=12, anchor=Anchor.BOTTOM_WIDE, inset=(10, 4), size=(0, 24))
194 panel.add_child(self.status)
195 for page in (
196 self.controls_page(),
197 self.text_page(),
198 self.layout_page(),
199 self.trees_page(),
200 self.menus_page(),
201 self.canvas_page(),
202 self.advanced_page(),
203 self.theme_page(),
204 ):
205 self.tabs.add_child(page)
206
207 def say(self, text: str):
208 self.status.text = text
209
210 def menu_items(self, menu: str, *labels: str | None) -> list[MenuItem]:
211 """One MenuItem per label, each reporting itself; None is a separator."""
212 return [
213 MenuItem(label, callback=partial(self.say, f"{menu} > {label}")) if label else MenuItem(separator=True)
214 for label in labels
215 ]
216
217 def controls_page(self) -> FormLayout:
218 form = FormLayout(name="Controls", separation=8)
219 buttons = form.add_field("Button:", HBoxContainer(separation=8))
220 self.button = buttons.add_child(Button("Click Me", on_press=self.count_click, tooltip="Counts your clicks"))
221 buttons.add_child(Button("Disabled", tooltip="A disabled button ignores the pointer")).disabled = True
222 self.clicks = 0
223
224 self.checkbox = form.add_field("CheckBox:", CheckBox("Enable feature", tooltip="Toggle a feature"))
225 self.checkbox.toggled.connect(lambda on: self.say(f"CheckBox: {'ON' if on else 'OFF'}"))
226 spin = form.add_field("SpinBox:", SpinBox(min_value=0, max_value=100, value=42, step=1))
227 spin.value_changed.connect(lambda v: self.say(f"SpinBox: {v:.0f}"))
228
229 column = form.add_field("Slider:", VBoxContainer(separation=4))
230 self.slider = column.add_child(Slider(min_value=0, max_value=100, value=50, tooltip="Drag to change"))
231 self.progress = column.add_child(ProgressBar(min_value=0, max_value=100, value=50))
232 self.slider.value_changed.connect(self.show_slider)
233
234 self.dropdown = form.add_field(
235 "DropDown:", DropDown(items=["Low", "Medium", "High", "Ultra"], selected_index=1)
236 )
237 self.dropdown.item_selected.connect(lambda _: self.say(f"DropDown: {self.dropdown.selected_text}"))
238 self.text_edit = form.add_field("TextEdit:", TextEdit(placeholder="Type here..."))
239 self.text_edit.text_changed.connect(lambda text: self.say(f"TextEdit: {text}"))
240 return form
241
242 def count_click(self):
243 self.clicks += 1
244 self.button.text = f"Clicked {self.clicks}x"
245 self.say(f"Button clicked {self.clicks} time(s)")
246
247 def show_slider(self, value: float):
248 self.progress.value = value
249 self.say(f"Slider: {value:.0f}")
250
251 def text_page(self) -> SplitContainer:
252 notes = (
253 "SimVX Engine\n\nA node-based game engine\nin pure Python.\n\nFeatures:\n- Node hierarchy\n- Signals\n- UI"
254 )
255 page = SplitContainer(name="Text")
256 page.add_child(MultiLineTextEdit(text=notes, show_line_numbers=True, tooltip="Multi-line text editor"))
257 right = page.add_child(SplitContainer(vertical=False))
258 self.code = right.add_child(CodeTextEdit(text=CODE, tooltip="Python editor with syntax highlighting"))
259 right.add_child(RichTextLabel(text=ANSI, tooltip="Rich text from ANSI colour codes"))
260 return page
261
262 def layout_page(self) -> VBoxContainer:
263 page = VBoxContainer(name="Layout", separation=10)
264 page.add_child(heading("GridContainer (4 columns)"))
265 grid = page.add_child(GridContainer(columns=4, separation=4))
266 for hex_code in GRID_COLOURS:
267 cell = grid.add_child(Panel(size=(100, 40), tooltip=hex_code))
268 cell.bg_colour = Colour.hex(hex_code)
269
270 page.add_child(heading("FormLayout"))
271 form = page.add_child(FormLayout(separation=6))
272 form.add_field("Name:", TextEdit(text="Player1"))
273 form.add_field("Speed:", SpinBox(min_value=0, max_value=500, value=120, step=10))
274 form.add_field("Status:", CheckBox("Active", checked=True))
275
276 page.add_child(heading("SplitContainer + ScrollContainer"))
277 split = page.add_child(SplitContainer(split_ratio=0.2, size=(0, 140)))
278 split.add_child(Panel()).add_child(Label("Left pane", anchor=Anchor.TOP_LEFT, inset=10))
279 self.scroll = split.add_child(ScrollContainer(separation=2)) # stacks its children
280 for i in range(20):
281 self.scroll.add_child(Button(f"Scrollable item {i + 1}"))
282
283 margin = page.add_child(MarginContainer(margin=12, size=(0, 50)))
284 margin.add_child(Panel()).add_child(Label("Inside MarginContainer", anchor=Anchor.TOP_LEFT, inset=(8, 2)))
285 return page
286
287 def trees_page(self) -> SplitContainer:
288 root = TreeItem(
289 "Scene",
290 [
291 TreeItem("Player", [TreeItem("Sprite"), TreeItem("Collider")]),
292 TreeItem("Enemies", [TreeItem("Goblin"), TreeItem("Dragon")]),
293 TreeItem("UI", [TreeItem("HUD"), TreeItem("Menu")]),
294 ],
295 )
296 tree = TreeView(root=root, tooltip="Scene hierarchy")
297 tree.item_selected.connect(lambda item: self.say(f"Tree: {item.text}"))
298
299 def make_row(index: int, recycled: Label | None) -> Label:
300 row = recycled or Label(font_size=12)
301 row.text = f" Item {index + 1:04d}"
302 return row
303
304 rows = VirtualScrollContainer(item_height=24, show_scrollbar=True, tooltip="Only the visible rows exist")
305 rows.set_data_source(1000, make_row)
306
307 page = SplitContainer(name="Trees")
308 page.add_child(titled("Scene Hierarchy", tree))
309 page.add_child(titled("Virtual Scroll (1000 items)", rows))
310 return page
311
312 def menus_page(self) -> VBoxContainer:
313 page = VBoxContainer(name="Menus", separation=10)
314 page.add_child(heading("Secondary MenuBar"))
315 menubar = page.add_child(MenuBar())
316 menubar.add_menu("Actions", self.menu_items("Actions", "Build", "Deploy"))
317 menubar.add_menu("Options", self.menu_items("Options", "Settings"))
318
319 page.add_child(heading("Toolbar with toggle group"))
320 toolbar = page.add_child(Toolbar())
321 tools = ButtonGroup()
322 tools.checked_changed.connect(lambda button: self.say(f"Tool: {button.text}"))
323 for tool in ("Brush", "Eraser", "Fill"):
324 toolbar.add_button(tool, group=tools)
325 toolbar.add_separator()
326 toolbar.add_button("Apply", on_press=lambda: self.say("Toolbar: Apply"))
327 toolbar.add_button("Reset", on_press=lambda: self.say("Toolbar: Reset"))
328
329 page.add_child(heading("PopupMenu"))
330 self.popup_button = page.add_child(Button("Show Popup Menu", on_press=self.show_popup))
331 self.popup = page.add_child(PopupMenu(items=self.menu_items("Popup", "Cut", "Copy", "Paste", None, "Delete")))
332 return page
333
334 def show_popup(self):
335 x, y, _, height = self.popup_button.get_global_rect()
336 self.popup.show(x, y + height)
337 self.say("Popup menu shown")
338
339 def canvas_page(self) -> VBoxContainer:
340 # A Node2D under a UI page draws in the page's space, clipped to it.
341 page = VBoxContainer(name="Canvas", separation=4)
342 page.add_child(heading("2D Drawing: Line2D, Polygon2D, Path2D, Noise, MeshInstance2D"))
343 page.add_child(CanvasDemo())
344 return page
345
346 def advanced_page(self) -> SplitContainer:
347 self.graph = GraphEdit()
348 for name, inputs, outputs, x, y in (
349 ("Source", [], ["Data"], 20, 20),
350 ("Process", ["In"], ["Out"], 220, 40),
351 ("Output", ["Result"], [], 420, 20),
352 ):
353 node = GraphNode(name=name, title=name, graph_position=(x, y))
354 for port in inputs:
355 node.add_input(port, type="float")
356 for port in outputs:
357 node.add_output(port, type="float")
358 self.graph.add_graph_node(node)
359 self.graph.connect_node("Source", 0, "Process", 0)
360 self.graph.connect_node("Process", 0, "Output", 0)
361
362 picker = ColourPicker()
363 picker.colour_changed.connect(lambda c: self.say(f"Colour: ({c[0]:.2f}, {c[1]:.2f}, {c[2]:.2f})"))
364
365 terminal = TerminalEmulator()
366 terminal.write("\033[1;36m=== SimVX Terminal ===\033[0m\n")
367 terminal.write("\033[32mWelcome\033[0m to the integrated terminal.\n")
368 terminal.write("\033[33m$\033[0m Ready for input.\n")
369
370 page = SplitContainer(name="Advanced", vertical=False)
371 top = page.add_child(SplitContainer())
372 top.add_child(titled("GraphEdit", self.graph))
373 top.add_child(titled("ColourPicker", picker))
374 page.add_child(titled("TerminalEmulator", terminal))
375 return page
376
377 def theme_page(self) -> VBoxContainer:
378 page = VBoxContainer(name="Theme", separation=10)
379 page.add_child(heading("Theme Switching"))
380 row = page.add_child(HBoxContainer(separation=8))
381 themes = ButtonGroup()
382 themes.checked_changed.connect(lambda button: self.use_theme(button.text))
383 for name in THEMES:
384 row.add_child(Button(name, group=themes, checked=name == "Dark", tooltip=f"{name} theme"))
385
386 page.add_child(heading("Live Preview"))
387 page.add_child(Button("Preview Button"))
388 page.add_child(Slider(min_value=0, max_value=100, value=65))
389 page.add_child(ProgressBar(min_value=0, max_value=100, value=65))
390 page.add_child(TextEdit(text="Theme preview text"))
391 page.add_child(CheckBox("Preview checkbox", checked=True))
392 return page
393
394 def use_theme(self, name: str):
395 set_theme(THEMES[name]())
396 self.say(f"Theme: {name}")
397
398
399if __name__ == "__main__":
400 App(title="SimVX UI Showcase", width=1280, height=720).run(WidgetShowcase())