Skip to main content

frust_core/
lib.rs

1//! Layer 1+2 of Frust: the declarative view API, the retained widget tree, box-constraint layout,
2//! and the rebuild/layout/paint pass skeleton. Modelled on xilem/Masonry, depending on neither,
3//! with no `unsafe`.
4//!
5//! * [`anim`] — the frame clock, curves, tweens, springs and animation controllers.
6//! * [`app`] — the [`RenderRoot`] each platform shell drives through rebuild, layout and paint.
7//! * [`mod@component`] — [`Component`], a subtree with retained local state and its own `Owner`.
8//! * [`event`] — input events and the [`EventCtx`] handlers mutate state through.
9//! * `hotpatch` (feature `hotpatch`) — the hot-patch seam every `Component::build` runs through.
10//! * [`hit`] — hit tests for content drawn under an arbitrary [`kurbo::Affine`].
11//! * [`input`] — slop/wheel constants, [`VelocityTracker`] and fling-decay math.
12//! * [`insets`] — the [`WindowInsets`] safe-area and keyboard inset model.
13//! * [`layout`] — the [`BoxConstraints`] box model.
14//! * [`overlay`] — the portal a widget floats a pod above the whole app through.
15//! * [`retained`] — retained-page keys and the renderer's resident-key ledger.
16//! * [`selection_toolbar`] — a text field's toolbar request and the policy/builder slots.
17//! * [`semantics`] — the accessibility tree pass behind [`SemanticsUpdate`].
18//! * [`shortcuts`] — the [`Chord`] spec and the per-paint keyboard-shortcut declarations.
19//! * [`sliver`] — the sliver protocol a scrolling viewport lays children out with.
20//! * [`tree`] — the [`WidgetTree`] arena and the read-only [`InspectNode`] walk.
21//! * [`view`] — the [`View`] trait, [`ChangeFlags`], [`BuildCtx`] and [`ViewSeq`].
22//! * [`widget`] — the [`Widget`] trait, its contexts, [`PaintScene`] and [`ChildPod`].
23
24pub mod anim;
25pub mod app;
26pub mod component;
27pub mod event;
28#[cfg(feature = "hotpatch")]
29pub mod hotpatch;
30pub mod input;
31pub mod insets;
32pub mod layout;
33pub mod overlay;
34pub mod retained;
35pub mod selection_toolbar;
36pub mod semantics;
37pub mod shortcuts;
38pub mod sliver;
39pub mod tree;
40pub mod view;
41pub mod widget;
42
43/// Hit-testing under an arbitrary [`kurbo::Affine`]: the helpers a transformed
44/// [`ChildPod`] uses, for canvas hit closures and pan/zoom containers.
45pub mod hit {
46    use kurbo::{Affine, Point, Rect};
47
48    /// `affine`'s inverse, or `None` for a singular or non-finite transform or an
49    /// overflowing inverse. Use it instead of [`Affine::inverse`], which returns
50    /// non-finite coefficients for a singular matrix rather than failing.
51    pub fn checked_inverse(affine: &Affine) -> Option<Affine> {
52        let det = affine.determinant();
53        if det == 0.0 || !det.is_finite() || !affine.is_finite() {
54            return None;
55        }
56        let inverse = affine.inverse();
57        inverse.is_finite().then_some(inverse)
58    }
59
60    /// Whether `point` hits local-space `rect` drawn under `affine` (content inside
61    /// `push_transform(affine)`): `point` is mapped back and tested half-open, like
62    /// `ChildPod::contains`, so a rotated rect hits along its rotated edges. A
63    /// non-invertible `affine` hits nothing.
64    pub fn point_in_transformed_rect(point: Point, rect: Rect, affine: &Affine) -> bool {
65        let Some(inverse) = checked_inverse(affine) else {
66            return false;
67        };
68        let local = inverse * point;
69        local.x >= rect.x0 && local.x < rect.x1 && local.y >= rect.y0 && local.y < rect.y1
70    }
71
72    #[cfg(test)]
73    mod tests {
74        use super::*;
75        use kurbo::Vec2;
76        use std::f64::consts::FRAC_PI_4;
77
78        #[test]
79        fn identity_matches_the_half_open_rect_test() {
80            let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
81            assert!(point_in_transformed_rect(
82                Point::ZERO,
83                rect,
84                &Affine::IDENTITY
85            ));
86            assert!(point_in_transformed_rect(
87                Point::new(9.99, 9.99),
88                rect,
89                &Affine::IDENTITY
90            ));
91            assert!(!point_in_transformed_rect(
92                Point::new(10.0, 5.0),
93                rect,
94                &Affine::IDENTITY
95            ));
96        }
97
98        #[test]
99        fn scale_and_translate_map_the_point_back() {
100            let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
101            let affine = Affine::translate(Vec2::new(100.0, 50.0)) * Affine::scale(2.0);
102            // Drawn at (100..120, 50..70).
103            assert!(point_in_transformed_rect(
104                Point::new(119.0, 69.0),
105                rect,
106                &affine
107            ));
108            assert!(!point_in_transformed_rect(
109                Point::new(121.0, 60.0),
110                rect,
111                &affine
112            ));
113            assert!(!point_in_transformed_rect(
114                Point::new(105.0, 49.0),
115                rect,
116                &affine
117            ));
118        }
119
120        #[test]
121        fn rotation_hits_the_diamond_not_its_bounding_box() {
122            let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
123            let affine = Affine::rotate_about(FRAC_PI_4, Point::new(50.0, 50.0));
124            // The box's own corner (1, 1) is outside the rotated diamond.
125            assert!(!point_in_transformed_rect(
126                Point::new(1.0, 1.0),
127                rect,
128                &affine
129            ));
130            // The diamond's top vertex sits at (50, 50 - 50·√2).
131            let top = affine * Point::new(0.0, 0.0);
132            assert!(point_in_transformed_rect(
133                top + Vec2::new(0.0, 1.0),
134                rect,
135                &affine
136            ));
137            assert!(!point_in_transformed_rect(
138                top - Vec2::new(0.0, 1.0),
139                rect,
140                &affine
141            ));
142        }
143
144        #[test]
145        fn a_singular_transform_hits_nothing() {
146            let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
147            for affine in [
148                Affine::scale(0.0),
149                Affine::scale_non_uniform(1.0, 0.0),
150                Affine::new([1.0, 2.0, 2.0, 4.0, 0.0, 0.0]),
151                Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]),
152            ] {
153                assert!(checked_inverse(&affine).is_none());
154                assert!(!point_in_transformed_rect(Point::ZERO, rect, &affine));
155            }
156        }
157    }
158}
159
160/// The [`accesskit`] vocabulary widgets fill a [`semantics::SemanticsCtx`] with,
161/// re-exported so `frust-widgets` and apps use it without their own dependency;
162/// the platform `accesskit_*` adapters live in the shells.
163pub use accesskit;
164pub use anim::{
165    AnimationController, AnimationStatus, Curve, FrameTime, Lerp, Spring, SpringDesc, Tween,
166};
167pub use app::{FocusRingPolicy, Orientation, RenderRoot, WindowMetrics};
168pub use component::{Component, ComponentView, ComponentWidget, component};
169pub use event::{
170    CursorIcon, EditCommand, EditingState, EventCtx, EventOutcome, EventResult, ImeContentType,
171    ImeEvent, ImeState, InputEvent, Key, KeyEvent, Modifiers, NamedKey, OverlayEvent,
172    OverlayEventKind, PointerButton, PointerEvent, PointerPhase, ScrollDelta,
173    has_pending_result_flush, mark_focus_orphaned, mark_pending_result_flush, take_focus_orphaned,
174    take_pending_result_flush,
175};
176#[cfg(feature = "hotpatch")]
177pub use hotpatch::{LayoutMismatch, SeamWitness, build_erased, set_patch_listener};
178pub use input::{
179    FLING_DECAY, FLING_STOP, MOUSE_SLOP, TOUCH_SLOP, VELOCITY_WINDOW_MS, VelocityTracker,
180    WHEEL_LINE_PX, fling_decay, fling_displacement,
181};
182pub use insets::{CornerInset, CornerInsets, EdgeInsets as WindowEdgeInsets, WindowInsets};
183pub use layout::BoxConstraints;
184pub use overlay::{
185    OutsideTap, OverlayBand, OverlayEntry, OverlayHit, OverlayInput, OverlayKey, OverlayKeys,
186    OverlayPod,
187};
188pub use retained::{RetainedEntry, RetainedKey, RetainedLedger};
189pub use selection_toolbar::{
190    SelectionToolbarActions, SelectionToolbarBuilder, SelectionToolbarPolicy,
191    SelectionToolbarRequest, install_selection_toolbar_builder_if_unset,
192    lock_selection_toolbar_policy, selection_toolbar_builder, selection_toolbar_policy,
193    set_selection_toolbar_builder, set_selection_toolbar_policy,
194};
195pub use semantics::{SemanticsCtx, SemanticsUpdate};
196pub use shortcuts::{
197    Chord, ChordParseError, ShortcutDecl, ShortcutHint, ShortcutId, ShortcutScope,
198};
199pub use sliver::{
200    GrowthDirection, ScrollAxis, ScrollDirection, SliverConstraints, SliverGeometry, SliverWidget,
201};
202pub use tree::{InspectNode, WidgetPod, WidgetTree};
203pub use view::{AnyView, BuildCtx, ChangeFlags, View, ViewSeq, WidgetId, any, views};
204pub use widget::{
205    ChildPod, CornerRadii, DashPattern, DiscardScene, HeroDirective, HeroFrames, LayoutCtx,
206    PaintCtx, PaintOutcome, PaintScene, TickClass, Widget,
207};