frus-shell 0.4.0

frus — the platform layer: window + event loop (Milestone 0)
Documentation
//! `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(android)]
mod android_settings;
#[cfg(android)]
mod android_system_bars;
mod app;
mod application;
#[cfg(any(android, test))]
mod autofill;
mod caret;
mod command;
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;
mod link;
mod logging;
/// Cross-platform `fetch` HTTP helper (behind the `net` feature).
#[cfg(feature = "net")]
pub mod net;
mod reload;
// How long the application takes to show its first frame (milestone 683, #211).
mod startup;
// 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 subscription;
mod theming;
#[cfg(windows)]
mod title_bar;
#[cfg(web)]
mod web_clipboard;
#[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;
pub use icon::AppIcon;
pub use link::{link_among, location_of_link};
pub use remote::RemoteData;
pub use subscription::Subscription;
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);
            }
        }
    };
}

/// `$XDG_CACHE_HOME/frus/<program>`, or `~/.cache/frus/<program>`.
#[cfg(target_os = "linux")]
fn linux_cache_dir() -> Option<std::path::PathBuf> {
    let base = std::env::var_os("XDG_CACHE_HOME")
        .map(std::path::PathBuf::from)
        .or_else(|| {
            std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".cache"))
        })?;
    let program = std::env::current_exe().ok()?.file_stem()?.to_os_string();
    Some(base.join("frus").join(program))
}

/// 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();
    // On Linux the renderer draws through Vulkan, which can keep its compiled pipelines between
    // launches: in the user's cache folder, under the program's name (milestone 685, #217).
    #[cfg(target_os = "linux")]
    if let Some(dir) = linux_cache_dir() {
        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 storage between launches:
    // the next start reads them instead of compiling them again (milestone 685, #217).
    if let Some(dir) = android_app.internal_data_path() {
        frus_gpu::set_pipeline_cache_dir(dir.join("frus-pipelines"));
    }

    // 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);
}