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