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