frus-shell 0.5.0

frus — the platform layer: window + event loop (Milestone 0)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! `frus-shell` — frus's **framework layer**.
//!
//! Creates a native window (through `winit`), initialises the `frus-gpu`
//! [`frus_gpu::Renderer`] and drives the `event → frame` loop for any [`Application`]. This
//! is the only platform-dependent layer.

#![deny(missing_docs)]

// The README's example is compiled by `cargo test --doc`, so it cannot rot.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
pub struct ReadmeDoctests;

// The `desktop` / `android` / `ios` / `web` aliases come from `build.rs`. They hold
// for **this** crate only: the `main!` macro below expands in the user's crate, so
// it keeps explicit `target_os` / `target_arch` predicates.

// Accessibility: AccessKit on desktop, a hand-rolled DOM bridge on the web — not
// Android, not iOS (see a11y.rs and a11y_web.rs).
#[cfg(desktop)]
mod a11y;
#[cfg(web)]
mod a11y_web;
#[cfg(web)]
mod address;
#[cfg(android)]
mod android_autofill;
#[cfg(android)]
mod android_clipboard;
#[cfg(android)]
mod android_ime;
#[cfg(android)]
mod android_link;
#[cfg(all(android, feature = "notifications"))]
mod android_notification;
#[cfg(android)]
mod android_settings;
#[cfg(android)]
mod android_system_bars;
mod app;
mod application;
#[cfg(any(android, test))]
mod autofill;
mod cache_dir;
mod caret;
mod command;
#[cfg(all(desktop, feature = "file-dialogs"))]
mod file_dialog;
mod file_drop;
mod frame_stats;
mod gesture;
#[cfg(any(web, test))]
mod history;
mod hover;
mod icon;
#[cfg(any(android, test))]
mod ime;
#[cfg(desktop)]
mod instance;
#[cfg(any(
    all(desktop, feature = "file-dialogs"),
    all(any(desktop, android), feature = "notifications")
))]
mod interface_thread;
mod link;
mod logging;
/// Cross-platform `fetch` HTTP helper (behind the `net` feature).
#[cfg(feature = "net")]
pub mod net;
#[cfg(all(any(desktop, android, web), feature = "notifications"))]
mod notification;
#[cfg(all(web, feature = "notifications"))]
mod notification_web;
mod reload;
// How long the application takes to show its first frame (milestone 683, #211).
// Global shortcuts (milestone 709).
mod shortcuts;
mod startup;
// The taskbar's badge and progress (milestone 708).
mod taskbar;
// The tray icon (milestone 706): its description everywhere, the system's own on the desktop.
mod tray;
// The Web side of a reload under `frus run`: the states kept across the page's reload.
#[cfg(web)]
mod web_dev;
// Hot reload: patches applied while the application runs (the `hot-reload` feature).
#[cfg(feature = "hot-reload")]
mod hot;
// The channel `frus run` restarts an application through.
#[cfg(any(desktop, android))]
mod dev;
// What the keys of `frus run` ask a running application, on every platform.
mod dev_commands;
mod remote;
/// The async runtime: one executor and a reactor, shared by every asynchronous
/// effect. Native only — on the Web the browser is the runtime.
#[cfg(not(web))]
pub(crate) mod runtime;
mod selection;
mod simulated;
mod subscription;
mod theming;
#[cfg(windows)]
mod title_bar;
#[cfg(web)]
mod web_clipboard;
#[cfg(any(web, test))]
mod web_editing;
#[cfg(web)]
mod web_icon;
#[cfg(any(web, test))]
mod web_keys;
mod widget_app;

/// A windowless driver for tests. Hidden: not part of what an application builds on.
#[cfg(any(test, feature = "testing"))]
#[doc(hidden)]
pub use app::testing;
pub use app::App;
pub use application::{Application, Lifecycle, LocationStrategy};
pub use command::Command;
#[cfg(all(desktop, feature = "file-dialogs"))]
pub use file_dialog::FileDialog;
pub use icon::AppIcon;
pub use link::{link_among, location_of_link};
#[cfg(all(any(desktop, android, web), feature = "notifications"))]
pub use notification::{NotificationError, NotificationResponse, SystemNotification};
pub use remote::RemoteData;
pub use shortcuts::GlobalShortcut;
pub use subscription::Subscription;
pub use taskbar::{Taskbar, TaskbarProgress};
pub use tray::Tray;
pub use widget_app::FrusApp;

