Skip to main content

frust_widgets/
lib.rs

1//! Baseline widget set: Text, Button, Image, Column/Row, Stack, ScrollView,
2//! CustomScrollView and its slivers, etc.
3//!
4//! Ships the [`text`] leaf plus the primitive layout containers —
5//! [`Row`]/[`Column`] ([`FlexView`]), [`Stack`], [`IndexedStack`], [`Padding`],
6//! [`Align`], and [`SizedBox`] — built as `View`/`Widget` pairs over `frust-core`'s
7//! [`AnyView`](frust_core::AnyView)/[`ChildPod`](frust_core::ChildPod)
8//! substrate. Containers own their children directly as `ChildPod`s (the arena
9//! stays single-root); see [`frust_core::widget::ChildPod`] for the rationale.
10//!
11//! # Shared child plumbing
12//!
13//! The container modules build/rebuild/teardown their heterogeneous children
14//! through the [`authoring::build_child`]/[`authoring::rebuild_child`]/
15//! [`authoring::teardown_child`] helpers and route pointer events through
16//! [`authoring::route_event`] (multi-child containers — `Flex`/`Stack`) or
17//! [`authoring::route_event_single`] (one-child wrappers —
18//! `Padding`/`Align`/`SizedBox`). Each child is an
19//! [`AnyView`](frust_core::AnyView) whose element (`Box<dyn Widget>`) is stored
20//! double-boxed inside a `ChildPod`, so a later rebuild can recover
21//! `&mut Box<dyn Widget>` to drive `AnyView`'s type-erased reconciliation.
22//!
23//! That toolkit is **public** — see the [`authoring`] module — so a design
24//! system is authored outside this crate against exactly the surface the
25//! baseline widgets use themselves. No design-system catalog lives here any
26//! more: the three built-in ones ship as their own plugin crates
27//! (`frust-glyph`/`frust-material`/`frust-cupertino`), each depending on the
28//! `frust` facade alone, so the authoring boundary is now enforced by the
29//! crate graph rather than by a source scan.
30
31mod align;
32pub mod authoring;
33mod button;
34mod canvas;
35mod checkbox;
36mod container;
37mod divider;
38pub mod drag;
39pub mod either;
40mod flex;
41mod gesture;
42mod icon;
43mod icon_button;
44pub mod icons;
45mod image;
46mod indexed_stack;
47mod list_view;
48pub mod motion;
49pub mod nav;
50mod overlay;
51mod padding;
52mod pan_zoom;
53pub mod physics;
54pub mod pinch;
55mod platform_view;
56mod radio;
57mod repaint_boundary;
58mod safe_area;
59mod scaffold;
60mod scroll;
61pub mod scroll_controller;
62mod scroll_core;
63mod scrollbar;
64mod selection_toolbar;
65mod shortcuts;
66mod sized;
67mod slider;
68mod slivers;
69mod stack;
70mod text;
71mod textinput;
72mod texture_view;
73mod viewport;
74
75use std::hash::{Hash, Hasher};
76
77pub use align::{Align, AlignView, AlignWidget, Alignment};
78pub use button::{Button, ButtonStyle, ButtonView, ButtonWidget, button};
79pub use canvas::{CanvasView, CanvasWidget, canvas};
80pub use checkbox::{Checkbox, CheckboxView, CheckboxWidget, checkbox};
81pub use container::{BorderStyle, ContainerView, ContainerWidget, colored_box, container};
82pub use divider::{DividerView, DividerWidget, divider};
83pub use drag::{
84    AUTO_SCROLL_EDGE_PX, AUTO_SCROLL_MAX_PX_PER_S, AutoScroll, AutoScrollZone,
85    AutoScrollZoneWidget, DRAG_THRESHOLD, DragCoordinator, DragHighlight, DragKind, DragPhase,
86    DragPolicy, DragSession, DragSourceId, DragState, DragStateChange, DragSubscription,
87    DragTargetId, DragTargetView, DragTargetWidget, DraggableView, DraggableWidget, GHOST_OPACITY,
88    HIGHLIGHT_FALLBACK, ReorderableListView, SourceFeedback, auto_scroll_zone, drag_target,
89    draggable, reorderable_list,
90};
91pub use either::{
92    Either, EitherOf3, EitherOf3Widget, EitherOf4, EitherOf4Widget, EitherOf5, EitherOf5Widget,
93    EitherOf6, EitherOf6Widget, EitherOf7, EitherOf7Widget, EitherOf8, EitherOf8Widget,
94    EitherWidget, either,
95};
96pub use flex::{
97    Axis, Column, CrossAxisAlignment, FlexChild, FlexView, FlexWidget, MainAxisAlignment, Row,
98    column, flexible, inflexible, keyed, row,
99};
100/// Whether a floated surface claims keys as a modal, re-exported for
101/// [`OverlaySlot::set_keys`] and [`OverlayPortalView::keys`].
102pub use frust_core::OverlayKeys;
103/// The axis a [`CustomScrollView`] scrolls along, re-exported so its
104/// [`axis`](CustomScrollView::axis) builder needs no direct `frust-core` import.
105pub use frust_core::ScrollAxis;
106pub use gesture::{GestureDetector, GestureDetectorView, GestureDetectorWidget};
107pub use icon::{Icon, IconData, IconSource, IconView, IconWidget, icon};
108pub use icon_button::{IconButton, IconButtonView, IconButtonWidget, icon_button};
109pub use image::{Image, ImageError, ImageFit, ImageSource, ImageView, ImageWidget};
110pub use indexed_stack::{IndexedStack, IndexedStackView, IndexedStackWidget, indexed_stack};
111pub use list_view::{ListView, ListViewWidget, list_view};
112pub use nav::hero::{HeroView, HeroWidget, hero};
113pub use nav::navigator::{
114    BackPolicy, NavigatorController, NavigatorId, NavigatorView, NavigatorWidget, PageBuilder,
115    PageVisibility, PopResult, PushOptions, ReplaceOptions, ResultCallback, RouteChangeCallback,
116    VisibilityCallback, navigator, overlay_host,
117};
118pub use nav::path::{Location, PathPattern, RouteParams};
119pub use nav::route::{NavRequest, NavWaker, RouteNavigator};
120pub use nav::route_state::{NavChange, RouteStack};
121pub use nav::router::{
122    DEFAULT_REDIRECT_LIMIT, ErrorBuilder, Redirect, Resolution, ResolvedPage, Route, RouteBuilder,
123    Router, shell_route,
124};
125pub use nav::transition::{PageTransition, Timing, TransitionSpec, TransitionState};
126pub use overlay::{
127    DEFAULT_OFFSET, DEFAULT_PADDING, OverlayAlign, OverlayAnchor, OverlayPlacement,
128    OverlayPortalView, OverlayPortalWidget, OverlaySide, OverlaySlot, overlay_portal, place,
129};
130pub use padding::{EdgeInsets, Padding, PaddingView, PaddingWidget};
131pub use pan_zoom::{
132    DEFAULT_MAX_SCALE, DEFAULT_MIN_SCALE, PanZoomController, PanZoomTransform, PanZoomView,
133    PanZoomWidget, pan_zoom,
134};
135pub use physics::effect::OverscrollEffect;
136pub use physics::parity::{
137    AlwaysScrollable, Bouncing, Clamping, DecelerationRate, NeverScrollable,
138};
139pub use physics::rubber_band::RubberBand;
140pub use physics::{
141    MAX_FLING_VELOCITY, MIN_FLING_VELOCITY, ScrollMetrics, ScrollPhysics, Simulation,
142    SpringDescription, Tolerance,
143};
144pub use pinch::{
145    PINCH_SLOP, PinchDetectorView, PinchDetectorWidget, PinchRecognizer, pinch_detector,
146};
147pub use platform_view::{
148    PlatformViewView, PlatformViewWidget, ShieldView, ShieldWidget, platform_view, shield,
149};
150pub use radio::{Radio, RadioView, RadioWidget, radio};
151pub use repaint_boundary::{RepaintBoundaryView, RepaintBoundaryWidget, repaint_boundary};
152pub use safe_area::{SafeAreaView, SafeAreaWidget, safe_area};
153pub use scaffold::{ScaffoldView, ScaffoldWidget, scaffold};
154pub use scroll::{ScrollInfo, ScrollView, ScrollWidget, scroll_view};
155pub use scroll_controller::{AnimateTo, ScrollController, ScrollSubscription};
156pub use scrollbar::{ScrollbarShow, ScrollbarStyle};
157pub use selection_toolbar::selection_toolbar;
158pub use shortcuts::{ShortcutsView, ShortcutsWidget, shortcuts};
159pub use sized::{SizedBox, SizedBoxView, SizedBoxWidget};
160pub use slider::{Slider, SliderView, SliderWidget, slider};
161pub use slivers::{
162    KeepAliveHandle, KeepAliveView, KeepAliveWidget, SliverBoxView, SliverBoxWidget,
163    SliverFillRemainingView, SliverFillRemainingWidget, SliverFixedExtentListView,
164    SliverFixedExtentListWidget, SliverGridDelegate, SliverGridView, SliverGridWidget,
165    SliverListView, SliverListWidget, SliverPaddingView, SliverPaddingWidget,
166    SliverPersistentHeaderDelegate, SliverPersistentHeaderView, SliverPersistentHeaderWidget,
167    keep_alive, sliver_box, sliver_fill_remaining, sliver_fixed_extent_list,
168    sliver_fixed_extent_list_keyed, sliver_grid, sliver_grid_keyed, sliver_list, sliver_padding,
169    sliver_persistent_header,
170};
171pub use stack::{Stack, StackView, StackWidget, stack};
172pub use text::{TextView, TextWidget, text};
173pub use textinput::{TextInput, TextInputView, TextInputWidget, text_input};
174pub use texture_view::{TextureFit, TextureView, TextureWidget, texture_fit_rect, texture_view};
175pub use viewport::{CustomScrollView, DEFAULT_CACHE_EXTENT, ViewportWidget, custom_scroll_view};
176
177/// A stable identity for a list child, so a container's reconciliation can match
178/// a child to its live widget *by key* across reorders/inserts instead of by
179/// position — the difference between "the third row's widget" and "row #42's
180/// widget" when the list is shuffled.
181///
182/// Built from any [`Hash`] value (an item id, a string name, an index) via the
183/// `From` impls below and [`keyed`](crate::keyed); the hashed `u64` is what the
184/// reconciler compares. Two children in the same list must not collide — a
185/// duplicate key is a `debug_assert` tripwire that falls back to positional
186/// reconciliation (see [`authoring::rebuild_children`]).
187#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
188pub struct ChildKey(u64);
189
190impl ChildKey {
191    /// Hash any [`Hash`] value into a `ChildKey`. Backs the `From` impls and
192    /// [`keyed`](crate::keyed)'s `impl Into<ChildKey>` argument.
193    pub fn new(value: impl Hash) -> Self {
194        let mut hasher = std::collections::hash_map::DefaultHasher::new();
195        value.hash(&mut hasher);
196        ChildKey(hasher.finish())
197    }
198}
199
200macro_rules! child_key_from {
201    ($($t:ty),* $(,)?) => {
202        $(
203            impl From<$t> for ChildKey {
204                fn from(value: $t) -> Self {
205                    ChildKey::new(value)
206                }
207            }
208        )*
209    };
210}
211
212// Common key types: integer ids/indices, chars, and string names. A blanket
213// `impl<T: Hash> From<T>` would collide with the reflexive `From<ChildKey>`, so
214// the ergonomic conversions are spelled out for the types keys are drawn from.
215child_key_from!(
216    u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, char, &str, String
217);
218
219/// Shared, GPU-free fixtures for the container layout/paint/event tests: a
220/// fixed-size [`Leaf`], a distinctive swap partner, an event-recording
221/// [`Probe`], and a recording [`RecordingScene`].
222///
223/// Compiled into this crate's own test build, and — behind the non-default
224/// `test-support` feature — into the library itself, so a design system authored
225/// against [`authoring`] outside this crate can test its containers against the
226/// same fixtures this crate's own container tests use — the design-system
227/// plugin crates are exactly that consumer. The feature is off by default: a
228/// normal app ships none of this.
229#[cfg(any(test, feature = "test-support"))]
230pub mod test_support {
231    use frust_core::{
232        AnyView, BoxConstraints, BuildCtx, ChangeFlags, EventCtx, EventResult, InputEvent,
233        LayoutCtx, PaintCtx, PaintScene, View, Widget, any,
234    };
235    use kurbo::{Affine, Point, Size};
236    use peniko::Color;
237
238    /// A leaf view of fixed intrinsic size that fills a rect on paint.
239    pub struct Leaf {
240        intrinsic: Size,
241    }
242
243    /// Build a [`Leaf`] with the given intrinsic width/height.
244    pub fn leaf(width: f64, height: f64) -> Leaf {
245        Leaf {
246            intrinsic: Size::new(width, height),
247        }
248    }
249
250    /// A [`Leaf`], type-erased for a `()`-state container.
251    pub fn leaf_any(width: f64, height: f64) -> AnyView<()> {
252        any(leaf(width, height))
253    }
254
255    impl Leaf {
256        /// Erase this leaf into an `AnyView<()>`.
257        pub fn into_any(self) -> AnyView<()> {
258            any(self)
259        }
260    }
261
262    /// Retained widget for [`Leaf`].
263    pub struct LeafWidget {
264        intrinsic: Size,
265    }
266
267    impl View<()> for Leaf {
268        type Element = LeafWidget;
269        fn build(&self, _ctx: &mut BuildCtx<'_>) -> LeafWidget {
270            LeafWidget {
271                intrinsic: self.intrinsic,
272            }
273        }
274        fn rebuild(
275            &self,
276            prev: &Self,
277            element: &mut LeafWidget,
278            _ctx: &mut BuildCtx<'_>,
279        ) -> ChangeFlags {
280            if prev.intrinsic != self.intrinsic {
281                element.intrinsic = self.intrinsic;
282                ChangeFlags::LAYOUT
283            } else {
284                ChangeFlags::NONE
285            }
286        }
287    }
288
289    impl Widget for LeafWidget {
290        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
291            bc.constrain(self.intrinsic)
292        }
293        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
294            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
295        }
296    }
297
298    /// A distinctive view whose widget always reports a 7x7 size — used to prove
299    /// an `AnyView` type-swap actually replaced the widget.
300    pub struct SwapLeaf;
301
302    /// Build the [`SwapLeaf`] swap partner.
303    pub fn swap_leaf() -> SwapLeaf {
304        SwapLeaf
305    }
306
307    impl SwapLeaf {
308        /// Erase this view into an `AnyView<()>`.
309        pub fn into_any(self) -> AnyView<()> {
310            any(self)
311        }
312    }
313
314    /// Retained widget for [`SwapLeaf`].
315    pub struct SwapLeafWidget;
316
317    impl View<()> for SwapLeaf {
318        type Element = SwapLeafWidget;
319        fn build(&self, _ctx: &mut BuildCtx<'_>) -> SwapLeafWidget {
320            SwapLeafWidget
321        }
322        fn rebuild(
323            &self,
324            _prev: &Self,
325            _element: &mut SwapLeafWidget,
326            _ctx: &mut BuildCtx<'_>,
327        ) -> ChangeFlags {
328            ChangeFlags::NONE
329        }
330    }
331
332    impl Widget for SwapLeafWidget {
333        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
334            bc.constrain(Size::new(7.0, 7.0))
335        }
336        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
337    }
338
339    /// A view whose widget fills its constraints and, on any event, records its
340    /// `id` into the `Vec<u32>` application state and reports `Handled`. Used to
341    /// prove event routing order.
342    pub struct Probe {
343        id: u32,
344    }
345
346    /// Build a [`Probe`] tagged with `id`.
347    pub fn probe(id: u32) -> Probe {
348        Probe { id }
349    }
350
351    impl Probe {
352        /// Erase this probe into an `AnyView<Vec<u32>>`.
353        pub fn into_any(self) -> AnyView<Vec<u32>> {
354            any(self)
355        }
356    }
357
358    /// Retained widget for [`Probe`].
359    pub struct ProbeWidget {
360        id: u32,
361    }
362
363    impl View<Vec<u32>> for Probe {
364        type Element = ProbeWidget;
365        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
366            ProbeWidget { id: self.id }
367        }
368        fn rebuild(
369            &self,
370            _prev: &Self,
371            _element: &mut ProbeWidget,
372            _ctx: &mut BuildCtx<'_>,
373        ) -> ChangeFlags {
374            ChangeFlags::NONE
375        }
376    }
377
378    impl Widget for ProbeWidget {
379        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
380            bc.max()
381        }
382        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
383        fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
384            ctx.state_mut::<Vec<u32>>().push(self.id);
385            ctx.request_redraw();
386            EventResult::Handled
387        }
388    }
389
390    /// A GPU-free [`PaintScene`] that records filled rects in paint order, plus
391    /// `push_layer`/`push_transform` calls and their pop counts — the
392    /// `motion::animated` wrappers' recording-scene tests assert against
393    /// these alongside the pre-existing `rects`.
394    #[derive(Default)]
395    pub struct RecordingScene {
396        pub rects: Vec<(Point, Size)>,
397        pub layers: Vec<(Point, Size, f32)>,
398        pub layer_pops: u32,
399        pub transforms: Vec<Affine>,
400        pub transform_pops: u32,
401    }
402
403    impl PaintScene for RecordingScene {
404        fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
405            self.rects.push((origin, size));
406        }
407        fn draw_text(&mut self, _origin: Point, _text: &str) {}
408        fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
409            self.layers.push((origin, size, alpha));
410        }
411        fn pop_layer(&mut self) {
412            self.layer_pops += 1;
413        }
414        fn push_transform(&mut self, transform: Affine) {
415            self.transforms.push(transform);
416        }
417        fn pop_transform(&mut self) {
418            self.transform_pops += 1;
419        }
420    }
421}