Widget Showcase¶

a tour of the built-in UI widgets, one family per tab.

â–¶ Run in browser

Tags: 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 a disabled one), CheckBox (toggled), SpinBox, a Slider driving a ProgressBar through value_changed, a DropDown (item_selected) and a single-line TextEdit (text_changed), lined up by a FormLayout whose add_field returns the control it adds.

  • Text: a MultiLineTextEdit with line numbers, a CodeTextEdit with Python highlighting, and a RichTextLabel reading ANSI colour codes, split by two SplitContainers (vertical=False stacks the second pair).

  • Layout: GridContainer(columns=4), FormLayout, a SplitContainer whose right pane is a ScrollContainer stacking a column of buttons, and a MarginContainer. The wheel scrolls the list wherever the pointer rests on it, buttons included.

  • Trees: a TreeView built from nested TreeItems, and a VirtualScrollContainer over 1000 rows: set_data_source(count, make_row) builds rows only for what is on screen, handing make_row a scrolled-off row to reuse.

  • Menus: a second MenuBar, a Toolbar whose Brush, Eraser and Fill share a ButtonGroup (checking one unchecks the rest, and checked_changed reports it), and a PopupMenu opened under its button with show(x, y).

  • Canvas: 2D nodes inside a UI page: an animated Line2D, a spinning Polygon2D, a marker riding a Path2D curve on a PathFollow2D, a star MeshInstance2D and a FastNoiseLite grid drawn in on_draw.

  • Advanced: a GraphEdit of connected GraphNodes, a ColourPicker and a TerminalEmulator.

  • 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())