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