Skip to content

Mental Model

lurq separates app services from the retained UI tree.

WinitWindow
owns App + Tree
forwards window/input events
requests redraws
App
shared fonts, theme, resources, storage, optional Tokio runtime, menus
Tree
root component/static root
retained nodes and component contexts
layout cache and last layout
input, hover, active, focus, drag, scroll, text selection state
render engine instance/factory
optional DevTools secondary tree

At a high level, one frame does this:

  1. The shell sends window size, scale factor, input events, and redraw events to Tree.
  2. Tree rebuilds dirty component subtrees.
  3. The layout engine computes LayoutResult from the retained node tree and viewport constraints.
  4. The tree resolves render commands from layout.
  5. The selected render engine draws the render list.
  6. Profiling and DevTools snapshots are updated when enabled.

Tree::pass(app, surface) is the low-level pass entry point. WinitWindow calls it for normal desktop apps.

Components return impl Into<Element>, but the runtime stores a retained internal Node tree. That retained tree is why lurq can:

  • reuse component instances across renders,
  • keep signal/store/memo/ref/effect state inside each Ctx,
  • update only dirty component subtrees,
  • preserve scroll, hover, active, focus, drag, and text editing state,
  • cache layout and redraw only when needed,
  • inspect the current tree in DevTools.

A component struct is persistent. Initialize state in create, read current props from ctx.props(), and return fresh UI from render.

struct SearchBox {
query: Signal<String>,
}
impl Component for SearchBox {
type Props = ();
fn create(ctx: &mut Ctx) -> Self {
Self { query: ctx.signal(String::new()) }
}
fn render(&self, _ctx: &mut Ctx) -> impl Into<Element> {
lurq::components::TextInput::new(self.query.clone())
.placeholder("Search")
.width(240.0)
}
}

Signals mark the owning context dirty. Refs do not.

Parents mount children through Ctx:

ctx.mount::<Header>(HeaderProps { title: "Docs" })
ctx.mount_keyed::<RowItem>(&item.id, item.clone())
ctx.mount_with::<Panel>(PanelProps { title: "Details" }, children)

Unkeyed mounts are matched by slot position and component type. Keyed mounts are matched by key and component type. Use keyed mounts for lists that reorder.

Layout is parent-down, child-up:

  1. Parent gives constraints to child.
  2. Child chooses a concrete size inside those constraints.
  3. Parent positions the child.

Row, Column, and Stack are the main containers. Modifiers such as .padding(...), .width(...), .flex(...), .align(...), .clip(), and .absolute_position(...) wrap the current element with layout behavior.

Input is resolved against the latest layout. The tree tracks hover path, active path, focus, dragging, scroll state, and cursor. Event handlers are attached with node modifiers:

use lurq::app::events::MouseEvent;
Text::new("Save")
.cursor(CursorIcon::Pointer)
.hovered(|style| style.background("#334155"))
.active(|style| style.background("#0f172a"))
.on_click(|event: MouseEvent| {
println!("clicked at {}, {}", event.x, event.y);
})

Nodes can also carry HTML-like id/class attributes (.id("save"), .class("row")) for browser-style lookup: tree.get_element_by_id("save") for reads and tree.get_element_by_id_mut("save") for mutation and typed interaction (click(), as_text_input().set_value(..)). A stable explicit ID also participates in node reconciliation, helping focus follow a control across sibling insertion or reordering. Classes only label nodes for lookup; neither attribute is a CSS styling selector. See Runtime And Retained Tree.

With the devtools feature, Tree::mount_devtools(&mut app) creates a secondary tree that renders with the same render engine factory. The main tree periodically syncs a snapshot into the DevTools tree during pass().

This means DevTools should follow the same layout/render/event rules as any other lurq UI. It also means debug metadata is feature-gated so production builds do not store signal values, prop trees, or profiler detail unless devtools is enabled.