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