Skip to main content

Module authoring

Module authoring 

Source
Expand description

The widget-authoring toolkit: the shared child plumbing, event routing and callback erasure every container/interactive widget is assembled from — the surface for authoring widgets, and whole design systems, outside this crate.

A container (a View/Widget pair owning children) or an interactive leaf (one reporting a press back into application state) needs three things frust-core deliberately does not provide:

§Hover: claim it, latch it, then correct it at paint time

There is no hover phase and no Enter/Leave event. A widget that wants hover chrome opts in with three things, each covering a case the others cannot:

  • In its uncaptured PointerPhase::Move arm, hit-test the event position against its own bounds and call EventCtx::claim_hover when it is inside — on every such move, not just on entry (the claim is per-pass, not sticky).
  • Latch that same hit test into an internal hover flag, and gate request_redraw on the flag actually changing. This is the frame source for hover gain (and for the link moving from a sibling onto this widget): claim_hover asks for no redraw, and the pipeline manufactures one only when a hover ends with nothing taking it. Gating on the change is also what keeps a pointer wandering inside one widget from repainting per event.
  • In paint, read PaintCtx::is_hovered as the authoritative value and self-correct the flag from it. This is not optional belt-and-braces: a pointer leaving the widget routes its next move onto whatever it moved onto, so the widget it left never gets an event saying so.

Everything else is the pipeline’s job. frust-core stamps the claim down the pod chain and strands the previous claimant by advancing a hover epoch, so a container needs no hover bookkeeping at all — route_event and route_event_single carry it for free, and a hand-rolled router that forwards through ChildPod::event_child does too. Because the claim is recorded as a path, an enclosing container reads hovered while the pointer is over a claiming child (CSS :hover semantics), and every off-path widget reads false.

A container that wants hover chrome of its own has one extra rule: it claims after routing the move to its children, never before. Only one claim per pass is recorded and the first one recorded wins, so an ancestor that claims before it forwards makes every descendant ineligible for the pass — the child under the pointer never reads hovered, while its latched flag keeps flipping and asking for a frame on every move. Claiming after routing makes the container’s claim a fallback: a child’s claim wins and the container still reads hovered through the path, and when no child claims the container’s own claim is what records.

A captured pointer can never create hover, so a drag never paints hover under the finger and a widget that captures its own gesture is hover-free for the length of it. Nothing distinguishes a touch contact from a mouse, though: a touch drag that captured nothing is an ordinary hover pass, so a non-capturing consumer can tint transiently under a finger — the Up at lift ends the link (see docs/LIMITATIONS.md’s hover-window-leave-standing).

Application code should prefer frust::authoring, which re-exports everything below plus the frust-core trait vocabulary and kurbo/peniko geometry — so an app depends on frust alone. The example spells its imports the long way only because this crate cannot name frust without a dev-dependency cycle; frust::authoring’s own module docs carry the facade-spelled version.

Stability: pre-1.0, and a real supported public API — not #[doc(hidden)] plumbing, so a change to any signature here is a breaking change, released as such. The three catalogs shipping with this crate (material, cupertino, glyph) consume exactly this surface and nothing more, so a third-party design system authored against it is at parity with the built-ins by construction.

§Example: a one-child container widget

A container that offsets its single child, wired through the full lifecycle — build, rebuild, teardown, layout, paint, event routing, semantics forwarding:

use frust_core::{
    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult,
    InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
};
use frust_widgets::authoring::{build_child, rebuild_child, route_event_single, teardown_child};
use kurbo::{Point, Size};

/// The declarative half: a child plus the offset to apply to it.
struct OffsetView<State: 'static> {
    offset: Point,
    child: AnyView<State>,
}

/// The view-fn app code calls; `any` erases the concrete child view.
fn offset<State: 'static, V: View<State>>(offset: Point, child: V) -> OffsetView<State> {
    OffsetView { offset, child: any(child) }
}

/// The retained half: the live child pod plus the applied offset.
struct OffsetWidget {
    offset: Point,
    child: ChildPod,
}

impl<State: 'static> View<State> for OffsetView<State> {
    type Element = OffsetWidget;

    fn build(&self, ctx: &mut BuildCtx<'_>) -> OffsetWidget {
        OffsetWidget { offset: self.offset, child: build_child(&self.child, ctx) }
    }

    fn rebuild(
        &self,
        prev: &Self,
        element: &mut OffsetWidget,
        ctx: &mut BuildCtx<'_>,
    ) -> ChangeFlags {
        let mut flags = ChangeFlags::NONE;
        if prev.offset != self.offset {
            element.offset = self.offset;
            flags |= ChangeFlags::LAYOUT;
        }
        flags | rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
    }

    fn teardown(&self, element: &mut OffsetWidget, ctx: &mut BuildCtx<'_>) {
        teardown_child(&self.child, &mut element.child, ctx);
    }
}