/// Cross-platform HTTP (`net` feature) — the [`fetch`] shorthand, the [`Request`]
/// builder (method / headers / body / timeout) and the [`FetchError`] error type.
#[cfg(feature = "net")]
pub use net::{fetch, fetch_bytes, FetchError, Method, Request, MAX_RESPONSE_BYTES};

/// Re-export: size classes and orientation, so the application can drive its own
/// responsive behaviour.
pub use frus_widgets::{Brightness, Locale, Orientation, ScrollPhysics, SizeClass, ThemeMode};

/// Re-export of the Android entry type (supplied by `winit` / `android-activity`),
/// so an application can type its `android_main` without depending on winit.
#[cfg(android)]
pub use winit::platform::android::activity::AndroidApp;

/// Re-exports **for the [`main!`] macro**: the application declares its single entry
/// point without depending on these crates directly.
#[doc(hidden)]
pub use anyhow;
#[doc(hidden)]
pub use log;
/// Names the application's crate for its logs; called by [`main!`] (milestone 604).
#[doc(hidden)]
pub use logging::name_app as __name_app;

/// With the `hot-reload` feature, what `main!` adds: a `main`, so that the application's
/// `src/lib.rs` can be compiled as the program a hot-reload patch replaces (`frus run --hot`,
/// milestone 670). Decided here, where the feature is known: the application's crate is never
/// asked about it.
#[doc(hidden)]
#[macro_export]
#[cfg(all(
    feature = "hot-reload",
    not(any(target_os = "android", target_arch = "wasm32"))
))]
macro_rules! __hot_main {
    () => {
        #[allow(dead_code)]
        fn main() -> $crate::anyhow::Result<()> {
            run()
        }
    };
}

/// On the web and on Android, the program a patch replaces is a binary too, which must have a
/// `main`; the application still starts from its `#[wasm_bindgen(start)]` function, or from
/// `android_main` on Android (milestone 680).
#[doc(hidden)]
#[macro_export]
#[cfg(all(
    feature = "hot-reload",
    any(target_arch = "wasm32", target_os = "android")
))]
macro_rules! __hot_main {
    () => {
        #[allow(dead_code)]
        fn main() {}
    };
}

/// Without the `hot-reload` feature, nothing.
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "hot-reload"))]
macro_rules! __hot_main {
    () => {};
}

