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