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:
- Child plumbing:
build_child/rebuild_child/teardown_childfor a singleAnyViewchild, andrebuild_childrenfor aVecof them (positional orChildKey-keyed reconciliation, with the focus/capture retention rules a hand-rolled diff gets wrong). - Event routing:
route_event(multi-child containers, reverse paint order + capture/focus fast paths) androute_event_single(one-child wrappers). - Callback erasure:
erase_callback/erase_callback_arg, turning a view-heldRc<dyn Fn(&mut State)>into theErasedCallback/ErasedArgCallbackadapter a non-generic widget holds, so the retained widget never becomes generic over the application-state type. - Child visitation:
visit_children!overVisitPods, writing a container’sWidget::visit_childrenbody from its child fields — the read-only seam that lets tooling walk into a container’s retained subtree (frust-core’sWidgetTree::inspect). One line per container; a leaf needs nothing. - Themed text roles:
ThemeTextColor(re-exported here) andTextView::themed_role— how a widget labels a childtextrun with the themed color role it should default to, instead of hardcoding a color; andThemeTextType(re-exported here) withTextView::themed_family— how it opts the run’s font family into the active theme’s type-scale role, resolved at layout so a live theme swap re-shapes it, instead of shaping inTextStyle::default()’s system UI font.
§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::Movearm, hit-test the event position against its own bounds and callEventCtx::claim_hoverwhen 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_redrawon the flag actually changing. This is the frame source for hover gain (and for the link moving from a sibling onto this widget):claim_hoverasks 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, readPaintCtx::is_hoveredas 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_childrenfor the container whoseimpl Widgetblock this is written in, forwarding the named fields in the order given.
Structs§
- Child
Pod - 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. - Image
Source - A decode-once, cheaply-clonable handle around a decoded RGBA8 image.
- Overlay
Key - The overlay registry’s own vocabulary, re-exported from
frust-coreso a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: anOverlaySlot’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 everyInputEvent::Overlaythe root routes into its surface. - Overlay
Placement - 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.
- Overlay
Slot - 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§
- Outside
Tap - The overlay registry’s own vocabulary, re-exported from
frust-coreso a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: anOverlaySlot’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. - Overlay
Align - 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.
- Overlay
Anchor - 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.
- Overlay
Band - The overlay registry’s own vocabulary, re-exported from
frust-coreso a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: anOverlaySlot’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. - Overlay
Input - The overlay registry’s own vocabulary, re-exported from
frust-coreso a design system naming a band, an input class, a light-dismiss policy or a surface identity does it through this one module: anOverlaySlot’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. - Overlay
Side - 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.
- Theme
Text Color - Which themed color role a text’s glyphs default to when no explicit
.color()/.style()was set and a theme is active. - Theme
Text Type - Which type-scale role a text’s font FAMILY resolves from at layout, when
opted into with
TextView::themed_familyand a theme is active — one variant perTypeScaleslot (15 baseline roles plus their 15_emphasizedsiblings).
Constants§
- PRESSED_
OPACITY - Pressed-state overlay opacity (source: androidx Compose Material3
StateTokensv0_210, retrieved 2026-07-17 — supersedes material-web v0.192’s 12%).
Traits§
- Visit
Pods - 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
ChildPodwrapping anAnyView’s element. - erase_
callback - Erase a view-held
Rc<dyn Fn(&mut State)>app-state callback into the widget-heldErasedCallbackadapter the interactive widgets invoke during the event pass. - erase_
callback_ arg - Like
erase_callback, but for callbacks that also carry a value argument (Checkbox’sbool, Slider’sf64). - 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
AnyViewchild in place through itsChildPod. - rebuild_
children - Diff a child list against its live
ChildPods, extracting each child’sAnyViewthroughview_of(identity for a plainVec<AnyView>,|c| &c.viewfor Flex’sFlexChildwrapper) and its optionalChildKeythroughkey_of(|_| Nonefor keyless containers,|c| c.keyfor 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
AnyViewchild through itsChildPod.
Type Aliases§
- Erased
ArgCallback - A widget-held,
State-erased callback adapter carrying one value argument (seeerase_callback_arg). - Erased
Callback - A widget-held,
State-erased app-state callback adapter (seeerase_callback). - Typed
ArgCallback - A view-held, typed callback carrying one value argument (Checkbox’s
bool, Slider’sf64), erased toErasedArgCallbackon build.