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