Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom Widgets and Layouts

Guido ships few widgets on purpose. Container is the box — padding, colours, corners, borders, events — and Text, Image and TextInput are the things that draw content. A checkbox, a toggle, a slider are compositions of those, written as functions or as #[component]s in your own app.

Occasionally that is not enough: you need to draw something the primitives cannot express, or arrange children in a way Flex and ZStack do not. Both are extension points, and both are reachable from outside the crate.

The two preludes

guido::prelude is the application’s vocabulary. Writing a widget or a layout needs a second one:

extern crate guido;
fn main() {
use guido::prelude::*;
use guido::widget_prelude::*;
;
}

widget_prelude adds what the Widget and Layout signatures name — Tree, WidgetId, Constraints, PaintContext, RenderNode, LayoutHints, ChildPaintOptions — and Transform, which is what a widget positions what it draws with. An application never names Transform: it declares translate, rotate and scale, and those compose into one.

It also carries with_signal_tracking and JobType. Neither is needed to write a widget any more — the framework scopes both passes — but they are what a widget reaches for to claim a narrower attribution than its own pass: one dynamic-children segment rather than the whole of it.

A widget

Widget has two required methods. Everything else — event, advance_animations, reconcile_children, layout_hints, register_children, refresh_paint_bounds, and the three a removed widget is asked about its exit, begin_exit, is_exiting and cancel_exit — has a default.

A row that cannot fit its children shrinks the ones that can be narrower, and it learns how narrow from what each recorded during its layout. A widget that records nothing is never shrunk. One that can give room up — it ellipsizes, or scales what it draws — says how far with ctx.set_minimum(size) in its layout, on both axes.

event is only called with what was routed to your widget: a pointer event when the widget can be under its point or the pointer still owes it one, a key when it holds the focus or is on the way to what does. It is not offered every event, so there is no need to check whether one was meant for it — and no way to hear the keyboard from a widget nothing has focused, other than a container’s on_key_down and on_key_up. Event Routing says where each kind goes.

One of those defaults is worth knowing about if your widget draws outside the box it was given. A parent narrows its children to the visible region before painting them, and it does that by their laid-out bounds — so a widget that paints elsewhere, because it transforms itself or casts something past its edges, has to say how far. That is refresh_paint_bounds, which reports it with Tree::set_own_paint_reach: a number for the same distance on every side, or an Outsets for a reach that has a direction, like a shadow cast downward. The framework calls it the same way it calls the other two — inside your widget’s own Paint scope — so read your signals plainly here too, and a write to one repaints your widget without reflowing anything. It runs after every layout and from the paint job, because either can be the last pass to have run.

Read them untracked and you get the bug this closed: a widget culled before it ever painted has read nothing, so nothing can tell it to come back, and a transform that would carry it into view reaches no one.

Which pass you publish from follows what your reach depends on. If it moves with something layout already tracks — a size, a font — publish it from layout, which is what the built-in text widgets do for their stroke and shadow. If it moves with a paint-only property such as a transform, publish it here.

extern crate guido;
use guido::prelude::*;
use guido::widget_prelude::*;
struct Bar {
    extent: Signal<f32>,
    measured: f32,
}

impl Widget for Bar {
    fn layout(&mut self, _ctx: &mut LayoutCtx, _constraints: Constraints) -> Size {
        let extent = self.extent.get();
        self.measured = extent;
        Size::new(extent, extent)
    }

    fn paint(&self, ctx: &mut PaintContext) {
        // Local coordinates: the parent has already placed this node.
        ctx.draw_rounded_rect(
            Rect::new(0.0, 0.0, self.measured, self.measured),
            Color::RED,
            0.0,
        );
    }
}
fn main() {}

Three things are worth spelling out.

Position lives in the Tree, not on the widget. layout measures and reports; the parent decides where the result goes. Paint therefore happens in local coordinates, with (0, 0) at the widget’s own origin.

A layout measures; it does not decide whether it runs. The framework calls it through LayoutCtx::layout_child, and that call holds the check that skips a widget with the same constraints and nothing dirty, the scope that makes the widget’s reads belong to it, the pass it is in, and the cache the size goes into. So there is nothing to remember: read the signals plainly and return a size.

A paint draws; it does not scope itself either. The framework calls it through PaintContext::paint_child, and that call holds the cull test that skips a widget nobody can see, the cache a clean one is served from, and the same scope layout gets — so a signal read while drawing belongs to this widget and repaints this widget. ctx.id() is which widget it is and ctx.tree() is the tree it is in.

