A Makepad app styled like a phone home-screen launcher that hosts isolated Splash
mini-apps. Read README.md for what it is and docs/DESIGN_DECISIONS.md for the
choices made.
cargo run/cargo run --release— the live GPU app (a resizable, phone-shaped desktop window).- Tracks a FORK BRANCH, not upstream dev:
kevinaboos/makepadsplash_resource_limits, because the per-isolate resource limits this launcher sets (splash_limits) are still open as makepad/makepad#1189. A fork branch rather than a path dep so a fresh clone builds; swap all three deps back togit … branch = "dev"the day it merges. (The host-services bridge #1181 and the dead-isolate GC fix #1186 are both upstream already.) For local makepad hacking, point all three at ONE path — the sibling../makepad-host-servicesworktree is that branch — includingmakepad-testunder[dev-dependencies], or the widget/editor/test types won't match.
- Headless UI suite:
MAKEPAD_TEST_PUMP_TICKS=1 HOST_LAUNCHER_FRESH=1 cargo test --test ui -- --test-threads=1. It drives a real (offscreen) build of the app viamakepad_testwith synthesized input and widget-state assertions.HOST_LAUNCHER_FRESH=1starts every instance from the default layout and skips persistence so tests are order-independent. MAKEPAD_TEST_PUMP_TICKS=1is not optional any more on a slow box. The tree has roughly doubled since the suite was written (~1400 nodes, ~250KB per snapshot), and the default 3 ticks per query means the heaviest states — two stacked modals over a live home screen whose clock widget repaints every second — spend more than the harness's 10s reply budget and fail astimed out waiting for hub response. The tests are fine; the pump is what's expensive. Verified byapp_info_lists_and_toggles_permissionsandapp_info_permission_sheet_offers_three_states, which fail with the default pump and pass with 1 on the same commit.- Generation tests need the offline agent:
HOST_LAUNCHER_AGENT_CMD=$PWD/target/debug/fake_acp(build it first). Without it the launcher spawns the REAL agent, which answers "Invalid API Key" and the test times out looking for an app that was never written. - A CLICK DURING AN OPENING ANIMATION lands where the widget will be, not where
it is, and the test then fails somewhere else entirely — as "the app ignored
the button", which reads like a broken app rather than a race. It has bitten
three tests now. Open a mini-app through
open_mini_app/open_sandbox_probe(click, wait for a widget the app draws, settle) rather than clicking straight after the icon. - KNOWN FAILING and not caused by recent work here — both verified by checking
out the pre-change tree and watching them fail identically:
dock_badge_removes_and_accepts_dropped_icon(a dragged icon does not land in the dock: 4 favourites instead of 5) andproviders_page_adds_a_key_behind_a_masked_field(the key form never opens after the row's + gutter is clicked — at failure everypr_addreports 0x0). Both are drag/click interactions, so suspect an event-routing change upstream before blaming this repo. - Do NOT edit any source (Rust or
.splash) while acargo test --test uirun is in progress: the harness rebuilds the app per session, and a mid-run edit yields spurious "app exited with code 0" failures. Let the run finish first. timed out waiting for hub responsemeans SLOW FRAMES, not a hang. The harness allows a hard 10s per reply, and everywidget_snapshot()first pumps 3Tickmessages — so one query costs ~4 rendered frames, and any state costing over ~2.5s a frame fails. Software rasterisation is what's expensive, so the lever is what gets composited (glass surfaces, overlay draw lists), not what's computed. A whole-suite run heats the box up enough that the heaviest tests tip over near the end; a lone retry of one is the way to tell a real failure from that.- The suite is slow for reasons that live in makepad, not here, and two PRs fix
most of it: makepad/makepad#1175 (the headless backend recompiled all 68 shaders
with
rustc -Oon EVERY app start — 38.7s of the ~45s each test spends, plus a per-frame re-conversion of the glyph atlas) and makepad/makepad#1176 (test harness knobs). Until they land, point all three makepad deps at a local checkout with those branches to get it — see the comment inCargo.toml; ALL THREE must be the same tree. With them: a steady frame goes 840ms -> 127ms, and the full suite goes from many hours to 67 min green. MAKEPAD_HEADLESS_DPI=1(needs #1175) renders at 1x instead of the hardcoded 2x. The suite only asserts logical geometry, so it is a straight 3-4x on raster for nothing lost — and it is what gets the slowest tests off the 10s cliff.MAKEPAD_TEST_PARALLEL=1 cargo test -- --test-threads=4(needs #1176) runs the suite in ~11-20 min instead of 67, but 2-3 tests fail per run and the set CHANGES between runs. Fine for local iteration, not for a verdict: several tests wait by counting polls, and how much wall clock and how many frames a poll buys both collapse under that load. Verify serially.- Long-press in headless: the desktop path emits no LongPress event, so forward raw
MouseDown… pollwidget_snapshot()past 0.5s …MouseUp. A baresleepwon't advance the app's timers; you must keep it pumping (seetests/ui.rs). - Live GPU verification: run with
HOST_LAUNCHER_AUTOMATION_DIR=<dir>and touch<dir>/shot_request(writes<dir>/frame.pngviacapture_next_frame_to_file),<dir>/tree_request(widget tree), or<dir>/activate_request(raise the window).HOST_LAUNCHER_DEBUG_STATE=open:<app>|drawer|editjumps to a state on startup.
- One widget family per file;
script_mod!UI at the top, then the#[derive]struct,ScriptHook,Widget, then theRefext and the file's action enum. - Comments: 2-3 lines max, say why not what, no em-dashes, no fix-history.
AppStateis shared to widgets viaScope::with_data; read it withscope.data.get::<AppState>().- Apply
SAFE_INSET_PAD_*in whichever widget draws into an edge.
Pure Splash, no Rust. Key rules (see ../makepad/splash.md):
- A plain
View{on_render}does NOT auto-run on first draw — callui.<view>.render()once at boot (let _boot = start_timeout(0.05, || ui.x.render())). - Dynamic lists whose items have per-row
on_clickhandlers don't re-render on array growth: use the pre-allocated fixed-slot pattern +ui.<row>.set_visible(). - Isolation:
mod.fsis a per-app jail,mod.run/mod.res/cx.quitare gone, and the network exists only when the user grants the declarednetworkpermission;uionly reaches this app's own widgets.std.time_now()/std.local_time()give the clock;std.start_intervalticks inside the isolate. - Host services (location, clipboard, notifications, IPC, files, …) go through
host.request(service, args, cb)behind per-app user grants — the full model, service catalog, and script API live indocs/PERMISSIONS.md. Apps declare permissions inpermissions_for(src/mini_apps/builtin.rs) and MUST stay fully usable with zero grants (fallback content, never a blank state). - Parser landmines for callback-heavy code: never end a
fn/closure with anif/else(use earlyreturn nils — a final if parses as an expression and errors); keep} else {on one line; the bridge result field isr.is_ok(okis a keyword,r.oknever parses as field access); and.to_chars()yields char CODES, use.split(...)for string work. - Service-broker debugging:
HOST_LAUNCHER_TRACE_SERVICES=1appends every bridge dispatch/response (app, service, grants, outcome) to/tmp/host_launcher_services.log— works inside the UI test harness too. - Apps must handle ANY host size (split-screen panes ~190w/~250h up to wide
desktop windows). Cap+center the column with
width: Fill{max: N}under analign: Align{x: 0.5}parent, and definefn on_app_resize(w, h)(called on open + every settled size change) to toggle pre-declared tiers viaset_visible(any widget answers it, Labels included — it lived onViewalone until 2026-08-25) — fonts/fixed sizes can't change at runtime. Seeapps/calculator.splashfor the canonical shape. - Compile-check every app's Splash from the CLI:
MAKEPAD=headless HOST_LAUNCHER_FRESH=1 HOST_LAUNCHER_DEBUG_STATE=validate cargo run. - Register a new app in
src/mini_apps/builtin.rs.