Skip to main content

frus_shell/
lib.rs

1//! `frus-shell` — frus's **framework layer**.
2//!
3//! Creates a native window (through `winit`), initialises the `frus-gpu`
4//! [`frus_gpu::Renderer`] and drives the `event → frame` loop for any [`Application`]. This
5//! is the only platform-dependent layer.
6
7#![deny(missing_docs)]
8
9// The README's example is compiled by `cargo test --doc`, so it cannot rot.
10#[cfg(doctest)]
11#[doc = include_str!("../README.md")]
12pub struct ReadmeDoctests;
13
14// The `desktop` / `android` / `ios` / `web` aliases come from `build.rs`. They hold
15// for **this** crate only: the `main!` macro below expands in the user's crate, so
16// it keeps explicit `target_os` / `target_arch` predicates.
17
18// Accessibility: AccessKit on desktop, a hand-rolled DOM bridge on the web — not
19// Android, not iOS (see a11y.rs and a11y_web.rs).
20#[cfg(desktop)]
21mod a11y;
22#[cfg(web)]
23mod a11y_web;
24#[cfg(web)]
25mod address;
26#[cfg(android)]
27mod android_autofill;
28#[cfg(android)]
29mod android_clipboard;
30#[cfg(android)]
31mod android_ime;
32#[cfg(android)]
33mod android_link;
34#[cfg(android)]
35mod android_settings;
36#[cfg(android)]
37mod android_system_bars;
38mod app;
39mod application;
40#[cfg(any(android, test))]
41mod autofill;
42mod caret;
43mod command;
44mod gesture;
45#[cfg(any(web, test))]
46mod history;
47mod hover;
48#[cfg(any(android, test))]
49mod ime;
50#[cfg(desktop)]
51mod instance;
52mod link;
53/// Cross-platform `fetch` HTTP helper (behind the `net` feature).
54#[cfg(feature = "net")]
55pub mod net;
56mod reload;
57mod remote;
58/// The async runtime: one executor and a reactor, shared by every asynchronous
59/// effect. Native only — on the Web the browser is the runtime.
60#[cfg(not(web))]
61pub(crate) mod runtime;
62mod selection;
63mod subscription;
64mod theming;
65#[cfg(web)]
66mod web_clipboard;
67mod widget_app;
68
69/// A windowless driver for tests. Hidden: not part of what an application builds on.
70#[cfg(any(test, feature = "testing"))]
71#[doc(hidden)]
72pub use app::testing;
73pub use app::App;
74pub use application::{Application, Lifecycle, LocationStrategy};
75pub use command::Command;
76pub use link::{link_among, location_of_link};
77pub use remote::RemoteData;
78pub use subscription::Subscription;
79pub use widget_app::FrusApp;
80
81/// Cross-platform HTTP (`net` feature) — the [`fetch`] shorthand, the [`Request`]
82/// builder (method / headers / body / timeout) and the [`FetchError`] error type.
83#[cfg(feature = "net")]
84pub use net::{fetch, fetch_bytes, FetchError, Method, Request, MAX_RESPONSE_BYTES};
85
86/// Re-export: size classes and orientation, so the application can drive its own
87/// responsive behaviour.
88pub use frus_widgets::{Brightness, Locale, Orientation, ScrollPhysics, SizeClass, ThemeMode};
89
90/// Re-export of the Android entry type (supplied by `winit` / `android-activity`),
91/// so an application can type its `android_main` without depending on winit.
92#[cfg(android)]
93pub use winit::platform::android::activity::AndroidApp;
94
95/// Re-exports **for the [`main!`] macro**: the application declares its single entry
96/// point without depending on these crates directly.
97#[doc(hidden)]
98pub use anyhow;
99#[doc(hidden)]
100pub use log;
101
102/// Declares a frus application's **single entry point**. Invoked **exactly once** (in
103/// the application's library), it generates the entry point of **every platform**, all
104/// delegating to the **same** application:
105///
106/// - **desktop and iOS**: a `run()` function, which the app's thin binary calls;
107/// - **Android**: the native `android_main` symbol the activity expects;
108/// - **web**: the `#[wasm_bindgen(start)]` function.
109///
110/// The `cfg`s below are written with **explicit** `target_os` / `target_arch`
111/// predicates rather than the `desktop` / `android` / `web` aliases from `build.rs`:
112/// the body of a `macro_rules!` expands in the **application's** crate, where those
113/// aliases are not defined. A `#[cfg(desktop)]` there would always be false, and the
114/// application would end up with no entry point at all.
115///
116/// The argument is an **expression** building the application (re-evaluated per
117/// platform, never shared):
118///
119/// ```ignore
120/// frus_shell::main!(App::default());
121/// ```
122///
123/// The web platform keeps its own `wasm-bindgen` dependency (targeted at `wasm32`),
124/// which the generated `#[wasm_bindgen(start)]` entry point refers to via
125/// `::wasm_bindgen`.
126#[macro_export]
127macro_rules! main {
128    ($app:expr $(,)?) => {
129        /// **Desktop and iOS** entry point: opens the window and drives the loop
130        /// (called by the application's thin binary, or by the `.app` bundle's `main`
131        /// on iOS). Generated by [`frus_shell::main!`].
132        #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
133        pub fn run() -> $crate::anyhow::Result<()> {
134            $crate::run($app)
135        }
136
137        /// **Android** entry point: called by the native activity. Generated by
138        /// [`frus_shell::main!`].
139        #[cfg(target_os = "android")]
140        #[no_mangle]
141        fn android_main(android_app: $crate::AndroidApp) {
142            if let ::core::result::Result::Err(err) = $crate::run_android($app, android_app) {
143                $crate::log::error!("frus (android) stopped: {:#}", err);
144            }
145        }
146
147        /// **Web** entry point: called when the wasm module loads. Generated by
148        /// [`frus_shell::main!`].
149        #[cfg(target_arch = "wasm32")]
150        #[::wasm_bindgen::prelude::wasm_bindgen(start)]
151        pub fn start() {
152            if let ::core::result::Result::Err(err) = $crate::run_web($app) {
153                $crate::log::error!("frus (web) stopped: {:#}", err);
154            }
155        }
156    };
157}
158
159/// Runs an application: opens the window and drives the event loop.
160///
161/// ```no_run
162/// # struct MyApp;
163/// # impl frus_shell::Application for MyApp {
164/// #     type Message = ();
165/// #     fn update(&mut self, _m: ()) -> frus_shell::Command<()> { frus_shell::Command::none() }
166/// #     fn view(&self, _t: &frus_widgets::Theme)
167/// #         -> Box<dyn frus_widgets::Widget<()>> { unimplemented!() }
168/// # }
169/// frus_shell::run(MyApp).unwrap();
170/// ```
171#[cfg(desktop)]
172pub fn run<A: Application>(mut app: A) -> anyhow::Result<()> {
173    // `RUST_LOG=info` to see the logs (GPU adapter, and so on).
174    env_logger::init();
175
176    // Live-reload (dev): rehydrate the state left behind by the previous binary,
177    // before `init` — see [`Application::restore_state`].
178    reload::restore_from_env(&mut app);
179
180    // An application that means to be one window: the second process hands its link to the
181    // first and ends; the first serves the ones that come after it.
182    if let Some(id) = app.instance_id() {
183        match instance::claim(&id, link::link_among(std::env::args()).as_deref()) {
184            instance::Claim::Handed => return Ok(()),
185            instance::Claim::First(server) => instance::serve_into_queue(server),
186        }
187    }
188
189    // A loop whose **user events** are messages: asynchronous effects deliver their
190    // result through an `EventLoopProxy<Message>`.
191    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event().build()?;
192    // Keep asking for frames while an animation runs; otherwise wait.
193    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
194
195    let proxy = event_loop.create_proxy();
196    let mut app = App::new(app, proxy);
197    event_loop.run_app(&mut app)?;
198    Ok(())
199}
200
201/// Runs an application on **iOS**. Same shape as [`run`] — winit takes care of
202/// `UIApplicationMain` itself and `wgpu` renders through Metal — but without the
203/// three desktop services that have no UIKit backend: no `env_logger` (stderr is
204/// not readable on a device), no `arboard` clipboard, no AccessKit.
205///
206/// The [`main!`] macro generates the same `run()` as on desktop: it is the `.app`
207/// bundle's binary that calls it from its `main`.
208///
209/// **Status: bootstrapping.** Lifecycle, safe-area insets, IME and the soft
210/// keyboard are not wired yet — see the ROADMAP.
211#[cfg(ios)]
212pub fn run<A: Application>(app: A) -> anyhow::Result<()> {
213    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event().build()?;
214    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
215
216    let proxy = event_loop.create_proxy();
217    let mut app = App::new(app, proxy);
218    event_loop.run_app(&mut app)?;
219    Ok(())
220}
221
222/// Runs an application in the **browser** (wasm + WebGPU): winit creates a
223/// `<canvas>` and **appends it** to the document, the renderer initialises
224/// asynchronously (nothing may block on the web), and the loop is handed to the
225/// browser through `spawn_app`, which never returns. Called from the application's
226/// `#[wasm_bindgen(start)]` entry point.
227#[cfg(web)]
228pub fn run_web<A: Application + 'static>(app: A) -> anyhow::Result<()> {
229    use winit::platform::web::EventLoopExtWebSys;
230
231    // Panics → the browser console, instead of an opaque "unreachable".
232    console_error_panic_hook::set_once();
233    let _ = console_log::init_with_level(log::Level::Info);
234
235    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event().build()?;
236    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
237
238    let proxy = event_loop.create_proxy();
239    let app = App::new(app, proxy);
240    // On the web the loop is driven by the browser (requestAnimationFrame) and
241    // `spawn_app` does not come back — hence returning `Ok` immediately.
242    event_loop.spawn_app(app);
243    Ok(())
244}
245
246/// Runs an application on **Android**: the native activity supplies the
247/// [`AndroidApp`], which is handed to the winit loop. Entry point called from
248/// `android_main`.
249#[cfg(android)]
250pub fn run_android<A: Application>(app: A, android_app: AndroidApp) -> anyhow::Result<()> {
251    use winit::platform::android::EventLoopBuilderExtAndroid;
252
253    // Logs go to logcat (`adb logcat`).
254    android_logger::init_once(
255        android_logger::Config::default().with_max_level(log::LevelFilter::Info),
256    );
257
258    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event()
259        .with_android_app(android_app.clone())
260        .build()?;
261    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
262
263    let proxy = event_loop.create_proxy();
264    let mut app = App::new(app, proxy);
265    // Input bridge (a real InputConnection): composition, swipe, CJK. On failure the
266    // shell falls back to key mode (TYPE_NULL).
267    android_ime::install(&android_app);
268    // Keep the activity handle around to query the system insets.
269    app.set_android_app(android_app);
270    event_loop.run_app(&mut app)?;
271
272    // Android keeps the **process** cached after the activity ends, but winit allows
273    // only one `EventLoop` per process: relaunching within the same process would
274    // fail immediately (the icon stops "responding" until Android reaps the cache).
275    // So we end the process, and the next launch starts clean.
276    std::process::exit(0);
277}