Skip to main content

Tesserae

A declarative Python GUI framework, powered by the Tesserae Render Engine (tre) -- a Rust/Python hybrid rendering, layout, animation, and accessibility engine.

Tesserae's own shape follows pyCopper (an earlier, standalone framework by the same author, powered by a direct GLFW/wgpu-py stack instead): app authors write YAML views, not Python widget-class trees, paired with a Python ViewModel per view -- a plain Signal-driven MVVM layer, not a whole-tree reconcile.

Status: pre-alpha, five vertical slices. App/Signal/View/ ViewModel/Component/Repeater exist and are exercised end to end by examples/counter/ (a single screen), examples/multi_screen/ (two screens, switched via App.show() from inside a real dispatched handler), examples/todo_list/ (a real dynamic list, one list Signal as the single source of truth, Repeater keeping components in sync automatically), and examples/app_shell/ and examples/app_shell_file/ (an app shell with docked panels, built in Python and from a *_Shell.yaml). Signal/Computed/Effect/batch/untrack/ ViewModel are Tesserae's own (M35) -- see Reactivity below. The MD3 widget catalog is real too: 67 declarative component: fragments for *_View.yaml files, plus tesserae.widgets for building widgets from Python. See the documentation for both.

Files: Tesserae reads them, tre gets data

You give Tesserae file paths -- views, component fragments, include:d files, images, themes, stylesheets, fonts. Tesserae reads, parses, decodes and watches them itself, and hands tre only data: a finished view built by Tesserae's own compiler onto tre's building blocks (M37), RGBA pixels and font bytes (register_font). Tesserae never gives tre a file path.

That's what lets Tesserae resolve include: and component: together, name the right file in every error, warn when a font would silently fall back, and hot-reload a running app when any of those files change (app.run(hot_reload=True)) -- views, includes, fragments and images, components added at run time (every row of a Repeater, in place), and the app's theme and stylesheet files.

Tesserae logs through loguru: hot reloads, failed reloads naming the file, and warnings. Call tesserae.configure_logging() in app.py for its console format, or logger.disable("tesserae") to silence it.

Install (development)

Tesserae needs tre 0.3.5.2 or newer, which is on PyPI as tesserae-engine (still import tre). Installing Tesserae installs it:

pip install -e ".[dev]"

Coming from a GitHub tre-... wheel, run pip uninstall tre first: pip treats tesserae-engine as a different project, and both would own the tre package. See the installation guide for testing against an unreleased tre checkout.

The real vertical slices

python examples/counter/app.py
python examples/multi_screen/app.py
python examples/todo_list/app.py
python examples/app_shell/app.py
python examples/app_shell_file/app.py

counter/: a real Signal-bound counter -- a Counter_View.yaml + Counter_ViewModel.py pair, loaded via App.load() and shown via App, a real dispatched click incrementing a bound label, then a genuine render loop.

