Skip to main content

frust_widgets/
lib.rs

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