Getting Started¶
Build one small app, a label and a button that counts, twice: declaratively
(a YAML view and a Python ViewModel) and imperatively (Python calls that
create nodes). Each step shows both, and every program here is a file in
examples/getting_started/
that Tesserae's tests run.
Install¶
You need Python 3.12 or newer and a desktop session with a Vulkan graphics driver (any current Linux, macOS or Windows install has one; see Installation if a window won't open).
mkdir counter && cd counter
python3 -m venv .venv
source .venv/bin/activate # fish: source .venv/bin/activate.fish
pip install tesserae-ui
tesserae-ui is the name on PyPI; you import tesserae. Everything below goes
in the counter folder, with the environment active.
1. A window¶
An App owns the window. Make one 360 by 160 with a dark background (so the
white text we add next reads on any desktop theme) and open it.
id: root
kind: Container
style: {flex_direction: vertical, width: 360, height: 160, gap: 12, padding: 16, background: "#1C1B1F"}
from pathlib import Path
from tesserae import App
app = App(width=360, height=160, title="Counter")
view = app.build_view(Path(__file__).parent / "Counter_View.yaml")
app.register("Counter", view, None) # a screen with no ViewModel yet
app.show("Counter")
app.run()
from tesserae import App
app = App(width=360, height=160, title="Counter")
window = app.window
window.root.set(flex_direction="vertical", gap=12, padding=16, fill=(0x1C, 0x1B, 0x1F, 0xFF))
app.run()
Declaratively the window shows a screen: app.build_view reads
Counter_View.yaml and app.register names it (None is its ViewModel, which
it doesn't need yet). Imperatively there is no file: window.root is the box
every node goes in, and one call sets its layout and background.
python app.py
An empty window titled "Counter" opens.
2. A node¶
A node is one thing on screen. Add a label that says Count: 0.
id: root
kind: Container
style: {flex_direction: vertical, width: 360, height: 160, gap: 12, padding: 16, background: "#1C1B1F"}
children:
- id: label
kind: Text
text: {content: "Count: 0", font_family: Roboto, font_size: 20}
style: {width: 160, height: 28, foreground: "#FFFFFF"}
from pathlib import Path
from tesserae import App
app = App(width=360, height=160, title="Counter")
view = app.build_view(Path(__file__).parent / "Counter_View.yaml")
app.register("Counter", view, None) # a screen with no ViewModel yet
app.show("Counter")
app.run()
from tesserae import App
app = App(width=360, height=160, title="Counter")
window = app.window
window.root.set(flex_direction="vertical", gap=12, padding=16, fill=(0x1C, 0x1B, 0x1F, 0xFF))
label = window.create("text", text="Count: 0", font_size=20, width=160, height=28, fill=(255, 255, 255, 255))
window.root.add_child(label)
app.run()
In YAML a node is an entry in children:: kind: Text makes a label and
text: holds what it says. In code, window.create("text", ...) makes the node
and window.root.add_child(...) puts it in the window. The text's colour is
foreground in YAML and fill= in code.
If Python prints an error
A mistake in a view file ends in a traceback; read the last line. It names the file, the widget and the problem, and for a misspelt word says what you probably meant:
ValueError: .../Counter_View.yaml: widget "label": unknown style field(s) ['foregorund'] -- did you mean 'foreground'?
3. A second node¶
Add a button: a purple, rounded box with a centred text node in it.
id: root
kind: Container
style: {flex_direction: vertical, width: 360, height: 160, gap: 12, padding: 16, background: "#1C1B1F"}
children:
- id: label
kind: Text
text: {content: "Count: 0", font_family: Roboto, font_size: 20}
style: {width: 160, height: 28, foreground: "#FFFFFF"}
- id: button
kind: Rect
style: {width: 96, height: 40, background: "#6750A4", corner_radius: 20, align_content: left}
children:
- id: button_label
kind: Text
text: {content: Count, font_family: Roboto, font_size: 16, text_align: center}
style: {width: 96, height: 20, foreground: "#FFFFFF"}
from pathlib import Path
from tesserae import App
app = App(width=360, height=160, title="Counter")
view = app.build_view(Path(__file__).parent / "Counter_View.yaml")
app.register("Counter", view, None) # a screen with no ViewModel yet
app.show("Counter")
app.run()
from tesserae import App
app = App(width=360, height=160, title="Counter")
window = app.window
window.root.set(flex_direction="vertical", gap=12, padding=16, fill=(0x1C, 0x1B, 0x1F, 0xFF))
label = window.create("text", text="Count: 0", font_size=20, width=160, height=28, fill=(255, 255, 255, 255))
window.root.add_child(label)
button = window.create("box", width=96, height=40, corner_radius=20, fill=(0x67, 0x50, 0xA4, 0xFF),
align_items="center")
button.add_child(window.create("text", text="Count", font_size=16, width=96, height=20,
fill=(255, 255, 255, 255), text_align="center"))
window.root.add_child(button)
app.run()
A button is a Rect (a coloured box) with a Text in it. The two ways name
things a little differently:
| YAML view | Python |
|---|---|
kind: Text / kind: Rect |
window.create("text", ...) / window.create("box", ...) |
style: {width: 96, ...} |
width=96 |
foreground, background ("#RRGGBB") |
fill=(r, g, b, a) |
text: {content: "...", font_size: 20} |
text="...", font_size=20 |
children: |
node.add_child(other) |
4. Make it count¶
from tesserae import Signal, ViewModel
class CounterViewModel(ViewModel):
def __init__(self, view):
self.count = 0
self.label_text = Signal("Count: 0")
super().__init__(view) # last: it reads the Signals above
def increment(self):
self.count += 1
self.label_text.set(f"Count: {self.count}")
id: root
kind: Container
style: {flex_direction: vertical, width: 360, height: 160, gap: 12, padding: 16, background: "#1C1B1F"}
children:
- id: label
kind: Text
text: {content: "Count: 0", font_family: Roboto, font_size: 20}
bindings: {text: "{{ label_text.get() }}"}
style: {width: 160, height: 28, foreground: "#FFFFFF"}
- id: button
kind: Rect
handlers: {on_click: increment}
style: {width: 96, height: 40, background: "#6750A4", corner_radius: 20, align_content: left}
children:
- id: button_label
kind: Text
text: {content: Count, font_family: Roboto, font_size: 16, text_align: center}
style: {width: 96, height: 20, foreground: "#FFFFFF"}
from pathlib import Path
from tesserae import App
from Counter_ViewModel import CounterViewModel
app = App(width=360, height=160, title="Counter")
app.load(Path(__file__).parent / "Counter_View.yaml", CounterViewModel)
app.show("Counter")
app.run()
from tesserae import App
app = App(width=360, height=160, title="Counter")
window = app.window
window.root.set(flex_direction="vertical", gap=12, padding=16, fill=(0x1C, 0x1B, 0x1F, 0xFF))
label = window.create("text", text="Count: 0", font_size=20, width=160, height=28, fill=(255, 255, 255, 255))
window.root.add_child(label)
button = window.create("box", width=96, height=40, corner_radius=20, fill=(0x67, 0x50, 0xA4, 0xFF),
align_items="center")
button.add_child(window.create("text", text="Count", font_size=16, width=96, height=20,
fill=(255, 255, 255, 255), text_align="center"))
window.root.add_child(button)
clicks = 0
def count():
global clicks
clicks += 1
label.set(text=f"Count: {clicks}")
button.on("click", count)
button.set(role="button", label="Count", focusable=True, cursor="pointer") # Tab, then Enter or Space
app.run()
Declaratively the state lives in a ViewModel, paired with the view by name
(Counter_View.yaml and Counter_ViewModel.py). Its Signal holds the label's
text; the view's bindings: keeps the label showing it, and handlers: calls
increment on a click. app.load pairs the two files.
Imperatively the state is a variable and you are the view: button.on("click",
count) runs count, which sets the label with label.set(text=...).
Run it. Click the button, or press Tab and then Enter or Space, and the count goes up. The two versions are the same app.
Hot reload
Change app.run() to app.run(hot_reload=True) and edit
Counter_View.yaml while the app is open: the window updates at once and the
count stays. See Hot Reload.
Which to use¶
Use declarative for the screens you design: layout, styling, themes and
bindings live in the view file, and the ViewModel holds only your logic. Use
imperative for what is made while the app runs; the
Widgets in Python guide covers it. The two mix freely,
because a declarative screen is made of the same nodes you create in code:
view.node("button") returns any node the YAML declared.
Next¶
- Tutorial: build a bigger app with a custom window, several views and ViewModels, components and styling, in one flat folder; or as a project, with the files in folders and found by name.
- Projects and the
tesseraecommand: start an app from a template, with its files in standard folders. - Apps & Screens, Layout and Themes.
- Components: every MD3 component, with its YAML.