Hot Reload¶
Tesserae reloads a screen when you edit any file it was built from: the
*_View.yaml itself, any file it include:s, any *_Component.yaml
fragment it uses, and any image it shows. Inside an App, it also
re-styles the running app when a theme or stylesheet file changes (see
below). The live view is updated in
place -- widgets that didn't change keep their identity, focus and any
running animation.
In an app¶
app = App()
app.load("Home_View.yaml", HomeViewModel)
app.show("Home")
app.run(hot_reload=True)
Every screen built from a file is watched while the app runs: one
App.load() made, and one built with app.build_view(...) and given to
App.register() (the view keeps the file it came from). Save a
change and it appears in the window straight away, the screen's
ViewModel and bindings kept. A screen built from a spec dict has no file,
so it isn't watched; the log says so when hot reload starts.
An edit reaches a window left alone: the watcher runs on its own thread,
and other Python threads run while a window sits idle. See the
Python API for App.run.
Behind the scenes each screen gets a ViewWatcher on a background
thread. It listens for file-change events (using
watchfiles), rebuilds the view
there -- so file reading and component expansion never slow the UI --
and hands only the final update to the event loop through
App.thread_handle() (Python API).
When an edit is broken¶
If a reload fails -- invalid YAML, an unknown component, a widget tre
rejects -- Tesserae logs one ERROR line naming the file and the error
(the traceback is at DEBUG), and the screen stays as it was. The app
and the watcher keep running: fix the file, save, and it reloads. Each
reload that works is logged at INFO. See Logging.
Using ViewWatcher directly¶
For a view you load yourself:
from tesserae.spec import ViewWatcher, load_view
view = load_view("Home_View.yaml")
watcher = ViewWatcher(view, "Home_View.yaml")
Then either:
- Inside a running app -- start it with the app's thread handle, and stop it when you're done:
watcher.start(app.thread_handle())
...
watcher.stop()
- In a loop you control yourself -- call
poll()regularly. It checks each file's modification time and size, reloads if anything changed, and returnsTruewhen it did. A failed reload raises (once per edit) and leaves the view as it was.
while running:
if watcher.poll():
print("reloaded")
watcher.files lists every file being watched. It's recomputed on every
reload, so a newly added include, fragment or image is picked up.
Components added at run time¶
A component built with tesserae.instantiate (a Repeater's rows, say)
comes from its own *_View.yaml, and that file is watched too.
Editing it reloads every live instance in place. Each keeps its nodes,
its own ViewModel and its bound values, and a list of rows keeps its
order. One watcher per file serves all its instances, so ten rows cost
one rebuild, not ten. Files a component include:s, and component:
fragments it uses, are watched with it.
A component added while the app runs is watched as well, including the
first one of a new file. It's found in any view on the app's window,
even one the app doesn't otherwise know, since instantiate
registers it with the app. A row removed before the edit is left alone. A
broken edit leaves every instance as it was and is logged once for the
file:
hot reload: watching component TodoItem_View.yaml (3 instance(s))
reloaded /path/to/TodoItem_View.yaml (3 instance(s))
The window view¶
A kind: Window view is watched like any view, and so is each view it embeds (view:). An edit is applied in place:
| Edit | What happens |
|---|---|
the window's title, min_width, min_height or borderless |
the OS window is set again |
the title_bar: |
the bar is rebuilt where it is, and still the title bar |
a view: with a route: added or removed |
the screen is registered or dropped; the current screen stays showing |
a DockPanel added, removed, renamed or moved to another zone |
docked there; a panel the file didn't move stays where the user dragged it |
| a zone's size | set, and a zone the file didn't change keeps the size the user dragged it to |
| an embedded view's file | that view is rebuilt in place, with its ViewModel; editing the host keeps an embedded view whose view: and with: didn't change, with its state |
A change that can't be applied is logged as an error and the app keeps running.
Theme and stylesheet files¶
App.run(hot_reload=True) also watches the theme files given to
App(default_theme=..., custom_theme=...). When you save one, Tesserae
re-reads both on the watcher thread, then re-themes every screen built
by app.load() or app.build_view() through
app.set_theme_specs(...). Bound values stay live. A theme given as a
*_spec= dict has no file, so it isn't watched. A broken edit is logged
at ERROR like a failed view reload, and the app keeps its previous theme.
Stylesheet files are watched the same way:
- The default stylesheet from
App(stylesheet=...): saving it re-styles every screen that uses the default, throughapp.set_stylesheet_spec(...). Screens loaded with their own stylesheet are left alone. - A screen's own stylesheet from
app.load(..., stylesheet=...): saving it re-styles only the screens loaded with that file.
A stylesheet tre rejects is reported naming the file, and every
screen keeps its old stylesheet.
Handlers fire when bindings are re-applied¶
After a theme or stylesheet change, tre re-applies every binding so
bound nodes keep their live values. It does this by setting each bound
value again, so a declared on_change handler on a bound node (a
Checkbox with checked: bound, a TextField with text: bound)
runs once per reload, even though the value didn't change. The same
thing happens when a view is first attached and on a view-file reload.
Write on_change handlers so that running one again with the same value
is harmless: set a Signal to the node's current value, don't toggle
it or append to a list. This is a known behaviour of tre.
Limits¶
- Don't use
tre's ownView.poll_reload(). Tesserae givestrethe finished view as data, never a file, sotrehas nothing to watch.