Skip to main content

app

Macro app 

Source
macro_rules! app {
    (@emit_mobile $root:ty, $($setup:block)?) => { ... };
    (@emit $root:ty, $($setup:block)?) => { ... };
    (@emit_desktop $root:ty, $config:expr, $($setup:block)?) => { ... };
    ($root:ty $(,)?) => { ... };
    ($root:ty, setup = $setup:block $(,)?) => { ... };
    ($root:ty, desktop = $config:expr $(,)?) => { ... };
    ($root:ty, setup = $setup:block, desktop = $config:expr $(,)?) => { ... };
    ($root:ty, desktop = $config:expr, setup = $setup:block $(,)?) => { ... };
}
Expand description

The canonical app entry point: one line binds a root Component to all three platforms.

use frust::{AnyView, Component, any, text};

#[derive(Default)]
struct App;

impl Component for App {
    type State = i32;

    fn init(&self) -> i32 {
        0
    }

    fn build(&self, state: &mut i32) -> AnyView<i32> {
        any(text(format!("count: {state}")).size(32.0))
    }
}

frust::app!(App);

Expands to:

  • Android: #[cfg(target_os = "android")] frust::android_app!(...) bound to Root::State/Root::build, mirroring android_app!’s own not-self-gating contract (its generated symbols only compile where the Android FFI glue they call into exists).
  • iOS: frust::ios_app!(...), invoked unconditionally — like a direct ios_app! call, it self-gates: every symbol it emits is itself #[cfg(target_os = "ios")], so the invocation expands to nothing off-iOS. Plus a #[cfg(target_os = "ios")] __frust_main stub: the generated main.rs calls __frust_main() under #[cfg(not(target_os = "android"))], and Xcode builds that bin target, so the symbol must exist on iOS even though no desktop shell does. The stub explains itself on stderr and exits non-zero rather than silently doing nothing — a real iOS app is launched by its Xcode host through the C-ABI entry points above, never by running this binary.
  • Every other target (desktop): a hidden #[doc(hidden)] pub fn __frust_main() that runs Root through run (or, with a desktop = { .. } argument, run_with_setup_and_config), printing the error and exiting non-zero on failure. app! never emits a fn main itself — a generated project splits lib.rs (where app! is called) from main.rs (a one-line fn main() { <crate>::__frust_main() }, scaffolded by frust create and never hand-edited), since only the binary crate may define main.

Root must implement Component + Default: the macro constructs a Root twice with no arguments (once to build each platform’s state factory, once to close over build) — a root component is stateless configuration (any real data lives in its Component::State), so two independent, short-lived instances are inexpensive and behaviorally identical.

§setup = { .. }: run code before the shell exists

An optional second argument is a setup block the macro emits at the top of each platform’s entry, before the shell is constructed:

frust::app!(MyApp, setup = { my_design_system::install(); });

This exists because a design system’s installer must reach set_default_theme/register_app_fonts before a shell reads them, and Android has no main at all (its entry is the JNI nativeInit android_app! generates). Any plugin can use it; it is not tied to one design system — frust_glyph::install is just the first caller.

When it runs, identically on all three platforms: on the UI thread, after ReactiveRuntime::init, under the root reactive Owner, immediately before the root component’s Component::init — and therefore before any shell construction, which is the one point a shell reads set_default_theme’s slot and drains the pending-font registry. The expansions differ per platform (Android/iOS place the block inside the state factory android_app!/ios_app! receive, which jni_glue::create_handle/ffi_glue::init call before building their AppHandle; the desktop arm routes through run_with_setup); the ordering contract above is what is guaranteed.

The one-argument form is unchanged: no setup tokens are emitted, the two mobile state factories expand to exactly what they always did, and the desktop arm’s run_with_setup(root, || {}) is literally what run itself is.

§desktop = { .. }: name the app on desktop

A second optional argument, alongside or instead of setup, is an expression evaluating to a DesktopConfig — the app’s desktop identity (name, reverse-DNS id, window icon, native menu bar, close policy):

frust::app!(MyApp, desktop = {
    frust::DesktopConfig::new()
        .with_app_name("Huddle")
        .with_app_id("dev.frust.huddle")
        .with_menu_spec(
            frust::MenuSpec::new()
                .with_item(frust::MenuItemSpec::role(frust::MenuRole::Quit)),
        )
});

The two may be combined in either order (setup = { .. }, desktop = { .. } or the reverse). The expression is emitted only into the desktop __frust_main, so it may name desktop-only types like DesktopConfig without any cfg of its own and the mobile arms never see it — an app whose menu/identity matters on desktop still compiles unchanged for Android and iOS. It is evaluated once, at startup: the macro passes it straight into the run call, so it runs before the setup block (which runs inside, under the reactive owner) and before the shell is constructed — a config expression must not depend on what setup installs.

Omitting it is exactly today’s behavior: DesktopConfig::default()’s window, through the same run_with_setup call the pre-config macro emitted.