Ctx
Overview
Section titled “Overview”Ctx is the per-component render context. It is passed to Component::create and Component::render.
Use it to:
- create reactive state owned by the component
- mount child components
- pass context values down the tree
- read slot children supplied by a parent
- create element refs and interaction state
- register effects, watchers, keyed list slots, and error boundaries
use lurq::{ app::{component::Component, ctx::Ctx}, core::Signal, node::Element,};
struct Counter { count: Signal<i32>,}
impl Component for Counter { type Props = ();
fn create(ctx: &mut Ctx) -> Self { Self { count: ctx.signal(0) } }
fn render(&self, _ctx: &mut Ctx) -> impl Into<Element> { let count = self.count.clone(); lurq::components::Text::new(&format!("Count: {}", self.count.get())) .on_click(move |_| count.update(|n| *n += 1)) }}Dirty State
Section titled “Dirty State”ctx.is_dirty();is_dirty reports whether this component context is marked dirty. Runtime uses this internally to decide whether a
component subtree needs to render again.
Application code usually does not need to call it.
let props = ctx.props::< Self::Props>();props returns the current props for the component that owns this context. Component props must implement PartialEq;
reused child components rerender when the incoming props differ from the props stored on their context.
Manual Root Contexts
Section titled “Manual Root Contexts”let mut ctx = Ctx::new_root();new_root creates a standalone root context. Runtime normally creates and owns root contexts for mounted components, so
application code rarely needs this directly. It is useful for tests and low-level component mounting.
Standalone contexts do not have a runtime theme unless one is attached internally by Tree.
Shared App Services
Section titled “Shared App Services”Mounted contexts retain a clone of the shared App handle. ctx.app_ref() and ctx.app_ref_mut() access those services safely even after the caller moves or drops its original handle. A standalone Ctx::new_root() has no app services until attached by the runtime.
With persistent_storage, use ctx.persistent_value::<T>(key), ctx.set_persistent_value(key, value), ctx.read_bulk(...), and ctx.write_bulk(...) for app-wide storage. Storage reads are not reactive; see Persistent Storage for typed values, file configuration, and bulk operations.
Signals
Section titled “Signals”let count = ctx.signal(0);
count.get();count.set(1);count.update( | n| * n += 1);ctx.signal(initial) creates a Signal<T> and wires it to the current component. When the signal changes, the
component context is marked dirty.
Use Signal for state that should trigger a render when it changes.
With the devtools feature enabled, T must implement DevtoolsInspectable so DevTools can show signal values.
Without devtools, Signal<T> has no debug-inspection bound.
Batch Updates
Section titled “Batch Updates”ctx.batch(| | {count.set(1);count.set(2);});batch defers dirty marking for contexts in the same component tree until the closure completes.
Stores And Lenses
Section titled “Stores And Lenses”#[derive(Clone, lurq::DevtoolsInspectable)]struct User { name: String, age: u32,}
let user = ctx.store(User { name: "Ada".into(), age: 36 });let name = user.lens(| user| user.name.clone(),| user, name| user.name = name,);
name.set("Grace".into());ctx.store(initial) creates structured reactive state. Like signals, store updates mark the owning component dirty.
Use lenses when child code should read or update one field without taking ownership of the whole store value.
let count = ctx.signal(0);let doubled = ctx.memo({let count = count.clone();move | | count.get() * 2});
let value = doubled.get();ctx.memo(f) creates a derived value. The memo tracks reactive reads inside f and recomputes when those dependencies
change.
let latest_id = ctx.create_ref::<Option<u64> > (None);latest_id.set(Some(42));create_ref creates persistent non-reactive state. Updating a ref does not mark the component dirty.
Use refs for handles, cached values, counters, or other state that should survive renders but should not cause renders.
Effects
Section titled “Effects”let count = ctx.signal(0);
ctx.on_effect({let count = count.clone();move | | println ! ("count = {}", count.get())});on_effect runs immediately and reruns when any tracked reactive value read inside the effect changes.
Effects are retained by the context, so they live as long as the component context lives.
Watchers
Section titled “Watchers”let count = ctx.signal(0);
ctx.watch( & count, | value| {println ! ("count changed to {value}");});watch subscribes to a specific signal and keeps the subscription alive for the context lifetime.
Use watch when you want an explicit callback for one signal instead of automatic dependency tracking.
The callback may set the watched signal. Its notification comes after the current one; see writes from callbacks.
Context Values
Section titled “Context Values”Static Context
Section titled “Static Context”#[derive(Clone)]struct Locale(String);
ctx.provide(Locale("en-US".into()));Descendants can read the value by type:
if let Some(locale) = ctx.use_context::<Locale>() {println ! ("locale = {}", locale.0);}provide stores a cloned value by type. use_context returns None if neither this component nor an ancestor
provided that type. A provided value shadows an ancestor’s value of the same type for this component and its
descendants.
How long a value stays provided depends on where it is provided:
- In
create: for the component’s lifetime. It survives re-renders of the component and of its ancestors (the inherited contexts are refreshed and the component’s own values are layered back on top) until the component provides another value of the same type. - In
render: for that render. Code after theprovidecall and the children mounted by that render see it; the next render starts from the inherited andcreate-time values, so a value the render no longer provides is removed for the children it mounts from then on. Providing inrendergives the value a new revision, which re-renders the reused children that receive it.
create_context follows the same rules.
Reactive Context
Section titled “Reactive Context”let theme_name = ctx.create_context("light".to_string());theme_name.set("dark".to_string());Descendants can consume it:
let theme_name = ctx.consume_context::<String>().unwrap();let current = theme_name.get();create_context stores a ReactiveContext<T> and subscribes the creating context to changes. consume_context
retrieves the reactive context and subscribes the consuming context to changes.
ReactiveContext<T> requires T: Clone + Hash + Send + Sync + 'static so it can detect meaningful value changes.
theme() returns the current runtime theme. Root and child contexts get the theme from Tree::mount_root.
Theme typography exposes named text styles. Text::new uses theme.typography().body, and
Text::new("Label").variant(TypographyStyle::Label) resolves the named style during layout.
See Theme for the full palette, typography, radius, spacing, and form role tables.
use lurq::{ app::theme::{PaletteColor, SpacingSize, TypographyStyle}, layout::text_style::TextStyle, node::color::Color,};
ctx.theme().set_palette_color(PaletteColor::Accent, Color::from_hex("#2563eb"));ctx.theme().set_spacing_value(SpacingSize::Sm, 8.0);
ctx.theme().set_typography_style(TypographyStyle::Body, TextStyle {font_size: 16.0,..TextStyle::default ()});
ctx.theme().set_typography_style(TypographyStyle::Label,TextStyle {font_size: 13.0,..TextStyle::default ()},);Use strict theme keys with APIs such as .background(PaletteColor::Accent),
Text::new("Label").variant(TypographyStyle::Label), .rounded(RadiusSize::Md), .spacing(SpacingSize::Sm), and
.padding(SpacingSize::Md). The lookup methods palette_color, typography_style, radius_value, and spacing_value
are also available when component code needs the concrete value.
Components that call ctx.theme() during render subscribe to theme version changes. Mutating that theme rerenders those
subscriber components on the next pass. Use theme.lens(getter, setter) when component code needs a focused handle for
one theme value:
let brand = ctx.theme().lens(| theme| theme.palette_color(PaletteColor::Accent),move | theme, color| theme.set_palette_color(PaletteColor::Accent, color),);
brand.set(Color::from_hex("#2563eb"));Only call theme() from a context managed by runtime. A manually-created root context without a theme will panic.
Window
Section titled “Window”let window = ctx.window();
let logical = window.logical_size();let minimized = window.is_minimized;let full_screen = window.is_full_screen;let decorated = window.is_decorated;ctx.window() returns a reactive window handle. Reading it subscribes the component to window resize, move, scale,
minimized, fullscreen, and decoration-state changes.
The handle dereferences to WindowInfo, so geometry helpers such as position(), resolved_size(), logical_size(),
logical_width(), and logical_height() are available directly.
Window commands are queued and applied by the active platform shell:
let window = ctx.window();
window.close();window.set_minimized(true);window.set_full_screen(true);window.set_decorations(false);window.set_title("Report.md - Editor");window.set_title_bar_color(lurq::node::color::Color::from_hex("#101215"));window.set_icon(lurq::app::WindowIcon::from_rgba(vec![255, 0, 0, 255], 1, 1));window.set_corner_radius(lurq::app::WindowCornerRadius::RoundedSmall);window.set_border_color(lurq::app::WindowBorderColor::None);window.resize(1280, 720);window.move_to(120, 80);resize(width, height) asks for a client size in physical pixels. A minimized window is restored first, since
Windows does not resize a minimized window. A maximized or full-screen window stays in its mode: a resize never takes
the user out of full screen. While a window is minimized its resolved_* size is its client area (a
sliver on Windows), and WinitWindow::on_size_changed is not called: it reports the window’s size once per change, never
a minimized one.
close() bypasses close handlers. Use request_close() for a vetoable request and on_close_requested(...) to retain, accept, or cancel it. See Window lifecycle and native menus.
Use ctx.window_opener() for a cloneable handle that opens secondary windows. ctx.breakpoint() and ctx.responsive(...) subscribe to viewport breakpoint changes; see Theme.
Use set_decorations(false) or set_decorated(false) for a custom title bar. Rust reserves move as a keyword, so
direct move calls use window.r#move(x, y); move_to(x, y) is provided for normal method syntax.
set_title changes the OS window title after the window has opened: the title bar text, the taskbar button and
Alt+Tab on Windows, and the Window menu and Mission Control on macOS. title() returns the title last requested, or the
title the window was created with (WinitWindow::with_title, the secondary window’s title); it does not subscribe the
component. Setting the title the window already has queues nothing, so a component can call
ctx.window().set_title(...) from render with a title derived from its state, for example the active tab.
set_icon accepts a WindowIcon built from RGBA pixels (None clears it). On Windows the icon is both the small
title-bar icon and the big icon of the taskbar button and Alt+Tab, scaled by Windows to each size; on macOS it does
nothing, since the Dock and the app switcher show the application bundle’s icon. set_title_bar_color and set_corner_radius customize native
window chrome where the platform supports it; with the winit shell, title bar color maps to the Windows title background
API, while corner radius maps to the Windows corner preference API and macOS AppKit content-view layer clipping.
set_border_color sets the 1px compositor border Windows 11 (build 22000+) draws around every window, including
undecorated ones (DWMWA_BORDER_COLOR): WindowBorderColor::Default, None, or Color(...). border_color() returns
the value the shell last applied. Unsupported platforms no-op. Use clear_icon(), clear_title_bar_color(),
reset_corner_radius(), and set_border_color(WindowBorderColor::Default) to return those settings to the platform
default.
For normal custom desktop chrome, prefer WindowChrome. It disables native decorations when custom chrome is active,
renders the draggable title bar and content area, owns resize hit zones, handles standard window controls, and uses the
active shell’s native drag/resize behavior where available. It also hides the compositor border, because
ChromeBorderPolicy decides the frame outline.
use lurq::{ components::{ChromeTitleBar, Text, WindowChrome, WindowControls}, node::color::Color,};
WindowChrome::new() .title_bar( ChromeTitleBar::new() .leading(Text::new("My App").padding_horizontal(12.0)) .trailing(Text::new("ready").color(Color::from_hex("#94a3b8"))) .controls(WindowControls::new()), ) .content(app_content(ctx)) .overlay(fullscreen_modal_layer(ctx)) .mount(ctx)window.start_drag(), window.start_resize(direction), and window.stop_drag() remain available as low-level escape
hatches for custom shells or highly specialized chrome. If you use them directly, start native drag or resize from the
left mouse-button press handler and stop propagation so underlying widgets do not also handle the same press:
use lurq::{ app::{ events::{MouseButton, MouseEvent}, WindowResizeDirection, }, components::Rect, node::CursorIcon,};
let window = ctx.window();
Rect::new(8.0, 8.0).cursor(CursorIcon::NwseResize).on_mouse_down( move | event: MouseEvent| {if event.button == MouseButton::Left {window.start_resize(WindowResizeDirection::SouthEast);event.prevent_default();event.stop_immediate_propagation();}})With the winit shell, start_drag() and start_resize(...) use native platform APIs where possible. On Windows, the
shell posts a non-client mouse press at the cursor position, so the system move or size loop runs from the event loop
rather than inside your handler. The window repaints at each new size during a live edge drag.
Frame Capture
Section titled “Frame Capture”With the screenshot feature (devtools enables it automatically), the window handle can capture the next fully
composed frame to a PNG file:
let window = ctx.window();
window.screenshot("frame.png");
window.screenshot_region("toolbar.png", lurq::app::ScreenshotRegion { x: 0.0, y: 0.0, width: 480.0, height: 64.0,});
let card_ref = ctx.element_ref(); // attached elsewhere with .ref_element(card_ref.clone())window.screenshot_node("card.png", &card_ref);screenshot() captures the whole window. screenshot_region() crops to a logical-pixel window region — the same units
as element bounds() — which is converted to physical pixels and clamped to the viewport at capture time; a region with
no visible area is skipped with a warning. screenshot_node() reads an ElementRef’s bounds from the last completed
layout pass and crops to them, so capture after the element has painted.
Synthetic Input
Section titled “Synthetic Input”inject_input and inject_inputs queue synthetic events that the shell delivers inside its event loop, exactly where
the matching OS events would have arrived. Positions are physical pixels in the window’s coordinate space — the same
units a captured frame is measured in.
use lurq::app::SyntheticInput;
let window = ctx.window();
window.inject_input(SyntheticInput::click(120.0, 48.0));window.inject_inputs(SyntheticInput::text("hello"));SyntheticInput provides constructors for mouse moves, presses, releases, clicks, wheel scrolls, key presses, and text
entry, plus with_modifiers for shift/ctrl/alt/meta combinations.
Slot Children
Section titled “Slot Children”Parents pass slot children with mount_with or mount_keyed_with:
ctx.mount_with::<Panel>(PanelProps { title: "Info" }, vec![ lurq::components::Text::new("Panel body").into(),])The child component reads them through its own context:
fn render(&self, ctx: &mut Ctx) -> impl Into<Element> { let child_count = ctx.children().len();
lurq::components::Column::new() .child(lurq::components::Text::new(&self.title)) .child(lurq::components::Text::new(&format!("{child_count} slot children")))}ctx.has_children();ctx.children();children() returns an empty slice when no slot children were provided. Clone elements from this slice when forwarding them into a container, for example Column::new().with_children(ctx.children().iter().cloned()).
Element Refs
Section titled “Element Refs”let element_ref = ctx.element_ref();
lurq::components::Rect::new(100.0, 40.0).ref_element(element_ref.clone())After layout, the ref exposes the element rect:
let (x, y, width, height) = element_ref.rect();let attached = element_ref.is_attached();let hovered = element_ref.hovered();let active = element_ref.active();let focused = element_ref.focused();Use element refs when code outside normal layout traversal needs an element’s measured rect or current interaction flags.
Request focus with ctx.focus(&element_ref) after retaining the ref in create or obtaining its render slot. The request runs after reconciliation, including for newly mounted fields; the last request wins, and absent targets are ignored. element_ref.focused() tracks focus reactively during render; focus_signal() exposes the same state as a signal.
Refs created during render are retained by call position. Refs created in create must be stored on the component. Attach each ref to one live node.
Element refs can also scope outside-click hooks:
let panel_ref = ctx.element_ref();ctx.on_click_outside(panel_ref.clone(), | _ | {println ! ("clicked outside the panel");});
lurq::components::Rect::new(240.0, 160.0).ref_element(panel_ref)on_click_outside fires on left clicks whose pointer position is outside the referenced element’s measured bounds. The
hook is render-scoped: if the component stops calling it, the listener is removed on the next render.
ctx.element_ref_mut() returns an owned core::ElementRefMut, the same type returned by Tree::find_element_mut. These refs can be retained across passes; the borrowed ElementHandle returned by Tree::get_element_by_id_mut must be resolved again for each lookup:
let element_ref = ctx.element_ref_mut();
lurq::components::Rect::new(100.0, 40.0).ref_element(element_ref.clone());
element_ref.set_relative_bounds(15.0, 20.0, 120.0, 60.0);Interaction State
Section titled “Interaction State”let state = ctx.interaction();
lurq::components::Rect::new(100.0, 40.0).interactive(state.clone()).on_mouse_enter( | | println!("hover"))InteractionState tracks runtime interaction flags:
state.is_hovered();state.is_active();state.is_focused();Hover, active, and focus are updated by runtime input dispatch.
Mounting Child Components
Section titled “Mounting Child Components”ctx.mount::<Counter>(());ctx.mount_keyed::<TodoItem>(todo.id.as_str(), todo.clone());mountmatches children by slot position and component type.mount_keyedmatches by key and component type within the parent context, allowing keyed children to move between slots.- Matching children reuse the existing component instance and context.
- Non-matching children are unmounted and replaced.
Use keyed mounts for dynamic lists where identity matters.
Mounting With Slot Children
Section titled “Mounting With Slot Children”ctx.mount_with::<Panel>(props.clone(), vec![lurq::components::Text::new("body").into()]);ctx.mount_keyed_with::<Panel>("settings", props, vec![lurq::components::Text::new("body").into()]);These work like mount and mount_keyed, but pass slot children into the child context.
mount_offstage::<C>(props, active) and mount_keyed_offstage::<C>(key, props, active) retain a component while excluding its output when active is false. Offstage components keep state but do not participate in layout, painting, hit testing, dirty refreshes, timers, or future polling until active again. Tasks they already run on Tokio keep running, and their results are applied once the component is active again; see Task lifetime. When a component becomes active again, its output takes back the runtime state it had when it went offstage: scroll offsets, text-input carets and selections, open selects, and canvases. This includes scroll areas whose ScrollState the component does not hold, so a tabbed page does not need to keep one per scroll area to keep its scroll position. Router::mount_offstage behaves the same for the routed page.
Keyed List Helper
Section titled “Keyed List Helper”let elements = ctx.for_each(self .items.get(),| item| item.id,| _ctx, item| {lurq::components::Row::new().child(lurq::components::Text::new(& item.title))},);
lurq::components::Column::new().with_children(elements)for_each creates keyed child contexts for arbitrary render closures, not just Component implementations. It is
useful when each list item needs its own local context for child mounts, effects, or refs.
Error Boundary
Section titled “Error Boundary”ctx.error_boundary(| ctx| risky_component(ctx),| | lurq::components::Text::new("Something went wrong"),)error_boundary catches panics from the component closure and returns the fallback element instead.
Timers
Section titled “Timers”use std::time::Duration;
let timeout = ctx.create_timeout(Duration::from_secs(2), | | { /* fires once */ });timeout.start();
let interval = ctx.create_interval(Duration::from_millis(500), | | { /* fires repeatedly */ });interval.start();Timeout has .start(), .restart(), .cancel(), and .is_active(). Interval has .start(), .restart(),
.stop(), and .is_active(). Create timers in Component::create and store them in the struct.
See Futures And Timers for full details.
Futures
Section titled “Futures”let handle = ctx.future(deps, | deps| async move {Ok::< _, String > ("result".to_owned())});let state = handle.state().get();ctx.future runs an async operation that restarts when deps changes. Returns a FutureHandle with a reactive
Signal<FutureState<T, E>>. Use it for finite async work with one result. For continuous subscriptions, use
ctx.stream; manually chaining ctx.future completions can leave a render-dependent re-arm gap.
let handle = ctx.stream(deps, |deps, emitter: StreamEmitter<String, String>| async move { loop { let item = next_item(deps).await; if !emitter.emit(item) { break; } }});let latest = handle.state().get();ctx.stream runs a dependency-keyed async producer that can emit many values through StreamEmitter. Returns a
StreamHandle with a reactive Signal<FutureState<T, E>>.
let action = ctx.future_action( | args: String| async move {Ok::< _, String > (args)});action.run("go".to_owned());ctx.future_action creates a future that only runs when .run(args) is called.
See Futures And Timers for full details.
Queries
Section titled “Queries”Requires the query feature. Provide a QueryClient once in a stable component’s create method, then observe named query descriptors during render:
let user = ctx.query(get_user(user_id));let data = user.data();
let queries = ctx.query_client();queries.invalidate(get_user(user_id));queries.invalidate(get_user::all());The #[lurq::query] macro defines the descriptor constructor and its all() selector. Queries share cached results and running requests across components. See Queries for setup, state, invalidation, and lifecycle details.
Requires the form feature.
let form = ctx.form(FormOptions::new().field("user", "Ada")).on_submit( | values| { /* handle submission */ });Returns a FormHandle that owns field signals and a submit callback. See Forms for full details.
Routing
Section titled “Routing”Requires the router feature.
let router = ctx.router(Routes::new().route("/", | _ctx| Element::new()));let navigator = ctx.navigator();let path = ctx.route_path();let params = ctx.route_params();ctx.router creates a RouterHandle from a route table. ctx.navigator reads the current router navigator from
context, and route_path / route_params expose the current match during render.
See Routing for route definitions, layouts, links, guards, and testing patterns.
Internationalization
Section titled “Internationalization”Requires the i18n feature.
let label = ctx.t("hello");let greeting = ctx.t_args("welcome", [("name", "Ada")]);let ns_label = ctx.t_ns("errors", "not_found");let i18n = ctx.i18n();Translation lookups are reactive — components re-render when the locale changes. See Internationalization for full details.
Modals
Section titled “Modals”lurq::components::Modal::new(lurq::components::Text::new("Modal content")).open( self .open.clone()).target(lurq::components::Root);Modals are render-flow components. Use Modal::new(...).open(signal) and choose a target with Parent, Root, or an
ElementRef. See Modals for full details.
Render Lifecycle Methods
Section titled “Render Lifecycle Methods”ctx.begin_render();begin_render resets the child cursor before rendering children. Runtime and the component mounting internals call this
as part of normal rendering.
Application components normally should not call render lifecycle methods directly.