multi_screen/: two independent screens (Home/Settings), each its own *_View.yaml/*_ViewModel.py pair, switched via App.show(name) from inside each screen's own real dispatched on_click handler -- proving a switch works even when triggered reentrantly, from the handler App.show itself is dispatching into.

todo_list/: a real dynamic list, driven by tesserae.Repeater -- one list Signal of stable item ids (TodoViewModel.items) is the single source of truth; adding a "to-do" appends an id, removing one drops it -- the Repeater keeps exactly one independent TodoItem Component + ViewModel (TodoItem_View.yaml/TodoItem_ViewModel.py) alive per id, instantiating/removing automatically. Proves the full real multi- instance lifecycle: add, toggle a two-way-bound checkbox, remove (mutating the shared items Signal from inside the item's own dispatched handler), add again, all through real dispatched clicks and one live window.

app_shell/: an app framed by a top app bar, a navigation rail and a status bar, with tool panels docked left, right and bottom and screens as center tabs (M45), all built in Python with tesserae.shell.AppShell and tesserae.docking.Dock.

app_shell_file/: the same studio declared in Studio_Shell.yaml and built by app.load_shell() (M52). Its panels are views named like screens, found next to the shell file; its rail shows screens; and the shell file itself is hot-reloaded.

Components

tesserae.instantiate(parent, path, viewmodel_cls, into, *args, **kwargs) embeds another view's own YAML as a real, independent Component with its own ViewModel, built in its host's window with the host's theme and stylesheet. parent is a View or another Component (they nest); into is the Node to embed under (e.g. view.node("item_list")); extra positional/keyword args are forwarded to viewmodel_cls(component, *args, **kwargs) -- the real, common case for a component that needs its own data or a callback to notify its parent when it removes itself (see examples/todo_list/).

component, viewmodel = instantiate(view, "Card_View.yaml", CardViewModel, container)

Call this once per instance for multiple simultaneous instances (a list where each row is its own independent component) -- each call is fully independent, even reusing the same path repeatedly. component.remove() tears the instance down for real, unsubscribing its own Signals first.

Repeater

Repeater automates the add/remove bookkeeping instantiate/ Component.remove() otherwise need by hand: point it at a list Signal and it keeps exactly one Component + ViewModel alive per item currently present, diffed by a real key.

from tesserae import Repeater, Signal

items = Signal([])  # the single source of truth
repeater = Repeater(view, items, "Card_View.yaml", CardViewModel, container)

items.update(lambda lst: [*lst, new_id])              # adds one
items.update(lambda lst: [i for i in lst if i != id])  # removes one

key (default: the item itself) and args (default: lambda item: (item,), forwarded to viewmodel_cls(component, *args)) are both overridable for items that are dicts/dataclasses rather than bare ids -- see examples/todo_list/'s own Todo_ViewModel.py.

Real, deliberate scope boundary: Repeater only ever adds or removes instances to match the set of keys present -- it never reorders an already-present key's own position, and never re-applies a changed item's own data to an existing instance (that's the item's own ViewModel's job, via its own Signals). Reordering isn't supported for the same real reason tre's own Reconciler doesn't: engine_core ::Tree has no child-reorder primitive today. Repeater.remove() tears every remaining instance down and unsubscribes, mirroring Component .remove()'s own real teardown ordering.

Reactivity

Tesserae's reactivity (tesserae.reactive, taken over from tre in M35 with the same behaviour):

from tesserae import Computed, Effect, batch

total = Computed(lambda: price.get() * quantity.get())  # derived, cached
Effect(lambda: print(f"total is now {total.get()}"))     # side effect only

def apply_discount():
    price.update(lambda p: p * 0.9)
    quantity.set(quantity.get() + 1)

batch(apply_discount)  # total recomputes once, not twice

Computed/Effect duck-type against Signal's own subscribe shape, so a {{ }} binding can point straight at a Computed.get() value with no special handling. See tre's own examples/reactivity.py for a full, live-window proof of Computed-of-Computed chains, Effect, and batch() composing together.

Naming convention

Every real view is a *_View.yaml + *_ViewModel.py pair (mirroring pyCopper's own real MVVM naming) -- enforced at runtime by App.load(view_path, viewmodel_cls, name=None):

from tesserae import App
from Counter_ViewModel import CounterViewModel

app = App(width=240, height=120, title="My App")
view, viewmodel = app.load("Counter_View.yaml", CounterViewModel)
app.show("Counter")  # registered under the inferred prefix

Raises ValueError immediately if the view file doesn't end in _View.yaml, the ViewModel's own defining file doesn't end in _ViewModel.py, or the two prefixes don't match -- catching a mismatched pair at load time rather than a cryptic failure later when a handler name doesn't resolve. App.register(name, view, viewmodel) stays available directly for the real cases load() doesn't cover (a ViewModel that needs a live reference to App itself at construction time, e.g. to call app.show(...) from its own handler -- see examples/multi_screen/).

app.py is the real entry point: it loads (or registers) each pair and calls App.show(name) to pick which one is currently on screen -- switching later (another App.show(other_name) call, typically from a registered handler) doesn't re-parse the YAML or re-construct the ViewModel; both stay alive, Signal subscriptions intact, for the life of the app.

Architecture

See ARCHITECTURE.md.

Explicitly deferred

Named for the record, not designed in detail yet -- each is real, future work once this foundation is proven further:

  • App-level state stores shared across screens (#13), routing beyond a plain named App.show(name) (history/back-stack, URL-style deep links; #12), a tesserae new CLI scaffolding tool (#14).
  • Publishing to PyPI as tesserae-ui (#15); tesserae there is an unrelated project. The distribution is already named tesserae-ui in pyproject.toml, and the import name stays tesserae. tre must be published first, as tesserae-engine (tre there is an unrelated regex library; tre #19, in progress), and Tesserae's dependency then switches to it. Publishing is .github/workflows/release.yml: publishing a GitHub Release builds, tests the built wheel and uploads it with PyPI trusted publishing (the pypi environment). It refuses to run while the dependency is still the bare tre, or when the tag doesn't match the version.

Every known gap has a scoped issue: Tesserae's issues.

Metadata

Release files for tesserae-ui 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tesserae-ui 0.1.0
File Size Uploaded
tesserae_ui-0.1.0.tar.gz 529.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tesserae-ui 0.1.0
File Interpreter ABI Platform
tesserae_ui-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 746.0 kB

Release files / tesserae_ui-0.1.0.tar.gz

Download URL tesserae_ui-0.1.0.tar.gz
Size 529.2 kB
Tags Source
SHA-256 checksum
How to use checksums
bf19e075b06ba68e804f039b7a7508e2a65f17b18457d937ece34acc1ab6ec2e
BLAKE2b-256 checksum
How to use checksums
b2dfbb48d19f840d2feba38972afec9759cf3fe8a26215260b0ecf117a9bbf93
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / tesserae_ui-0.1.0-py3-none-any.whl

Download URL tesserae_ui-0.1.0-py3-none-any.whl
Size 216.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5ae57486bb09f3c19ee07a8f04b8bba7c21bc6d23763fe1a9968a4af11428cee
BLAKE2b-256 checksum
How to use checksums
321a162434e2b108c2f2e3187d5fb3d1825a05c8d2f9c89df5ec6eb9dba9e926
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page