App state

Choose a state’s owner by how long the value should live and who should share it. A text field’s temporary input may belong to one piece. A document selection usually belongs to a window. A login session may belong to the whole app.

ScopeLifetimeAPITypical use
PieceUntil its owning scope is disposedSignal::newInput or expanded state within a component
WindowUntil the window closesT::scoped(…), then T::ambient()A document’s selection and current page
AppUntil the process exitsT::app()A session or cache shared by windows

For window state, use Ambient as shown below. This uses Day’s reactive scopes and window registry, so the ownership rules are shared by all backends. The Ambient tests in mock_e2e.rs cover the behavior.

The shape: a Copy struct of handles

use day::prelude::*;

/// State shared by the pieces in one window.
#[derive(Clone, Copy)]
struct Scene {
    items: Store<Keyed<Item>>,
    selected: Signal<Option<u32>>,
    section: Signal<Section>,
}

impl Ambient for Scene {
    fn create() -> Self {
        Scene {
            items: Store::new(Keyed::default()),
            selected: Signal::new(None),
            section: Signal::new(Section::Home),
        }
    }
}

Signal, Memo, Trigger, and Store are pointer-sized Copy handles. Copying Scene copies those handles, so event handlers and page functions can access the same state without cloning the underlying values. Item and Section in this example are app-defined types.

Providing it

pub fn root() -> impl Piece {
    day::register_new_window(|| window_shell());   // use the same builder for each new window
    app_menu(menus());
    window_shell()
}

fn window_shell() -> impl Piece {
    Scene::scoped(move |scene| my_ui(scene))            // one Scene per window
}

Every piece built inside reads it back by type:

fn my_page() -> impl Piece {
    let scene = Scene::ambient();
    label(move || scene.selected.get().map(|i| i.to_string()).unwrap_or_default())
}

Pass Scene as an argument when the function allows it. Use Scene::ambient() when a callback signature cannot accept state, such as a navigation page builder. It finds the Scene provided by the enclosing scope.

For state that belongs to the whole app rather than to a window (a login session, a sync engine, a document cache several windows share), use T::app() instead. It creates the value on the reactive root scope the first time anything asks and returns that instance for the rest of the process, from any window, any menu action, and any task.

Two rules

ambient() resolves while a piece builds. Read it in the piece’s body and capture the value; calling it inside a reactive closure works on the first run and panics on the next, because a re-running reaction is no longer inside the scope that provided the value.

let scene = Scene::ambient();                       // ✅ read once, at build
label(move || format!("{:?}", scene.selected.get()))

label(move || format!("{:?}", Scene::ambient().selected.get()))        // ❌ panics when the label re-runs

Per-window state must be created at build time. T::scoped does this for you (it defers through piece_fn). Writing it by hand with with_environment(Scene::create(), …) creates the value in the caller’s scope instead, and since a piece’s construction runs before its build, every window would get the first one’s.

The focused-window rule

App-wide menu actions need to act on the window that is focused when the command runs. Capturing one window’s Scene when installing the menu would keep targeting that window. Use Scene::focused() inside the action instead:

fn front(f: impl Fn(Scene) + 'static) -> impl Fn() + 'static {
    move || if let Some(scene) = Scene::focused() { f(scene) }
}

menu_item("New Item").shortcut(Shortcut::new("n").shift()).action(front(|s| s.new_item()))

T::focused() (SwiftUI’s @FocusedValue) resolves through the key window’s own scope, falling back to the primary window when no secondary one is key, which is the steady state on macOS, where the primary window’s delegate reports no focus events of its own. A window is marked key the moment it is registered, so a command fired immediately after File ▸ New Window already lands on the new window rather than on the one behind it. The reverse holds for a window on its way out: a phone presents a secondary window as a cover and animates its dismissal, and from the moment the close is requested the window behind it is the front one again, so a command fired during that animation acts on it rather than on the departing sheet.

A toolbar already belongs to the piece that declares it. Put its declarations in the window builder, and capture that window’s state directly. See Toolbars.

Why not thread_local!

thread_local! { static SELECTED: Signal<Option<u32>> = Signal::global(None); }   // ❌

A thread-local selection is shared by every window on the UI thread. That may go unnoticed until a second window opens and selecting a row in one window changes the other. Put the selection in Scene when each window should have its own value.

The exception is state that really is process-wide and really has no owner: Signal::global is still the right tool for, say, a network-reachability flag that the whole app observes. T::app() is the typed version of the same idea, and is preferable when the state has more than one field.

The primitives underneath

Ambient is a convenience layer over four functions you can use directly:

with_environment(value, || content)provide value to a subtree
environment::<T>()the nearest provided T, or None
focused_environment::<T>()the T provided by the focused window
app_environment::<T>(make)the app-wide T, created once

All four sit on day-reactive’s scope context (Scope::provide / Scope::use_context), which walks a scope’s ancestors, so “ambient” means provided by an ancestor of the scope this piece is building in.

See also