Theme Presets

every built-in AppTheme side by side.

▶ Run in browser

Tags: ui theme

An AppTheme is the palette and spacing every widget reads its colours from. The engine ships presets as class methods: AppTheme.dark(), abyss(), midnight(), light(), monokai(), solarised_dark() and nord().

A theme applies to a subtree. Each column here assigns one preset with a single self.theme = theme, and every widget below it (buttons, a panel, text edits, a slider, a progress bar, checkboxes and tabs) resolves that preset through get_theme() and draws in its colours. A widget with no theme of its own anywhere above it uses the application default. Custom drawing can read the same palette: each column paints its background from theme.bg and a thin top bar from theme.accent in its on_draw.

The button row pins one button to each Button.VisualState with set_visual_state_override, so normal, hover, pressed, disabled, focused and checked are all visible at once instead of only under the pointer.

The columns sit in an HBoxContainer inside a ScrollContainer. The mouse wheel scrolls it vertically, and because the scroll area takes keyboard focus (focus_mode=FocusMode.ALL, then grab_focus() once it is in the tree), the left and right arrow keys scroll it horizontally on a window too narrow for every column.

Source

  1# /// simvx
  2# tags = ["ui", "theme"]
  3# ///
  4"""Theme Presets: every built-in AppTheme side by side.
  5
  6An `AppTheme` is the palette and spacing every widget reads its colours from.
  7The engine ships presets as class methods: `AppTheme.dark()`, `abyss()`,
  8`midnight()`, `light()`, `monokai()`, `solarised_dark()` and `nord()`.
  9
 10A theme applies to a subtree. Each column here assigns one preset with a single
 11`self.theme = theme`, and every widget below it (buttons, a panel, text edits,
 12a slider, a progress bar, checkboxes and tabs) resolves that preset through
 13`get_theme()` and draws in its colours. A widget with no theme of its own
 14anywhere above it uses the application default. Custom drawing can read the
 15same palette: each column paints its background from `theme.bg` and a thin
 16top bar from `theme.accent` in its `on_draw`.
 17
 18The button row pins one button to each `Button.VisualState` with
 19`set_visual_state_override`, so normal, hover, pressed, disabled, focused and
 20checked are all visible at once instead of only under the pointer.
 21
 22The columns sit in an `HBoxContainer` inside a `ScrollContainer`. The mouse
 23wheel scrolls it vertically, and because the scroll area takes keyboard focus
 24(`focus_mode=FocusMode.ALL`, then `grab_focus()` once it is in the tree), the
 25left and right arrow keys scroll it horizontally on a window too narrow for
 26every column.
 27"""
 28
 29from simvx.core.ui import (
 30    Anchor,
 31    AppTheme,
 32    Button,
 33    CheckBox,
 34    Control,
 35    FocusMode,
 36    HBoxContainer,
 37    Label,
 38    Panel,
 39    ProgressBar,
 40    ScrollContainer,
 41    Slider,
 42    TabContainer,
 43    TextEdit,
 44    VBoxContainer,
 45)
 46from simvx.graphics import App
 47
 48THEMES = {
 49    "Dark": AppTheme.dark(),
 50    "Abyss": AppTheme.abyss(),
 51    "Midnight": AppTheme.midnight(),
 52    "Light": AppTheme.light(),
 53    "Monokai": AppTheme.monokai(),
 54    "Solarised": AppTheme.solarised_dark(),
 55    "Nord": AppTheme.nord(),
 56}
 57
 58COLUMN_W, COLUMN_H, GAP = 210.0, 780.0, 4.0
 59
 60
 61class ThemeColumn(VBoxContainer):
 62    """One preset applied to a column of sample widgets."""
 63
 64    def __init__(self, name: str, theme: AppTheme):
 65        super().__init__(size_x=COLUMN_W, separation=4)
 66        self.theme = theme  # every widget below inherits it
 67
 68        self.add_child(Label(name, font_size=15, alignment="centre"))
 69
 70        self.section("Buttons")
 71        for state in Button.VisualState:
 72            button = self.add_child(Button(state.value.title()))
 73            button.set_visual_state_override(state)
 74
 75        self.section("Panel")
 76        panel = self.add_child(Panel(size_y=36))
 77        panel.add_child(Label("Panel content", font_size=12, anchor=Anchor.TOP_LEFT, inset=(6, 8)))
 78
 79        self.section("TextEdit")
 80        self.add_child(TextEdit("Editable text"))
 81        self.add_child(TextEdit(placeholder="Placeholder"))
 82
 83        self.section("Slider")
 84        self.add_child(Slider(0, 100, value=65))
 85
 86        self.section("Progress")
 87        self.add_child(ProgressBar(value=72, size_y=18))
 88
 89        self.section("CheckBox")
 90        self.add_child(CheckBox("Enabled", checked=True))
 91        self.add_child(CheckBox("Disabled feature"))
 92
 93        self.section("Tabs")
 94        tabs = self.add_child(TabContainer(size_y=60))
 95        for tab in ("Scene", "Debug", "Output"):
 96            tabs.add_child(Control(name=tab))
 97
 98    def section(self, title: str):
 99        self.add_child(Label(title, font_size=10))
100
101    def on_draw(self, renderer):
102        _, _, w, h = self.get_rect()
103        renderer.rect((0, 0), (w, h), fill=self.theme.bg)
104        renderer.rect((0, 0), (w, 2), fill=self.theme.accent)
105
106
107class ThemeGallery(ScrollContainer):
108    """Every preset in a column of its own, in a strip that scrolls both ways."""
109
110    def __init__(self):
111        super().__init__(anchor=Anchor.FULL_RECT, inset=(6, 6, 0, 0), focus_mode=FocusMode.ALL)
112        columns = [ThemeColumn(name, theme) for name, theme in THEMES.items()]
113        # A container keeps the size it is given rather than growing to its
114        # children, so the strip is sized to hold every column.
115        strip = (len(columns) * (COLUMN_W + GAP), COLUMN_H)
116        self.add_child(HBoxContainer(size=strip, separation=GAP, children=columns))
117
118    def on_ready(self):
119        super().on_ready()
120        self.grab_focus()  # arrow keys scroll only the focused control
121
122
123if __name__ == "__main__":
124    App(width=1510, height=800, title="Theme Presets").run(ThemeGallery())