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