Skip to main content

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.

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.

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:
impl Counter {
    fn increment(&mut self, cx: &mut Context<Self>) {
        self.count += 1;
        cx.notify(); // schedule a re-render for views showing this counter
    }
}
cx.notify() is idempotent within a single event dispatch — calling it multiple times in one update schedules exactly one repaint.
You do not need to call cx.notify() if your entity implements Render and you update it through cx.update_entity() or .update(). Kael automatically propagates dirty state to the associated window. You only need cx.notify() when you want to explicitly signal to observers that your state has changed.

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:
let second = cx.new(|cx: &mut Context<Counter>| {
    cx.observe(&first_counter, |this, observed, cx| {
        this.count = observed.read(cx).count * 2;
        cx.notify();
    })
    .detach();

    Counter { count: 0 }
});
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:
struct CounterChanged {
    increment: usize,
}

impl EventEmitter<CounterChanged> for Counter {}

// In the subscriber's build closure:
cx.subscribe(&first_counter, |this, _first, event: &CounterChanged, cx| {
    this.count += event.increment * 2;
    cx.notify();
})
.detach();
Subscriptions are cancelled automatically when the 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:
first_counter.update(cx, |first, cx| {
    let old_count = first.count;
    first.count += 5;
    cx.emit(CounterChanged { increment: 5 });
    cx.notify();
});
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:
cx.observe_global::<AppSettings>(|this, cx| {
    // called whenever AppSettings global is updated
    this.font_size = cx.read_global::<AppSettings, _>(|s, _| s.font_size);
    cx.notify();
})
.detach();

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:
cx.on_app_quit(|this, cx| async move {
    this.flush_pending_writes().await;
})
.detach();
The quit future has a hard 100ms deadline. Kael will proceed with shutdown regardless once this timeout expires.

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:
let task: Task<Vec<u8>> = cx.background_spawn(async {
    // Runs on a background thread. No access to App or Window here.
    fetch_remote_data().await
});

// The task is cancelled if dropped. Call .detach() to fire-and-forget:
task.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:
cx.spawn(async move |this, cx| {
    let data = fetch_remote_data().await;

    // Update the entity from the async context:
    this.update(cx, |entity, cx| {
        entity.data = data;
        cx.notify();
    })
    .ok();
})
.detach();
The entity handle passed to the closure is a 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.
Use .detach() on Task<()> for fire-and-forget spawns, or store the Task<T> in your entity’s fields if you need to cancel it later by dropping.

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 Task cancels 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 a Task<T> to get the result.
let task = cx.background_spawn(async { expensive_computation() });
let result = task.await; // blocks current async context until done

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:
  1. An entity calls cx.notify(), or an event is emitted to a subscriber that calls cx.notify().
  2. Kael records the entity’s window as needing a repaint.
  3. On the next display sync (VSync), Kael renders only the windows with dirty views.
  4. After the frame is painted, all dirty flags are cleared.
  5. If no events arrive, no entities become dirty, and no rendering occurs.
This means a Kael application drawing a static screen uses no measurable CPU — the GPU is idle, the main thread is sleeping, and the background executor has no work to do.

cx.observe_release() — reacting to entity destruction

You can also watch for when an entity is dropped from the application:
cx.observe_release(&some_entity, |this, released_entity, cx| {
    this.handle_peer_disconnected(released_entity, cx);
})
.detach();
This is useful for teardown logic when one entity depends on another’s lifetime.

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:
use kael::prelude::*;

#[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)]
#[derive(Action)]
pub struct SelectAll;
#[register_action] — for actions where you implement the Action trait manually (e.g. actions with payload fields):
use kael::{SharedString, register_action};

#[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)]
pub struct Paste {
    pub content: SharedString,
}

impl kael::Action for Paste {
    fn boxed_clone(&self) -> Box<dyn kael::Action> { Box::new(self.clone()) }
    fn partial_eq(&self, other: &dyn kael::Action) -> bool {
        other.as_any().downcast_ref::<Self>().map_or(false, |o| o == self)
    }
    fn name(&self) -> &'static str { "Paste" }
    fn name_for_type() -> &'static str { "Paste" }
    fn build(value: serde_json::Value) -> anyhow::Result<Box<dyn kael::Action>> {
        Ok(Box::new(serde_json::from_value::<Self>(value)?))
    }
}

register_action!(Paste);
Actions are dispatched through the element tree and can be handled by any element via .on_action():
div()
    .on_action(cx.listener(|this, _action: &SelectAll, _window, cx| {
        this.select_all(cx);
    }))
Bind an action to a key in the application keymap using cx.bind_keys():
cx.bind_keys([KeyBinding::new("cmd-a", SelectAll, None)]);

Summary

APIWhen 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

Build docs developers (and LLMs) love