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 toRoot::State/Root::build, mirroringandroid_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 directios_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_mainstub: the generatedmain.rscalls__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 runsRootthroughrun(or, with adesktop = { .. }argument,run_with_setup_and_config), printing the error and exiting non-zero on failure.app!never emits afn mainitself — a generated project splitslib.rs(whereapp!is called) frommain.rs(a one-linefn main() { <crate>::__frust_main() }, scaffolded byfrust createand never hand-edited), since only the binary crate may definemain.
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.