Skip to main content

frust_core/
lib.rs

1//! Layer 1+2 of Frust: the declarative view API, the retained widget tree,
2//! box-constraint layout, and the rebuild/layout/paint pass skeleton.
3//!
4//! The design mirrors `xilem_core`'s proven `View` lifecycle and Masonry's
5//! `tree_arena`-backed widget tree, but owns its implementation — there is **no
6//! xilem/masonry dependency and no `unsafe`** in this crate.
7//!
8//! # Layers
9//!
10//! * [`view`] — layer 1: the [`View`](view::View) trait, [`ChangeFlags`], and
11//!   [`BuildCtx`](view::BuildCtx). Views are cheap descriptors produced by
12//!   `fn build(&mut State) -> impl View<State>`.
13//! * [`widget`] — layer 2: the [`Widget`](widget::Widget) trait and its
14//!   layout/paint/event contexts, the [`PaintScene`](widget::PaintScene) paint
15//!   boundary, and container-owned [`ChildPod`](widget::ChildPod) children.
16//! * [`event`] — layer 2 input: [`InputEvent`](event::InputEvent)/
17//!   [`PointerEvent`](event::PointerEvent) and the [`EventCtx`](event::EventCtx)
18//!   handlers mutate state through.
19//! * [`input`] — pure gesture helpers: slop/wheel constants, a
20//!   [`VelocityTracker`](input::VelocityTracker), and the fling-decay math the
21//!   interactive widgets build on.
22//! * [`layout`] — the [`BoxConstraints`](layout::BoxConstraints) box model.
23//! * [`tree`] — the [`WidgetTree`](tree::WidgetTree) arena wrapper,
24//!   [`WidgetPod`](tree::WidgetPod), and the read-only
25//!   [`InspectNode`](tree::InspectNode) walk tooling reads the tree through.
26//! * [`app`] — the [`RenderRoot`](app::RenderRoot) that drives rebuild → layout
27//!   → paint. This is what each platform shell owns.
28//! * [`component`] — [`Component`](component::Component), Flutter's
29//!   `StatefulWidget` analog: a subtree with retained local state, a
30//!   per-component reactive `Owner`, and a state boundary the outer view tree
31//!   never sees.
32//! * [`overlay`] — the overlay portal: an owner-hosted, root-painted,
33//!   root-routed pod a widget floats above the whole app (menus, popovers,
34//!   tooltips, selection toolbars).
35//! * [`selection_toolbar`] — what a text field publishes when it has a
36//!   selection, plus the process-global policy/builder slots that decide who
37//!   draws the toolbar.
38//! * [`hit`] — hit-testing helpers for content drawn under an arbitrary
39//!   [`kurbo::Affine`]: the test [`ChildPod`](widget::ChildPod) uses for a
40//!   transformed pod, exposed for canvas hit closures and pan/zoom containers.
41
42pub mod anim;
43pub mod app;
44pub mod component;
45pub mod event;
46pub mod input;
47pub mod insets;
48pub mod layout;
49pub mod overlay;
50pub mod selection_toolbar;
51pub mod semantics;
52pub mod tree;
53pub mod view;
54pub mod widget;
55
56/// Hit-testing under an arbitrary [`kurbo::Affine`].
57///
58/// [`ChildPod::set_transform`](widget::ChildPod::set_transform) places a child
59/// under a transform; these are the helpers it hit-tests with, exposed so a
60/// canvas hit closure or a pan/zoom container answers "is this point on that
61/// shape" exactly the way the pod does.
62pub mod hit {
63    use kurbo::{Affine, Point, Rect};
64
65    /// `affine`'s inverse, or `None` when it has none — a singular (zero
66    /// determinant) or non-finite transform, or one whose inverse overflows.
67    ///
68    /// [`Affine::inverse`] on a singular matrix divides by zero and returns
69    /// non-finite coefficients rather than failing; this is the checked form
70    /// every caller that maps a point back through a transform should use.
71    pub fn checked_inverse(affine: &Affine) -> Option<Affine> {
72        let det = affine.determinant();
73        if det == 0.0 || !det.is_finite() || !affine.is_finite() {
74            return None;
75        }
76        let inverse = affine.inverse();
77        inverse.is_finite().then_some(inverse)
78    }
79
80    /// Whether `point` lands inside `rect` once `rect` is drawn under `affine`.
81    ///
82    /// `rect` is in the content's own (local) space and `affine` maps that space
83    /// into the space `point` is in, so this is the hit test for content painted
84    /// inside `push_transform(affine)`: `point` is mapped back through the
85    /// inverse and tested against `rect` with the same half-open convention as
86    /// [`ChildPod::contains`](crate::widget::ChildPod::contains) (`x0 <= x < x1`,
87    /// `y0 <= y < y1`). A rotated rect therefore hits along its rotated edges,
88    /// not its axis-aligned bounding box.
89    ///
90    /// A transform with no inverse ([`checked_inverse`]) collapses the rect to a
91    /// line or a point, which nothing can hit: the answer is `false`, never a
92    /// panic.
93    pub fn point_in_transformed_rect(point: Point, rect: Rect, affine: &Affine) -> bool {
94        let Some(inverse) = checked_inverse(affine) else {
95            return false;
96        };
97        let local = inverse * point;
98        local.x >= rect.x0 && local.x < rect.x1 && local.y >= rect.y0 && local.y < rect.y1
99    }
100
101    #[cfg(test)]
102    mod tests {
103        use super::*;
104        use kurbo::Vec2;
105        use std::f64::consts::FRAC_PI_4;
106
107        #[test]
108        fn identity_matches_the_half_open_rect_test() {
109            let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
110            assert!(point_in_transformed_rect(
111                Point::ZERO,
112                rect,
113                &Affine::IDENTITY
114            ));
115            assert!(point_in_transformed_rect(
116                Point::new(9.99, 9.99),
117                rect,
118                &Affine::IDENTITY
119            ));
120            assert!(!point_in_transformed_rect(
121                Point::new(10.0, 5.0),
122                rect,
123                &Affine::IDENTITY
124            ));
125        }
126
127        #[test]
128        fn scale_and_translate_map_the_point_back() {
129            let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
130            let affine = Affine::translate(Vec2::new(100.0, 50.0)) * Affine::scale(2.0);
131            // Drawn at (100..120, 50..70).
132            assert!(point_in_transformed_rect(
133                Point::new(119.0, 69.0),
134                rect,
135                &affine
136            ));
137            assert!(!point_in_transformed_rect(
138                Point::new(121.0, 60.0),
139                rect,
140                &affine
141            ));
142            assert!(!point_in_transformed_rect(
143                Point::new(105.0, 49.0),
144                rect,
145                &affine
146            ));
147        }
148
149        #[test]
150        fn rotation_hits_the_diamond_not_its_bounding_box() {
151            let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
152            let affine = Affine::rotate_about(FRAC_PI_4, Point::new(50.0, 50.0));
153            // The box's own corner (1, 1) is outside the rotated diamond.
154            assert!(!point_in_transformed_rect(
155                Point::new(1.0, 1.0),
156                rect,
157                &affine
158            ));
159            // The diamond's top vertex sits at (50, 50 - 50·√2).
160            let top = affine * Point::new(0.0, 0.0);
161            assert!(point_in_transformed_rect(
162                top + Vec2::new(0.0, 1.0),
163                rect,
164                &affine
165            ));
166            assert!(!point_in_transformed_rect(
167                top - Vec2::new(0.0, 1.0),
168                rect,
169                &affine
170            ));
171        }
172
173        #[test]
174        fn a_singular_transform_hits_nothing() {
175            let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
176            for affine in [
177                Affine::scale(0.0),
178                Affine::scale_non_uniform(1.0, 0.0),
179                Affine::new([1.0, 2.0, 2.0, 4.0, 0.0, 0.0]),
180                Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]),
181            ] {
182                assert!(checked_inverse(&affine).is_none());
183                assert!(!point_in_transformed_rect(Point::ZERO, rect, &affine));
184            }
185        }
186    }
187}
188
189/// Re-export of the [`accesskit`] accessibility vocabulary (roles, node
190/// builders, toggle/value state) widgets use to populate a
191/// [`semantics::SemanticsCtx`]. Re-exported here — the sole crate that depends
192/// on accesskit — so `frust-widgets` (and app code) name the vocabulary
193/// through `frust_core::accesskit::*` without a direct dependency; the
194/// platform `accesskit_*` adapter crates live in the shells, not here.
195pub use accesskit;
196pub use anim::{
197    AnimationController, AnimationStatus, Curve, FrameTime, Lerp, Spring, SpringDesc, Tween,
198};
199pub use app::{Orientation, RenderRoot, WindowMetrics};
200pub use component::{Component, ComponentView, ComponentWidget, component};
201pub use event::{
202    CursorIcon, EditCommand, EditingState, EventCtx, EventOutcome, EventResult, ImeContentType,
203    ImeEvent, ImeState, InputEvent, Key, KeyEvent, Modifiers, NamedKey, OverlayEvent,
204    OverlayEventKind, PointerButton, PointerEvent, PointerPhase, ScrollDelta,
205    has_pending_result_flush, mark_focus_orphaned, mark_pending_result_flush, take_focus_orphaned,
206    take_pending_result_flush,
207};
208pub use input::{
209    FLING_DECAY, FLING_STOP, MOUSE_SLOP, TOUCH_SLOP, VELOCITY_WINDOW_MS, VelocityTracker,
210    WHEEL_LINE_PX, fling_decay, fling_displacement,
211};
212pub use insets::{CornerInset, CornerInsets, EdgeInsets as WindowEdgeInsets, WindowInsets};
213pub use layout::BoxConstraints;
214pub use overlay::{
215    OutsideTap, OverlayBand, OverlayEntry, OverlayHit, OverlayInput, OverlayKey, OverlayPod,
216};
217pub use selection_toolbar::{
218    SelectionToolbarActions, SelectionToolbarBuilder, SelectionToolbarPolicy,
219    SelectionToolbarRequest, install_selection_toolbar_builder_if_unset,
220    lock_selection_toolbar_policy, selection_toolbar_builder, selection_toolbar_policy,
221    set_selection_toolbar_builder, set_selection_toolbar_policy,
222};
223pub use semantics::{SemanticsCtx, SemanticsUpdate};
224pub use tree::{InspectNode, WidgetPod, WidgetTree};
225pub use view::{AnyView, BuildCtx, ChangeFlags, View, WidgetId, any};
226pub use widget::{
227    ChildPod, CornerRadii, DashPattern, DiscardScene, HeroDirective, HeroFrames, LayoutCtx,
228    PaintCtx, PaintOutcome, PaintScene, TickClass, Widget,
229};