impl Widget for OffsetWidget {
    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
        let size = self.child.layout_child(ctx, bc);
        self.child.set_origin(self.offset);
        bc.constrain(size)
    }

    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
        self.child.paint_child(ctx, scene);
    }

    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
        // Never re-hit-test a captured child by hand — this helper owns the
        // capture/focus fast paths and the blur-on-outside-tap rule.
        route_event_single(&mut self.child, ctx, event)
    }

    fn semantics(&self, ctx: &mut SemanticsCtx) {
        // A transparent wrapper still MUST forward, or the child's whole
        // subtree drops out of the accessibility tree.
        self.child.semantics_child(ctx);
    }

    // The read-only tooling seam: publish every pod this widget owns.
    frust_widgets::authoring::visit_children!(child);
}

Macros§

visit_children
Implement Widget::visit_children for the container whose impl Widget block this is written in, forwarding the named fields in the order given.

Structs§

ChildPod
Re-exported so the visit_children! expansion can name the type without the call site importing it. A container’s owned child: a boxed widget plus the layout geometry and capture bookkeeping the container maintains for it.
ImageSource
A decode-once, cheaply-clonable handle around a decoded RGBA8 image.
OverlayKey
The overlay registry’s own vocabulary, re-exported from frust-core so a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: an OverlaySlot’s setters take exactly these types, and a widget that cannot name them cannot configure one. An overlay owner’s identity, allocated once by the owner and quoted back to it by every InputEvent::Overlay the root routes into its surface.
OverlayPlacement
The overlay-portal authoring surface: the slot a widget that floats its own surface holds, what that surface is anchored to, and the placement geometry every anchored pattern needs.
OverlaySlot
The overlay-portal authoring surface: the slot a widget that floats its own surface holds, what that surface is anchored to, and the placement geometry every anchored pattern needs.

Enums§

OutsideTap
The overlay registry’s own vocabulary, re-exported from frust-core so a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: an OverlaySlot’s setters take exactly these types, and a widget that cannot name them cannot configure one. What a registered surface wants to hear about a press that landed on nothing floated — the light-dismiss policy, a per-surface boolean everywhere this pattern exists.
OverlayAlign
The overlay-portal authoring surface: the slot a widget that floats its own surface holds, what that surface is anchored to, and the placement geometry every anchored pattern needs.
OverlayAnchor
The overlay-portal authoring surface: the slot a widget that floats its own surface holds, what that surface is anchored to, and the placement geometry every anchored pattern needs.
OverlayBand
The overlay registry’s own vocabulary, re-exported from frust-core so a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: an OverlaySlot’s setters take exactly these types, and a widget that cannot name them cannot configure one. Which z-band a registered surface paints and hit-tests in.
OverlayInput
The overlay registry’s own vocabulary, re-exported from frust-core so a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: an OverlaySlot’s setters take exactly these types, and a widget that cannot name them cannot configure one. Whether a registered surface takes pointer input at all.
OverlaySide
The overlay-portal authoring surface: the slot a widget that floats its own surface holds, what that surface is anchored to, and the placement geometry every anchored pattern needs.
ThemeTextColor
Which themed color role a text’s glyphs default to when no explicit .color()/.style() was set and a theme is active.
ThemeTextType
Which type-scale role a text’s font FAMILY resolves from at layout, when opted into with TextView::themed_family and a theme is active — one variant per TypeScale slot (15 baseline roles plus their 15 _emphasized siblings).

Constants§

PRESSED_OPACITY
Pressed-state overlay opacity (source: androidx Compose Material3 StateTokens v0_210, retrieved 2026-07-17 — supersedes material-web v0.192’s 12%).

Traits§

VisitPods
A field shape a container can hold children in, visitable in declaration order — the traversal half of the visit_children! seam.

Functions§

build_child
Build a ChildPod wrapping an AnyView’s element.
erase_callback
Erase a view-held Rc<dyn Fn(&mut State)> app-state callback into the widget-held ErasedCallback adapter the interactive widgets invoke during the event pass.
erase_callback_arg
Like erase_callback, but for callbacks that also carry a value argument (Checkbox’s bool, Slider’s f64).
place
The overlay-portal authoring surface: the slot a widget that floats its own surface holds, what that surface is anchored to, and the placement geometry every anchored pattern needs.
rebuild_child
Reconcile one AnyView child in place through its ChildPod.
rebuild_children
Diff a child list against its live ChildPods, extracting each child’s AnyView through view_of (identity for a plain Vec<AnyView>, |c| &c.view for Flex’s FlexChild wrapper) and its optional ChildKey through key_of (|_| None for keyless containers, |c| c.key for Flex) — the one shared reconciler for every multi-child container.
route_event
Route a pointer/scroll/keyboard/IME event — or a broadcast — to a container’s children.
route_event_single
Route a pointer/scroll event — or a broadcast — to a container’s single child.
teardown_child
Tear down one AnyView child through its ChildPod.

Type Aliases§

ErasedArgCallback
A widget-held, State-erased callback adapter carrying one value argument (see erase_callback_arg).
ErasedCallback
A widget-held, State-erased app-state callback adapter (see erase_callback).
TypedArgCallback
A view-held, typed callback carrying one value argument (Checkbox’s bool, Slider’s f64), erased to ErasedArgCallback on build.