Components
Components
Section titled “Components”Overview
Section titled “Overview”Components are structs that implement Component. They hold persistent state and return an Element tree from
render.
See Ctx for the full Ctx API used inside create and render, and Reactivity for signals,
stores, memos, effects, and contexts.
use lurq::{ app::{component::Component, ctx::Ctx}, core::Signal, layout::{Alignment, text_style::{FontWeight, TextStyle}}, node::{Element, color::Color},};
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 dec = self.count.clone(); let inc = self.count.clone(); let value = self.count.get();
lurq::components::Row::new() .spacing(12.0) .align_items(Alignment::Center) .child( lurq::components::Rect::new(36.0, 36.0) .background("#ef4444") .rounded(6.0) .on_click(move |_| dec.update(|n| *n -= 1)), ) .child(lurq::components::Text::styled( &format!("{value}"), TextStyle { font_size: 24.0, weight: FontWeight::Bold, color: Color::from_hex("#1e293b"), ..TextStyle::default() }, )) .child( lurq::components::Rect::new(36.0, 36.0) .background("#22c55e") .rounded(6.0) .on_click(move |_| inc.update(|n| *n += 1)), ) }}Component Trait
Section titled “Component Trait”pub trait Component: Send + Sync + 'static { type Props: Send + PartialEq + 'static;
fn create(ctx: &mut Ctx) -> Self; fn render(&self, ctx: &mut Ctx) -> impl Into<Element>;
fn after_layout(&self) {} fn on_mounted(&self) {} fn on_unmounted(&self) {}}| Method | Called | Purpose |
|---|---|---|
create | Once, when the component is mounted | Initialize persistent state |
render | On mount and when the component is dirty | Return the current element tree |
after_layout | After a committed layout, with element refs updated | Read measurements or initialize Canvas drawing |
on_mounted | After first render | Setup hooks that need a mounted component |
on_unmounted | Before the component is removed | Cleanup |
Components receive props through the Props associated type. The current props are stored on the component context and
can be read with ctx.props::<Self::Props>().
struct Greeting;
#[derive(Clone, PartialEq, lurq::DevtoolsInspectable)]struct GreetingProps { name: String,}
impl Component for Greeting { type Props = GreetingProps;
fn create(_ctx: &mut Ctx) -> Self { Self }
fn render(&self, ctx: &mut Ctx) -> impl Into<Element> { let props = ctx.props::<Self::Props>(); lurq::components::Text::new(&format!("Hello, {}!", props.name)) }}Use () for components with no props. Reused components rerender when their props compare unequal, so custom props must
implement PartialEq.
Read changing props in render; copying them into the struct only in create would keep the initial value after a parent updates them.
When the devtools feature is enabled, props must also implement DevtoolsInspectable.
#[derive(Clone, PartialEq, lurq::DevtoolsInspectable)]struct GreetingProps { name: String,}Mounting Children
Section titled “Mounting Children”Mount child components inside render with Ctx.
fn render(&self, ctx: &mut Ctx) -> impl Into<Element> { lurq::components::Column::new() .spacing(16.0) .child(ctx.mount::<Header>(HeaderProps { title: "App" })) .child(ctx.mount::<Counter>(())) .child(ctx.mount::<Footer>(()))}Unkeyed Mounts
Section titled “Unkeyed Mounts”ctx.mount::<C>(props) matches children by position and component type. If the same component type stays at the same
slot, its instance is reused. The child rerenders when its props change or its own context is dirty.
Keyed Mounts
Section titled “Keyed Mounts”ctx.mount_keyed::<C>(key, props) matches children by key and type. Use keyed mounts for lists that can reorder.
lurq::components::Column::new().with_children( self.items.get().iter().map(|item| { ctx.mount_keyed::<TodoItem>(&item.id, item.clone()) }),)Slot Children
Section titled “Slot Children”Use mount_with or mount_keyed_with when a component needs children supplied by its parent.
ctx.mount_with::<Panel>(PanelProps { title: "Tools" }, vec![ lurq::components::Text::new("content").into(),])Custom Window Chrome
Section titled “Custom Window Chrome”WindowChrome is a strict wrapper for custom desktop chrome. It disables native decorations when custom chrome is active,
renders the title bar and content area, owns the draggable title-bar behavior, adds resize hit zones, and provides
standard window controls.
use lurq::{ components::{ChromeTitleBar, Text, WindowChrome, WindowControls}, node::color::Color,};
fn render(&self, ctx: &mut Ctx) -> impl Into<Element> { 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().on_close(|| { // Optional app cleanup before the window closes. })), ) .content(app_content(ctx)) .overlay(fullscreen_modal_layer(ctx)) .mount(ctx)}WindowControls::on_close is a cleanup callback before unconditional close. For a confirmation dialog, use a custom button calling window.request_close() as shown in Window lifecycle.
Apps provide title-bar slots and content. They should not manually call window.start_drag() or render resize handles for
normal custom chrome. WindowChrome handles title-bar drag, double-click maximize, resize edge and corner hit zones,
control-button event propagation, and platform drag/resize fallbacks in the active shell.
Use WindowChrome::overlay(...) for fullscreen modals or app-level overlay layers that should cover content but stay
below chrome resize hit zones. WindowChrome renders content first, then overlays, then chrome borders and resize
handles, so modal layers do not make an undecorated window impossible to resize.
ChromeBorderPolicy decides the frame outline. PlatformDefault paints a 1px #252a32 line on Windows and none
elsewhere; Visible { size, color } paints the given line; Hidden paints none. Custom chrome also sets
WindowBorderColor::None on the window, because Windows 11 DWM otherwise draws its own 1px border around undecorated
windows, which would show even with Hidden. The title bar’s bottom line is separate: ChromeTitleBar::new() draws a
1px #252a32 bottom border, and border_bottom(None) removes it.
Use WindowChromeProps for policy-level changes:
use lurq::{ components::{ChromeBorderPolicy, ResizeHandlePlacement, ResizeHandlePolicy, WindowChromeMode, WindowChromeProps}, node::color::Color,};
let props = WindowChromeProps::new() .mode(WindowChromeMode::AlwaysCustom) .resize_handles(ResizeHandlePolicy::Enabled { size: 6.0 }) .resize_placement(ResizeHandlePlacement::Overlay) .border(ChromeBorderPolicy::Visible { size: 1.0, color: Color::from_hex("#252a32").into(), });Resize hit zones are invisible strips (3 px by default) along each edge and corner. With the default
ResizeHandlePlacement::Overlay, they cover the outermost pixels of the content, and the content fills the window below
the title bar: a 1440 px wide window gives 1440 px of content. A press in those pixels starts a resize instead of
reaching the content. ResizeHandlePlacement::Inset reserves a gutter instead: content and overlays are inset by the
handle size on the left, right, and bottom, and the gutter is painted with WindowChrome::frame_background (or the
content background). Maximized and fullscreen windows have no resize handles or gutter.
WindowChrome::metrics() returns WindowChromeMetrics, which can be used for modal or overlay coordinate adjustment
when a component needs to reason about the custom chrome content area. Metrics include whether custom chrome is enabled,
the title-bar height, resize handle size and placement, and border size. content_x, content_width, and
content_height subtract the resize gutter only with ResizeHandlePlacement::Inset.
Styling Window Controls
Section titled “Styling Window Controls”By default, Windows-style controls are 46 px wide text glyphs with a red close hover. Each control’s content and colors
can be replaced. Colors accept Color, hex strings, and PaletteColor roles, including PaletteColor::Extra:
use lurq::{ app::theme::{PaletteColor, TypographyStyle}, components::{WindowControlContent, WindowControlKind, WindowControls},};
// "icon" is an app typography role that selects the icon font at 14 px;// the ICON_* constants are that font's code points.let icon = |glyph: &str| WindowControlContent::glyph(glyph, TypographyStyle::extra("icon"));
let controls = WindowControls::new() .button_size(46.0, 32.0) .content(WindowControlKind::Minimize, icon(ICON_MINIMIZE)) .content(WindowControlKind::Maximize, icon(ICON_MAXIMIZE)) .content(WindowControlKind::Restore, icon(ICON_RESTORE)) .content(WindowControlKind::Close, icon(ICON_CLOSE)) .foreground(PaletteColor::TextSecondary) .hover_background(PaletteColor::extra("chrome_hover")) .active_background(PaletteColor::extra("chrome_active"));hover_background and active_background apply to every control, close included. To style one control, for example
to keep a red close hover, use control_colors(kind, WindowControlColors { .. }).
WindowControlContent::element(|foreground| ...) builds any other content, such as an SVG icon, and receives the
configured foreground color. Hover and active change the background only; the foreground stays the same. Maximize
content is shown while the window is restored and Restore content while it is maximized.
macOS traffic lights use TrafficLightColors. The defaults are the macOS 11+ colors sampled from the system controls,
because Apple does not publish them: close #FF5F57, minimize #FEBC2E, zoom #28C840. Override them with
WindowControls::traffic_lights(TrafficLightColors { .. }).
Built-In DnD Components
Section titled “Built-In DnD Components”DragContainer, Draggable, and DropZone are real components. Use their mount helpers for the explicit one-child
API.
Draggable and DropZone are blank behavior wrappers. Each requires exactly one slot child and leaves layout, sizing,
and initial positioning to that child.
DragContainer requires exactly one slot child as the drag surface. By default, DragContainerProps::new() bounds
descendant draggables to that surface.
use lurq::components::{DragContainer, DragContainerProps, Draggable, DraggableProps, Rect, Stack};
fn render(&self, ctx: &mut Ctx) -> impl Into<Element> { let card = Draggable::mount( ctx, DraggableProps::new().on_drag_move(|event| { println!("drag delta: {}, {}", event.delta_x, event.delta_y); }), Rect::new(64.0, 64.0) .background("#3b82f6") .absolute_position(24.0, 24.0), );
DragContainer::mount( ctx, DragContainerProps::new(), Stack::new() .size(360.0, 220.0) .child(card), )}Use DragContainerProps::new().bounds(DragBounds::None) for an unbounded drag surface.
DropZone marks its single child as a drop target. Visual styling is supplied by that child.
use lurq::components::{DropZone, DropZoneProps, Rect};
fn render(&self, ctx: &mut Ctx) -> impl Into<Element> { DropZone::mount( ctx, DropZoneProps::new().on_drop(|event| { println!("dropped from {:?} onto {:?}", event.source_id, event.target_id); }), Rect::new(140.0, 80.0) .background("#22c55e33") .border_inside(1.0, lurq::node::color::Color::from_hex("#22c55e")), )}Use DraggableProps::on_drag_start, on_drag_move, and on_drag_end for high-level draggable callbacks. Low-level
node handlers with the same names remain available for custom behavior. The runtime keeps the active drag captured
across rerenders and dispatches on_drop to the hit DropZone on release.
Signal
Section titled “Signal”let count = ctx.signal(0);
count.get(); // tracked readcount.get_untracked(); // untracked readcount.set(42); // replacecount.update( | n| * n += 1); // mutate in placecount.with( | n| format!("{n}")); // tracked borrowWriting to a signal marks the owning component dirty. Runtime rebuilds dirty component output before layout, rendering, event dispatch, and by-ID/by-class element lookup.
let count = ctx.signal(0);let doubled = ctx.memo({let count = count.clone();move | | count.get() * 2});
let value = doubled.get();A memo tracks signals read during computation and updates dependents only when its value changes.
let handle = ctx.create_ref::<Option<u64> > (None);
handle.set(Some(123));let current = handle.get();Refs persist across renders but are not reactive.
Create signals, stores, memos, refs, effects, and watchers in create and retain their handles on the component. Calling these constructors repeatedly during render creates new state or subscriptions. Render-scoped APIs such as ctx.future, ctx.stream, ctx.future_action, and ctx.query retain their own call slots.
Use stores and lenses for structured reactive state.
let user = ctx.store(User { name: "Ada".into(), age: 36 });let name = user.lens(| u| u.name.clone(),| u, name| u.name = name,);name.set("Grace".into());Effects And Watchers
Section titled “Effects And Watchers”let count = ctx.signal(0);
ctx.on_effect({let count = count.clone();move | | println ! ("count = {}", count.get())});Effects run immediately and rerun when any tracked signal read inside the effect changes.
ctx.watch( & count, | value| {println ! ("count changed to {value}");});Watchers run when the watched signal changes.
Dirty Tracking
Section titled “Dirty Tracking”The high-level cycle is:
- Component reads reactive state during
render. - A signal/store/memo update marks the component dirty.
- Runtime detects dirty components before work that needs a fresh tree.
- Dirty component subtrees are rendered again.
- Layout/render/event lookup use the updated internal tree.
Parent components do not force child components to recreate if the child slot still matches.
Lifecycle
Section titled “Lifecycle”impl Component for MyComponent { type Props = ();
fn create(_ctx: &mut Ctx) -> Self { Self }
fn render(&self, _ctx: &mut Ctx) -> impl Into<Element> { lurq::components::Text::new("mounted") }
fn on_mounted(&self) { println!("mounted"); }
fn on_unmounted(&self) { println!("unmounted"); }}Batch Updates
Section titled “Batch Updates”ctx.batch(| | {signal_a.set(1);signal_b.set(2);signal_c.set(3);});Batching coalesces context dirty propagation until the batch ends.