frust/lib.rs
1//! Facade crate: the public `frust` framework API.
2//!
3//! App authors depend on this single crate. It exposes [`run`], the canonical
4//! app entry point, and curates the view/widget/reactive
5//! vocabulary from the underlying framework crates so the declarative call
6//! shape reads exactly as the spec promises:
7//!
8//! ```no_run
9//! use frust::{Component, View, column, text};
10//!
11//! #[derive(Default)]
12//! struct Counter;
13//!
14//! impl Component for Counter {
15//! type State = i32;
16//!
17//! fn init(&self) -> i32 {
18//! 0
19//! }
20//!
21//! // The whole UI is one declarative tree in `build`; helpers that keep it
22//! // shallow are `Self::` associated fns in a separate inherent impl.
23//! fn build(&self, state: &mut i32) -> impl View<i32> {
24//! column()
25//! .child(Self::greeting("Hello from Frust"))
26//! .child(text(format!("count: {state}")).size(32.0))
27//! }
28//! }
29//!
30//! impl Counter {
31//! fn greeting(message: &str) -> impl View<i32> {
32//! text(message.to_owned()).size(24.0)
33//! }
34//! }
35//!
36//! frust::app!(Counter);
37//! # fn main() {}
38//! ```
39//!
40//! The composition rule: stateless pieces are `Self::` helper fns (or widget
41//! fns) called from `build`; a piece with its own state is a child [`Component`] mounted
42//! with [`component`], which gives it a private state boundary. [`run`] (or
43//! the [`app!`] macro, which also covers the mobile and web shells) starts it.
44//!
45//! ## Low-level escape hatch: `App::new`
46//!
47//! [`App`] is the low-level primitive [`run`] and [`app!`] expand into
48//! (`App::new(state, move |state| AnyView::new(root.build(state)))`): a single ambient
49//! `State` and a build closure, with no [`Component`] state-boundary or
50//! retained local state. Ordinary apps use a [`Component`] with [`run`] or
51//! [`app!`]; reach for `App::new` directly only when you need that raw
52//! `(state, build)` pair:
53//!
54//! ```no_run
55//! use frust::text;
56//!
57//! struct AppState {
58//! greeting: String,
59//! }
60//!
61//! // The build closure is generic over its return (`impl View<State>`), so it
62//! // returns the concrete view directly, with no erasure.
63//! let build = |state: &mut AppState| text(state.greeting.clone()).size(32.0);
64//!
65//! frust::App::new(AppState { greeting: "Hello from Frust".into() }, build)
66//! .run()
67//! .unwrap();
68//! ```
69//!
70//! ## Layout containers
71//!
72//! The fluent `column()` / `row()` builders take each child as-is (they erase
73//! internally); the tuple-style primitives take a tuple of mixed view types
74//! (up to 12, nest beyond), so no [`any`] is written:
75//!
76//! ```
77//! use frust::{
78//! Align, Alignment, Column, Component, EdgeInsets, Padding, Row, SizedBox, View, text,
79//! };
80//!
81//! #[derive(Default)]
82//! struct Layouts;
83//!
84//! impl Component for Layouts {
85//! type State = ();
86//!
87//! fn init(&self) {}
88//!
89//! fn build(&self, _state: &mut ()) -> impl View<()> {
90//! Column((
91//! text("title").size(24.0),
92//! Row((text("left"), text("right"))),
93//! Padding(EdgeInsets::all(8.0), text("padded")),
94//! Align(Alignment::CENTER, text("centered")),
95//! SizedBox(Some(0.0), Some(12.0)),
96//! ))
97//! }
98//! }
99//! # let _ = Layouts;
100//! ```
101//!
102//! The fluent builders also express conditional and iterated children
103//! (`.when`, `.children`) without any erasure:
104//!
105//! ```
106//! use frust::{column, row, stack, text};
107//!
108//! # fn demo(cond: bool) -> frust::FlexView<()> {
109//! let _overlay = stack::<()>().child(text("base")).child(text("badge"));
110//! let _line = row::<()>().children(["a", "b"].map(text));
111//! column()
112//! .child(text("a"))
113//! .flex(1, text("b"))
114//! .when(cond, |c| c.child(text("c")))
115//! # }
116//! # let _ = demo(true);
117//! ```
118
119pub use frust_core::component::{Component, ComponentView, component};
120pub use frust_core::view::{AnyView, View, ViewSeq, any, views};
121pub use frust_widgets::{Either, EitherWidget, either};
122// [`NavigatorController::push_with_options`] (an existing method on the
123// already-flat-re-exported [`NavigatorController`] below) needed its argument
124// type actually constructible from `frust::` — added here as part of the
125// baseline re-export list, alongside the other bare navigator vocabulary
126// (`NavigatorController`/`NavigatorId`/`PageBuilder`/`PopResult`) it was
127// missing:
128//
129// - [`PushOptions`] carries a pushed page's back-press [`BackPolicy`] (and,
130// for a [`BackPolicy::DismissAnimated`] overlay, the shared dismiss-signal
131// cell) alongside opacity/transition/result.
132// - [`PushOptions::on_visibility`]/[`NavigatorController::transition`] hand
133// back [`PageVisibility`]/[`TransitionState`] respectively — both
134// otherwise unnameable without [`PageVisibility`]/[`VisibilityCallback`]/
135// [`TransitionState`] themselves in scope.
136//
137// This closes a re-export gap: a design-system plugin's own modal helper
138// (`frust_glyph::show_glyph_dialog` is the shipped example —
139// `push_with_options(.., PushOptions::transparent())` with a custom
140// [`BackPolicy`]) depends on exactly this seam, so an app- or plugin-authored
141// dialog/sheet could reach the *method* through [`NavigatorController`] but
142// never construct a call to it. See this file's
143// `push_with_options_dismiss_animated` test module below for the worked
144// `BackPolicy::DismissAnimated` + dismiss-signal example — an app-authored
145// modal staging its own exit on Android back instead of vanishing.
146pub use frust_widgets::{
147 AUTO_SCROLL_EDGE_PX, AUTO_SCROLL_MAX_PX_PER_S, Align, AlignView, Alignment, AlwaysScrollable,
148 AnimateTo, AutoScroll, AutoScrollZone, AutoScrollZoneWidget, Axis, BackPolicy, BorderStyle,
149 Bouncing, Button, ButtonStyle, ButtonView, Checkbox, CheckboxView, ChildKey, Clamping, Column,
150 ContainerView, ContainerWidget, CrossAxisAlignment, DRAG_THRESHOLD, DecelerationRate,
151 DividerView, DividerWidget, DragCoordinator, DragHighlight, DragKind, DragPhase, DragPolicy,
152 DragSession, DragSourceId, DragState, DragStateChange, DragSubscription, DragTargetId,
153 DragTargetView, DragTargetWidget, DraggableView, DraggableWidget, EdgeInsets, FlexChild,
154 FlexView, GHOST_OPACITY, GestureDetector, GestureDetectorView, HIGHLIGHT_FALLBACK, HeroView,
155 Icon, IconButton, IconButtonView, IconData, IconSource, IconView, IconWidget, Image,
156 ImageError, ImageFit, ImageSource, ImageView, ListView, ListViewWidget, MAX_FLING_VELOCITY,
157 MIN_FLING_VELOCITY, MainAxisAlignment, NavigatorController, NavigatorId, NavigatorView,
158 NeverScrollable, OverlayAlign, OverlayPlacement, OverlayPortalView, OverlaySide,
159 OverscrollEffect, Padding, PaddingView, PageBuilder, PageTransition, PageVisibility, PopResult,
160 PushOptions, Radio, RadioView, RadioWidget, ReorderableListView, ResultCallback, Row,
161 RubberBand, SafeAreaView, ScaffoldView, ScrollController, ScrollInfo, ScrollMetrics,
162 ScrollPhysics, ScrollSubscription, ScrollView, Simulation, SizedBox, SizedBoxView, Slider,
163 SliderView, SourceFeedback, SpringDescription, Stack, StackView, TextInput, TextInputView,
164 TextView, Timing, Tolerance, TransitionSpec, TransitionState, VisibilityCallback,
165 auto_scroll_zone, button, checkbox, colored_box, column, container, divider, drag_target,
166 draggable, flexible, hero, icon, icon_button, inflexible, keyed, list_view, overlay_portal,
167 radio, reorderable_list, row, safe_area, scaffold, scroll_view, slider, stack, text,
168 text_input,
169};
170// [`ItemAlignment`] (the alignment `ScrollController::scroll_to_item` lands a
171// row at) is not in `frust_widgets`'s own flat re-export list, so the block
172// above cannot carry it; it is reachable only as
173// `frust_widgets::scroll_controller::ItemAlignment`. Re-exported by that path
174// here so an app calling `scroll_to_item` through this facade can name the
175// alignment argument without depending on `frust-widgets` directly.
176pub use frust_widgets::scroll_controller::ItemAlignment;
177
178/// The overlay portal's own vocabulary, flat-re-exported from `frust-core`:
179/// which z-band a floated surface sits in, whether the pointer reaches it, and
180/// what a press landing outside it delivers.
181///
182/// [`overlay_portal`] floats a surface above the whole app, anchored to its
183/// child's bounds and painted after the entire main tree — so it escapes both
184/// the child's paint order and every ancestor's clip, and follows the child
185/// across scroll and relayout with nothing subscribed. The surface is mounted
186/// while [`OverlayPortalView::overlay`] is `Some`; a widget that hosts a
187/// surface of its own instead reaches for `authoring::OverlaySlot`.
188///
189/// Lifted flat for [`EditCommand`]'s reason: an app or design system naming a
190/// band, an input class or a light-dismiss policy is configuring a portal, not
191/// authoring a widget.
192///
193/// ```
194/// use frust::{
195/// OverlayBand, OverlayInput, OverlayPlacement, OverlaySide, View, overlay_portal, text,
196/// };
197///
198/// struct App {
199/// hovering: bool,
200/// }
201///
202/// impl App {
203/// // A tooltip: above its trigger, in the topmost band, and transparent to the
204/// // pointer so hovering the trigger is never interrupted by its own tip.
205/// fn trigger(state: &mut App) -> impl View<App> {
206/// overlay_portal(text("Save"))
207/// .overlay(state.hovering.then(|| text("Save the document")))
208/// .placement(OverlayPlacement::on(OverlaySide::Top))
209/// .band(OverlayBand::Tooltip)
210/// .input(OverlayInput::Transparent)
211/// }
212/// }
213/// # let _ = App::trigger;
214/// ```
215pub use frust_core::{OutsideTap, OverlayBand, OverlayInput};
216
217/// The clipboard/selection command vocabulary, flat-re-exported from
218/// `frust-core::event` (and also available through
219/// [`authoring::EditCommand`]).
220///
221/// [`InputEvent::EditCommand`](authoring::InputEvent) delivers one of
222/// [`EditCommand::Copy`], [`EditCommand::Cut`], [`EditCommand::Paste`] or
223/// [`EditCommand::SelectAll`] down the focus chain, already decoded by the shell
224/// from the platform's own gesture — `Cmd+C` on macOS, `Ctrl+C` or `Ctrl+Insert`
225/// elsewhere, a hardware clipboard key, an Android `ACTION_PROCESS_TEXT`, an iOS
226/// edit-menu tap — so app and design-system code never decodes a chord itself.
227///
228/// Lifted flat rather than left inside [`authoring`] for [`CursorIcon`]'s reason:
229/// a design system's own context menu or toolbar builder names these verbs in
230/// its public API without being a widget author. `Paste` carries its text
231/// because the shell has already read the host clipboard; its [`Debug`] redacts
232/// that text, since a paste payload can be a password or a token.
233pub use frust_core::EditCommand;
234
235/// The file-drop vocabulary for handling file drops from the OS.
236/// [`InputEvent::FileDrop`] carries a [`FileDropEvent`], which embeds a
237/// [`FileDropPhase`] (`Hover`, `Drop`, `Cancel`, or the broadcast-only `Ended`
238/// that follows every `Drop`/`Cancel`) and the dropped file paths
239/// (only on `Drop`). A widget handling drops is hit-tested and bubbles like
240/// [`InputEvent::Scroll`]. Lifted flat for the same reason [`EditCommand`] is:
241/// an app's file-drop handler names these types in its public API without being
242/// a widget author.
243pub use frust_core::event::{FileDropEvent, FileDropPhase};
244
245/// The selection-toolbar seam: the request a text field publishes when it
246/// has a selection ([`SelectionToolbarRequest`]/[`SelectionToolbarActions`]),
247/// and the knobs that decide who draws it.
248///
249/// [`set_selection_toolbar_policy`] chooses
250/// [`SelectionToolbarPolicy::Framework`] (the default — a field floats its
251/// own pod through the overlay portal) or [`SelectionToolbarPolicy::Native`]
252/// (the platform's own edit menu, e.g. iOS's `UIEditMenuInteraction`) — see
253/// that type's own docs for the two-route split. This is **not** an
254/// unconditional free choice, though: a platform shell whose system owns the
255/// menu can LOCK it instead, with `frust_core::lock_selection_toolbar_policy`
256/// — once locked, [`set_selection_toolbar_policy`] is refused rather than
257/// obeyed, so an app cannot silently undo a platform requirement it doesn't
258/// know exists. iOS's shell locks [`SelectionToolbarPolicy::Native`] at
259/// start-up for exactly this reason: a framework-drawn toolbar's own Paste
260/// button would break the exemption that keeps iOS's per-app paste-permission
261/// prompt from firing on every paste.
262///
263/// [`set_selection_toolbar_builder`] installs the view a `Framework`-policy
264/// pod floats, **replacing** whatever the framework's own baseline (installed
265/// at bootstrap, before the first frame) or an earlier design system already
266/// installed. A design system's own installer should reach for
267/// `frust_core::install_selection_toolbar_builder_if_unset` instead — the
268/// cooperative, set-if-unset half of the same pair — so two catalogs linked
269/// into one binary never fight over the slot, and neither ever undoes an
270/// app's own explicit override.
271///
272/// ```
273/// use frust::{
274/// SelectionToolbarPolicy, lock_selection_toolbar_policy, set_selection_toolbar_policy,
275/// };
276///
277/// // A shell whose platform owns its own system edit menu locks the route,
278/// // so that requirement wins no matter what app code does afterward.
279/// let claimed = lock_selection_toolbar_policy(SelectionToolbarPolicy::Native);
280/// assert!(claimed, "the first claim always takes the lock");
281///
282/// // An app's later override is refused rather than obeyed once locked.
283/// set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
284/// ```
285pub use frust_core::{
286 SelectionToolbarActions, SelectionToolbarPolicy, SelectionToolbarRequest,
287 lock_selection_toolbar_policy, set_selection_toolbar_builder, set_selection_toolbar_policy,
288};
289
290/// Platform-view embedding (platform-views feature): reserve
291/// layout space for a native view (a map, a video player, ...) composited
292/// alongside the frust surface. [`platform_view`] takes the
293/// `"dev.frust.<Factory>"`-style native factory name registered on each
294/// platform and returns a builder ([`PlatformViewView`]) over the
295/// params/size contract — flat-re-exported from `frust-widgets` so app code
296/// never names that crate directly.
297///
298/// # Paint contract (Mode B)
299///
300/// Under Mode B compositing the frust surface itself is translucent, so
301/// **any region this slot's parent doesn't paint over is a window straight
302/// through to the native view (or the OS background) behind it** — this is
303/// the mechanism Mode B relies on, not a bug. Size/position a slot
304/// deliberately, and don't rely on an unpainted sibling region staying
305/// opaque.
306///
307/// Mode B is selected by the generated host's `FRUST_TRANSLUCENT_SURFACE`
308/// (Android)/`translucentSurface` (iOS) build-time constant — **not** from
309/// Rust: that constant drives, in one host-glue branch, the native window's
310/// pixel format (`PixelFormat.TRANSLUCENT`/`CAMetalLayer.isOpaque = false`),
311/// the native-sibling z-order/subview arrangement, and the
312/// `nativeSetSurfaceMode`/`frust_set_surface_mode` call together
313/// (flipping only some of these is a host-template defect). There is
314/// no app-Rust opt-in call; the generated project's template is
315/// the sole route into Mode B.
316///
317/// ```no_run
318/// use frust::{Component, View, column, platform_view, text};
319///
320/// #[derive(Default)]
321/// struct MapDemo;
322///
323/// impl Component for MapDemo {
324/// type State = ();
325///
326/// fn init(&self) -> Self::State {}
327///
328/// fn build(&self, _state: &mut Self::State) -> impl View<Self::State> {
329/// column()
330/// .child(text("map below"))
331/// .child(Self::map())
332/// }
333/// }
334///
335/// impl MapDemo {
336/// fn map() -> impl View<()> {
337/// platform_view("dev.frust.MapFactory")
338/// .params_json(r#"{"style":"dark"}"#)
339/// .size(320.0, 240.0)
340/// }
341/// }
342///
343/// frust::app!(MapDemo);
344/// # fn main() {}
345/// ```
346///
347/// # Z-shields ([`shield`])
348///
349/// A slot marked `.interactive()` forwards a touch-DOWN inside its rect to the
350/// native view, including one that landed on frust chrome painted over it (the
351/// OS-side hit test knows nothing about the frust scene). Wrap that chrome in
352/// [`shield`] and it keeps winning input: the wrapper reports the rect it
353/// painted every frame, and the shell hands the overlapping ones to the host
354/// with the slot's placement.
355pub use frust_widgets::{PlatformViewView, ShieldView, platform_view, shield};
356
357/// Declarative custom painting over the [`PaintScene`] trait object — a chart,
358/// a node-and-edge graph, a game board — without hand-rolling a `View`/
359/// `Widget` pair. [`canvas`] takes a paint closure that runs in **local
360/// space** (the widget's own top-left is always `(0, 0)`, and painting past
361/// its own size is clipped, never a bug to chase) — flat-re-exported from
362/// `frust-widgets` so app code never names that crate directly. See
363/// [`CanvasView`]'s own doc for the full builder contract (`.size`/`.expand`
364/// sizing, `.on_hit`-gated `.on_tap`/`.on_pointer`, and `.repaint_key` for
365/// paint-only dirtying driven by data outside the ordinary `View` diff).
366///
367/// ```no_run
368/// use frust::authoring::{PaintCtx, PaintScene};
369/// use frust::{Component, View, canvas};
370/// use kurbo::{Point, Size};
371/// use peniko::Color;
372///
373/// #[derive(Default)]
374/// struct Clock;
375///
376/// impl Component for Clock {
377/// type State = u32;
378///
379/// fn init(&self) -> Self::State {
380/// 0
381/// }
382///
383/// fn build(&self, state: &mut Self::State) -> impl View<Self::State> {
384/// Self::face(*state)
385/// }
386/// }
387///
388/// impl Clock {
389/// fn face(ticks: u32) -> impl View<u32> {
390/// canvas(move |scene: &mut dyn PaintScene, size: Size, _ctx: &PaintCtx| {
391/// scene.fill_rect(Point::ZERO, size, Color::from_rgb8(0x10, 0x10, 0x10));
392/// })
393/// .expand()
394/// .repaint_key(ticks)
395/// }
396/// }
397///
398/// frust::app!(Clock);
399/// # fn main() {}
400/// ```
401pub use frust_widgets::{CanvasView, CanvasWidget, canvas};
402
403/// A pan/zoom viewport over one child — a node-and-edge graph, a map, a large
404/// image — without hand-rolling the gesture math. [`pan_zoom`] lays its child
405/// out at its natural size and places it under a scale-then-translate
406/// transform ([`PanZoomTransform`]) the user drives: primary drag pans (unless
407/// the child claims the press), a touch pinch or a desktop ctrl/⌘+wheel and
408/// trackpad pinch zooms about the gesture's focal point, clamped to
409/// `.min_scale`/`.max_scale`. A plain wheel still reaches the child, and the
410/// child sees its own unscaled local coordinates at any zoom. Flat-re-exported
411/// from `frust-widgets` so app code never names that crate directly; see
412/// [`PanZoomView`]'s own doc for the full contract (`.inertia` glide,
413/// `.on_transform` notification, and the [`PanZoomController`] handle's
414/// `jump_to`/`fit_to_bounds`/`fit_rect`).
415///
416/// ```no_run
417/// use frust::authoring::{PaintCtx, PaintScene};
418/// use frust::{Component, PanZoomController, PanZoomTransform, View, canvas, pan_zoom};
419/// use kurbo::{Point, Size};
420/// use peniko::Color;
421///
422/// #[derive(Default)]
423/// struct Board;
424///
425/// struct BoardState {
426/// zoom: PanZoomController,
427/// transform: PanZoomTransform,
428/// }
429///
430/// impl Component for Board {
431/// type State = BoardState;
432///
433/// fn init(&self) -> Self::State {
434/// BoardState {
435/// zoom: PanZoomController::new(),
436/// transform: PanZoomTransform::IDENTITY,
437/// }
438/// }
439///
440/// fn build(&self, state: &mut Self::State) -> impl View<Self::State> {
441/// pan_zoom(Self::surface())
442/// .max_scale(4.0)
443/// .inertia(true)
444/// .controller(state.zoom.clone())
445/// .on_transform(|state: &mut BoardState, t| state.transform = t)
446/// }
447/// }
448///
449/// impl Board {
450/// fn surface() -> impl View<BoardState> {
451/// canvas(|scene: &mut dyn PaintScene, size: Size, _ctx: &PaintCtx| {
452/// scene.fill_rect(Point::ZERO, size, Color::from_rgb8(0x20, 0x20, 0x20));
453/// })
454/// .size(Size::new(2000.0, 1500.0))
455/// }
456/// }
457///
458/// frust::app!(Board);
459/// # fn main() {}
460/// ```
461pub use frust_widgets::{
462 PanZoomController, PanZoomTransform, PanZoomView, PanZoomWidget, pan_zoom,
463};
464
465/// The vendored Material Symbols starter icon set,
466/// flat-re-exported so app code names `frust::icons::HOME` rather than the
467/// underlying `frust-widgets` crate. Each entry is an
468/// [`IconSource`](crate::IconSource) usable directly with [`icon`](crate::icon);
469/// an app can also supply its own vector icons via
470/// [`IconData::from_path`](crate::IconData::from_path).
471pub use frust_widgets::icons;
472
473/// The declarative router vocabulary (a go_router-subset layer),
474/// flat-re-exported from `frust-widgets` so app code never names that
475/// crate directly: [`Router`] resolves a location against a [`Route`] table
476/// (built via [`RouteBuilder`]) into [`Resolution`]/[`ResolvedPage`]s driving
477/// a [`NavigatorController`], with `:param`/query parsing
478/// ([`Location`]/[`PathPattern`]/[`RouteParams`]), per-route/top-level
479/// [`Redirect`]s (loop-guarded at [`DEFAULT_REDIRECT_LIMIT`]), and an
480/// [`ErrorBuilder`] fallback for an unmatched location.
481///
482/// A page builder receives the location's query merged **under** its path
483/// captures, so `/terminal?session=abc` reads its own parameter.
484///
485/// [`RouteNavigator`] is the seam a screen navigates through: a `Send + Sync`
486/// queue of [`NavRequest`] data (paths and names, never closures) that rides
487/// `provide_context` — the [`Router`] itself cannot, since it holds `Rc` page
488/// builders. [`RouterDeepLinks::track`] drains it every rebuild, so a request
489/// queued in an event handler (on any thread — the queue never panics
490/// off-thread) applies on the next frame.
491///
492/// [`shell_route`] is the nested-navigator binding: its children resolve onto a
493/// second [`NavigatorController`] the app owns and its own page — the chrome
494/// wrapping that inner navigator — stays retained while they do, so navigating
495/// between siblings inside the shell never rebuilds the chrome. See its docs
496/// for the keep rule and the per-verb table.
497pub use frust_widgets::{
498 DEFAULT_REDIRECT_LIMIT, ErrorBuilder, Location, NavChange, NavRequest, NavWaker, PathPattern,
499 Redirect, Resolution, ResolvedPage, Route, RouteBuilder, RouteNavigator, RouteParams,
500 RouteStack, Router, shell_route,
501};
502
503/// The page-transition **resolve/drive** path: the framework's own reduce-
504/// motion collapse policy, re-exported so an app-authored animation reuses it
505/// rather than re-deriving it.
506///
507/// [`resolve_spec`] applies `Theme.motion.reduce_motion`'s hard accessibility
508/// rule to a [`TransitionSpec`] — collapsing any animated preset to a
509/// `≤120ms` linear cross-fade — and resolves a bare [`Timing::ThemeDefault`]
510/// against the active [`MotionScheme`]. [`make_driver`] then turns the
511/// resolved [`Timing`] into a running [`TransitionDriver`] (advanced each
512/// frame via `TransitionDriver::advance`) plus the [`SpringDesc`] to settle it
513/// with later (an interactive edge-swipe's release fling). This is exactly
514/// the pair `frust-widgets`' own navigator and
515/// [`motion::switcher::PatternSwitcher`](crate::motion::switcher::PatternSwitcher)
516/// drive their transitions through — before this re-export, an app widget
517/// implementing its own page/dialog animation had to hand-roll the
518/// reduce-motion collapse (read `Theme.motion.reduce_motion`, apply the
519/// `120ms` + linear + zero-motion rule) instead of calling the framework's one
520/// implementation, so the two would silently drift the next time the
521/// framework's rule changes.
522///
523/// Not otherwise re-exported at `frust-widgets`' own crate root (reached here
524/// via `frust_widgets::nav::transition`, a `pub mod` two levels down) — this
525/// is the facade's job precisely so app code never has to know that.
526///
527/// ```
528/// use frust::{Curve, PageTransition, Theme, Timing, TransitionDriver, TransitionSpec};
529///
530/// // A theme with reduce_motion on: resolve_spec collapses the spec to the
531/// // framework's own linear cross-fade, with no app-side collapse logic.
532/// let mut theme = Theme::neutral();
533/// theme.motion.reduce_motion = true;
534///
535/// let spec = TransitionSpec::duration(PageTransition::SlideUp);
536/// let resolved = frust::resolve_spec(spec, Some(&theme.motion));
537/// assert_eq!(resolved.preset, PageTransition::ReducedCrossfade);
538/// assert_eq!(
539/// resolved.timing,
540/// Timing::Duration(std::time::Duration::from_millis(120), Curve::Linear)
541/// );
542///
543/// let (driver, _settle_spring) = frust::make_driver(resolved.timing);
544/// assert!(matches!(driver, TransitionDriver::Auto(_)));
545/// ```
546pub use frust_widgets::nav::transition::{TransitionDriver, make_driver, resolve_spec};
547
548/// The `drag` module: drop targets, draggable sources, auto-scroll zones,
549/// and reorderable lists, all coordinated through a single ambient
550/// [`DragCoordinator`]. Re-exported **wholesale**
551/// (`pub use frust_widgets::drag;`), mirroring `frust_widgets::icons`'s and
552/// `motion`'s existing wholesale-module precedent, so later types under
553/// `frust_widgets::drag` ride along under `frust::drag::*` with no further
554/// facade edits.
555///
556/// ```
557/// use frust::drag::{DragCoordinator, draggable, drag_target, auto_scroll_zone};
558///
559/// let _coordinator = DragCoordinator::new();
560/// ```
561pub use frust_widgets::drag;
562
563/// The `motion` module: declarative
564/// implicit-animation wrappers (`AnimatedOpacity`/`AnimatedScale` today;
565/// `switcher`/`patterns` land later) over `frust-core`'s `anim`
566/// vocabulary. Re-exported **wholesale**
567/// (`pub use frust_widgets::motion;`), mirroring `frust_widgets::icons`'s
568/// existing wholesale-module precedent — the only other one in this facade —
569/// so later types under `frust_widgets::motion` ride along under
570/// `frust::motion::*` with no further facade edits (see that module's own
571/// docs for the full rationale).
572///
573/// ```
574/// use frust::motion::{AnimatedOpacity, AnimatedOpacityView, AnimatedScale};
575/// use frust::text;
576///
577/// let _opacity: AnimatedOpacityView<()> = AnimatedOpacity(1.0, text("hi"));
578/// let _scale: frust::motion::AnimatedScaleView<()> = AnimatedScale(1.0, text("hi"));
579/// ```
580pub use frust_widgets::motion;
581
582/// Everything needed to author a custom `View`/`Widget` pair.
583///
584/// **`frust` alone is sufficient**: an app that implements its own
585/// layout/paint/event widget should need no framework dependency but this
586/// crate. If something is reachable through neither this module nor the flat
587/// facade, that is a bug — file it rather than reaching for `frust-core`
588/// directly.
589///
590/// `use frust::authoring::*;` covers the widget-authoring vocabulary proper.
591/// Two neighbouring surfaces are deliberately *not* duplicated here because
592/// they are already reachable flat, and a real widget usually wants them too:
593///
594/// - [`frust::input`](crate::input) — gesture constants such as `TOUCH_SLOP`.
595/// - The animation vocabulary — [`AnimationController`](crate::AnimationController),
596/// [`Curve`](crate::Curve), [`FrameTime`](crate::FrameTime) and friends.
597///
598/// For anything the by-name lists below omit, reach for the whole-crate valves:
599/// [`frust::kurbo`](crate::kurbo), [`frust::peniko`](crate::peniko),
600/// [`frust::accesskit`](crate::accesskit).
601///
602/// Not feature-gated (unlike the design-system re-exports above): an app that
603/// disables every catalog (`--no-default-features`) still needs this seam to
604/// build its own design system, so it always resolves.
605///
606/// # The `EditingState` split
607///
608/// `frust_core::event::EditingState` (flat, right here in `authoring`) and
609/// `frust_text::editor::EditingState` (nested in [`text`](authoring::text))
610/// are **two genuinely distinct types** — the first is the retained
611/// IME-surface payload an event-routing widget publishes/receives
612/// (`ImeState`/`ImeEvent`), the second is `TextEditor`'s own byte-indexed
613/// editing snapshot. A glob importing both into one scope will not compile
614/// (`use frust::authoring::*; use frust::authoring::text::*;` collides on the
615/// name); reach for the flat one for event/IME plumbing and
616/// `authoring::text::EditingState` only alongside a `TextEditor` you're
617/// driving yourself.
618///
619/// # Example: a one-child container widget
620///
621/// A container that offsets its single child, wired through the full
622/// lifecycle — build, rebuild, teardown, layout, paint, event routing,
623/// semantics forwarding — with **only** `frust` named. Ported from
624/// `frust_widgets::authoring`'s own worked example (the toolkit this module
625/// re-exports), which an app cannot reach directly without depending on
626/// `frust-widgets` itself.
627///
628/// ```
629/// use frust::authoring::*;
630///
631/// /// The declarative half: a child plus the offset to apply to it.
632/// struct OffsetView<State: 'static> {
633/// offset: Point,
634/// child: AnyView<State>,
635/// }
636///
637/// /// The view-fn app code calls; `any` erases the child because the view
638/// /// *stores* it in a field (storing is the one place erasure is needed).
639/// fn offset<State: 'static, V: View<State>>(offset: Point, child: V) -> OffsetView<State> {
640/// OffsetView { offset, child: any(child) }
641/// }
642///
643/// /// The retained half: the live child pod plus the applied offset.
644/// struct OffsetWidget {
645/// offset: Point,
646/// child: ChildPod,
647/// }
648///
649/// impl<State: 'static> View<State> for OffsetView<State> {
650/// type Element = OffsetWidget;
651///
652/// fn build(&self, ctx: &mut BuildCtx<'_>) -> OffsetWidget {
653/// OffsetWidget { offset: self.offset, child: build_child(&self.child, ctx) }
654/// }
655///
656/// fn rebuild(
657/// &self,
658/// prev: &Self,
659/// element: &mut OffsetWidget,
660/// ctx: &mut BuildCtx<'_>,
661/// ) -> ChangeFlags {
662/// let mut flags = ChangeFlags::NONE;
663/// if prev.offset != self.offset {
664/// element.offset = self.offset;
665/// flags |= ChangeFlags::LAYOUT;
666/// }
667/// flags | rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
668/// }
669///
670/// fn teardown(&self, element: &mut OffsetWidget, ctx: &mut BuildCtx<'_>) {
671/// teardown_child(&self.child, &mut element.child, ctx);
672/// }
673/// }
674///
675/// impl Widget for OffsetWidget {
676/// fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
677/// let size = self.child.layout_child(ctx, bc);
678/// self.child.set_origin(self.offset);
679/// bc.constrain(size)
680/// }
681///
682/// fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
683/// self.child.paint_child(ctx, scene);
684/// }
685///
686/// fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
687/// // Never re-hit-test a captured child by hand — this helper owns the
688/// // capture/focus fast paths and the blur-on-outside-tap rule.
689/// route_event_single(&mut self.child, ctx, event)
690/// }
691///
692/// fn semantics(&self, ctx: &mut SemanticsCtx) {
693/// // A transparent wrapper still MUST forward, or the child's whole
694/// // subtree drops out of the accessibility tree.
695/// self.child.semantics_child(ctx);
696/// }
697/// }
698/// # fn main() {}
699/// ```
700pub mod authoring {
701 // tier 1 — the trait vocabulary (frust-core)
702 /// The cursor vocabulary a widget requests through
703 /// [`EventCtx::set_cursor`](frust_core::event::EventCtx::set_cursor) — lifted
704 /// so a design system's own controls can name a shape (`Pointer` on a button,
705 /// `Text` over an editable, `Grabbing` on a live drag) without a direct
706 /// `frust-core` dependency. Also re-exported flat as
707 /// [`frust::CursorIcon`](crate::CursorIcon).
708 pub use frust_core::CursorIcon;
709 pub use frust_core::{
710 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, DiscardScene, EventCtx,
711 EventOutcome, EventResult, HeroDirective, HeroFrames, InputEvent, Key, KeyEvent, LayoutCtx,
712 Modifiers, NamedKey, PaintCtx, PaintOutcome, PaintScene, PointerButton, PointerEvent,
713 PointerPhase, ScrollDelta, SemanticsCtx, SemanticsUpdate, TickClass, View, ViewSeq, Widget,
714 WidgetId, any, views,
715 };
716 pub use frust_widgets::{Either, EitherWidget, either};
717
718 /// The clipboard/selection vocabulary an editable widget matches on —
719 /// [`InputEvent::EditCommand`] carries one of these four verbs, already
720 /// decoded from whatever chord, hardware key or edit-menu tap produced it.
721 /// A widget answers copy/cut through
722 /// [`EventCtx::write_clipboard`](frust_core::EventCtx::write_clipboard) and
723 /// asks for a paste through
724 /// [`EventCtx::request_paste`](frust_core::EventCtx::request_paste); the shell
725 /// owns the host clipboard at both ends. Also re-exported flat as
726 /// [`frust::EditCommand`](crate::EditCommand), for the same reason
727 /// [`CursorIcon`] is: a design system's public API can name a verb without
728 /// authoring a widget.
729 pub use frust_core::EditCommand;
730
731 /// The file-drop vocabulary for handling file drops from the OS.
732 /// [`InputEvent::FileDrop`] carries a [`FileDropEvent`], which embeds a
733 /// [`FileDropPhase`] (`Hover`, `Drop`, `Cancel`, or the broadcast-only `Ended`
734 /// that follows every `Drop`/`Cancel`) and the dropped file paths
735 /// (only on `Drop`). A widget handling drops is hit-tested and bubbles like
736 /// [`InputEvent::Scroll`]. Also re-exported flat as [`frust::FileDropEvent`]
737 /// and [`frust::FileDropPhase`].
738 pub use frust_core::event::{FileDropEvent, FileDropPhase};
739
740 /// The paint vocabulary [`PaintScene`]'s per-corner and dashed methods name
741 /// — `fill_rounded_rect_radii`/`push_clip_rounded_radii` take a
742 /// [`CornerRadii`], `stroke_path_dashed` a [`DashPattern`]. Lifted for the
743 /// same reason [`WindowInsets`] is: a widget calling those methods cannot
744 /// otherwise name their arguments. Re-exported through `frust-core`'s paint
745 /// surface, so this is the same type [`scene::CornerRadii`] names.
746 pub use frust_core::{CornerRadii, DashPattern};
747
748 /// The event-pass/IME-surface `EditingState` — see this module's own docs
749 /// for the split against [`text::EditingState`](self::text::EditingState),
750 /// `frust_text::editor`'s distinct, byte-indexed type. `ImeContentType` is
751 /// the input-purpose hint an editable widget publishes on its `ImeState` so
752 /// a shell can lock a secret field's keyboard down.
753 pub use frust_core::{EditingState, ImeContentType, ImeEvent, ImeState};
754
755 /// The inset vocabulary [`LayoutCtx::window_insets`]/[`PaintCtx::window_insets`]
756 /// return — lifted because a widget that lays itself out around the status
757 /// bar, notch, or on-screen keyboard cannot otherwise name the value those
758 /// accessors hand it. A bar laying out around the iPadOS window control
759 /// names the corner value, [`CornerInsets`].
760 pub use frust_core::{CornerInset, CornerInsets, WindowEdgeInsets, WindowInsets};
761
762 /// The window-shape value recovered via
763 /// `use_context::<`[`WindowMetrics`]`>()` inside `Component::build` — lifted
764 /// alongside [`WindowInsets`] for the same reason: a widget or component
765 /// laying itself out around window size/scale/orientation cannot otherwise
766 /// name the value. See [`WindowMetrics`]'s own doc for the plain-value
767 /// delivery contract, the alongside-not-superseding relationship with
768 /// [`WindowInsets`], and the derived-orientation/rebuild-cost notes.
769 pub use frust_core::{Orientation, WindowMetrics};
770
771 /// The accessibility-node vocabulary a widget that *contributes* a
772 /// semantics node needs — as opposed to one that only forwards a child's.
773 ///
774 /// [`SemanticsCtx::push_node`] is
775 /// `(role: Role, build: impl FnOnce(&mut Node)) -> NodeId`;
776 /// [`SemanticsCtx::push_container`] takes the same two plus a
777 /// `visit: impl FnOnce(&mut SemanticsCtx)` for its children. `Action`,
778 /// `Live` and `Toggled` are lifted alongside them because they are what
779 /// the `build` closure actually reaches for — `node.add_action(Action::Click)`
780 /// is in eight of `frust-widgets`' own widgets, and a list of exports that
781 /// stopped at `Node` would not be closed over `Node`'s own signature.
782 ///
783 /// For the rest of the crate, use the whole-crate
784 /// [`frust::accesskit`](crate::accesskit) valve.
785 pub use frust_core::accesskit::{Action, Live, Node, NodeId, Role, Toggled};
786
787 // tier 2 — child/event/callback plumbing, verbatim
788 pub use frust_widgets::authoring::*;
789
790 // tier 3 — geometry/paint. NOTE the split: `Stroke` is kurbo, `Fill` is peniko
791 // (kurbo owns the geometry vocabulary, peniko the paint vocabulary).
792 pub use kurbo::{Affine, BezPath, Line, Point, Rect, RoundedRect, Shape, Size, Stroke, Vec2};
793 pub use peniko::{Brush, Color, Fill, ImageData};
794
795 /// Text shaping/measurement for widgets laying out their own glyph runs.
796 ///
797 /// Nests `EditingState` deliberately: `frust_text::editor::EditingState` is
798 /// `TextEditor`'s own byte-indexed editing snapshot, a **distinct type**
799 /// from the flat `authoring::EditingState` (the retained IME-surface
800 /// payload) — see this module's parent docs for the full split. Keeping it
801 /// nested here rather than flattened avoids the glob collision a flat
802 /// re-export of both would cause.
803 pub mod text {
804 pub use frust_text::{EditOp, EditingState, EditingStateBytes, TextEditor};
805 pub use frust_text::{
806 FamilyName, FontFamily, FontStyle, FontWeight, GenericSlot, LineHeight, TextAlign,
807 TextContext, TextLayout, TextOverflow, TextStyle,
808 };
809 /// Types named in [`TextContext`]'s own public signatures —
810 /// `register_fonts() -> Result<Vec<RegisteredFamily>, FontError>` and
811 /// `shape_cache_stats() -> ShapeCacheStats`. Without these an app could
812 /// call the methods but never name what they return.
813 pub use frust_text::{FontError, RegisteredFamily, ShapeCacheStats};
814 /// The index bridge between the two `EditingState`s this seam exposes:
815 /// [`super::EditingState`] (core/IME) counts UTF-16 code units, while
816 /// [`EditingStateBytes`] counts bytes. A widget driving its own
817 /// [`TextEditor`] against the IME surface needs to convert between
818 /// them — `frust_widgets::textinput` calls `utf16_to_byte` to place an
819 /// IME-supplied cursor into its byte-indexed editor; `byte_to_utf16`
820 /// is the return direction, used inside `frust-text` itself to build
821 /// an `EditingState` back out of editor state.
822 pub use frust_text::{byte_to_utf16, utf16_to_byte};
823 }
824
825 /// The renderer-agnostic display list, for widgets painting below `PaintScene`.
826 ///
827 /// `CornerRadii`/`DashPattern` are the same types the flat
828 /// [`authoring::CornerRadii`](super::CornerRadii)/[`authoring::DashPattern`](super::DashPattern)
829 /// re-exports name — listed here too because they are named by
830 /// `Command::RoundedRect`/`PathStyle`, which a widget recording commands
831 /// directly has to match on.
832 pub mod scene {
833 pub use frust_scene::{
834 Command, CornerRadii, DashPattern, FontHandle, Glyph, GlyphRun, PathStyle, Scene,
835 SceneBuilder, ShaderProgram, arc_path,
836 };
837 }
838}
839
840/// The GPU substrate seam, behind the non-default `gpu` cargo feature: the
841/// `frust-gpu` device/texture/pipeline vocabulary an app — or a future
842/// 3D-rendering crate sitting beside the facade — needs to register an
843/// externally owned GPU texture and draw it through
844/// [`authoring::scene::SceneBuilder::scene_texture`], without a direct
845/// `frust-render`/`frust-gpu` dependency of its own.
846///
847/// [`Context`] is `frust-gpu`'s `RenderContext` (the device/instance
848/// foundation every shell already owns) under the name this seam exposes it
849/// as. [`Texture`]/[`TextureDesc`]/[`RenderTarget`] describe a GPU texture
850/// and where it can be rendered into; [`SceneTextureId`] is the
851/// process-unique id [`Texture::as_scene_texture`] mints, and is the value a
852/// [`authoring::scene::Command::SceneTexture`] resolves against — an id
853/// nothing binds simply draws nothing, never an error (see
854/// `scene_texture`'s own docs). [`CommandBuffer`], [`ShaderLibrary`], and
855/// [`RenderPipelineDesc`] are the lower-level encode/pipeline vocabulary a
856/// caller driving its own render pass beside the engine's needs.
857///
858/// This seam is deliberately narrow: nothing above RENDER depends on
859/// `frust-gpu` today (`docs/ARCHITECTURE.md`'s GPU-substrate layer
860/// boundary), and this feature-gated re-export is the one sanctioned
861/// exception — an app opts in explicitly, and the facade's own default build
862/// carries none of it.
863///
864/// A real [`Texture`] is built from a [`TextureDesc`] naming a
865/// `wgpu::TextureFormat`/`wgpu::TextureUsages` pair directly — this crate
866/// does not re-export `wgpu` itself (only the GPU-substrate *types* built on
867/// it), so a caller filling in those two fields depends on `wgpu` in its own
868/// right, exactly as any other code driving a render pass beside the engine
869/// does. Once minted, [`Texture::as_scene_texture`]'s id composes with
870/// [`authoring::scene::SceneBuilder::scene_texture`] with no GPU device in
871/// the loop at all:
872///
873/// ```
874/// use frust::authoring::Rect;
875/// use frust::authoring::scene::{Command, Scene, SceneBuilder};
876/// use frust::gpu::Texture;
877///
878/// // Checked at compile time over any texture/view pair a caller supplies —
879/// // `Texture::as_scene_texture()`'s minted id composes with
880/// // `SceneBuilder::scene_texture` with no live `Texture` needed to prove it.
881/// fn draw_registered_texture<T, V>(texture: &Texture<T, V>, scene: &mut Scene, dest: Rect) {
882/// let mut builder = SceneBuilder::new(scene);
883/// builder.scene_texture(texture.as_scene_texture().get(), dest);
884/// }
885///
886/// // The same recording behavior, exercised at runtime with the same opaque
887/// // `u64` id a minted `SceneTextureId::get()` ultimately is.
888/// let id = 42;
889/// let dest = Rect::new(0.0, 0.0, 64.0, 64.0);
890/// let mut scene = Scene::new();
891/// SceneBuilder::new(&mut scene).scene_texture(id, dest);
892///
893/// match &scene.commands()[0] {
894/// Command::SceneTexture { id: recorded, .. } => assert_eq!(*recorded, id),
895/// other => panic!("expected SceneTexture, got {other:?}"),
896/// }
897/// ```
898///
899/// # Reaching the shell's own live device: [`with_context`]
900///
901/// Everything above builds a *standalone* [`Context`] — useful for a
902/// headless harness, but a second device, not the one the running shell
903/// already created and is presenting frames through. [`with_context`] reaches
904/// that one instead: the shell's render executor installs its
905/// [`DeviceHandle`] (the device/queue/adapter pair, cheap to clone since
906/// wgpu's own types are `Arc`-backed) into a process-wide slot the first time
907/// a surface — and so a device — comes up
908/// (`frust_shell_common::gpu::install_gpu_handle`, see that module's docs),
909/// and [`with_context`] reads it back through the identical seam
910/// (`frust_shell_common::gpu::gpu_handle`). It answers `None` before that
911/// first surface exists (desktop's zero-config preview window, a mobile
912/// shell before its first frame) — check the `Option`, never assume `Some`.
913/// Never blocks: once installed, the read is a lock-free `OnceLock::get`, so
914/// calling this from the UI thread is always safe.
915///
916/// Only the desktop shell installs one today; the two mobile shells forward
917/// the `gpu` feature but do not install yet (an accepted gap — see
918/// `frust-shell-android`/`frust-shell-ios`'s own crate docs), so
919/// `with_context` always answers `None` there for now.
920///
921/// ```
922/// // No device exists in this doctest process, so `with_context` answers
923/// // `None` — exactly the state an app sees before its shell's first frame.
924/// let adapter_name = frust::gpu::with_context(|handle| handle.caps.adapter_name.clone());
925/// assert!(adapter_name.is_none());
926/// ```
927///
928/// # Rendering your own texture every frame: [`ExternalPass`]
929///
930/// Everything above binds a texture whose *pixels* something else already
931/// produced. A caller whose pixels are produced by GPU work of its own — a 3D
932/// scene, a simulation, a video frame converted on the GPU — needs that work
933/// recorded inside the frame that samples it, and needs it recorded every
934/// frame. That is [`ExternalPass`]: implement it, register it under an id,
935/// and the engine tier's renderer calls it once per frame with the frame's
936/// own device, queue and `wgpu::CommandEncoder` ([`ExternalFrame`]) before
937/// anything of the scene is recorded.
938///
939/// The four steps a widget owning such a texture takes:
940///
941/// 1. **Mint an id**, once, and keep it: [`SceneTextureId::mint`] when the
942/// widget has no [`Texture`] to mint from (the usual case here — the pass
943/// creates and re-creates its own target), or
944/// [`Texture::as_scene_texture`] when it does. One id survives every
945/// re-creation of the underlying texture, which is what lets the display
946/// list keep naming the same thing across a resize.
947/// 2. **Register a pass** under it with [`register_external_pass`], handing
948/// over an `Arc<dyn ExternalPass>`. It answers `false` if that id already
949/// has a pass, and changes nothing — a registration is a claim, never a
950/// silent takeover.
951/// 3. **Paint the id.** In `Widget::paint`, record
952/// [`authoring::PaintScene::draw_scene_texture`] with
953/// `id.get()` and the destination rectangle. The pass's binding and the
954/// display list naming it meet inside one frame, so the ordering is
955/// already right; an id whose pass has not bound anything yet simply draws
956/// nothing that frame, never an error.
957/// 4. **Unregister on teardown** with [`unregister_external_pass`] — in
958/// `View::teardown` (a component's `ComponentWidget::teardown`) or
959/// `on_cleanup` for reactive state, never a hand-rolled `Drop`
960/// (`docs/CODE_STANDARDS.md`'s State & Reactivity rule: `Drop` order
961/// across a component's state/element/owner triple is not a contract,
962/// `on_cleanup`/`teardown` are). The next *drained* frame clears the
963/// engine's binding for that id before it runs any pass, so no view is
964/// kept alive for a texture whose owner is gone — though unregistering is
965/// not itself a barrier: a drain already mid-flight when it runs may still
966/// call this pass's `record` once more, and the unbind
967/// lands only once a frame is actually drained, not while the surface is
968/// idle.
969///
970/// A widget whose texture is *animating* asks for the next frame exactly as
971/// any other animating widget does — `PaintCtx::request_frame` (or
972/// `request_frame_paced` for a decorative loop). A pass is called once per
973/// frame the app actually renders; it does not drive the frame loop, and
974/// registering one does not by itself keep frames coming.
975///
976/// ```text
977/// struct Starfield { target: Mutex<Option<wgpu::Texture>> }
978///
979/// impl frust::gpu::ExternalPass for Starfield {
980/// fn record(&self, frame: &mut frust::gpu::ExternalFrame<'_>) {
981/// let texture = self.ensure_target(frame.device()); // my own target
982/// let view = texture.create_view(&Default::default());
983/// {
984/// let mut pass = frame.encoder().begin_render_pass(&/* … */);
985/// // … draw into `view` …
986/// } // pass ends here
987/// frame.bind_texture((WIDTH, HEIGHT), view); // engine draws it, under this pass's own id
988/// }
989/// }
990/// ```
991///
992/// Four rules that go with it:
993///
994/// - **A pass renders into its own target, never into the frame's.** The
995/// engine clears the frame's colour attachment when it records the scene,
996/// so pixels a pass wrote there are gone; what survives is what the scene
997/// composites from a bound texture. A pass also never submits — the
998/// renderer submits the whole frame, once, which is what puts the pass's
999/// work ahead of the scene's in a single command buffer.
1000/// - **A pass never shares the frame's own depth attachment.** It records
1001/// only into attachments it owns, on a target it owns, sized however that
1002/// target needs to be — [`ExternalFrame`] hands out no depth view of its
1003/// own. `frust-gpu::encoder`'s two depth caller rules — the module
1004/// [`CommandBuffer`] comes from — are for a *host* sharing one depth buffer
1005/// across renderers of its own; they have nothing to do with a pass
1006/// registered here.
1007/// - **The result is always composited blended**, never treated as opaque:
1008/// the engine does not read the caller's texels, so it cannot know they are
1009/// (`docs/LIMITATIONS.md`'s `engine-scene-texture-always-blended`). Return
1010/// premultiplied colour, the convention every paint in an engine frame
1011/// travels in.
1012/// - **A pass's recorded work is submitted only if the frame is** — a
1013/// refused engine frame drops the encoder unsubmitted, and every command a
1014/// pass recorded into it goes with it — **but [`ExternalFrame::bind_texture`]
1015/// is not part of that encoder**; it writes the engine's registry directly,
1016/// inside `record`, so its side effect survives a refusal the recorded draw
1017/// commands do not. A pass whose target must never be sampled half-written
1018/// binds only once that target genuinely holds something, rather than
1019/// relying on the frame that was meant to fill it having been accepted.
1020///
1021/// The registry itself is shell-agnostic — a process-wide map with no device,
1022/// window or platform in it, registerable from anywhere at any time. The
1023/// gate is which frame path drains it: any engine-tier `SurfaceRenderer`
1024/// frame, `submit` and `submit_deferred` alike, which is what the Android and
1025/// iOS shells call exactly as desktop does — a registered pass is not
1026/// desktop-only by construction, unlike [`with_context`], which genuinely
1027/// does answer `None` on mobile today (no shell there has installed a
1028/// [`DeviceHandle`] yet). Only the desktop headless path has actually been
1029/// *exercised* so far (`docs/LIMITATIONS.md`'s
1030/// `facade-external-pass-desktop-only`) — mobile is wired and compiles the
1031/// seam, but no registered pass has been driven through either shell's own
1032/// frame loop yet.
1033///
1034/// A panic inside `record` is caught in a debug build: the pass is reported,
1035/// unregistered and unbound, and the frame is recorded without it. The
1036/// workspace's `release` profile is `panic = "abort"`, so that is a
1037/// development net rather than a shipped guarantee — a pass must not panic.
1038#[cfg(feature = "gpu")]
1039pub mod gpu {
1040 pub use frust_gpu::{
1041 CommandBuffer, DeviceHandle, RenderContext as Context, RenderPipelineDesc, RenderTarget,
1042 SceneTextureId, ShaderLibrary, Texture, TextureDesc,
1043 };
1044 /// The pre-scene pass seam (see the module docs' *Rendering your own
1045 /// texture every frame*), from the renderer that owns the frame the
1046 /// passes are recorded into rather than from `frust-gpu`: the registry is
1047 /// process-wide, but only an engine-tier frame drains it.
1048 pub use frust_render::{
1049 ExternalFrame, ExternalPass, register_external_pass, unregister_external_pass,
1050 };
1051 /// The wgpu graphics library, re-exported for plugins that record an
1052 /// [`ExternalPass`] so they do not need a direct `wgpu` dependency.
1053 #[cfg(feature = "gpu")]
1054 pub use wgpu;
1055
1056 /// Run `f` against the shell-owned live [`DeviceHandle`], or `None` if no
1057 /// shell has installed one yet (see the module docs' *Reaching the
1058 /// shell's own live device* section).
1059 ///
1060 /// Distinct from [`Context`] (`frust-gpu`'s `RenderContext`), which an app
1061 /// can build a *standalone* device from (`Context::new()` +
1062 /// `ensure_device_headless`/a real surface) — that path always creates a
1063 /// second device. `with_context` never creates one; it only reads back
1064 /// the one the running shell already owns.
1065 pub fn with_context<R>(f: impl FnOnce(&DeviceHandle) -> R) -> Option<R> {
1066 frust_shell_common::gpu::gpu_handle::<DeviceHandle>().map(f)
1067 }
1068}
1069
1070/// The accessibility vocabulary crate, whole — the long-tail valve behind
1071/// [`authoring`]'s by-name `Node`/`NodeId`/`Role`, for the rest of what a
1072/// semantics-contributing widget may need (`Action`, `Live`, `Toggled`, …).
1073/// Re-exported through `frust-core`, which owns the `accesskit` version pin
1074/// (see `docs/DEVELOPMENT.md` § Version-Pin Policy) — naming it here keeps an
1075/// app on that single pinned version rather than declaring its own.
1076pub use frust_core::accesskit;
1077/// The geometry crate, whole, for types [`authoring`] does not lift by name
1078/// (e.g. `kurbo::Circle`) — the long-tail escape valve alongside the by-name
1079/// list above.
1080pub use kurbo;
1081/// The brush/color crate, whole, for types [`authoring`] does not lift by name
1082/// (e.g. `peniko::Blob`, `peniko::color::DynamicColor`) — the long-tail escape
1083/// valve alongside [`Color`]/the by-name list above.
1084pub use peniko;
1085
1086mod back_glue;
1087mod route_state;
1088mod router_glue;
1089
1090/// Android back-press ⇄ navigator auto-wiring:
1091/// [`attach_back_handler`]/[`BackHandler`] pop a [`NavigatorController`] on a
1092/// platform back press and keep `frust-reactive`'s `handles_back` flag in
1093/// sync with the stack depth, so a shell knows whether a root-level back should
1094/// fall through to the platform (activity finish). Mirrors
1095/// [`RouterDeepLinks`]'s shape and, like it, is the ONLY place in the facade
1096/// that sees both `frust-widgets`' `NavigatorController` and
1097/// `frust-reactive`'s back-press source together — see [`BackHandler`]'s doc
1098/// for the consume/dedupe and timing contracts. Call [`BackHandler::track`]
1099/// from every `Component::build`.
1100pub use back_glue::{BackHandler, attach_back_handler};
1101
1102/// Build a [`NavigatorView`] driven by `controller` — the facade's back-aware
1103/// wrapper over [`frust_widgets::navigator`].
1104///
1105/// Interposes on the flat widget re-export: same signature and return shape, but
1106/// every rebuild it *additionally* auto-wires Android/gesture back handling for
1107/// `controller` — so an app using `frust::navigator` gets the full back contract
1108/// (dismissable overlay dismiss → navigation pop → app exit at the root) with
1109/// **zero** back-specific app code, and with `frust::handles_back` reporting
1110/// whether a root-level press should fall through to the platform.
1111///
1112/// The wiring routes a consumed press through
1113/// [`NavigatorController::request_back`] (honoring each page's back policy —
1114/// pop / animated-dismiss / veto — rather than a bare pop) and computes
1115/// `handles_back` from the navigator's predictive-back interest. It shares a
1116/// single process-wide consumption source with any explicit [`BackHandler`] on
1117/// the same controller, so constructing a `BackHandler` *and* calling
1118/// `frust::navigator` (as `examples/huddle` does) still consumes each press
1119/// exactly once — no double-pop. See [`back_glue`]'s module docs for the
1120/// consume/dedupe and timing contracts.
1121///
1122/// ```no_run
1123/// use frust::{Component, NavigatorController, View, navigator, text};
1124///
1125/// #[derive(Default)]
1126/// struct App;
1127///
1128/// struct AppState {
1129/// nav: NavigatorController<AppState>,
1130/// }
1131///
1132/// impl Component for App {
1133/// type State = AppState;
1134///
1135/// fn init(&self) -> AppState {
1136/// AppState { nav: NavigatorController::new() }
1137/// }
1138///
1139/// fn build(&self, state: &mut AppState) -> impl View<AppState> {
1140/// // Back handling is automatic — no BackHandler needed.
1141/// navigator(&state.nav, Self::home)
1142/// }
1143/// }
1144///
1145/// impl App {
1146/// fn home() -> impl View<AppState> {
1147/// text("home")
1148/// }
1149/// }
1150///
1151/// frust::app!(App);
1152/// # fn main() {}
1153/// ```
1154///
1155/// Also sets the platform-aware edge-swipe default
1156/// ([`NavigatorView::platform_pop_swipe`]) to `cfg!(target_os = "ios")`: the
1157/// interactive pop-swipe is on by default on iOS and off on Android/desktop
1158/// (where the system/window-manager back gesture already exists), with no
1159/// app-side wiring. An app can still override it per navigator
1160/// ([`NavigatorView::pop_swipe`]) or per page
1161/// ([`frust_widgets::PushOptions::pop_swipe`]).
1162///
1163/// `initial` may return any [`View`]; it is erased inside
1164/// [`frust_widgets::navigator`].
1165pub fn navigator<State: 'static, V: View<State>>(
1166 controller: &NavigatorController<State>,
1167 initial: impl Fn() -> V + 'static,
1168) -> NavigatorView<State> {
1169 back_glue::auto_wire(controller);
1170 frust_widgets::navigator(controller, initial).platform_pop_swipe(cfg!(target_os = "ios"))
1171}
1172
1173/// Build a **root overlay host** driven by `controller`, wrapping the app's
1174/// whole root view — the facade's back-aware wrapper over
1175/// [`frust_widgets::overlay_host`].
1176///
1177/// The host is a [`navigator`] whose root page is the entire app (chrome, tab
1178/// shell, inner navigator and all) and whose pushed pages are app-level modals,
1179/// with two defaults changed: no edge-swipe pop, and no host transition (each
1180/// overlay stages its own). Because it sits *above* every piece of chrome, an
1181/// overlay pushed here dims and blocks chrome that an overlay on an inner
1182/// navigator cannot reach — and the chrome goes inert to pointers *and* to
1183/// assistive technology for free, since the navigator routes input and forwards
1184/// accessibility nodes for the top page only.
1185///
1186/// Like [`navigator`], every rebuild additionally auto-wires back handling for
1187/// `controller` — here as a **host**, the rank that claims a press ahead of any
1188/// plain navigator (**R44-back**), whenever and however often either wires. A
1189/// host with no overlays open claims nothing, so back falls through to the inner
1190/// navigator exactly as before the host existed. See [`back_glue`]'s module docs
1191/// for the arbitration contract.
1192///
1193/// ```no_run
1194/// use frust::{Component, NavigatorController, View, navigator, overlay_host, stack, text};
1195///
1196/// #[derive(Default)]
1197/// struct App;
1198///
1199/// struct AppState {
1200/// nav: NavigatorController<AppState>,
1201/// overlays: NavigatorController<AppState>,
1202/// }
1203///
1204/// impl Component for App {
1205/// type State = AppState;
1206///
1207/// fn init(&self) -> AppState {
1208/// AppState {
1209/// nav: NavigatorController::new(),
1210/// overlays: NavigatorController::new(),
1211/// }
1212/// }
1213///
1214/// fn build(&self, state: &mut AppState) -> impl View<AppState> {
1215/// let nav = state.nav.clone();
1216/// // The former root view moves INSIDE the host's page builder — which
1217/// // is also what puts the inner navigator's wiring after the host's.
1218/// overlay_host(&state.overlays, move || Self::shell(nav.clone()))
1219/// }
1220/// }
1221///
1222/// impl App {
1223/// fn shell(nav: NavigatorController<AppState>) -> impl View<AppState> {
1224/// stack()
1225/// .child(navigator(&nav, || text("home")))
1226/// .child(text("persistent chrome"))
1227/// }
1228/// }
1229///
1230/// frust::app!(App);
1231/// # fn main() {}
1232/// ```
1233///
1234/// Also sets the platform-aware edge-swipe default like [`navigator`] does,
1235/// though it never actually changes the host's own behaviour: `overlay_host`
1236/// pins an *explicit* [`NavigatorView::pop_swipe(false)`](NavigatorView::pop_swipe)
1237/// (an edge swipe must never dismiss an overlay), which outranks the platform
1238/// slot by construction.
1239///
1240/// `app` may return any [`View`]; it is erased inside
1241/// [`frust_widgets::overlay_host`].
1242pub fn overlay_host<State: 'static, V: View<State>>(
1243 controller: &NavigatorController<State>,
1244 app: impl Fn() -> V + 'static,
1245) -> NavigatorView<State> {
1246 back_glue::auto_wire_overlay_host(controller);
1247 frust_widgets::overlay_host(controller, app).platform_pop_swipe(cfg!(target_os = "ios"))
1248}
1249
1250/// Router ⇄ deep-link auto-wiring: [`router_with_deep_links`]/
1251/// [`RouterDeepLinks`] resolve a [`Router`]'s start location from the process's
1252/// cold-start deep link (falling back to an app-supplied default) and keep
1253/// navigating it on every subsequent warm link — see [`RouterDeepLinks`]'s doc
1254/// for the precedence and dedupe contracts. This is the ONLY place in the
1255/// facade that sees both `frust-widgets`' `Router` and `frust-reactive`'s
1256/// deep-link source together; neither underlying crate depends on the other.
1257pub use router_glue::{RouterDeepLinks, router_with_deep_links};
1258
1259/// The reactive route-state observable: [`RouteObserver`] is the signal
1260/// face over `frust_widgets`' signal-free `RouteStack`/[`NavChange`] — the
1261/// counterpart to [`RouteNavigator`] (*intent*, queued requests) that reads
1262/// *fact* (the last-published stack) instead. Construct once (typically in
1263/// `Component::init`) and attach with
1264/// [`observe`](RouteObserver::observe)`(navigator(...))`;
1265/// [`RouterDeepLinks::routes`] hands out the one it wired for a router-driven
1266/// navigator. See `route_state`'s module docs for why this bridge lives in
1267/// the facade rather than `frust-widgets`.
1268pub use route_state::RouteObserver;
1269
1270/// The design-token vocabulary: the [`Theme`] bundle plus its
1271/// component token tables, flat-re-exported from `frust-theme` so app code
1272/// never names that crate directly. A root component reads the active theme via
1273/// [`use_context`]`::<`[`Theme`]`>()`; a widget reads it during paint/layout via
1274/// `PaintCtx::theme_as`/`LayoutCtx::theme_as` (or `Theme::from_paint_ctx`).
1275///
1276/// Includes the glass material tokens — this crate ships the opaque recipe,
1277/// and a design system authors its own translucent one over the same types:
1278///
1279/// ```
1280/// use frust::GlassScale;
1281///
1282/// let glass = GlassScale::opaque_material();
1283/// assert_eq!(glass.chrome.blur_radius_intent, 0.0);
1284/// assert!(glass.control.is_opaque());
1285/// ```
1286///
1287/// Also the composable-theming surface:
1288/// [`ThemeBuilder`] (`defineTheme`/`copyWith` analog), the no-lock-in typed
1289/// extension slot ([`ThemeExtensions`]) plus its first consumer
1290/// [`StatusPalette`]/[`StatusColors`] (success/warning/info), and the motion
1291/// vocabulary ([`MotionDurations`]/[`EasingSet`]) — all flat-re-exported so an
1292/// app (or a design-system plugin) authors a theme against `frust::*` alone.
1293pub use frust_theme::{
1294 Brightness, ColorScheme, CosmeticLoopRate, DesignLanguage, EasingSet, Elevation,
1295 ElevationLevel, FontFace, GlassFill, GlassMaterial, GlassScale, MotionDurations, MotionScheme,
1296 MotionSpring, NativeTypefaces, ShadowSpec, ShapeScale, StatusColors, StatusPalette,
1297 SurfaceRole, Theme, ThemeBuilder, ThemeExtensions, TypeScale,
1298};
1299
1300/// The color type every [`ColorScheme`] role is expressed in
1301/// ([`peniko::Color`]), re-exported so app code can author its own color
1302/// values (e.g. a custom accent palette that composes onto a baseline
1303/// [`ColorScheme`]) without naming `peniko` directly — the same
1304/// facade-only-dependency rule the theme re-exports above follow. Construct
1305/// one with [`Color::from_rgb8`]/[`Color::new`]; read its channels via
1306/// `Color::components` (`[f32; 4]`, straight-alpha RGBA).
1307pub use peniko::Color;
1308
1309/// App-facing theme override:
1310/// [`set_app_theme`] forces the app's active [`Theme`] end-to-end — both
1311/// delivery paths a shell owned exclusively before this (widget paint/layout
1312/// via `RenderRoot::set_theme`, and `use_context::<Theme>()` via
1313/// `provide_context`) — reflecting the change the next time the running shell
1314/// polls (once per frame; desktop before rebuild, mobile at the top of the
1315/// frame callback). [`clear_app_theme`] returns to the platform's own
1316/// light/dark-derived default. See `frust_shell_common::theme_override`'s
1317/// module docs for the full layering rationale, the thread contract (a plain
1318/// `Mutex`-guarded process-global — no UI-thread panic, unlike
1319/// [`push_deep_link`]), and the override-wins-over-appearance rule (an app
1320/// override, once set, is never overridden back by a live platform dark-mode
1321/// flip until [`clear_app_theme`] runs).
1322///
1323/// ```no_run
1324/// use frust::{Brightness, Theme, set_app_theme};
1325///
1326/// // Force one appearance end-to-end regardless of what the platform reports
1327/// // — e.g. an in-app light/dark toggle. Any `Theme` works here; a design
1328/// // system passes its own baseline instead of the neutral floor.
1329/// set_app_theme(Theme::neutral().with_brightness(Brightness::Dark));
1330/// ```
1331pub use frust_shell_common::{clear_app_theme, set_app_theme};
1332
1333/// Design-system-facing base-theme seed:
1334/// [`set_default_theme`] supplies the *starting* theme a shell seeds itself
1335/// with, in place of its own built-in fallback — the seam a design-system
1336/// plugin's `install()` calls. Unlike [`set_app_theme`], this does NOT pin
1337/// brightness: the shell keeps re-deriving light/dark from the platform's own
1338/// appearance against this same base, so a design-system-themed app installed
1339/// this way still honours system dark mode. See
1340/// `frust_shell_common::theme_default`'s module docs for the full precedence
1341/// order ([`set_app_theme`] override → [`set_default_theme`] base → the
1342/// shell's built-in fallback) and the brightness-following contrast with
1343/// [`set_app_theme`] spelled out in full.
1344///
1345/// ```no_run
1346/// use frust::{Color, Theme, set_default_theme};
1347///
1348/// // A design-system plugin's install() call, seeding its own base theme as
1349/// // the app's starting point without pinning brightness. A real installer
1350/// // hands over its whole token set; this one edits a single role off the
1351/// // neutral floor to keep the example dependency-free.
1352/// let base = Theme::builder(Theme::neutral())
1353/// .map_colors_light(|mut c| {
1354/// c.primary = Color::from_rgb8(0x6B, 0x4E, 0xFF);
1355/// c
1356/// })
1357/// .build();
1358/// set_default_theme(base);
1359/// ```
1360pub use frust_shell_common::set_default_theme;
1361
1362/// App-facing system-UI (system-bar) override:
1363/// [`set_system_ui_mode`] requests a status-/navigation-bar visibility mode —
1364/// the Flutter `SystemChrome.setEnabledSystemUIMode` analog — reaching
1365/// whichever shell is running the next time it polls (once per frame,
1366/// mirroring [`set_app_theme`]'s delivery timing). See
1367/// `frust_shell_common::system_ui`'s module docs for the full layering
1368/// rationale, the thread contract (a plain `Mutex`-guarded process-global,
1369/// callable from any thread), the FFI wire format each mobile shell exports, and
1370/// where Android/iOS diverge from the five-mode vocabulary.
1371///
1372/// ```no_run
1373/// use frust::{SystemUiMode, set_system_ui_mode};
1374///
1375/// // Hide all system bars; an edge swipe re-shows them.
1376/// set_system_ui_mode(SystemUiMode::Immersive);
1377/// ```
1378pub use frust_shell_common::{SystemUiMode, SystemUiOverlay, set_system_ui_mode};
1379
1380/// App-facing pending-font registry:
1381/// [`register_app_fonts`] pushes raw font bytes (TTF/OTF, or a TTC/OTC
1382/// collection) to be registered into the running shell's `TextContext` the
1383/// next time it drains this registry (construction time, and once per
1384/// frame -- each shell's own wiring). See
1385/// `frust_shell_common::font_registry`'s module docs for the full layering
1386/// rationale and thread contract (mirrors [`set_app_theme`]'s: a plain
1387/// `Mutex`-guarded process-global, callable from any thread).
1388///
1389/// ```no_run
1390/// // Push bundled font bytes (e.g. loaded via `include_bytes!` at the app
1391/// // crate's own build) before or after the app starts running; the shell
1392/// // picks them up on its next drain.
1393/// let font_bytes: Vec<u8> = vec![];
1394/// frust::register_app_fonts(font_bytes);
1395/// ```
1396pub use frust_shell_common::font_registry::register_app_fonts;
1397
1398// No app-facing translucent-surface opt-in lives here: Mode B is a
1399// build-time HOST configuration selected by the
1400// generated template's `FRUST_TRANSLUCENT_SURFACE`/`translucentSurface`
1401// constant, never a runtime Rust call — see [`platform_view`]'s Mode B
1402// section above. `frust_shell_common::declare_host_translucent_surface`
1403// exists only for the generated host glue (Android's `nativeSetSurfaceMode`,
1404// iOS's `frust_set_surface_mode`) to call from the same branch that already
1405// configured the native window translucent, and is deliberately not
1406// re-exported past that crate.
1407
1408/// App-facing **resolved** surface mode — the
1409/// read-only outward half of the Mode B seam whose setter is deliberately
1410/// absent (see the comment above): what the platform actually gave this
1411/// process, not what the host asked for.
1412///
1413/// [`resolved_surface_mode`] answers [`ResolvedSurfaceMode::Unknown`] until a
1414/// shell publishes (no surface yet, or the desktop preview, which has no Mode
1415/// B host seam), then `Opaque`/`Translucent` — or
1416/// [`ResolvedSurfaceMode::RefusedTranslucent`], the case this exists for: the
1417/// host declared Mode B and the platform resolved the surface opaque anyway
1418/// (no matching `CompositeAlphaMode`; see `docs/NATIVE_WIDGETS_ARCHITECTURE.md`'s
1419/// Mode-B paragraph). frust's paint side degrades to
1420/// the Mode A contract on its own, but the host's native-sibling z-order was
1421/// fixed at build time, so a sibling arranged *behind* the surface is
1422/// invisible **and untappable**. Branch on
1423/// [`ResolvedSurfaceMode::translucency_refused`] to render a deliberate
1424/// fallback instead of a dead rect.
1425///
1426/// **This is a poll, not a subscription** — the same contract as
1427/// [`set_app_theme`]'s slot: reading it subscribes to nothing and a change
1428/// never wakes a frame by itself. Read it during a rebuild (or paint/an event
1429/// handler) on the UI thread, exactly where you'd read any other
1430/// process-global shell state, and if the answer must change your UI's shape,
1431/// write it into your own state so the normal dirty path runs.
1432///
1433/// ```no_run
1434/// use frust::{ResolvedSurfaceMode, resolved_surface_mode};
1435///
1436/// // A native-widget slot deciding whether its platform sibling can actually
1437/// // be seen this frame.
1438/// let native_sibling_visible = match resolved_surface_mode() {
1439/// ResolvedSurfaceMode::RefusedTranslucent => false,
1440/// ResolvedSurfaceMode::Unknown
1441/// | ResolvedSurfaceMode::Opaque
1442/// | ResolvedSurfaceMode::Translucent => true,
1443/// };
1444/// ```
1445pub use frust_shell_common::{ResolvedSurfaceMode, resolved_surface_mode};
1446
1447/// The animation vocabulary: the shell-fed frame clock ([`FrameTime`])
1448/// plus the pure easing/interpolation/spring math a widget or app advances it
1449/// through, flat-re-exported from `frust-core::anim`. Time enters from the
1450/// shell during paint (`PaintCtx::frame_time`); nothing here reads a clock.
1451pub use frust_core::anim::{
1452 AnimationController, AnimationStatus, Curve, FrameTime, Lerp, Spring, SpringDesc, Tween,
1453};
1454
1455/// The window's shape, flat-re-exported from `frust-core::app` so app code
1456/// (not just widget authors, see [`authoring::WindowMetrics`]) can recover it
1457/// via `use_context::<`[`WindowMetrics`]`>()` inside `Component::build` — a
1458/// component laying itself out around window size/scale/orientation reads
1459/// this the same way it reads a [`Theme`] via `use_context`. Delivered as a
1460/// plain value (not an `RwSignal`); see [`WindowMetrics`]'s own doc for the
1461/// derived-[`Orientation`] and rebuild-cost notes.
1462pub use frust_core::{Orientation, WindowMetrics};
1463
1464/// The pointer-cursor vocabulary, flat-re-exported from `frust-core::event` (and
1465/// also available through [`authoring::CursorIcon`]).
1466///
1467/// A widget asks for a shape from its own pointer handling —
1468/// `ctx.set_cursor(CursorIcon::Pointer)` — and the desktop shell applies whatever
1469/// the pass resolved; the request is per-pass and stateless, so a widget that
1470/// stops asking falls back to [`CursorIcon::Default`] with nothing to clear. Lifted
1471/// flat rather than left inside [`authoring`] because a design-system plugin's
1472/// public builder API can *name* a cursor (a `Button::cursor(..)` override, a
1473/// disabled control asking for [`CursorIcon::NotAllowed`]) without being a widget
1474/// author itself. `#[non_exhaustive]`: match with a wildcard arm.
1475///
1476/// Honoured on desktop only — the mobile shells never read the resolved value, so
1477/// setting a cursor unconditionally is safe on every platform.
1478pub use frust_core::CursorIcon;
1479
1480/// Pure input/gesture helpers (slop constants, [`input::VelocityTracker`], the
1481/// fling-decay math) re-exported for app authors and advanced widgets.
1482pub mod input {
1483 pub use frust_core::input::{
1484 FLING_DECAY, FLING_STOP, MOUSE_SLOP, TOUCH_SLOP, VELOCITY_WINDOW_MS, VelocityTracker,
1485 WHEEL_LINE_PX, fling_decay, fling_displacement,
1486 };
1487}
1488
1489/// The reactive-programming vocabulary [`Component`] state is built
1490/// on: signals, memos, and context, flat-re-exported from `frust-reactive`/
1491/// `reactive_graph` so app authors never name either crate directly.
1492pub use frust_reactive::{RwSignal, on_cleanup, provide_context, use_context};
1493
1494/// The deep-link read surface (`frust-reactive`'s `app_links`-
1495/// style process-wide source — see its module docs for the semantics): a
1496/// mobile shell delivers a platform link via `frust-reactive`'s
1497/// [`push_deep_link`], and app code reads it here —
1498/// [`deep_links()`] returns a [`DeepLinks`] snapshot ([`DeepLinks::initial`])
1499/// plus the live, trackable [`DeepLinks::latest`] signal a
1500/// [`Component::build`] reads to react to cold-start and subsequent links
1501/// uniformly. Router auto-wiring (resolving `deep_links()` against a
1502/// [`Router`]) is a separate opt-in, not automatic here.
1503///
1504/// ```no_run
1505/// use frust::{Component, Route, Router, View, deep_links, text};
1506///
1507/// struct AppState;
1508///
1509/// #[derive(Default)]
1510/// struct NavDemo;
1511///
1512/// impl Component for NavDemo {
1513/// type State = AppState;
1514///
1515/// fn init(&self) -> AppState {
1516/// AppState
1517/// }
1518///
1519/// fn build(&self, _state: &mut AppState) -> impl View<AppState> {
1520/// let _router = Self::router();
1521/// // A late-subscribed read: `initial` sees a cold-start link (if any);
1522/// // `latest` is the live signal a rebuild tracks for warm links.
1523/// let links = deep_links();
1524/// let _ = links.initial;
1525/// let _ = links.latest;
1526/// text("nav demo")
1527/// }
1528/// }
1529///
1530/// impl NavDemo {
1531/// fn router() -> Router<AppState> {
1532/// // A route builder returns any `View`, so it needs no erasure.
1533/// Router::new(vec![Route::new("/", |_params| text("home"))])
1534/// }
1535/// }
1536/// # let _ = NavDemo;
1537/// ```
1538/// [`push_deep_link`] is normally called by a mobile shell's platform-link
1539/// handler; it is also re-exported here as the desktop dev seam
1540/// (no shell writes on desktop yet) — `examples/navdemo`'s
1541/// "simulate deep link" button calls it directly to demonstrate warm-link
1542/// navigation without a real platform link.
1543pub use frust_reactive::{DeepLink, DeepLinks, deep_links, push_deep_link};
1544
1545/// The Android back-press source (`frust-reactive`'s
1546/// process-wide back source — see its `back` module docs). A mobile shell
1547/// delivers a hardware/gesture back press via [`push_back_press`], and the
1548/// facade's [`BackHandler`] reads it via [`back_presses`] to pop a navigator;
1549/// [`set_handles_back`]/[`handles_back`] are the "framework consumes the next
1550/// back" flag a shell polls to decide whether a root-level back falls through
1551/// to the platform. App code normally uses [`attach_back_handler`] rather than
1552/// these directly; [`push_back_press`] is also the desktop dev seam (no shell
1553/// writes on desktop yet).
1554pub use frust_reactive::{
1555 BackPresses, back_presses, handles_back, push_back_press, set_handles_back,
1556};
1557
1558/// The **menu-activation** read surface (`frust-reactive`'s process-wide menu
1559/// source — see its module docs): a per-OS desktop shell drains its native menu
1560/// queue once per frame and reports each activation, and app code observes it
1561/// here. [`menu_events()`] returns the live [`MenuEvents`] handle whose `latest`
1562/// signal carries a [`MenuEvent`] — the activated item's `id` exactly as the
1563/// app wrote it in its [`MenuSpec`], plus a monotonic `sequence` so choosing
1564/// the same item twice reads as two activations rather than one stale value.
1565///
1566/// ```no_run
1567/// use frust::{Component, Get, View, menu_events, text};
1568///
1569/// #[derive(Default)]
1570/// struct MenuDemo;
1571///
1572/// impl Component for MenuDemo {
1573/// type State = ();
1574///
1575/// fn init(&self) -> Self::State {}
1576///
1577/// fn build(&self, _state: &mut Self::State) -> impl View<Self::State> {
1578/// // A tracked read: this rebuild re-runs when an item is activated.
1579/// let label = match menu_events().latest.get() {
1580/// Some(event) => format!("chose {}", event.id),
1581/// None => "nothing chosen yet".to_string(),
1582/// };
1583/// text(label)
1584/// }
1585/// }
1586/// ```
1587///
1588/// Available on **every** target, unlike the [`MenuSpec`] vocabulary that
1589/// describes the menu itself: a component reading menu events compiles on
1590/// Android/iOS too (where nothing ever pushes one), so shared component code
1591/// needs no `cfg` of its own.
1592///
1593/// Unlike [`push_deep_link`], the *push* side is deliberately **not**
1594/// re-exported: a menu activation has exactly one producer — the per-OS shell
1595/// that owns the platform menu — and nothing in an app simulates one the way
1596/// `examples/navdemo` simulates a warm deep link.
1597pub use frust_reactive::{MenuEvent, MenuEvents, menu_events};
1598pub use reactive_graph::computed::Memo;
1599pub use reactive_graph::signal::{ReadSignal, WriteSignal, signal};
1600// The access traits the signal types' methods are defined through — without
1601// these in scope, `sig.get()`/`sig.set(..)`/`sig.update(..)` do not compile,
1602// so a facade-only consumer could name the types but never use them.
1603pub use reactive_graph::traits::{Get, GetUntracked, Set, Track, Update, With, WithUntracked};
1604
1605/// Spawns a `Send` future on the background reactive runtime (Tokio-backed —
1606/// see `frust_reactive::ReactiveRuntime`). A thin wrapper over
1607/// `any_spawner::Executor::spawn`; app authors never name `any_spawner`.
1608pub fn spawn(fut: impl std::future::Future<Output = ()> + Send + 'static) {
1609 any_spawner::Executor::spawn(fut);
1610}
1611
1612/// Spawns a `!Send` future on the UI-thread local task queue, drained each
1613/// frame by the shell (`ReactiveRuntime::pump_local`). A thin wrapper over
1614/// `any_spawner::Executor::spawn_local`; must be called on the UI thread —
1615/// see `frust_reactive::ReactiveRuntime::pump_local`'s doc for the panic
1616/// this triggers off-thread.
1617pub fn spawn_local(fut: impl std::future::Future<Output = ()> + 'static) {
1618 any_spawner::Executor::spawn_local(fut);
1619}
1620
1621/// The heavy-work idiom: [`AsyncValue`] state,
1622/// [`use_task`] (the blessed load/compute helper), [`UseTask`] handle, and
1623/// [`spawn_blocking`] (the CPU-bound entry point) — Frust's counterpart to
1624/// Flutter's `compute()`/`FutureBuilder`, with explicit cancellation on
1625/// component teardown. `spawn_blocking` joins the existing
1626/// [`spawn`]/[`spawn_local`] routing pair (async IO / UI-thread `!Send` /
1627/// one-off CPU work). App crates need no new dependency: this is the whole
1628/// heavy-work surface. See `frust_reactive::task` for the threading contract.
1629pub use frust_reactive::{AsyncValue, TaskError, UseTask, spawn_blocking, use_task};
1630
1631mod image_async;
1632
1633/// Off-thread image decode: [`decode_image_async`] wraps
1634/// the existing synchronous `ImageSource::decode` in `spawn_blocking`, so it
1635/// composes with [`use_task`] for the full load/error/ready idiom without
1636/// ever blocking the UI thread on a decode. See `image_async`'s module docs
1637/// for why this lives in the facade rather than `frust-widgets` (which stays
1638/// reactive-free by charter) and for the Arc-move contract the decoded
1639/// [`ImageSource`] crosses threads under.
1640pub use image_async::{ImageDecodeError, decode_image_async};
1641
1642// Re-export the Android JNI-bridge macro so generated apps write
1643// `frust::android_app!(AppState, build)`. `pub use` of a
1644// `#[macro_export]` macro re-exports it on edition 2021+; the macro only expands
1645// to real code where its call site is `#[cfg(target_os = "android")]`, so this is
1646// inert on desktop.
1647pub use frust_shell_android::android_app;
1648
1649// Re-export the iOS C-ABI-bridge macro so generated apps write
1650// `frust::ios_app!(AppState, build)`. Unlike `android_app!`,
1651// the invocation is unconditional — the macro's generated `frust_*` exports
1652// are each `#[cfg(target_os = "ios")]`, so it is inert off-iOS.
1653pub use frust_shell_ios::ios_app;
1654
1655// Hidden, wasm32-only re-export of the `wasm-bindgen` crate.
1656// [`web_app!`]'s generated `#[wasm_bindgen(start)]` shim references the
1657// attribute through `$crate::__wasm_bindgen::prelude::wasm_bindgen` rather
1658// than a bare `wasm_bindgen::prelude::wasm_bindgen`, so a generated app never
1659// needs `wasm-bindgen` in its own `Cargo.toml` — the same reason
1660// `frust-shell-android`'s `android_app!` routes its JNI types through that
1661// crate's own `__jni` re-export instead of naming `jni` directly (see that
1662// crate's `lib.rs`).
1663#[cfg(target_arch = "wasm32")]
1664#[doc(hidden)]
1665pub use wasm_bindgen as __wasm_bindgen;
1666
1667// Hidden, wasm32-only re-export of the browser shell crate, for the identical
1668// reason as `__wasm_bindgen` above. [`web_app!`]'s generated shim hands the
1669// initialized state and build closure to `__frust_shell_web::run_app`
1670// (`fn run_app<State: 'static, Build, V>(state: State, build: Build)`,
1671// mirroring `frust_shell_desktop::run_desktop_with`'s `(state, build, ..)`
1672// convention), the browser shell's entry point: it wraps `spawn_app`, which
1673// owns the canvas-bound event loop and the `requestAnimationFrame` frame
1674// pipeline, and reports an event-loop construction failure through the `log`
1675// facade because the `wasm_bindgen(start)` shim has nowhere to return one to.
1676// The reference lives inside a `macro_rules!` body, so it is type-checked
1677// only where a caller invokes [`web_app!`]/[`app!`] for a wasm32 target —
1678// `cargo check --target wasm32-unknown-unknown -p frust-ui --tests` covers it.
1679#[cfg(target_arch = "wasm32")]
1680#[doc(hidden)]
1681pub use frust_shell_web as __frust_shell_web;
1682
1683/// Browser panic hook + console log sink, installed once at the top of
1684/// [`web_app!`]'s generated start shim — the wasm32 counterpart of the
1685/// bring-up boilerplate a desktop binary gets for free from a terminal.
1686/// Without the panic hook a Rust panic reaches the browser console as a bare
1687/// `unreachable` trap with no message; without the log sink, `log::warn!`
1688/// and friends (including `frust-render`/`frust-gpu`/`wgpu`'s own records)
1689/// go nowhere, since stderr is a silent no-op in a browser. Mirrors the
1690/// proven shape in the browser render probe (`examples/web-spike/src/main.rs`'s
1691/// `mod web::start`), at `Warn` rather than that probe's `Debug`/`Info` — a
1692/// shipped app's default should not carry wgpu's naga typifier chatter.
1693///
1694/// `#[doc(hidden)]` and free-standing (not inside the macro body): this
1695/// function only calls real, already-present dependencies
1696/// (`console_error_panic_hook`, `console_log`, `log`), so it is safe to keep
1697/// as ordinary always-compiled code rather than deferring it into
1698/// `web_app!`'s uninstantiated macro text the way [`__frust_shell_web`]'s
1699/// `run_app` reference must be.
1700#[cfg(target_arch = "wasm32")]
1701#[doc(hidden)]
1702pub fn __web_bootstrap() {
1703 console_error_panic_hook::set_once();
1704 let _ = console_log::init_with_level(log::Level::Warn);
1705}
1706
1707/// Brings up the process-wide [`frust_reactive::ReactiveRuntime`] (the w0-04
1708/// wasm arm — `ReactiveRuntime::init`'s `#[cfg(target_family = "wasm")]` arm
1709/// calls `Executor::init_wasm_bindgen()` explicitly, since no automatic wasm
1710/// executor default exists; see `frust_reactive::runtime`'s module docs) and
1711/// runs `state_init` under its root [`frust_reactive::Owner`], exactly the
1712/// [`run_with_setup_and_config`] shape every other platform's entry uses —
1713/// so a signal or context `state_init` creates already has a runtime and an
1714/// owner to be created under.
1715///
1716/// Real, already-present dependencies only (`frust-reactive`), so — like
1717/// [`__web_bootstrap`] — this stays ordinary always-compiled code rather than
1718/// living inside [`web_app!`]'s macro text.
1719#[cfg(target_arch = "wasm32")]
1720#[doc(hidden)]
1721pub fn __web_init_state<State: 'static>(state_init: impl FnOnce() -> State) -> State {
1722 let rt = frust_reactive::ReactiveRuntime::init(std::sync::Arc::new(|| {}));
1723 rt.with_owner(state_init)
1724}
1725
1726/// Install the baseline framework-drawn selection-toolbar view
1727/// ([`frust_widgets::selection_toolbar`]) as the process's default
1728/// [`SelectionToolbarBuilder`](frust_core::SelectionToolbarBuilder), **set-if-
1729/// unset** (`frust_core::install_selection_toolbar_builder_if_unset`) — so a
1730/// design system that already installed its own keeps it, whether that
1731/// install happened earlier in `main` (before `frust::app!`/`frust::run`
1732/// ran at all) or inside an `app!` `setup = { .. }` block.
1733///
1734/// Called once, on the UI thread, at the same point on every platform this
1735/// crate starts an app from — after any `setup = { .. }` block and
1736/// immediately before the root [`Component::init`] (Android/iOS/wasm32, from
1737/// inside [`app!`]'s `@emit_mobile` closures) or before the desktop shell is
1738/// constructed ([`run_desktop_configured`], which every desktop entry point —
1739/// [`App::run`], [`run`], [`run_with_setup`], [`run_desktop_config`],
1740/// [`run_with_setup_and_config`] and `app!`'s desktop arm — funnels through).
1741/// Running it *after* setup, not before, is deliberate: a design system's own
1742/// cooperative install (also `install_selection_toolbar_builder_if_unset`,
1743/// called from inside a `setup = { .. }` block) must get first claim on the
1744/// empty slot, or this baseline would win the race and leave the design
1745/// system's own catalog toolbar never installed.
1746///
1747/// `#[doc(hidden)]`, not part of the public API: reached only through
1748/// `$crate::` from [`app!`]'s macro expansion and this crate's own desktop
1749/// entry points, exactly like [`__web_bootstrap`]/[`__web_init_state`].
1750#[doc(hidden)]
1751pub fn __install_default_selection_toolbar() {
1752 frust_core::install_selection_toolbar_builder_if_unset(std::sync::Arc::new(|req, _win| {
1753 frust_widgets::selection_toolbar(req)
1754 }));
1755}
1756
1757/// The browser counterpart of [`android_app!`]/[`ios_app!`]: binds a
1758/// [`Component`]'s state and build closure to the browser shell's wasm-bindgen
1759/// entry point. Takes the identical two-argument (state type + build
1760/// expression, state built via `Default`) / three-argument (state type +
1761/// explicit state-init expression + build expression) shape
1762/// `android_app!` does, and `app!` drives it through the three-argument form
1763/// exactly the way it drives `android_app!`/`ios_app!` (see `@emit_mobile`
1764/// below) — most apps reach this through `app!`/`web_app!` rather than
1765/// hand-writing the explicit `state_init` form.
1766///
1767/// Expands to two `#[cfg(target_arch = "wasm32")]` functions — so, like
1768/// [`ios_app!`], the invocation itself is unconditional and self-gating:
1769/// calling `web_app!` off wasm32 expands to nothing. `__frust_web_start` is
1770/// `#[wasm_bindgen(start)]` — wasm-bindgen's own module-init entry point, run
1771/// once when the browser instantiates the compiled `.wasm` — and its body is
1772/// a single call into `__frust_web_run`, kept deliberately separate: feeding
1773/// `wasm_bindgen`'s attribute macro a body built straight out of
1774/// `$state_init`/`$build` (closures that can carry a macro-substituted
1775/// `$($setup)?` block nested inside another closure — see `app!`'s
1776/// `@emit_mobile` arm) trips its own re-parse of the function into a spurious
1777/// syntax error on that nested-block shape; a plain, macro-fragment-free call
1778/// is all it ever sees. `__frust_web_run` carries the real work, in order:
1779///
1780/// 1. [`__web_bootstrap`]: installs the panic hook and console log sink.
1781/// 2. [`__web_init_state`]: brings up the [`frust_reactive::ReactiveRuntime`]
1782/// and runs `$state_init` under its root owner — the point at which a
1783/// `setup = { .. }` block bundled into `$state_init` by `app!` (see
1784/// `@emit_mobile`) runs, identically ordered to every other platform.
1785/// 3. Hands the initialized state and `$build` to
1786/// `frust_shell_web::run_app` (via the hidden [`__frust_shell_web`]
1787/// re-export) — the browser shell's own entry point, which owns the
1788/// canvas-bound event loop and the `requestAnimationFrame` frame pipeline
1789/// (see [`__frust_shell_web`]'s doc comment for the contract).
1790#[macro_export]
1791macro_rules! web_app {
1792 ($state_ty:ty, $build:expr $(,)?) => {
1793 $crate::web_app!(
1794 $state_ty,
1795 <$state_ty as ::core::default::Default>::default,
1796 $build
1797 );
1798 };
1799 ($state_ty:ty, $state_init:expr, $build:expr $(,)?) => {
1800 // Factored out of the `#[wasm_bindgen(start)]` function below rather
1801 // than inlined into it: `wasm_bindgen`'s attribute macro re-parses
1802 // the function it is attached to, and a body built straight out of
1803 // `$state_init`/`$build` — themselves closures that may carry a
1804 // macro-substituted `$($setup)?` block nested inside another closure
1805 // (see `app!`'s `@emit_mobile` arm) — trips it into a spurious parse
1806 // error on that nested-block shape. A plain, macro-fragment-free call
1807 // is all `#[wasm_bindgen(start)]` ever sees; this function carries
1808 // the real work instead.
1809 #[cfg(target_arch = "wasm32")]
1810 fn __frust_web_run() {
1811 $crate::__web_bootstrap();
1812 let __frust_state: $state_ty = $crate::__web_init_state($state_init);
1813 $crate::__frust_shell_web::run_app(__frust_state, $build);
1814 }
1815
1816 #[cfg(target_arch = "wasm32")]
1817 #[$crate::__wasm_bindgen::prelude::wasm_bindgen(start)]
1818 pub fn __frust_web_start() {
1819 __frust_web_run();
1820 }
1821 };
1822}
1823
1824/// The desktop app's identity and native-integration vocabulary, re-exported
1825/// from the desktop core so app code never names a shell crate:
1826/// [`DesktopConfig`] (the whole declaration — app name, reverse-DNS id, window
1827/// icon, menu bar, last-window-close policy — handed to [`App::desktop`],
1828/// [`run_desktop_config`] or [`app!`]'s `desktop = { .. }` argument),
1829/// [`MenuSpec`]/[`MenuItemSpec`]/[`MenuRole`] (the platform-independent native
1830/// menu tree a per-OS shell translates into an NSApp menu bar or an `HMENU`),
1831/// [`DesktopIconData`] (decoded, tightly-packed RGBA8 — decoding a
1832/// PNG/ICO/ICNS is the caller's job), and [`DEFAULT_APP_NAME`] (the window
1833/// title a config that names nothing still gets).
1834///
1835/// `frust-shell-desktop`'s `IconData` is re-exported **renamed**: the facade
1836/// already carries `frust-widgets`' [`IconData`], an in-UI vector icon, and the
1837/// two are unrelated (one is a `BezPath` a widget paints, the other is the
1838/// window/taskbar bitmap the OS shows). The `Desktop` prefix matches
1839/// [`DesktopConfig`], whose `with_window_icon` is its only consumer.
1840///
1841/// ```no_run
1842/// use frust::{DesktopConfig, MenuItemSpec, MenuRole, MenuSpec};
1843///
1844/// let file = MenuSpec::new()
1845/// .with_item(MenuItemSpec::item("file.open", "Open…").with_accelerator("CmdOrCtrl+O"))
1846/// .with_item(MenuItemSpec::separator())
1847/// .with_item(MenuItemSpec::role(MenuRole::Quit));
1848///
1849/// let config = DesktopConfig::new()
1850/// .with_app_name("Huddle")
1851/// .with_app_id("dev.frust.huddle")
1852/// .with_menu_spec(MenuSpec::new().with_item(MenuItemSpec::submenu("File", file)));
1853/// # let _ = config;
1854/// ```
1855///
1856/// **Desktop-only**, unlike the [`menu_events`] read side: these types are
1857/// defined in `frust-shell-desktop`, which is not in a mobile build's or a
1858/// wasm32 build's dependency graph at all (see this crate's `Cargo.toml`).
1859/// Code shared with a mobile or wasm32 target keeps a `DesktopConfig` behind
1860/// its own
1861/// `#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]`
1862/// — or, more simply, writes it inline in [`app!`]'s `desktop = { .. }`
1863/// argument, which the macro already emits only on the targets that have it.
1864#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
1865pub use frust_shell_desktop::{
1866 DEFAULT_APP_NAME, DesktopConfig, IconData as DesktopIconData, MenuItemSpec, MenuRole, MenuSpec,
1867};
1868
1869/// A Frust application: the app state plus the `build` closure that maps it
1870/// to a view tree.
1871///
1872/// This is the low-level primitive. [`app!`] and [`run`] expand into
1873/// `App::new(state, move |state| AnyView::new(root.build(state)))` (see
1874/// [`run_with_setup_and_config`]); ordinary apps write a [`Component`] and
1875/// start it with those instead of constructing an `App` by hand.
1876///
1877/// Construct with [`App::new`] and start the event loop with [`App::run`],
1878/// optionally naming a desktop identity with [`App::desktop`] in between.
1879///
1880/// The view type is intentionally *not* a parameter of this struct: capturing a
1881/// build fn's `impl View<State>` opaque return type into a stored type
1882/// parameter defeats method resolution (the opaque type's trait bounds can't
1883/// be re-proven on the already-typed value). Instead [`App::run`] infers the
1884/// view type freshly at the call site, so `App::new(state, build).run()`
1885/// compiles for both `impl View` and concrete-typed `build`.
1886// On a mobile target the fields are consumed only by the desktop-gated `run`,
1887// so they read as dead there; the app is driven through `android_app!`/JNI or
1888// `ios_app!`/C-ABI instead.
1889#[cfg_attr(
1890 any(target_os = "android", target_os = "ios", target_arch = "wasm32"),
1891 allow(dead_code)
1892)]
1893pub struct App<State, Build> {
1894 state: State,
1895 build: Build,
1896 /// The desktop identity [`App::run`] hands the shell —
1897 /// [`DesktopConfig::default()`] (today's zero-config preview window) unless
1898 /// [`App::desktop`] replaced it.
1899 ///
1900 /// Absent on mobile rather than carried and ignored: the type itself lives
1901 /// in the desktop core, which is not in a mobile build's graph at all.
1902 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
1903 config: DesktopConfig,
1904}
1905
1906impl<State, Build> App<State, Build> {
1907 /// Create an app from an initial `state` and its `build` closure.
1908 ///
1909 /// `build` is a `FnMut(&mut State) -> impl View<State>` re-run each frame to
1910 /// produce the current view tree.
1911 pub fn new(state: State, build: Build) -> Self {
1912 Self {
1913 state,
1914 build,
1915 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
1916 config: DesktopConfig::default(),
1917 }
1918 }
1919}
1920
1921#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
1922impl<State: 'static, Build> App<State, Build> {
1923 /// Give the app a desktop identity — name, reverse-DNS id, window icon,
1924 /// native menu bar, last-window-close policy (see [`DesktopConfig`]).
1925 ///
1926 /// Optional: an app that never calls this runs with
1927 /// [`DesktopConfig::default()`], which is the dev-preview window exactly as
1928 /// it has always been. Each per-OS shell reads the fields it can act on and
1929 /// ignores the rest (Linux has no native menu bar, macOS wants an
1930 /// application icon rather than a window one, and so on).
1931 ///
1932 /// ```no_run
1933 /// # struct AppState;
1934 /// # fn build(_: &mut AppState) -> impl frust::View<AppState> + use<> { frust::text("hi") }
1935 /// // `App::new` is the low-level primitive; apps normally use a `Component`.
1936 /// frust::App::new(AppState, build)
1937 /// .desktop(frust::DesktopConfig::new().with_app_name("Huddle"))
1938 /// .run()
1939 /// .unwrap();
1940 /// ```
1941 pub fn desktop(mut self, config: DesktopConfig) -> Self {
1942 self.config = config;
1943 self
1944 }
1945
1946 /// Run the app in the desktop window until it is closed.
1947 ///
1948 /// Blocks the calling thread on the platform event loop. Returns once the
1949 /// window closes, or an error if the window/GPU surface could not be
1950 /// created. The concrete view type `V` is inferred from `build`.
1951 ///
1952 /// Desktop-only: on Android the app is driven by the JNI bridge that
1953 /// [`android_app!`] generates and on iOS by the C-ABI entry points
1954 /// [`ios_app!`] generates, not by this loop.
1955 pub fn run<V>(self) -> anyhow::Result<()>
1956 where
1957 V: View<State>,
1958 Build: FnMut(&mut State) -> V + 'static,
1959 {
1960 let Self {
1961 state,
1962 build,
1963 config,
1964 } = self;
1965 run_desktop_configured(state, build, config)
1966 }
1967}
1968
1969/// Start the shared desktop core with this target's native shell attached.
1970///
1971/// The extension is built *before* the config moves into the core: every per-OS
1972/// constructor takes `&DesktopConfig` and clones out the fields it acts on
1973/// (`app_id`, `window_icon`, `menu_spec`, the close policy), while the core
1974/// itself takes ownership to title the window.
1975///
1976/// This is the single desktop choke point every desktop entry funnels
1977/// through — [`App::run`] directly, and [`run`]/[`run_with_setup`]/
1978/// [`run_desktop_config`]/[`run_with_setup_and_config`]/`app!`'s desktop arm
1979/// indirectly, via `App::new(..).desktop(config).run()` — which is why
1980/// [`__install_default_selection_toolbar`] sits here rather than duplicated
1981/// across each of those: whatever path an app took to get here, any `setup`
1982/// it ran (and any explicit override that ran even earlier, in `main`) has
1983/// already had its chance to claim the selection-toolbar builder slot before
1984/// this call, and this is the one point strictly before the shell — and
1985/// therefore the first frame — starts.
1986#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
1987fn run_desktop_configured<State, Build, V>(
1988 state: State,
1989 build: Build,
1990 config: DesktopConfig,
1991) -> anyhow::Result<()>
1992where
1993 State: 'static,
1994 V: View<State>,
1995 Build: FnMut(&mut State) -> V + 'static,
1996{
1997 __install_default_selection_toolbar();
1998 let extensions = desktop_extensions(&config);
1999 frust_shell_desktop::run_desktop_with(state, build, config, extensions)
2000}
2001
2002/// The per-OS shell selection: one arm per shell crate this crate's manifest
2003/// gates in, under the identical `cfg` the dependency itself carries.
2004///
2005/// This function is the whole of the facade's platform knowledge on desktop —
2006/// nothing else here names a per-OS shell crate.
2007#[cfg(target_os = "macos")]
2008fn desktop_extensions(
2009 config: &DesktopConfig,
2010) -> impl frust_shell_desktop::DesktopExtensions + use<> {
2011 frust_shell_macos::MacosExtensions::new(config)
2012}
2013
2014/// See the macOS arm above.
2015#[cfg(target_os = "windows")]
2016fn desktop_extensions(
2017 config: &DesktopConfig,
2018) -> impl frust_shell_desktop::DesktopExtensions + use<> {
2019 frust_shell_windows::WindowsExtensions::new(config)
2020}
2021
2022/// See the macOS arm above.
2023#[cfg(target_os = "linux")]
2024fn desktop_extensions(
2025 config: &DesktopConfig,
2026) -> impl frust_shell_desktop::DesktopExtensions + use<> {
2027 frust_shell_linux::LinuxExtensions::new(config)
2028}
2029
2030/// The fallback arm: a desktop target with no per-OS shell crate of its own (a
2031/// BSD, say) runs the shared winit core with the whole-set no-op extension —
2032/// window, input, theme and accessibility all work, and only the native
2033/// identity/menu integration is absent. `config` is still threaded through, so
2034/// such a host keeps its window title.
2035#[cfg(not(any(
2036 target_os = "android",
2037 target_os = "ios",
2038 target_os = "macos",
2039 target_os = "windows",
2040 target_os = "linux",
2041 target_arch = "wasm32"
2042)))]
2043fn desktop_extensions(
2044 config: &DesktopConfig,
2045) -> impl frust_shell_desktop::DesktopExtensions + use<> {
2046 let _ = config;
2047 frust_shell_desktop::NoExtensions
2048}
2049
2050/// Run a root [`Component`] in the desktop preview shell until the window
2051/// closes — the canonical `runApp` equivalent for the Component model.
2052///
2053/// `root.init()` seeds the component's retained `State` once; the view
2054/// `root.build(state)` returns is erased into an `AnyView<C::State>` and driven
2055/// through the same desktop preview loop [`App::run`] uses, rebuilding every
2056/// frame. The erasure is required, not cosmetic: `build`'s `impl View` return
2057/// type captures the `&self`/`&mut State` borrows, so a closure handing it out
2058/// unerased would let them escape.
2059///
2060/// Initializes the process-wide [`frust_reactive::ReactiveRuntime`] with a
2061/// no-op waker *before* `root.init()` runs, since a signal or context created
2062/// there must already have a runtime to be created under — the desktop
2063/// preview loop's own later `ReactiveRuntime::init` call swaps in the real
2064/// proxy waker on top of this seed, the documented swap-on-reinit behavior.
2065///
2066/// `root.init()` runs under the runtime's root [`Owner`] (via
2067/// [`with_owner`](frust_reactive::ReactiveRuntime::with_owner)): `init`
2068/// unblocks signal/executor creation, but `provide_context`/`on_cleanup` are
2069/// no-ops unless an `Owner` is ambient, and a root component has no enclosing
2070/// component to supply one — so the root component registers against the root
2071/// owner (process lifetime, never disposed). The desktop loop separately wraps
2072/// each per-frame rebuild in the same owner.
2073///
2074/// Desktop-only, matching [`App::run`]: on Android the app is driven by
2075/// [`android_app!`]/JNI and on iOS by [`ios_app!`]'s C-ABI entry points
2076/// instead, not by this loop.
2077///
2078/// Zero-config: the window is [`DesktopConfig::default()`]'s. Name the app, its
2079/// icon or its menu bar with [`run_desktop_config`] (or [`app!`]'s
2080/// `desktop = { .. }` argument) instead.
2081#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
2082pub fn run<C: Component>(root: C) -> anyhow::Result<()> {
2083 run_with_setup(root, || {})
2084}
2085
2086/// [`run`] with a **setup** closure run once, immediately before
2087/// `root.init()` — the desktop half of [`app!`]'s `setup = { .. }` block.
2088///
2089/// `setup` runs on this (the UI) thread, after
2090/// [`frust_reactive::ReactiveRuntime::init`] and under the runtime's root
2091/// [`Owner`], and therefore *before* the desktop shell is constructed — which
2092/// is the one moment the shell reads [`set_default_theme`]'s slot and drains
2093/// [`register_app_fonts`]' registry. That is exactly the placement `app!`'s
2094/// Android/iOS arms get for free (both run the block inside the state factory
2095/// `create_handle`/`ffi_glue::init` calls before building their `AppHandle`),
2096/// so the ordering contract is identical on all three platforms.
2097///
2098/// A design-system plugin's installer (`frust_glyph::install`, or any other
2099/// design system's equivalent) is the intended payload; app code normally
2100/// reaches this through [`app!`] rather than calling it directly.
2101///
2102/// Desktop-only, matching [`run`]/[`App::run`].
2103#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
2104pub fn run_with_setup<C: Component>(root: C, setup: impl FnOnce()) -> anyhow::Result<()> {
2105 run_with_setup_and_config(root, setup, DesktopConfig::default())
2106}
2107
2108/// [`run`] with the app's desktop identity ([`DesktopConfig`]) — the
2109/// no-setup half of the config-carrying pair, and the function
2110/// [`app!`]'s `desktop = { .. }` argument routes through when no
2111/// `setup = { .. }` block accompanies it.
2112///
2113/// ```no_run
2114/// # use frust::{Component, View, text};
2115/// # #[derive(Default)]
2116/// # struct MyApp;
2117/// # impl Component for MyApp {
2118/// # type State = ();
2119/// # fn init(&self) -> Self::State {}
2120/// # fn build(&self, _state: &mut Self::State) -> impl View<Self::State> { text("hi") }
2121/// # }
2122/// frust::run_desktop_config(
2123/// MyApp,
2124/// frust::DesktopConfig::new()
2125/// .with_app_name("Huddle")
2126/// .with_app_id("dev.frust.huddle"),
2127/// )
2128/// .unwrap();
2129/// ```
2130///
2131/// Desktop-only, matching [`run`]/[`App::run`].
2132#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
2133pub fn run_desktop_config<C: Component>(root: C, config: DesktopConfig) -> anyhow::Result<()> {
2134 run_with_setup_and_config(root, || {}, config)
2135}
2136
2137/// [`run_with_setup`] and [`run_desktop_config`] at once: the single desktop
2138/// entry point the other three delegate to, differing only in which of `setup`
2139/// and `config` they default.
2140///
2141/// `setup` keeps the ordering contract [`run_with_setup`] documents; `config`
2142/// reaches the shared desktop core and this target's native shell together (see
2143/// [`App::desktop`]).
2144///
2145/// Desktop-only, matching [`run`]/[`App::run`].
2146#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
2147pub fn run_with_setup_and_config<C: Component>(
2148 root: C,
2149 setup: impl FnOnce(),
2150 config: DesktopConfig,
2151) -> anyhow::Result<()> {
2152 let rt = frust_reactive::ReactiveRuntime::init(std::sync::Arc::new(|| {}));
2153 let state = rt.with_owner(|| {
2154 setup();
2155 root.init()
2156 });
2157 App::new(state, move |state: &mut C::State| {
2158 __frust_root_build(&root, state)
2159 })
2160 .desktop(config)
2161 .run()
2162}
2163
2164/// Build the root component's view for a root driver: the desktop [`run`]
2165/// family and every closure [`app!`] generates.
2166///
2167/// Under the `hotpatch` feature the build goes through the hot-patch seam
2168/// ([`frust_core::build_erased`]) with the driver's own
2169/// `SeamWitness::of::<C>()` — the driver ran `init`, so its layout is the
2170/// creator's — and a refused build keeps the tree already on screen until the
2171/// session restarts. Without the feature it is the direct, erased call.
2172///
2173/// The two definitions are the feature gate `app!` relies on: its expansion
2174/// lives in the app crate, which has no `hotpatch` feature of its own, so the
2175/// macro calls this unconditionally and never writes the `cfg` itself.
2176#[cfg(feature = "hotpatch")]
2177#[doc(hidden)]
2178pub fn __frust_root_build<C: Component>(
2179 root: &C,
2180 state: &mut C::State,
2181) -> impl View<C::State> + use<C> {
2182 frust_core::hotpatch::RootView::new(frust_core::build_erased(
2183 root,
2184 state,
2185 frust_core::SeamWitness::of::<C>(),
2186 ))
2187}
2188
2189/// Build the root component's view for a root driver (see the `hotpatch`
2190/// definition): the direct call, erased so the view owns no borrow of `root`
2191/// or `state`.
2192#[cfg(not(feature = "hotpatch"))]
2193#[doc(hidden)]
2194pub fn __frust_root_build<C: Component>(
2195 root: &C,
2196 state: &mut C::State,
2197) -> impl View<C::State> + use<C> {
2198 AnyView::new(root.build(state))
2199}
2200
2201/// Register the app's hot-patch anchor, the `__frust_hotpatch_anchor`
2202/// function [`app!`] defines in the app crate, with frust-hotpatch; until it
2203/// is registered every patch is refused. The `hotpatch`-feature definition;
2204/// without the feature this is a no-op, so `app!` can call it unconditionally.
2205#[cfg(feature = "hotpatch")]
2206#[doc(hidden)]
2207pub fn __frust_register_hotpatch_anchor(anchor: extern "C" fn()) {
2208 frust_core::hotpatch::set_anchor(anchor);
2209}
2210
2211/// Register the app's hot-patch anchor: a no-op without the `hotpatch`
2212/// feature (see the feature-on definition).
2213#[cfg(not(feature = "hotpatch"))]
2214#[doc(hidden)]
2215pub fn __frust_register_hotpatch_anchor(_anchor: extern "C" fn()) {}
2216
2217/// The canonical app entry point: one line binds a root
2218/// [`Component`] to all three platforms.
2219///
2220/// ```no_run
2221/// use frust::{Component, View, text};
2222///
2223/// #[derive(Default)]
2224/// struct App;
2225///
2226/// impl Component for App {
2227/// type State = i32;
2228///
2229/// fn init(&self) -> i32 {
2230/// 0
2231/// }
2232///
2233/// fn build(&self, state: &mut i32) -> impl View<i32> {
2234/// Self::label(*state)
2235/// }
2236/// }
2237///
2238/// impl App {
2239/// fn label(count: i32) -> impl View<i32> {
2240/// text(format!("count: {count}")).size(32.0)
2241/// }
2242/// }
2243///
2244/// frust::app!(App);
2245/// # fn main() {}
2246/// ```
2247///
2248/// Expands to:
2249/// - **Android**: `#[cfg(target_os = "android")] frust::android_app!(...)`
2250/// bound to `Root::State`/`Root::build`, mirroring [`android_app!`]'s own
2251/// not-self-gating contract (its generated symbols only compile where the
2252/// Android FFI glue they call into exists).
2253/// - **iOS**: `frust::ios_app!(...)`, invoked unconditionally — like a
2254/// direct `ios_app!` call, it self-gates: every symbol it emits is itself
2255/// `#[cfg(target_os = "ios")]`, so the invocation expands to nothing
2256/// off-iOS. Plus a `#[cfg(target_os = "ios")] __frust_main` **stub**: the
2257/// generated `main.rs` calls `__frust_main()` under
2258/// `#[cfg(not(target_os = "android"))]`, and Xcode builds that bin target,
2259/// so the symbol must exist on iOS even though no desktop shell does. The
2260/// stub explains itself on stderr and exits non-zero rather than silently
2261/// doing nothing — a real iOS app is launched by its Xcode host through the
2262/// C-ABI entry points above, never by running this binary.
2263/// - **Every other target** (desktop): a hidden
2264/// `#[doc(hidden)] pub fn __frust_main()` that runs `Root` through
2265/// [`run`] (or, with a `desktop = { .. }` argument,
2266/// [`run_with_setup_and_config`]), printing the error and exiting non-zero
2267/// on failure. `app!`
2268/// never emits a `fn main` itself — a generated project splits `lib.rs`
2269/// (where `app!` is called) from `main.rs` (a one-line `fn main() {
2270/// <crate>::__frust_main() }`, scaffolded by `frust create` and never
2271/// hand-edited), since only the binary crate may define `main`.
2272///
2273/// `Root` must implement `Component + Default`: the macro constructs a `Root`
2274/// twice with no arguments (once to build each platform's state factory,
2275/// once to close over `build`) — a root component is stateless configuration
2276/// (any real data lives in its `Component::State`), so two independent,
2277/// short-lived instances are inexpensive and behaviorally identical.
2278///
2279/// # `setup = { .. }`: run code before the shell exists
2280///
2281/// An optional second argument is a **setup block** the macro emits at the top
2282/// of each platform's entry, before the shell is constructed:
2283///
2284/// ```no_run
2285/// # use frust::{Component, View, text};
2286/// # #[derive(Default)]
2287/// # struct MyApp;
2288/// # impl Component for MyApp {
2289/// # type State = ();
2290/// # fn init(&self) -> Self::State {}
2291/// # fn build(&self, _state: &mut Self::State) -> impl View<Self::State> { text("hi") }
2292/// # }
2293/// frust::app!(MyApp, setup = { my_design_system::install(); });
2294/// # mod my_design_system {
2295/// # pub fn install() { frust::set_default_theme(frust::Theme::neutral()); }
2296/// # }
2297/// # fn main() {}
2298/// ```
2299///
2300/// This exists because a design system's installer must reach
2301/// [`set_default_theme`]/[`register_app_fonts`] *before* a shell reads them,
2302/// and Android has no `main` at all (its entry is the JNI `nativeInit`
2303/// `android_app!` generates). Any plugin can use it; it is not tied to one
2304/// design system — `frust_glyph::install` is just the first caller.
2305///
2306/// **When it runs**, identically on all three platforms: on the UI thread,
2307/// after `ReactiveRuntime::init`, under the root reactive `Owner`, immediately
2308/// before the root component's `Component::init` — and therefore before any
2309/// shell construction, which is the one point a shell reads
2310/// `set_default_theme`'s slot and drains the pending-font registry. The
2311/// *expansions* differ per platform (Android/iOS place the block inside the
2312/// state factory `android_app!`/`ios_app!` receive, which
2313/// `jni_glue::create_handle`/`ffi_glue::init` call before building their
2314/// `AppHandle`; the desktop arm routes through [`run_with_setup`]); the
2315/// ordering contract above is what is guaranteed.
2316///
2317/// The one-argument form emits no setup tokens: the two mobile state
2318/// factories carry only the anchor registration below ahead of `init`, and the
2319/// desktop arm's `run_with_setup(root, || {})` is literally what [`run`] itself
2320/// is.
2321///
2322/// # The hot-patch anchor
2323///
2324/// In a debug build every expansion also defines the app's hot-patch anchor,
2325/// `#[unsafe(no_mangle)] pub extern "C" fn __frust_hotpatch_anchor() {}`, and
2326/// registers it at startup (first thing in the desktop `__frust_main` and in
2327/// the Android/iOS state factories). The symbol is emitted whatever the
2328/// features; the registration only does something with the facade's
2329/// `hotpatch` feature on. It is omitted under `cfg(test)` and on wasm32. Each
2330/// root build goes through the same hidden root-build function the desktop
2331/// [`run`] family uses, which routes it through the hot-patch seam under that
2332/// feature.
2333///
2334/// ```
2335/// # use frust::{Component, View, text};
2336/// # #[derive(Default)]
2337/// # struct MyApp;
2338/// # impl Component for MyApp {
2339/// # type State = ();
2340/// # fn init(&self) -> Self::State {}
2341/// # fn build(&self, _state: &mut Self::State) -> impl View<Self::State> { text("hi") }
2342/// # }
2343/// frust::app!(MyApp);
2344/// # fn main() {
2345/// #[cfg(debug_assertions)]
2346/// __frust_hotpatch_anchor();
2347/// # }
2348/// ```
2349///
2350/// # `desktop = { .. }`: name the app on desktop
2351///
2352/// A second optional argument, alongside or instead of `setup`, is an
2353/// expression evaluating to a [`DesktopConfig`] — the app's desktop identity
2354/// (name, reverse-DNS id, window icon, native menu bar, close policy):
2355///
2356/// ```no_run
2357/// # use frust::{Component, View, text};
2358/// # #[derive(Default)]
2359/// # struct MyApp;
2360/// # impl Component for MyApp {
2361/// # type State = ();
2362/// # fn init(&self) -> Self::State {}
2363/// # fn build(&self, _state: &mut Self::State) -> impl View<Self::State> { text("hi") }
2364/// # }
2365/// frust::app!(MyApp, desktop = {
2366/// frust::DesktopConfig::new()
2367/// .with_app_name("Huddle")
2368/// .with_app_id("dev.frust.huddle")
2369/// .with_menu_spec(
2370/// frust::MenuSpec::new()
2371/// .with_item(frust::MenuItemSpec::role(frust::MenuRole::Quit)),
2372/// )
2373/// });
2374/// # fn main() {}
2375/// ```
2376///
2377/// The two may be combined in either order
2378/// (`setup = { .. }, desktop = { .. }` or the reverse). The expression is
2379/// emitted **only** into the desktop `__frust_main`, so it may name
2380/// desktop-only types like [`DesktopConfig`] without any `cfg` of its own and
2381/// the mobile arms never see it — an app whose menu/identity matters on desktop
2382/// still compiles unchanged for Android and iOS. It is evaluated once, at
2383/// startup: the macro passes it straight into the run call, so it runs
2384/// *before* the `setup` block (which runs inside, under the reactive owner) and
2385/// before the shell is constructed — a config expression must not depend on
2386/// what `setup` installs.
2387///
2388/// Omitting it is exactly today's behavior:
2389/// [`DesktopConfig::default()`]'s window, through the same
2390/// [`run_with_setup`] call the pre-config macro emitted.
2391#[macro_export]
2392macro_rules! app {
2393 // Internal arms, shared by the public forms below. `$($setup:block)?` is
2394 // empty for the one-argument form, so the two mobile state factories expand
2395 // byte-identically to the pre-setup macro and the desktop arm's
2396 // `run_with_setup(root, || {})` is `run`'s own body. Listed FIRST because
2397 // macro_rules cannot recover from a `$root:ty` fragment that fails to parse:
2398 // were the public arms first, `@emit` would be fed to `:ty` and error out
2399 // instead of falling through to these arms.
2400 //
2401 // The mobile half is factored into `@emit_mobile` because it is identical
2402 // for every public form — only the desktop `__frust_main` differs between
2403 // `@emit` (zero-config) and `@emit_desktop` (config-carrying), and two
2404 // hand-maintained copies of the JNI/C-ABI bindings would be free to drift.
2405 (@emit_mobile $root:ty, $($setup:block)?) => {
2406 // The app-owned hot-patch anchor: one fixed, unmangled symbol whose
2407 // address the patch builder and frust-hotpatch both resolve, so a
2408 // binary without `main` (an Android cdylib) is anchored too. Emitted in
2409 // every debug build because this expansion cannot see the facade's
2410 // `hotpatch` feature; registering it is the feature-gated half. Not under
2411 // `cfg(test)`, where a crate may hold several fixtures and the symbol
2412 // must stay unique, and not on wasm32, which frust-hotpatch does not
2413 // support.
2414 #[cfg(all(debug_assertions, not(test), not(target_arch = "wasm32")))]
2415 #[doc(hidden)]
2416 #[unsafe(no_mangle)]
2417 pub extern "C" fn __frust_hotpatch_anchor() {}
2418
2419 #[cfg(target_os = "android")]
2420 $crate::android_app!(
2421 <$root as $crate::Component>::State,
2422 || {
2423 #[cfg(all(debug_assertions, not(test)))]
2424 $crate::__frust_register_hotpatch_anchor(__frust_hotpatch_anchor);
2425 $($setup)?
2426 $crate::__install_default_selection_toolbar();
2427 $crate::Component::init(&<$root as ::core::default::Default>::default())
2428 },
2429 {
2430 let __frust_root = <$root as ::core::default::Default>::default();
2431 move |state: &mut <$root as $crate::Component>::State| {
2432 $crate::__frust_root_build(&__frust_root, state)
2433 }
2434 }
2435 );
2436
2437 $crate::ios_app!(
2438 <$root as $crate::Component>::State,
2439 || {
2440 #[cfg(all(debug_assertions, not(test)))]
2441 $crate::__frust_register_hotpatch_anchor(__frust_hotpatch_anchor);
2442 $($setup)?
2443 $crate::__install_default_selection_toolbar();
2444 $crate::Component::init(&<$root as ::core::default::Default>::default())
2445 },
2446 {
2447 let __frust_root = <$root as ::core::default::Default>::default();
2448 move |state: &mut <$root as $crate::Component>::State| {
2449 $crate::__frust_root_build(&__frust_root, state)
2450 }
2451 }
2452 );
2453
2454 // The fourth platform: [`web_app!`] self-gates the same way
2455 // `ios_app!` does (its one generated symbol is itself
2456 // `#[cfg(target_arch = "wasm32")]`), so this call is additionally
2457 // gated here too — belt-and-braces, matching `android_app!`'s
2458 // call-site-gated style right above, and making the wasm32 arm easy
2459 // to find beside the other two platforms' calls without reading
2460 // `web_app!`'s own body.
2461 #[cfg(target_arch = "wasm32")]
2462 $crate::web_app!(
2463 <$root as $crate::Component>::State,
2464 || {
2465 $($setup)?
2466 $crate::__install_default_selection_toolbar();
2467 $crate::Component::init(&<$root as ::core::default::Default>::default())
2468 },
2469 {
2470 let __frust_root = <$root as ::core::default::Default>::default();
2471 move |state: &mut <$root as $crate::Component>::State| {
2472 $crate::__frust_root_build(&__frust_root, state)
2473 }
2474 }
2475 );
2476
2477 // The iOS `__frust_main` stub. The generated `main.rs` calls
2478 // `__frust_main()` under `#[cfg(not(target_os = "android"))]` and the
2479 // Xcode phase builds that bin target, so the symbol must exist on iOS
2480 // even though iOS has no desktop shell to start. It logs and exits
2481 // non-zero rather than returning quietly: reaching it means something
2482 // ran the binary directly instead of letting the Xcode host drive
2483 // `ios_app!`'s C-ABI entry points, and a silent success would look like
2484 // an app that started and vanished.
2485 #[cfg(target_os = "ios")]
2486 #[doc(hidden)]
2487 pub fn __frust_main() {
2488 eprintln!(
2489 "frust: __frust_main is the desktop entry point and does nothing on iOS \
2490 — an iOS app is started by its Xcode host, which drives the C-ABI entry \
2491 points `frust::app!` generates."
2492 );
2493 ::std::process::exit(1);
2494 }
2495 };
2496 // Zero-config desktop entry: byte-identical to the pre-config macro.
2497 (@emit $root:ty, $($setup:block)?) => {
2498 $crate::app!(@emit_mobile $root, $($setup)?);
2499
2500 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
2501 #[doc(hidden)]
2502 pub fn __frust_main() {
2503 #[cfg(all(debug_assertions, not(test)))]
2504 $crate::__frust_register_hotpatch_anchor(__frust_hotpatch_anchor);
2505 let __frust_setup = || { $($setup)? };
2506 if let Err(e) = $crate::run_with_setup(
2507 <$root as ::core::default::Default>::default(),
2508 __frust_setup,
2509 ) {
2510 eprintln!("frust: {e:#}");
2511 ::std::process::exit(1);
2512 }
2513 }
2514 };
2515 // Config-carrying desktop entry. `$config` is emitted only inside this
2516 // `cfg`-gated function, so it may name desktop-only types (`DesktopConfig`
2517 // and the menu vocabulary) while the mobile arms above stay untouched.
2518 (@emit_desktop $root:ty, $config:expr, $($setup:block)?) => {
2519 $crate::app!(@emit_mobile $root, $($setup)?);
2520
2521 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
2522 #[doc(hidden)]
2523 pub fn __frust_main() {
2524 #[cfg(all(debug_assertions, not(test)))]
2525 $crate::__frust_register_hotpatch_anchor(__frust_hotpatch_anchor);
2526 let __frust_setup = || { $($setup)? };
2527 if let Err(e) = $crate::run_with_setup_and_config(
2528 <$root as ::core::default::Default>::default(),
2529 __frust_setup,
2530 $config,
2531 ) {
2532 eprintln!("frust: {e:#}");
2533 ::std::process::exit(1);
2534 }
2535 }
2536 };
2537 ($root:ty $(,)?) => {
2538 $crate::app!(@emit $root,);
2539 };
2540 ($root:ty, setup = $setup:block $(,)?) => {
2541 $crate::app!(@emit $root, $setup);
2542 };
2543 ($root:ty, desktop = $config:expr $(,)?) => {
2544 $crate::app!(@emit_desktop $root, $config,);
2545 };
2546 ($root:ty, setup = $setup:block, desktop = $config:expr $(,)?) => {
2547 $crate::app!(@emit_desktop $root, $config, $setup);
2548 };
2549 // The same pair the other way round: an app author writing the identity
2550 // first should not meet a macro error over argument order.
2551 ($root:ty, desktop = $config:expr, setup = $setup:block $(,)?) => {
2552 $crate::app!(@emit_desktop $root, $config, $setup);
2553 };
2554}
2555
2556/// Compile-only smoke of [`app!`]'s `setup = { .. }` form: a
2557/// `Component + Default` fixture bound to all four platforms in one call —
2558/// `cargo test --workspace` compiles this on host (criterion 1: `__frust_main`
2559/// present, no Android JNI symbols), and `cargo check --target
2560/// aarch64-linux-android -p frust-ui --tests` / `--target
2561/// aarch64-apple-ios-sim -p frust-ui --tests` compile it for the two mobile
2562/// targets (criterion 2: the respective platform's exports appear,
2563/// `__frust_main` absent on Android). Lives behind `cfg(test)` — never
2564/// linked into a cdylib/staticlib/binary, so the fixed JNI/C-ABI export names
2565/// `app!` stamps out (via `android_app!`/`ios_app!`) never collide with a
2566/// real generated app's; [`web_app!`]'s single `#[wasm_bindgen(start)]`
2567/// function is the same kind of fixed, per-crate-unique export name, for the
2568/// same reason.
2569///
2570/// The fourth platform's own compile-check, `cargo check --target
2571/// wasm32-unknown-unknown -p frust-ui --tests`, type-checks `web_app!`'s
2572/// generated shim (which calls `frust_shell_web::run_app` — see
2573/// [`__frust_shell_web`]'s doc comment) and is part of the documented wasm
2574/// gate in `docs/DEVELOPMENT.md`.
2575///
2576/// **Exactly one `app!` invocation may be compiled per target** — a second one
2577/// would stamp the same fixed JNI/C-ABI export names — so this fixture takes
2578/// the *setup* form (the newer, ordering-critical arm, whose per-platform
2579/// expansions differ) and [`macro_expansion_no_setup`]/
2580/// [`macro_expansion_desktop_config`] below carry the remaining forms on host
2581/// only. The one-argument form's mobile expansion is
2582/// unchanged from the pre-setup macro (`@emit $root,` emits no setup tokens)
2583/// and every generated project plus `examples/huddle`/`examples/glyph-catalog`
2584/// exercises it.
2585///
2586/// The rule binds *targets*, not modules: the host-only fixtures below invoke
2587/// `app!` again, which is fine because on host the macro emits a plain
2588/// `pub fn __frust_main` (one per module, no fixed export name) and the
2589/// `android_app!`/`ios_app!` halves expand to nothing.
2590///
2591/// The setup payload here names no design system
2592/// ([`set_default_theme`] with [`Theme::neutral`] rather than a plugin's
2593/// `install()`), so this fixture depends on the facade alone.
2594#[cfg(test)]
2595mod macro_expansion {
2596 // `#[allow(dead_code)]`: a zero-field unit struct's derived `Default::default()`
2597 // call (inside `app!`'s generated `__frust_main`/state-factory closures) is
2598 // not recognized as a "construction site" by the dead-code lint the way a
2599 // struct-literal expression is, even though it is genuinely used.
2600 #[derive(Default)]
2601 #[allow(dead_code)]
2602 struct TestApp;
2603
2604 impl crate::Component for TestApp {
2605 type State = u32;
2606
2607 fn init(&self) -> u32 {
2608 0
2609 }
2610
2611 fn build(&self, state: &mut u32) -> impl crate::View<u32> {
2612 *state += 1;
2613 crate::any(crate::text(format!("{state}")))
2614 }
2615 }
2616
2617 crate::app!(
2618 TestApp,
2619 setup = {
2620 crate::set_default_theme(crate::Theme::neutral());
2621 }
2622 );
2623}
2624
2625/// Compile-only smoke of [`app!`]'s **one-argument** form, host-only for the
2626/// one-invocation-per-target reason spelled out on [`macro_expansion`] above:
2627/// on a mobile target this module is cfg'd out so the fixture there stays the
2628/// single setup-form invocation.
2629#[cfg(all(
2630 test,
2631 not(any(target_os = "android", target_os = "ios", target_arch = "wasm32"))
2632))]
2633mod macro_expansion_no_setup {
2634 #[derive(Default)]
2635 #[allow(dead_code)]
2636 struct TestAppNoSetup;
2637
2638 impl crate::Component for TestAppNoSetup {
2639 type State = u32;
2640
2641 fn init(&self) -> u32 {
2642 0
2643 }
2644
2645 fn build(&self, state: &mut u32) -> impl crate::View<u32> {
2646 *state += 1;
2647 crate::any(crate::text(format!("{state}")))
2648 }
2649 }
2650
2651 crate::app!(TestAppNoSetup);
2652}
2653
2654/// Compile-only smoke of [`app!`]'s **`desktop = { .. }`** forms — all three of
2655/// them (config alone, and combined with `setup` in either order), one per
2656/// submodule because each expansion defines its own `__frust_main`.
2657///
2658/// Host-only, for two reasons: the one-invocation-per-target rule spelled out
2659/// on [`macro_expansion`] above, and [`DesktopConfig`] itself, which is not in
2660/// a mobile build's dependency graph at all — which is precisely the property
2661/// these fixtures pin, since a `desktop = { .. }` expression must reach only
2662/// the desktop `__frust_main` and never the mobile state factories.
2663///
2664/// The config payload exercises the full re-exported vocabulary
2665/// ([`DesktopConfig`] + [`MenuSpec`]/[`MenuItemSpec`]/[`MenuRole`]) through the
2666/// facade only, so a re-export dropped from `lib.rs` fails to compile here
2667/// rather than in an app.
2668#[cfg(all(
2669 test,
2670 not(any(target_os = "android", target_os = "ios", target_arch = "wasm32"))
2671))]
2672mod macro_expansion_desktop_config {
2673 #[derive(Default)]
2674 #[allow(dead_code)]
2675 struct TestAppDesktop;
2676
2677 impl crate::Component for TestAppDesktop {
2678 type State = u32;
2679
2680 fn init(&self) -> u32 {
2681 0
2682 }
2683
2684 fn build(&self, state: &mut u32) -> impl crate::View<u32> {
2685 *state += 1;
2686 crate::any(crate::text(format!("{state}")))
2687 }
2688 }
2689
2690 /// The identity an app would write inline in its own `desktop = { .. }`
2691 /// block, factored out so the three fixtures below differ only in the macro
2692 /// arm they take.
2693 // `#[allow(dead_code)]`: its only call sites are inside the `__frust_main`
2694 // bodies `app!` generates, which nothing in a test binary ever calls — the
2695 // point of these fixtures is that they *compile*.
2696 #[allow(dead_code)]
2697 fn fixture_config() -> crate::DesktopConfig {
2698 crate::DesktopConfig::new()
2699 .with_app_name("Fixture")
2700 .with_app_id("dev.frust.fixture")
2701 .with_menu_spec(
2702 crate::MenuSpec::new().with_item(crate::MenuItemSpec::submenu(
2703 "File",
2704 crate::MenuSpec::new()
2705 .with_item(
2706 crate::MenuItemSpec::item("file.open", "Open…")
2707 .with_accelerator("CmdOrCtrl+O"),
2708 )
2709 .with_item(crate::MenuItemSpec::separator())
2710 .with_item(crate::MenuItemSpec::role(crate::MenuRole::Quit)),
2711 )),
2712 )
2713 }
2714
2715 mod config_only {
2716 crate::app!(super::TestAppDesktop, desktop = { super::fixture_config() });
2717 }
2718
2719 mod setup_then_config {
2720 crate::app!(
2721 super::TestAppDesktop,
2722 setup = {
2723 crate::set_default_theme(crate::Theme::neutral());
2724 },
2725 desktop = { super::fixture_config() }
2726 );
2727 }
2728
2729 mod config_then_setup {
2730 crate::app!(
2731 super::TestAppDesktop,
2732 desktop = { super::fixture_config() },
2733 setup = {
2734 crate::set_default_theme(crate::Theme::neutral());
2735 }
2736 );
2737 }
2738}
2739
2740/// The root drivers' build ([`__frust_root_build`]), with or without the
2741/// `hotpatch` feature: one real `build` per call, and a view that materialises.
2742/// Under the feature the build crosses the seam with the driver's own witness,
2743/// which matches, so it must behave exactly as the direct call does.
2744#[cfg(test)]
2745mod root_build {
2746 use crate::{Component, View};
2747 use frust_core::BuildCtx;
2748
2749 struct Counter;
2750
2751 impl Component for Counter {
2752 type State = u32;
2753
2754 fn init(&self) -> u32 {
2755 0
2756 }
2757
2758 fn build(&self, state: &mut u32) -> impl View<u32> {
2759 *state += 1;
2760 crate::any(crate::text(format!("{state}")))
2761 }
2762 }
2763
2764 #[test]
2765 fn the_root_build_runs_build_once_and_yields_a_buildable_view() {
2766 let root = Counter;
2767 let mut state = root.init();
2768 let view = crate::__frust_root_build(&root, &mut state);
2769 assert_eq!(state, 1);
2770 let mut next_id = 0u64;
2771 let mut ctx = BuildCtx::new(&mut next_id);
2772 let mut element = view.build(&mut ctx);
2773 let again = crate::__frust_root_build(&root, &mut state);
2774 assert_eq!(state, 2);
2775 again.rebuild(&view, &mut element, &mut ctx);
2776 }
2777}
2778
2779/// The config-threading half of the desktop run path: [`App::desktop`] is what
2780/// carries an app's [`DesktopConfig`] to the shell, and a zero-config
2781/// [`App::new`] still carries [`DesktopConfig::default()`] — the window the
2782/// preview has always opened.
2783///
2784/// Opening a window isn't testable headless, so this asserts against the value
2785/// `run` would hand `run_desktop_with` rather than the window itself.
2786#[cfg(all(
2787 test,
2788 not(any(target_os = "android", target_os = "ios", target_arch = "wasm32"))
2789))]
2790mod desktop_config_threading {
2791 use crate::{App, DesktopConfig, View, text};
2792
2793 fn logic(_state: &mut ()) -> impl View<()> + use<> {
2794 text("fixture")
2795 }
2796
2797 #[test]
2798 fn a_fresh_app_carries_the_zero_config_desktop_identity() {
2799 let app = App::new((), logic);
2800 assert_eq!(app.config, DesktopConfig::default());
2801 assert_eq!(app.config.window_title(), crate::DEFAULT_APP_NAME);
2802 }
2803
2804 #[test]
2805 fn desktop_threads_the_app_name_into_the_config_the_run_path_receives() {
2806 let app = App::new((), logic).desktop(DesktopConfig::new().with_app_name("Huddle"));
2807 assert_eq!(app.config.app_name.as_deref(), Some("Huddle"));
2808 // The title the shared core titles its window with (and the name the
2809 // macOS shell builds its application menu around).
2810 assert_eq!(app.config.window_title(), "Huddle");
2811 }
2812
2813 #[test]
2814 fn desktop_replaces_the_whole_config_rather_than_merging() {
2815 let app = App::new((), logic)
2816 .desktop(DesktopConfig::new().with_app_name("First"))
2817 .desktop(DesktopConfig::new().with_app_id("dev.frust.second"));
2818 assert_eq!(app.config.app_name, None);
2819 assert_eq!(app.config.app_id.as_deref(), Some("dev.frust.second"));
2820 }
2821}
2822
2823/// Facade-level regression test: [`run`] runs a root
2824/// [`Component`]'s `init` under the process-wide root [`Owner`], so a
2825/// `provide_context` there actually registers (rather than silently no-opping
2826/// with no ambient owner). Opening a preview window isn't testable headless, so
2827/// this exercises `run`'s init-wrapping *equivalent* directly — the same
2828/// `ReactiveRuntime::init` + `with_owner(|| root.init())` shape — and asserts
2829/// the context resolves for code running under that same owner (the extent the
2830/// desktop loop's per-frame rebuild also runs in).
2831#[cfg(test)]
2832mod root_owner_wrap {
2833 use frust_reactive::{ReactiveRuntime, provide_context, use_context};
2834
2835 use crate::Component;
2836
2837 // A context type unique to this test so it can't collide with any other
2838 // test providing context on the shared process-wide root owner.
2839 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
2840 struct RunCtxMarker(u32);
2841
2842 struct RootWithContext;
2843 impl Component for RootWithContext {
2844 type State = u32;
2845 fn init(&self) -> u32 {
2846 provide_context(RunCtxMarker(7));
2847 0
2848 }
2849 fn build(&self, _state: &mut u32) -> impl crate::View<u32> {
2850 crate::any(crate::text(String::new()))
2851 }
2852 }
2853
2854 #[test]
2855 fn run_init_wrapping_makes_root_context_resolvable() {
2856 // Mirror `run()`'s first two lines: init the runtime with a no-op waker,
2857 // then run the root's `init` under the root owner.
2858 let rt = ReactiveRuntime::init(std::sync::Arc::new(|| {}));
2859 let root = RootWithContext;
2860
2861 let resolved = rt.with_owner(|| {
2862 // `init` provides the context...
2863 let _state = root.init();
2864 // ...and (still under the same root owner, as a per-frame rebuild
2865 // would be) it resolves.
2866 use_context::<RunCtxMarker>()
2867 });
2868
2869 assert_eq!(
2870 resolved,
2871 Some(RunCtxMarker(7)),
2872 "run() must wrap root.init() in the root Owner so provide_context sticks"
2873 );
2874 }
2875}
2876
2877/// Facade-level check that the glass material tokens
2878/// ([`GlassScale`]/[`GlassMaterial`]/[`GlassFill`]) re-export through the
2879/// `frust` facade — including enough of the type to let a design-system
2880/// plugin author its own translucent recipe against `frust::*` alone.
2881#[cfg(test)]
2882mod glass_reexport {
2883 use crate::{GlassFill, GlassMaterial, GlassScale, ShadowSpec, Theme};
2884
2885 // A build-time proof the types name-resolve through the facade.
2886 #[allow(dead_code)]
2887 fn _uses_all(_f: GlassFill, _m: GlassMaterial, _s: GlassScale) {}
2888
2889 #[test]
2890 fn the_baseline_exposes_its_opaque_glass_through_the_facade() {
2891 assert!(Theme::neutral().glass.chrome.is_opaque());
2892 assert_eq!(Theme::neutral().glass, GlassScale::opaque_material());
2893 }
2894
2895 #[test]
2896 fn a_design_system_can_author_a_translucent_scale_through_the_facade() {
2897 // The out-of-tree half of the same seam: every type a translucent
2898 // recipe needs (`GlassScale`/`GlassMaterial`/`GlassFill`/`ShadowSpec`)
2899 // must be constructible from `frust::*` with no `frust-theme`
2900 // dependency — this is how a Cupertino-style plugin ships its own
2901 // glass without the framework carrying the recipe.
2902 let lens = GlassMaterial {
2903 blur_radius_intent: 75.0,
2904 fills_light: vec![GlassFill::new(1.0, 1.0, 1.0, 0.34)],
2905 fills_dark: vec![GlassFill::new(0.0, 0.0, 0.0, 0.41)],
2906 hairline_alpha: 0.5,
2907 shadow: ShadowSpec {
2908 y_offset: 18.0,
2909 blur_std_dev: 24.0,
2910 color_alpha: 0.30,
2911 },
2912 };
2913 assert!(!lens.is_opaque());
2914 let theme = Theme::builder(Theme::neutral())
2915 .glass(GlassScale {
2916 chrome: lens.clone(),
2917 bar: lens.clone(),
2918 control: lens,
2919 })
2920 .build();
2921 assert!(!theme.glass.control.is_opaque());
2922 assert_ne!(theme.glass, GlassScale::opaque_material());
2923 }
2924}
2925
2926/// Facade-level check that the pointer-cursor vocabulary ([`CursorIcon`])
2927/// re-exports through the `frust` facade — flat *and* through
2928/// [`authoring`](crate::authoring), since a design-system plugin reaches for it
2929/// from both sides (its public builder API names a shape; its widget internals
2930/// request one).
2931#[cfg(test)]
2932mod cursor_icon_reexport {
2933 use crate::CursorIcon;
2934
2935 // A build-time proof the type name-resolves through the facade, flat and
2936 // through the authoring seam, and that the two are the same type.
2937 #[allow(dead_code)]
2938 fn _uses_both(flat: CursorIcon, nested: crate::authoring::CursorIcon) {
2939 let _same: CursorIcon = nested;
2940 let _also: crate::authoring::CursorIcon = flat;
2941 }
2942
2943 #[test]
2944 fn the_cursor_vocabulary_resolves_through_the_facade() {
2945 // Every shape a desktop-class design system needs is nameable from
2946 // `frust::` alone — no `frust-core` dependency — and `Default` is what an
2947 // unrequested pass resolves to, so a plugin can compare against it.
2948 assert_eq!(CursorIcon::default(), CursorIcon::Default);
2949 let shapes = [
2950 CursorIcon::Default,
2951 CursorIcon::Pointer,
2952 CursorIcon::Text,
2953 CursorIcon::Grab,
2954 CursorIcon::Grabbing,
2955 CursorIcon::ColResize,
2956 CursorIcon::RowResize,
2957 CursorIcon::NotAllowed,
2958 ];
2959 assert_ne!(shapes[1], CursorIcon::Default, "the shapes are distinct");
2960 // `#[non_exhaustive]`: an out-of-tree match must carry a wildcard arm, so
2961 // this is the shape a plugin's own mapping has to take.
2962 for shape in shapes {
2963 let described = match shape {
2964 CursorIcon::Pointer => "clickable",
2965 CursorIcon::Text => "editable",
2966 _ => "other",
2967 };
2968 assert!(!described.is_empty());
2969 }
2970 }
2971}
2972
2973/// Facade-level check that the native-typeface vocabulary
2974/// ([`FontFace`]/[`NativeTypefaces`]) re-export through the
2975/// `frust` facade — the seam a design system attaches via [`ThemeExtensions`]
2976/// for a native-widgets plugin to consume.
2977#[cfg(test)]
2978mod native_typefaces_reexport {
2979 use crate::{FontFace, NativeTypefaces};
2980
2981 // A build-time proof the types name-resolve through the facade.
2982 #[allow(dead_code)]
2983 fn _uses_all(_f: FontFace, _n: NativeTypefaces) {}
2984
2985 #[test]
2986 fn native_typefaces_resolve_through_facade() {
2987 // FontFace constructor is const and can be used in a static context.
2988 let face = FontFace::new("test-family", b"test bytes");
2989 assert_eq!(face.family, "test-family");
2990 assert_eq!(face.bytes, b"test bytes");
2991
2992 // NativeTypefaces can be constructed and used as a ThemeExtensions payload.
2993 let typefaces = NativeTypefaces {
2994 button: Some(face),
2995 body: Some(face),
2996 };
2997 assert!(typefaces.button.is_some());
2998 assert!(typefaces.body.is_some());
2999 }
3000}
3001
3002/// Facade-level check that the pluggable scroll-physics vocabulary
3003/// ([`ScrollPhysics`]/[`Bouncing`]/[`OverscrollEffect`]/[`RubberBand`],
3004/// alongside the rest of `frust_widgets::physics`'s flat re-export) resolves
3005/// through the `frust` facade — the seam [`ScrollView::physics`]/
3006/// [`ScrollView::overscroll_effect`] need a consumer to actually construct an
3007/// argument for without reaching into `frust_widgets` directly.
3008#[cfg(test)]
3009mod scroll_physics_reexport {
3010 use crate::{
3011 Bouncing, OverscrollEffect, RubberBand, ScrollPhysics, ScrollView, scroll_view, text,
3012 };
3013
3014 // A build-time proof the types name-resolve through the facade.
3015 #[allow(dead_code)]
3016 fn _uses_all(_p: &dyn ScrollPhysics, _e: OverscrollEffect) {}
3017
3018 #[test]
3019 fn physics_and_effect_types_resolve_through_facade() {
3020 let view: ScrollView<()> = scroll_view(text("hi"))
3021 .physics(Bouncing::new())
3022 .overscroll_effect(OverscrollEffect::Stretch);
3023 let _ = view;
3024 // RubberBand — the pre-seam feel, now an opt-in rather than any
3025 // platform's default — is directly constructible through the facade
3026 // too, which is the whole point of it staying reachable.
3027 let _: RubberBand = RubberBand::new();
3028 }
3029}
3030
3031/// Acceptance test: an app-authored animation composed entirely
3032/// from facade names ([`resolve_spec`]/[`make_driver`]/[`TransitionDriver`]),
3033/// with **no** hand-rolled reduce-motion branch anywhere in this module — the
3034/// bar this test sets is the *absence* of an app-side `if reduce_motion { .. }`
3035/// check, not merely that these names resolve.
3036#[cfg(test)]
3037mod motion_resolve_reuse {
3038 use std::time::Duration;
3039
3040 use crate::{
3041 Curve, PageTransition, Theme, Timing, TransitionDriver, make_driver, resolve_spec,
3042 };
3043
3044 /// Under `Theme.motion.reduce_motion`, the framework's own collapse policy
3045 /// (`≤120ms` linear cross-fade) applies via a single [`resolve_spec`] call
3046 /// — the app never re-derives it.
3047 #[test]
3048 fn resolve_spec_collapses_under_reduce_motion_without_app_derivation() {
3049 let mut theme = Theme::neutral();
3050 theme.motion.reduce_motion = true;
3051
3052 let spec = crate::TransitionSpec::duration(PageTransition::SlideUp);
3053 let resolved = resolve_spec(spec, Some(&theme.motion));
3054
3055 assert_eq!(
3056 resolved.preset,
3057 PageTransition::ReducedCrossfade,
3058 "reduce_motion collapses any animated preset to ReducedCrossfade"
3059 );
3060 assert_eq!(
3061 resolved.timing,
3062 Timing::Duration(Duration::from_millis(120), Curve::Linear),
3063 "reduce_motion collapses to the framework's own <=120ms linear timing"
3064 );
3065
3066 // Drive the resolved timing through the same `make_driver` the
3067 // navigator/PatternSwitcher use — an app widget advances `driver` from
3068 // its own paint pass exactly like they do.
3069 let (driver, _settle_spring) = make_driver(resolved.timing);
3070 assert!(
3071 matches!(driver, TransitionDriver::Auto(_)),
3072 "a Duration timing always drives TransitionDriver::Auto"
3073 );
3074 }
3075
3076 /// With reduce_motion off, a bare [`Timing::ThemeDefault`] still resolves
3077 /// to a concrete timing through the same call — the non-collapsed half of
3078 /// the same resolve path, still with no app-side branching.
3079 #[test]
3080 fn resolve_spec_resolves_theme_default_when_reduce_motion_is_off() {
3081 let theme = Theme::neutral();
3082 assert!(!theme.motion.reduce_motion);
3083
3084 let spec = crate::TransitionSpec::new(PageTransition::SlideUp, Timing::ThemeDefault);
3085 let resolved = resolve_spec(spec, Some(&theme.motion));
3086
3087 assert_eq!(
3088 resolved.preset,
3089 PageTransition::SlideUp,
3090 "preset is unchanged"
3091 );
3092 assert_ne!(
3093 resolved.timing,
3094 Timing::ThemeDefault,
3095 "ThemeDefault is resolved to a concrete timing, not passed through raw"
3096 );
3097
3098 let (_driver, _settle_spring) = make_driver(resolved.timing);
3099 }
3100}
3101
3102/// A second acceptance case, the sharper half: an app-authored modal using
3103/// [`PushOptions`]/[`BackPolicy`]/[`NavigatorController::push_with_options`] —
3104/// now facade-reachable — with [`BackPolicy::DismissAnimated`]
3105/// and a dismiss-signal cell, the same mechanism `frust-widgets`' own
3106/// `show_glyph_dialog` depends on. Previously `PushOptions`/`BackPolicy`
3107/// were unnameable through `frust::`, so this call could not be constructed at
3108/// all: Android back on an app-authored sheet had no way to stage the exit and
3109/// the sheet would vanish instead of sliding down (the `pop`-not-`request_back`
3110/// symptom this policy prevents).
3111///
3112/// Uses `NavigatorController::request_back` directly rather than the full
3113/// `frust::navigator`-auto-wired + `push_back_press()` path deliberately: the
3114/// auto-wiring reads `frust-reactive`'s process-wide back-press signal, which
3115/// needs an active `ReactiveRuntime` and races `back_glue`'s own
3116/// `TEST_LOCK`-guarded suite if run concurrently in the same test binary.
3117/// `request_back` is the exact call `back_glue::route_back` makes on a real
3118/// consumed press (see `back_glue`'s module docs), so this covers the same
3119/// mechanism without that shared global.
3120#[cfg(test)]
3121mod push_with_options_dismiss_animated {
3122 use std::cell::Cell;
3123 use std::rc::Rc;
3124
3125 use frust_core::RenderRoot;
3126 use frust_widgets::navigator as raw_navigator;
3127
3128 use crate::{AnyView, BackPolicy, NavigatorController, PushOptions, any, text};
3129
3130 fn page() -> AnyView<()> {
3131 any(text("modal"))
3132 }
3133
3134 #[test]
3135 fn dismiss_animated_stages_the_exit_instead_of_vanishing() {
3136 let controller: NavigatorController<()> = NavigatorController::new();
3137 let mut root: RenderRoot<(), _> = RenderRoot::new();
3138 let mut app = {
3139 let ctrl = controller.clone();
3140 move |_: &mut ()| raw_navigator(&ctrl, page)
3141 };
3142 let mut state = ();
3143 root.rebuild(&mut app, &mut state);
3144
3145 // An app-authored modal: transparent, DismissAnimated, with its own
3146 // shared dismiss-signal cell.
3147 let signal = Rc::new(Cell::new(0u64));
3148 controller.push_with_options(
3149 page,
3150 PushOptions::transparent()
3151 .back(BackPolicy::DismissAnimated)
3152 .dismiss_signal(signal.clone()),
3153 );
3154 root.rebuild(&mut app, &mut state);
3155 assert_eq!(controller.depth(), 2, "modal pushed");
3156 assert_eq!(signal.get(), 0, "no back yet");
3157
3158 // Android back: routed through `request_back`.
3159 controller.request_back();
3160 root.rebuild(&mut app, &mut state);
3161
3162 assert_eq!(
3163 controller.depth(),
3164 2,
3165 "DismissAnimated does not pop on back — the stack stages the exit \
3166 rather than the page vanishing"
3167 );
3168 assert_eq!(
3169 signal.get(),
3170 1,
3171 "back fired the dismiss signal exactly once, for the modal's own \
3172 widget to stage its exit animation on"
3173 );
3174 }
3175}
3176
3177/// [`__install_default_selection_toolbar`]'s two contracts: it installs the
3178/// framework baseline when the slot is empty, and a prior explicit
3179/// [`set_selection_toolbar_builder`] always survives it.
3180///
3181/// `frust-core` keeps its builder slot private and offers no way to reset it
3182/// (unlike its own in-crate tests, which reach the static directly) — this is
3183/// the only test in this crate's suite that touches the process-global slot,
3184/// so its very first touch below really is that slot's virgin state in this
3185/// process; every assertion after that is made deterministic by an explicit
3186/// `set_selection_toolbar_builder`/[`Arc::ptr_eq`] check instead of relying on
3187/// ambient state again.
3188#[cfg(test)]
3189mod selection_toolbar_bootstrap {
3190 use std::any::Any;
3191 use std::sync::Arc;
3192
3193 use frust_core::{BuildCtx, SelectionToolbarBuilder};
3194 use kurbo::{Rect, Size};
3195
3196 use crate::{
3197 __install_default_selection_toolbar, SelectionToolbarActions, SelectionToolbarRequest,
3198 View, any, set_selection_toolbar_builder, text,
3199 };
3200
3201 fn sample_request() -> SelectionToolbarRequest {
3202 SelectionToolbarRequest {
3203 anchor: Rect::new(0.0, 0.0, 10.0, 10.0),
3204 actions: SelectionToolbarActions {
3205 copy: true,
3206 ..Default::default()
3207 },
3208 present_menu: true,
3209 }
3210 }
3211
3212 #[test]
3213 fn installs_the_baseline_when_unset_and_yields_to_a_prior_explicit_install() {
3214 // The slot's virgin state (see the module doc comment above): nothing
3215 // has claimed it yet in this process, so the bootstrap call must take
3216 // it.
3217 __install_default_selection_toolbar();
3218 let installed = frust_core::selection_toolbar_builder()
3219 .expect("the bootstrap install must take the empty slot");
3220
3221 // Prove it really is the framework baseline — the widget it builds is
3222 // `frust_widgets::selection_toolbar`'s own type, not merely
3223 // "something" — by comparing the boxed elements' concrete `TypeId`s,
3224 // the same type-swap-detection technique
3225 // `frust_widgets::authoring::rebuild_child_tracked` uses internally.
3226 let sample = sample_request();
3227 let probe_view = installed(&sample, Size::new(400.0, 600.0));
3228 let mut probe_id = 0u64;
3229 let probe_widget = View::<()>::build(&probe_view, &mut BuildCtx::new(&mut probe_id));
3230
3231 let baseline_view = frust_widgets::selection_toolbar(&sample);
3232 let mut baseline_id = 0u64;
3233 let baseline_widget =
3234 View::<()>::build(&baseline_view, &mut BuildCtx::new(&mut baseline_id));
3235
3236 let probe_any: &dyn Any = &*probe_widget;
3237 let baseline_any: &dyn Any = &*baseline_widget;
3238 assert_eq!(
3239 probe_any.type_id(),
3240 baseline_any.type_id(),
3241 "the installed default must build selection_toolbar's own widget"
3242 );
3243
3244 // Now the other half: an explicit `set_selection_toolbar_builder`
3245 // always outranks the bootstrap's own set-if-unset call, whatever the
3246 // slot already held going in (the framework's own baseline, just
3247 // installed above, included).
3248 let marker: SelectionToolbarBuilder = Arc::new(|_req, _win| any(text("prior-marker")));
3249 set_selection_toolbar_builder(Arc::clone(&marker));
3250 __install_default_selection_toolbar();
3251 let after = frust_core::selection_toolbar_builder().expect("a builder is installed");
3252 assert!(
3253 Arc::ptr_eq(&after, &marker),
3254 "a prior explicit set_selection_toolbar_builder must survive the bootstrap install"
3255 );
3256 }
3257}
3258
3259/// Facade-level check that the drag-and-drop vocabulary
3260/// ([`DragCoordinator`], [`draggable`], [`drag_target`], [`auto_scroll_zone`],
3261/// [`reorderable_list`], [`AutoScroll`], [`DragPolicy`],
3262/// [`DragHighlight`], [`SourceFeedback`]) resolves through the `frust` facade —
3263/// both flat and via the `drag::` module path. File-drop types (from `frust_core::event`)
3264/// are also reachable for event handling.
3265#[cfg(test)]
3266mod drag_reexport {
3267 #[allow(unused_imports)]
3268 use crate::{
3269 DragCoordinator, DragKind, DragPolicy, FileDropEvent, FileDropPhase, SourceFeedback,
3270 authoring::Point, auto_scroll_zone, drag_target, draggable, reorderable_list,
3271 };
3272
3273 // A build-time proof the types name-resolve through the facade, flat.
3274 #[allow(dead_code)]
3275 fn _uses_drag_types(
3276 _coord: &DragCoordinator,
3277 _kind: DragKind,
3278 _feed: SourceFeedback,
3279 _policy: DragPolicy,
3280 ) {
3281 }
3282
3283 #[test]
3284 fn drag_types_resolve_through_facade() {
3285 // Flat access to the coordinator handle.
3286 let _coordinator = DragCoordinator::new();
3287
3288 // Module-path access via `frust::drag::*`.
3289 let _: crate::drag::DragCoordinator = crate::drag::DragCoordinator::new();
3290 let _: crate::drag::DragHighlight = crate::drag::DragHighlight::Hover;
3291 let _: crate::drag::AutoScroll = crate::drag::AutoScroll::default();
3292
3293 // File drop types are re-exported at the facade root.
3294 let _: FileDropPhase = FileDropPhase::Hover;
3295 let _: FileDropEvent = FileDropEvent {
3296 phase: FileDropPhase::Hover,
3297 position: Point::new(0.0, 0.0),
3298 paths: vec![],
3299 };
3300 }
3301}