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