frus-shell 0.2.1

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 gesture;
#[cfg(any(web, test))]
mod history;
mod hover;
#[cfg(any(android, test))]
mod ime;
#[cfg(desktop)]
mod instance;
mod link;
/// Cross-platform `fetch` HTTP helper (behind the `net` feature).
#[cfg(feature = "net")]
pub mod net;
mod reload;
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(web)]
mod web_clipboard;
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 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;

/// 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::run($app)
        }

        /// **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) {
            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() {
            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<()> {
    // `RUST_LOG=info` to see the logs (GPU adapter, and so on).
    env_logger::init();

    // 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<()> {
    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;

    // Panics → the browser console, instead of an opaque "unreachable".
    console_error_panic_hook::set_once();
    let _ = console_log::init_with_level(log::Level::Info);

    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;

    // Logs go to logcat (`adb logcat`).
    android_logger::init_once(
        android_logger::Config::default().with_max_level(log::LevelFilter::Info),
    );

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