Kael’s reactivity model is built around a single principle: nothing re-renders unless it needs to. Every entity tracks whether it has changed since the last frame. When an entity marks itself dirty, the window it belongs to is scheduled for a repaint. Between frames, if no entity is dirty, Kael does nothing — resulting in 0% CPU usage at idle. This section explains the full set of primitives that drive this model, from manual dirty marking to typed event emission and async task integration.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Augani/kael/llms.txt
Use this file to discover all available pages before exploring further.
cx.notify() — marking an entity dirty
The most direct way to trigger a re-render is to call cx.notify() from within an entity’s context. This tells Kael that the entity’s state has changed and any views that depend on it should re-render on the next frame:
cx.notify() is idempotent within a single event dispatch — calling it multiple times in one update schedules exactly one repaint.
cx.observe() — watching another entity for state changes
cx.observe() lets one entity react whenever another entity calls cx.notify(). The callback receives a mutable reference to the observer, a handle to the observed entity, and the observer’s context:
observe returns a Subscription. Dropping the subscription cancels it. Call .detach() to keep it alive as long as both entities exist.
cx.subscribe() — listening for typed events
cx.subscribe() is the companion to cx.emit(). Where observe reacts to generic state changes, subscribe reacts to specific, typed event payloads. The emitting entity must implement EventEmitter<E> for the relevant event type:
Subscription handle is dropped, making it safe to store them in your entity’s fields and clean them up via Drop.
cx.emit() — broadcasting a typed event
Call cx.emit(event) from within an entity’s context to broadcast an event to all current subscribers. The event is dispatched synchronously before emit returns:
cx.emit() is only available when the entity implements EventEmitter<E> for the event type. This is enforced at compile time.Observing global state changes
You can also react whenever a global value is updated:Cleanup with cx.on_app_quit()
Register a callback to run before the application exits. The callback returns a future, giving you up to SHUTDOWN_TIMEOUT (100ms) to flush buffers, save state, or close connections:
Background tasks with cx.background_spawn()
For Send work that doesn’t need access to the app — network requests, file I/O, CPU-heavy computation — use cx.background_spawn(). It spawns a future on the background thread pool and returns a Task<R> you must hold or detach:
Foreground tasks with cx.spawn()
When you need to await something and then update entity state, use cx.spawn(). The closure receives a WeakEntity<T> and an AsyncApp context that can be held across await points:
WeakEntity<T> — it may have been dropped by the time the async work completes, so .update() returns Result and you should handle the None case gracefully.
The Task<T> type
Task<T> is Kael’s handle to a spawned async computation. It behaves like a JoinHandle:
- Drop to cancel — dropping a
Taskcancels the underlying future if it hasn’t completed. .detach()— detaches the task from the handle, letting it run to completion independently..await— you can await aTask<T>to get the result.
How dirty tracking powers idle efficiency
The entire reactivity system converges on a single property: frames are only produced when at least one entity is dirty. The flow is:- An entity calls
cx.notify(), or an event is emitted to a subscriber that callscx.notify(). - Kael records the entity’s window as needing a repaint.
- On the next display sync (VSync), Kael renders only the windows with dirty views.
- After the frame is painted, all dirty flags are cleared.
- If no events arrive, no entities become dirty, and no rendering occurs.
cx.observe_release() — reacting to entity destruction
You can also watch for when an entity is dropped from the application:
Actions and keybindings
Actions are serializable commands that can be triggered by keyboard shortcuts, menu items, or programmatically. Kael provides two macros for defining and registering actions:#[derive(Action)] — for actions with no parameters:
#[register_action] — for actions where you implement the Action trait manually (e.g. actions with payload fields):
.on_action():
cx.bind_keys():
Summary
| API | When to use |
|---|---|
cx.notify() | Signal that your entity’s state changed |
cx.observe(entity, cb) | React to state changes in another entity |
cx.subscribe(entity, cb) | React to typed events from another entity |
cx.emit(event) | Broadcast a typed event to subscribers |
cx.on_app_quit(cb) | Register async cleanup on application exit |
cx.background_spawn(future) | Off-thread work with no app access |
cx.spawn(closure) | Off-thread work that updates entity state |
cx.observe_global::<G>(cb) | React to global state changes |
cx.observe_release(entity, cb) | React when another entity is dropped |
#[derive(Action)] | Define a zero-parameter dispatched command |
register_action!(T) | Register a custom Action implementation |