Skip to main content

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