The scale is the frame’s. Everything a paint draws is in logical pixels, and the renderer scales it to the surface. A widget that builds a raster of its own — a chart drawn into a buffer, a generated texture — wants one source pixel per device pixel, and ctx.scale_factor() says how many those are: the scale of the frame being painted, fractional where the compositor says so, 1 until it has said anything. Read it while painting and nothing else is needed: a scale change repaints the whole surface, and each surface answers with its own. Round rather than truncate, as the surface’s own buffer does, and keep the raster between paints if building it is not cheap — the sample below rebuilds it every time for brevity.

extern crate guido;
use guido::prelude::*;
use guido::widget_prelude::*;
use std::sync::Arc;
struct Noise;

impl Widget for Noise {
    fn layout(&mut self, _ctx: &mut LayoutCtx, constraints: Constraints) -> Size {
        constraints.constrain(Size::new(100.0, 20.0))
    }

    fn paint(&self, ctx: &mut PaintContext) {
        let scale = ctx.scale_factor();
        let (width, height) = ((100.0 * scale).round() as u32, (20.0 * scale).round() as u32);
        let pixels: Vec<u8> = (0..width * height * 4).map(|i| (i % 251) as u8).collect();
        ctx.draw_image(
            ImageSource::Rgba { width, height, pixels: Arc::from(pixels) },
            Rect::new(0.0, 0.0, 100.0, 20.0),
            ContentFit::Fill,
            None,
        );
    }
}
fn main() {}

This is newer than it looks: until #390 a paint really did have to open its own scope, and a widget that did not subscribed to nothing — it drew once and then stayed as it was. The advice here used to say that the read fell through to the parent container. It did not.

Which pass is running is ctx.measuring(). True while a natural size is being measured, before a surface exists, which is when an animated value reads as where it is going rather than where it is. A widget that animates nothing never has to ask.

A widget that draws a shape no box describes — a chart, a custom outline — builds a Path and hands it to ctx.draw_path; Arcs & Paths has the vocabulary.

A layout

Layout has one method: given the children and the constraints, place them and report the size the parent should use.

extern crate guido;
use guido::prelude::*;
use guido::widget_prelude::*;
struct Stagger {
    step: f32,
}

impl Layout for Stagger {
    fn layout(
        &mut self,
        ctx: &mut LayoutCtx,
        children: &[WidgetId],
        constraints: Constraints,
        origin: (f32, f32),
    ) -> Size {
        let mut size = Size::zero();
        for (i, &child_id) in children.iter().enumerate() {
            let child = ctx.layout_child(child_id, constraints).unwrap_or_default();

            let x = origin.0 + i as f32 * self.step;
            let y = origin.1 + i as f32 * self.step;
            ctx.tree().set_origin(child_id, x, y);

            size.width = size.width.max(x + child.width - origin.0);
            size.height = size.height.max(y + child.height - origin.1);
        }
        constraints.constrain(size)
    }
}
fn main() {}

It plugs into any container:

extern crate guido;
use guido::prelude::*;
use guido::widget_prelude::*;
fn card(_label: &str) -> Container { container() }
struct Stagger {
    step: f32,
}
impl Layout for Stagger {
    fn layout(
        &mut self,
        ctx: &mut LayoutCtx,
        children: &[WidgetId],
        constraints: Constraints,
        origin: (f32, f32),
    ) -> Size {
        let mut size = Size::zero();
        for (i, &child_id) in children.iter().enumerate() {
            let child = ctx.layout_child(child_id, constraints).unwrap_or_default();
            let x = origin.0 + i as f32 * self.step;
            let y = origin.1 + i as f32 * self.step;
            ctx.tree().set_origin(child_id, x, y);
            size.width = size.width.max(x + child.width - origin.0);
            size.height = size.height.max(y + child.height - origin.1);
        }
        constraints.constrain(size)
    }
}
fn main() {
container()
    .layout(Stagger { step: 8.0 })
    .child(card("one"))
    .child(card("two"))
;
}

The absence of a built-in grid is not a gap — this is where a grid goes.

If your layout reads signals of its own (a reactive spacing, an alignment that follows a setting), read them straight from layout: the container runs it inside its own tracking scope, so those reads are attributed correctly without you opening one.

ctx.tree_ref().with_widget(child_id, |w| w.layout_hints()) reports whether a child wants to fill an axis, which is what lets a layout give the remaining space to the children that asked for it, whether it is hidden, which takes no space at all, and whether it is leaving, playing its exit, which a ZStack does not let lead its size. ZStack in the guido source is the short example to read.

A layout can record the least its children can be arranged in the same way, with ctx.set_minimum, reading each child’s with ctx.tree_ref().minimum(child_id). That is what lets the container holding it give room up in a crowded row. One that records nothing keeps its container at the size it was laid out — the stagger above never shrinks — and ZStack’s, which records the largest of its children’s, is the short one to copy.

Checking your work

tests/external_widget.rs in the guido repository is a widget written against the public API only, with a test that lays it out and paints it. It is the shortest complete example of everything on this page.