Skip to main content

frust/
lib.rs

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