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