/// Declares a frus application's **single entry point**. Invoked **exactly once** (in
/// the application's library), it generates the entry point of **every platform**, all
/// delegating to the **same** application:
///
/// - **desktop and iOS**: a `run()` function, which the app's thin binary calls;
/// - **Android**: the native `android_main` symbol the activity expects;
/// - **web**: the `#[wasm_bindgen(start)]` function.
///
/// The `cfg`s below are written with **explicit** `target_os` / `target_arch`
/// predicates rather than the `desktop` / `android` / `web` aliases from `build.rs`:
/// the body of a `macro_rules!` expands in the **application's** crate, where those
/// aliases are not defined. A `#[cfg(desktop)]` there would always be false, and the
/// application would end up with no entry point at all.
///
/// The argument is an **expression** building the application (re-evaluated per
/// platform, never shared):
///
/// ```ignore
/// frus_shell::main!(App::default());
/// ```
///
/// The web platform keeps its own `wasm-bindgen` dependency (targeted at `wasm32`),
/// which the generated `#[wasm_bindgen(start)]` entry point refers to via
/// `::wasm_bindgen`.
#[macro_export]
macro_rules! main {
    ($app:expr $(,)?) => {
        /// **Desktop and iOS** entry point: opens the window and drives the loop
        /// (called by the application's thin binary, or by the `.app` bundle's `main`
        /// on iOS). Generated by [`frus_shell::main!`].
        #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
        pub fn run() -> $crate::anyhow::Result<()> {
            $crate::__name_app(::core::module_path!());
            $crate::run($app)
        }

        // Hot reload under `frus run --hot`: a `main`, with the `hot-reload` feature only.
        $crate::__hot_main!();

        /// **Android** entry point: called by the native activity. Generated by
        /// [`frus_shell::main!`].
        #[cfg(target_os = "android")]
        #[no_mangle]
        fn android_main(android_app: $crate::AndroidApp) {
            $crate::__name_app(::core::module_path!());
            if let ::core::result::Result::Err(err) = $crate::run_android($app, android_app) {
                $crate::log::error!("frus (android) stopped: {:#}", err);
            }
        }

        /// **Web** entry point: called when the wasm module loads. Generated by
        /// [`frus_shell::main!`].
        #[cfg(target_arch = "wasm32")]
        #[::wasm_bindgen::prelude::wasm_bindgen(start)]
        pub fn start() {
            $crate::__name_app(::core::module_path!());
            if let ::core::result::Result::Err(err) = $crate::run_web($app) {
                $crate::log::error!("frus (web) stopped: {:#}", err);
            }
        }
    };
}

/// Runs an application: opens the window and drives the event loop.
///
/// ```no_run
/// # struct MyApp;
/// # impl frus_shell::Application for MyApp {
/// #     type Message = ();
/// #     fn update(&mut self, _m: ()) -> frus_shell::Command<()> { frus_shell::Command::none() }
/// #     fn view(&self, _t: &frus_widgets::Theme)
/// #         -> Box<dyn frus_widgets::Widget<()>> { unimplemented!() }
/// # }
/// frus_shell::run(MyApp).unwrap();
/// ```
#[cfg(desktop)]
pub fn run<A: Application>(mut app: A) -> anyhow::Result<()> {
    startup::begin();
    // Where the renderer draws through Vulkan — Linux, or any desktop given `WGPU_BACKEND=vulkan`
    // — it keeps its compiled pipelines between launches: in the user's cache folder, under the
    // program's name (milestones 685 and 697, #217). Nothing is written elsewhere.
    if let Some(dir) = std::env::current_exe().ok().and_then(|exe| {
        cache_dir::desktop(
            std::env::consts::OS,
            |name| std::env::var_os(name),
            exe.file_stem()?,
        )
    }) {
        frus_gpu::set_pipeline_cache_dir(dir);
    }
    // The application's `log` output, in the terminal, with nothing to set up
    // (milestone 604). `RUST_LOG` still decides over the default.
    logging::init::<A>();

    // Live-reload (dev): rehydrate the state left behind by the previous binary,
    // before `init` — see [`Application::restore_state`].
    reload::restore_from_env(&mut app);

    // An application that means to be one window: the second process hands its link to the
    // first and ends; the first serves the ones that come after it.
    if let Some(id) = app.instance_id() {
        match instance::claim(&id, link::link_among(std::env::args()).as_deref()) {
            instance::Claim::Handed => return Ok(()),
            instance::Claim::First(server) => instance::serve_into_queue(server),
        }
    }

    // A loop whose **user events** are messages: asynchronous effects deliver their
    // result through an `EventLoopProxy<Message>`.
    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event().build()?;
    // Keep asking for frames while an animation runs; otherwise wait.
    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);

    let proxy = event_loop.create_proxy();
    let mut app = App::new(app, proxy);
    event_loop.run_app(&mut app)?;
    Ok(())
}

/// Runs an application on **iOS**. Same shape as [`run`] — winit takes care of
/// `UIApplicationMain` itself and `wgpu` renders through Metal — but without the
/// three desktop services that have no UIKit backend: no `env_logger` (stderr is
/// not readable on a device), no `arboard` clipboard, no AccessKit.
///
/// The [`main!`] macro generates the same `run()` as on desktop: it is the `.app`
/// bundle's binary that calls it from its `main`.
///
/// **Status: bootstrapping.** Lifecycle, safe-area insets, IME and the soft
/// keyboard are not wired yet — see the ROADMAP.
#[cfg(ios)]
pub fn run<A: Application>(app: A) -> anyhow::Result<()> {
    startup::begin();
    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event().build()?;
    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);

    let proxy = event_loop.create_proxy();
    let mut app = App::new(app, proxy);
    event_loop.run_app(&mut app)?;
    Ok(())
}

/// Runs an application in the **browser** (wasm + WebGPU): winit creates a
/// `<canvas>` and **appends it** to the document, the renderer initialises
/// asynchronously (nothing may block on the web), and the loop is handed to the
/// browser through `spawn_app`, which never returns. Called from the application's
/// `#[wasm_bindgen(start)]` entry point.
#[cfg(web)]
pub fn run_web<A: Application + 'static>(app: A) -> anyhow::Result<()> {
    use winit::platform::web::EventLoopExtWebSys;
    startup::begin();

    // Panics → the browser console, instead of an opaque "unreachable".
    console_error_panic_hook::set_once();
    // The application's `log` output, in the browser's console (milestone 604).
    logging::init::<A>();
    // The browser's operating system, before any theme is built: a web build is built for
    // no operating system, and the conventions to follow are those of the one the page is
    // read on (milestone 614).
    if let Some(navigator) = web_sys::window().map(|window| window.navigator()) {
        let platform = frus_widgets::TargetPlatform::from_browser(
            &navigator.platform().unwrap_or_default(),
            &navigator.user_agent().unwrap_or_default(),
            navigator.max_touch_points(),
        );
        frus_widgets::__set_detected_target_platform(platform);
        log::info!("target platform: {platform}, from the browser");
    }

    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event().build()?;
    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);

    let proxy = event_loop.create_proxy();
    let app = App::new(app, proxy);
    // On the web the loop is driven by the browser (requestAnimationFrame) and
    // `spawn_app` does not come back — hence returning `Ok` immediately.
    event_loop.spawn_app(app);
    Ok(())
}

/// Runs an application on **Android**: the native activity supplies the
/// [`AndroidApp`], which is handed to the winit loop. Entry point called from
/// `android_main`.
#[cfg(android)]
pub fn run_android<A: Application>(app: A, android_app: AndroidApp) -> anyhow::Result<()> {
    use winit::platform::android::EventLoopBuilderExtAndroid;
    startup::begin();
    // The renderer's compiled pipelines, kept in the application's cache folder between
    // launches: the next start reads them instead of compiling them again (milestones 685 and
    // 697, #217). The folder milestone 685 used inside `files` is removed.
    if let Some(files) = android_app.internal_data_path() {
        let (now, before) = (
            cache_dir::android(&files),
            cache_dir::android_before_697(&files),
        );
        // Not when they are the same folder: the cache would be wiped at every start.
        if before != now {
            let _ = std::fs::remove_dir_all(before);
        }
        frus_gpu::set_pipeline_cache_dir(now);
    }

    // The application's `log` output, and its panics, in logcat under its crate's name
    // (milestone 604).
    logging::init::<A>();

    let event_loop = winit::event_loop::EventLoop::<A::Message>::with_user_event()
        .with_android_app(android_app.clone())
        .build()?;
    event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);

    let proxy = event_loop.create_proxy();
    let mut app = App::new(app, proxy);
    // Input bridge (a real InputConnection): composition, swipe, CJK. On failure the
    // shell falls back to key mode (TYPE_NULL).
    android_ime::install(&android_app);
    // Keep the activity handle around to query the system insets.
    app.set_android_app(android_app);
    event_loop.run_app(&mut app)?;

    // Android keeps the **process** cached after the activity ends, but winit allows
    // only one `EventLoop` per process: relaunching within the same process would
    // fail immediately (the icon stops "responding" until Android reaps the cache).
    // So we end the process, and the next launch starts clean.
    std::process::exit(0);
}