- README: a build quick-start for macOS and Windows, the honest Linux
story, what CUDA is for and how to install the separation model.
- AGENTS.md: the `--remote` control surface protocol, so the harness it
documents is usable without reading platform/src/remote.rs.
- Cargo.toml: workspace membership for the crates this series adds and
removes.
- tools/: the Windows box scripts (wincmd, winps and friends, winrun) and
remote_smoke.sh — how a build gets driven on a remote machine.
- apps/asset-server: the standalone server binary and its README.
- Small follow-ups in libs/{windows,apple_sys,makepad_test,mbtile_reader,
converse} and apps/route, plus .gitignore and makepad.splash.
34 KiB
Makepad Agent Runbook
Driving a running app: use
--remote. Every makepad app started with--remoteserves a tiny localhost HTTP control surface: window list, PNG grabs, real mouse/key/text injection, widget rects, log tail, graceful quit. It replacesscreencapture -l,winid.swift, CGEvent scripting and the studio websocket bridge for all agent work. Full spec: App Remote Control.
Execution Policy
- Launch UI programs as standalone release binaries from this checkout. Do
not use the Studio remote bridge,
ObserveMount,RunItem, or anycargo-makepad studiowebsocket client. - Launch with
--remotewhenever you intend to look at or drive the app, and finish withGET /gq. Nothing of yours may outlive your task — never leave a test window on the user's screen. - Always use release builds for runtime validation, profiling, benchmarks,
timing checks, or any performance-sensitive command. Use
--releaseunless the user explicitly asks for a debug build. - Build with
cargo build --release -p <package>, then launch the resulting executable so its provenance is unambiguous. Do not use rawcargo run/cargo makepadto start a UI you will keep inspecting. - Stop or replace an older standalone instance of the same target before launching a freshly built one.
- Keep an interactive standalone app running when the user asks to play with it. Use a separate self-terminating capture run only when a screenshot is also needed.
cargo checkorcargo buildnever counts as UI verification. After changing UI/runtime code, rebuild and relaunch before trusting what you see. Do not keep inspecting an older already-running binary.- Command-line-only tasks (builds, tests, linting, file ops, grep, etc.) can be run directly in the shell.
- A standalone app's built-in screenshot/capture hook is valid for visual inspection.
- When adding a new example crate, update both the Cargo workspace and
makepad.splash.
Standalone Launch
cargo build --release -p <package>from this checkout.- Kill any older process of that same executable.
- Run
target/release/<bin> --remotefrom the repo root (so resource paths resolve), parse the port from the startup line, drive it over HTTP. - After code changes, repeat 1–3 before drawing conclusions.
GET /gqwhen you are done. Always.
App Remote Control (--remote)
Any makepad app launched with --remote runs a localhost HTTP server inside
the process and prints one line before the UI appears:
[makepad-remote] listening on 127.0.0.1:53412 pid=9931 app=makepad-example-splash grabs=/var/folders/…/T/makepad-remote/makepad-example-splash-9931
Port, pid, app name and the grab directory — everything needed to drive and
clean up the instance, with no discovery step. --remote=PORT pins the port;
MAKEPAD_REMOTE=1 (or =PORT) does the same via the environment. No app code
is involved: it lives in app_main!, so every app gets it for free.
Cheat sheet
Every route is a plain GET. Every answer is one line of JSON with short
keys and real numbers. Errors are {"err":"..."} with HTTP 404.
GET / returns this table as plain text, so an agent that finds the port
learns the whole API in one request.
| Route | Answer | Notes |
|---|---|---|
/ /help |
plain-text cheat sheet | self-describing; read this first |
/s ?w=ID |
{"app":…,"pid":…,"w":[{"i":0,"t":"Title","sz":[w,h],"px":[w,h],"dpi":2,"pos":[x,y]}]} |
sz = layout points, px = physical pixels |
/g ?w=&scale=&raw= |
{"png":"/abs/path.png","w":0,"sz":[w,h]} |
writes a file and returns the path (agents read images as files). raw=1 sends image/png bytes instead. scale=0.5 halves it |
/gq ?w=&scale= |
{"png":[paths…],"quit":1} |
grab every window, then quit. The canonical last call of a session |
/m ?k=&x=&y=&w=&b=&dx=&dy=&wait= |
{"ok":1,"f":frame} |
k=move|down|up|click|scroll; b=0 left, 1 right, 2 middle |
/click ?x=&y=&w=&wait= |
{"ok":1} |
alias for /m?k=click (move + down + up) |
/k ?t=TEXT or ?k=down|up|press&c=CODE |
{"ok":1} |
t= goes through the IME text path; c= takes KeyA/a/enter/Escape/ArrowLeft/F1/Key1… plus &shift=1&ctrl=1&alt=1&cmd=1 |
/t ?t=TEXT |
{"ok":1} |
same as /k?t= |
/snap ?q=&w=&all= |
{"s":[{"i":"id","ty":"Button","r":[x,y,w,h],"w":0,"t":"Click me"}]} |
how you find things to click. q= filters id/type/text; rects are window-local, ready to feed to /click |
/d /dump |
plain text widget tree | one indented line per widget, ending x y w h |
/log ?n=50&since=N |
{"n":lastseq,"l":["[E] …"]} |
ring buffer of the app's own log output — see errors without owning stdout |
/close ?w=ID |
{"ok":1} |
closes one window the normal way |
/quit |
{"ok":1} |
graceful shutdown, no final grab |
Add &wait=1 to any input route to have it answer only after the next frame
is drawn, so a following /g sees the result with no sleep.
Add &w=ID to target a window; omit it for the first one.
POST the same routes with a flat JSON body ({"x":10,"y":20}) when quoting a
query string is painful; the key names are the long ones (window, kind,
button, text, code).
The standard pattern
cargo build --release -p makepad-example-splash
./target/release/makepad-example-splash --remote > /tmp/app.log 2>&1 &
sleep 4
P=$(grep -o 'listening on 127.0.0.1:[0-9]*' /tmp/app.log | grep -o '[0-9]*$')
curl -s "http://127.0.0.1:$P/s" # {"app":…,"w":[{"i":0,…}]}
curl -s "http://127.0.0.1:$P/snap?q=press_demo" # find the button's rect
curl -s "http://127.0.0.1:$P/click?x=352&y=472&wait=1"
curl -s "http://127.0.0.1:$P/snap?q=press_status" # assert the app reacted
curl -s "http://127.0.0.1:$P/log?n=20" # any errors?
curl -s "http://127.0.0.1:$P/gq?scale=0.5" # final PNGs + quit
Read the returned png path with your image tool. tools/remote_smoke.sh is
this pattern as an executable end-to-end test across three example apps.
Rules
- Close what you open. When you are done with an instance you launched,
GET /gq(or/closeeach window, then/quit). Never leave test windows on the user's screen, and neverpkillwhen the protocol is available. - Never touch an instance the user is running. Launch your own.
- A vanished window or app with
[makepad-remote] user closed …in the log means the human dismissed it — it was in their way. Do not treat that as a crash and do not relaunch it. The app prints[makepad-remote] user closed window 1 ("Inspector Panel")and, when that was the last window,[makepad-remote] app exit: user closed the last window. Both lines go to stdout with or without--remote, and into the/logring. While the app lives,/s?w=1on such a window answers{"err":"window 1 closed by user"}rather than "no window 1". --remotewindows are tagged. Their title gets a[remote]suffix (both the OS title bar and makepad's own caption bar) so a human who finds one lingering knows it is an agent instance and can close it guilt-free.--remote-title-tag=NAMEchanges the tag;--remote-title-tag=offremoves it.
Semantics worth knowing
- Coordinates are layout points, window-local, y down — the same space
MouseDownEvent.absuses, and the same space/snapreports rects in. No dpi maths: a rect from/snapgoes straight into/click. - Window ids are stable
usizeslots (/s"i"). Every window-targeting route takesw=; omitting it means the first created window. A request for a window that never existed 404s with{"err":"no window 3"}. - Input takes the real path. Events are injected through
Cx::dispatch_studio_msg, the same function the studio bridge uses, with the samefingersbookkeeping — so hits, capture, tap counts and gestures behave exactly as they do for a human./clicksends move + down + up so hover-dependent widgets see what they expect. - Grabs are real frames, read back from the window's own presented
drawable on the frame after the request (the studio screenshot pipeline,
extended with per-window targeting). The UI thread is never blocked; the
HTTP thread waits. Grabs are written to
$TMPDIR/makepad-remote/<app>-<pid>/grab-w<window>-<seq>.png, monotonically numbered, with the last 32 per window retained. - Backends: macOS/Metal is fully supported. Linux GL and Vulkan support
grabs too. Windows/D3D11 has no screenshot readback yet, so
/gthere times out with{"err":"grab timeout …"}while every other route works. Android, OHOS and wasm compile to a no-op. - Cost when idle is zero. The event loop only upshifts its paint clock while a remote request is in flight.
Studio remote bridge (the older path)
The studio (studio/desktop + studio/hub) drives a hosted app over a
websocket with the StudioToApp / AppToStudio protocol
(platform/studio/src/studio.rs): MouseDown/Up/Move/Scroll, KeyDown/Up,
TextInput, TextCopy/Cut, GameInput, Screenshot, RunViewFrameRequest,
WidgetTreeDump, WidgetQuery, WidgetSnapshot, LiveChange, Custom,
Kill, plus the shared-swapchain messages Swapchain / WindowGeomChange /
Tick. libs/makepad_test is the programmatic client for it
(TestApp::try_click_center, try_type_text, try_screenshot, …) and
examples/*/tests/ui.rs are its test suites.
--remote reuses that vocabulary — the same message types, the same injection
function, the same screenshot pipeline — but exposes it as HTTP on the app
itself, with no studio, no hub, no build ids, and with per-window targeting
that the studio path lacks. Use --remote for agent work; the studio bridge
remains for the studio and for libs/makepad_test.
CLAUDE.md Body
The following is the current body of CLAUDE.md included verbatim for agent guidance parity.
Makepad Project Guide
Important: When Converting Syntax
Always search for existing usage patterns in the NEW crates (widgets, code_editor, studio) before making syntax changes. The old widgets and live_design! syntax is deprecated. When unsure about the correct syntax for something, grep for similar usage in widgets/src/ to find the correct pattern.
# Example: find how texture declarations work in new system
grep -r "texture_2d" widgets/src/
Critical: Always use Name: value syntax, never Name = value. The old Key = Value syntax no longer works. For named widget instances, use name := Type{...} syntax.
Running UI Programs
Launch UI apps as standalone release binaries from this checkout. Do not use the Studio remote bridge.
cargo build --release -p makepad-app-asset-ui
# stop any older instance of the same binary, then:
./target/release/makepad-app-asset-ui
For one-shot visual smoke of a small example:
RUST_BACKTRACE=1 cargo run -p makepad-example-splash --release & PID=$!; sleep 15; kill $PID 2>/dev/null; echo "Process $PID killed"
To look at or drive a running app, add --remote: the app serves a localhost
HTTP control surface (window list, PNG grabs, real mouse/key/text injection,
widget rects, log tail) and prints its port on startup. Finish every session
with GET /gq, which grabs each window and quits — never leave a test window
on screen. Full protocol: repo-root AGENTS.md.
./target/release/makepad-example-splash --remote > /tmp/app.log 2>&1 &
P=$(grep -o 'listening on 127.0.0.1:[0-9]*' /tmp/app.log | grep -o '[0-9]*$')
curl -s "http://127.0.0.1:$P/" # cheat sheet
curl -s "http://127.0.0.1:$P/gq" # final grab + quit
When measuring runtime or performance, prefer --release.
Cargo.toml Setup
[package]
name = "makepad-example-myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
makepad-widgets = { path = "../../widgets" }
Widgets DSL (script_mod!)
The new DSL uses script_mod! macro with runtime script evaluation instead of the old live_design! compile-time macros.
Imports and App Setup
use makepad_widgets::*;
app_main!(App);
script_mod!{
use mod.prelude.widgets.*
load_all_resources() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
window.inner_size: vec2(800, 600)
body +: {
// UI content here
}
}
}
}
}
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm); // Register all widgets
// Platform-specific initialization goes here (e.g., vm.cx().start_stdin_service() for macos)
App::from_script_mod(vm, self::script_mod)
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live] ui: WidgetRef,
}
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
// Handle widget actions
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}
Available Widgets (widgets/src/lib.rs)
Core: View, SolidView, RoundedView, ScrollXView, ScrollYView, ScrollXYView
Text: Label, H1, H2, H3, LinkLabel, TextInput
Buttons: Button, ButtonFlat, ButtonFlatter
Toggles: CheckBox, Toggle, RadioButton
Input: Slider, DropDown
Layout: Splitter, FoldButton, FoldHeader, Hr
Lists: PortalList
Navigation: StackNavigation, ExpandablePanel
Overlays: Modal, Tooltip, PopupNotification
Dock: Dock, DockSplitter, DockTabs, DockTab
Media: Image, Icon, LoadingSpinner
Special: FileTree, PageFlip, CachedWidget
Window: Window, Root
Markup: Html, Markdown (feature-gated)
Widget Definition Pattern
// Rust struct
#[derive(Script, ScriptHook, Widget)]
pub struct MyWidget {
#[source] source: ScriptObjectRef, // Required for script integration
#[walk] walk: Walk,
#[layout] layout: Layout,
#[redraw] #[live] draw_bg: DrawQuad,
#[live] draw_text: DrawText,
#[rust] my_state: i32, // Runtime-only field
}
// For widgets with animations, add Animator derive:
#[derive(Script, ScriptHook, Widget, Animator)]
pub struct AnimatedWidget {
#[source] source: ScriptObjectRef,
#[apply_default] animator: Animator,
// ...
}
Script Module Structure
script_mod!{
use mod.prelude.widgets_internal.* // For internal widget definitions
use mod.widgets.* // Access other widgets
// Register base widget (connects Rust struct to script)
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
// Create styled variant with defaults
mod.widgets.MyWidget = set_type_default() do mod.widgets.MyWidgetBase{
width: Fill
height: Fit
padding: theme.space_2
draw_bg +: {
color: theme.color_bg_app
}
}
}
Key Syntax Differences (Old vs New)
| Old (live_design!) | New (script_mod!) |
|---|---|
<BaseWidget> |
mod.widgets.BaseWidget{ } |
{{StructName}} |
#(Struct::register_widget(vm)) |
(THEME_COLOR_X) |
theme.color_x |
<THEME_FONT> |
theme.font_regular |
instance hover: 0.0 |
hover: instance(0.0) |
uniform color: #fff |
color: uniform(#fff) |
draw_bg: { } (replace) |
draw_bg +: { } (merge) |
default: off |
default: @off |
fn pixel(self) |
pixel: fn() |
item.apply_over(cx, live!{...}) |
script_apply_eval!(cx, item, {...}) |
Runtime Property Updates with script_apply_eval!
Use script_apply_eval! macro to dynamically update widget properties at runtime:
// Old system (live! macro with apply_over)
item.apply_over(cx, live!{
height: (height)
draw_bg: {is_even: (if is_even {1.0} else {0.0})}
});
// New system (script_apply_eval! macro)
script_apply_eval!(cx, item, {
height: #(height)
draw_bg: {is_even: #(if is_even {1.0} else {0.0})}
});
// For colors, use #(color) syntax
let color = self.color_focus;
script_apply_eval!(cx, item, {
draw_bg: {
color: #(color)
}
});
Note: In script_apply_eval!, use #(expr) for Rust expression interpolation instead of (expr).
Theme Access
Always use theme. prefix:
color: theme.color_bg_app
padding: theme.space_2
font_size: theme.font_size_p
text_style: theme.font_regular
Property Merging with +:
The +: operator merges with parent instead of replacing:
mod.widgets.MyButton = mod.widgets.Button{
draw_bg +: {
color: #f00 // Only overrides color, keeps other draw_bg properties
}
}
Shader Instance vs Uniform
instance(value)- Per-draw-call value (can vary per widget instance)uniform(value)- Shared across all instances using same shader
draw_bg +: {
hover: instance(0.0) // Each button has its own hover state
color: uniform(theme.color_x) // Shared base color
color_hover: instance(theme.color_y) // Per-instance if color varies
}
Animator Definition
animator: Animator{
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.1}}
apply: {
draw_bg: {hover: 0.0}
draw_text: {hover: 0.0}
}
}
on: AnimatorState{
from: {all: Snap} // Instant transition
apply: {
draw_bg: {hover: 1.0}
draw_text: {hover: 1.0}
}
}
}
}
Shader Functions
draw_bg +: {
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.box(0.0, 0.0, self.rect_size.x, self.rect_size.y, 4.0)
sdf.fill(self.color.mix(self.color_hover, self.hover))
return sdf.result
}
}
Note: Use .method() not ::method() in shaders.
Color Mixing (Method Chaining)
// Old nested style (avoid)
mix(mix(mix(color1, color2, hover), color3, down), color4, focus)
// New chained style (preferred)
color1.mix(color2, hover).mix(color3, down).mix(color4, focus)
App Structure Pattern
script_mod!{
use mod.prelude.widgets.*
load_all_resources() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
window.inner_size: vec2(1000, 700)
body +: {
// Your UI here
MyWidget{}
}
}
}
}
}
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm);
// Platform-specific initialization (e.g., vm.cx().start_stdin_service() for macos)
App::from_script_mod(vm, self::script_mod)
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live] ui: WidgetRef,
}
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
if self.ui.button(ids!(my_button)).clicked(actions) {
log!("Button clicked!");
}
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}
Widget ID References
Use := for named widget instances:
// In DSL
my_button := Button{text: "Click"}
// In Rust code
self.ui.button(ids!(my_button)).clicked(actions)
Template Definitions in Dock
Templates inside Dock are local; use let bindings at script level for reusable components:
script_mod!{
// Reusable at script level
let MyPanel = SolidView{
width: Fill
height: Fill
// ...
}
// Use directly
body +: {
MyPanel{} // Works because it's a let binding
}
}
Custom Draw Widget Example
#[derive(Script, ScriptHook, Widget)]
pub struct CustomDraw {
#[walk] walk: Walk,
#[layout] layout: Layout,
#[redraw] #[live] draw_quad: DrawQuad,
#[rust] area: Area,
}
impl Widget for CustomDraw {
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
cx.begin_turtle(walk, self.layout);
let rect = cx.turtle().rect();
self.draw_quad.draw_abs(cx, rect);
cx.end_turtle_with_area(&mut self.area);
DrawStep::done()
}
fn handle_event(&mut self, _cx: &mut Cx, _event: &Event, _scope: &mut Scope) {}
}
Script Object Storage: map vs vec
In script objects, properties are stored in two different places:
map: Containskey: valuepairs (regular properties)vec: Contains named template items (via:=syntax)
This distinction is important when working with on_after_apply or inspecting script objects directly.
Templates in List Widgets (PortalList, FlatList)
In list widgets, named IDs (using :=) define templates that are stored in the widget's templates HashMap. These are NOT regular properties - they go into the script object's vec and are collected via on_after_apply.
// In script_mod! - defining templates for a list
my_list := PortalList {
// Regular properties (go into struct fields)
width: Fill
height: Fill
scroll_bar: mod.widgets.ScrollBar {}
// Templates (named with :=) - stored in templates HashMap, NOT struct fields
Item := View {
height: 40
title := Label { text: "Default" }
}
Header := View {
draw_bg: { color: #333 }
}
}
The templates are collected in on_after_apply:
impl ScriptHook for PortalList {
fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue) {
if let Some(obj) = value.as_object() {
vm.vec_with(obj, |_vm, vec| {
for kv in vec {
if let Some(id) = kv.key.as_id() {
self.templates.insert(id, kv.value);
}
}
});
}
}
}
Then used during drawing:
while let Some(item_id) = list.next_visible_item(cx) {
let item = list.item(cx, item_id, id!(Item));
item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id));
item.draw_all(cx, &mut Scope::empty());
}
Key distinction: Regular properties like scroll_bar: mod.widgets.ScrollBar {} are applied directly to struct fields. Template definitions like Item := View {...} are stored separately for dynamic instantiation.
PortalList Usage
#[derive(Script, ScriptHook, Widget)]
pub struct MyList {
#[deref] view: View,
}
impl Widget for MyList {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
if let Some(mut list) = item.borrow_mut::<PortalList>() {
list.set_item_range(cx, 0, 100); // 100 items
while let Some(item_id) = list.next_visible_item(cx) {
let item = list.item(cx, item_id, id!(Item));
item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id));
item.draw_all(cx, &mut Scope::empty());
}
}
}
DrawStep::done()
}
}
FileTree Usage
impl Widget for FileTreeDemo {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
while self.file_tree.draw_walk(cx, scope, walk).is_step() {
self.file_tree.set_folder_is_open(cx, live_id!(root), true, Animate::No);
// Draw nodes recursively
self.draw_node(cx, live_id!(root));
}
DrawStep::done()
}
}
Registering Custom Draw Shaders
For custom draw types with shader fields, use script_shader:
script_mod!{
use mod.prelude.widgets_internal.*
// Register custom draw shader
set_type_default() do #(DrawMyShader::script_shader(vm)){
..mod.draw.DrawQuad // Inherit from DrawQuad
}
// Register widget that uses it
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
}
#[derive(Script, ScriptHook)]
#[repr(C)]
struct DrawMyShader {
#[deref] draw_super: DrawQuad,
#[live] my_param: f32,
}
Registering Components (non-Widget)
For structs that aren't full widgets but need script registration:
script_mod!{
// For components (not widgets)
mod.widgets.MyComponentBase = #(MyComponent::script_component(vm))
// For widgets (implements Widget trait)
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
}
Script Prelude Modules
Two prelude modules available:
mod.prelude.widgets_internal.*- For internal widget library developmentmod.prelude.widgets.*- For app development (includes all widgets)
script_mod!{
// App development - use widgets prelude
use mod.prelude.widgets.*
// Or for widget library internals
use mod.prelude.widgets_internal.*
use mod.widgets.*
}
Default Enum Values
For enums with a None variant that need Default, use standard Rust #[default] attribute instead of DefaultNone derive:
// Correct - use #[default] attribute on the None variant
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum MyAction {
SomeAction,
AnotherAction,
#[default]
None,
}
// Wrong - don't use DefaultNone derive
#[derive(Clone, Copy, Debug, PartialEq, DefaultNone)] // Don't do this
pub enum MyAction {
SomeAction,
None,
}
Multi-Module Script Registration Pattern
When refactoring a multi-file project (like studio) from live_design! to script_mod!:
- Each widget module defines its own
script_mod!that registers tomod.widgets.*:
// In studio_editor.rs
script_mod! {
use mod.prelude.widgets_internal.*
use mod.widgets.*
mod.widgets.StudioCodeEditorBase = #(StudioCodeEditor::register_widget(vm))
mod.widgets.StudioCodeEditor = set_type_default() do mod.widgets.StudioCodeEditorBase {
editor := CodeEditor {}
}
}
- The lib.rs aggregates all widget script_mods:
pub fn script_mod(vm: &mut ScriptVm) {
crate::module1::script_mod(vm);
crate::module2::script_mod(vm);
// ... all widget modules
}
- The app.rs calls them in correct order:
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm); // Base widgets first
crate::script_mod(vm); // Your widget modules
crate::app_ui::script_mod(vm); // UI that uses the widgets
App::from_script_mod(vm, self::script_mod)
}
}
- The app_ui.rs can then use registered widgets:
script_mod! {
use mod.prelude.widgets.*
// Now StudioCodeEditor is available from mod.widgets
let EditorContent = View {
editor := StudioCodeEditor {}
}
}
Cross-Module Sharing via mod Object
IMPORTANT: use crate.module.* does NOT work in script_mod. The crate. prefix is not available.
To share definitions between script_mod blocks in different files, store them in the mod object:
// In app_ui.rs - export to mod.widgets namespace
script_mod! {
use mod.prelude.widgets.*
// This makes AppUI available as mod.widgets.AppUI
mod.widgets.AppUI = Window{
// ...
}
}
// In app.rs - import via mod.widgets
script_mod! {
use mod.prelude.widgets.*
use mod.widgets.* // Now AppUI is in scope
load_all_resources() do #(App::script_component(vm)){
ui: Root{ AppUI{} }
}
}
The mod object is the only way to share data between script_mod blocks.
Prelude Alias Syntax
When defining a prelude, use name:mod.path to create an alias:
mod.prelude.widgets = {
..mod.std, // Spread all of mod.std into scope
theme:mod.theme, // Create 'theme' as alias for mod.theme
draw:mod.draw, // Create 'draw' as alias for mod.draw
}
Without the alias (just mod.theme,), the module is included but has no name - you can't access it!
Let Bindings are Local
let bindings in script_mod are LOCAL to that script_mod block. They cannot be:
- Accessed from other script_mod blocks
- Used as property values directly (e.g.,
content +: MyLetBindingwon't work)
To use a let binding, instantiate it: MyLetBinding{} or store it in mod.* for cross-module access.
Debug Logging with ~
Use ~expression to log the value of an expression during script evaluation:
script_mod! {
~mod.theme // Logs the theme object
~mod.prelude.widgets // Logs what's in the prelude
~some_variable // Logs a variable's value (or "not found" error)
}
Common Pitfalls
Widget ID references: Named widget instances use := in the DSL and plain names in Rust id macros:
- DSL defines
code_block := View { ... }→ Rust usesid!(code_block) - DSL defines
my_button := Button { ... }→ Rust usesids!(my_button)
-
Missing
#[source]: All Script-derived structs need#[source] source: ScriptObjectRef -
Template scope: Templates defined inside Dock aren't available outside; use
letat script level -
Uniform vs Instance: Use
instance()for per-widget varying colors (like hover states on backgrounds) -
Forgot
+:: Without+:, you replace the entire property instead of merging -
Theme access: Always
theme.color_x, neverTHEME_COLOR_Xor(theme.color_x) -
Missing widget registration: Call
crate::makepad_widgets::script_mod(vm)inApp::run()before your ownscript_mod. Note: the oldlive_design!system and its crates are archived underold/ -
Draw shader repr: Custom draw shaders need
#[repr(C)]for correct memory layout -
DefaultNone derive: Don't use
DefaultNonederive - use standard#[derive(Default)]with#[default]attribute on theNonevariant -
Script_mod call order: Widget modules must be registered BEFORE UI modules that use them. Always call
lib.rs::script_modbeforeapp_ui::script_mod -
pubkeyword invalid in script_mod: Don't usepub mod.widgets.X = ..., just usemod.widgets.X = .... Visibility is controlled by the Rust module system, not script_mod. -
Syntax for Inset/Align/Walk: Use constructor syntax -
margin: Inset{left: 10}notmargin: {left: 10},align: Align{x: 0.5 y: 0.5}notalign: {x: 0.5, y: 0.5} -
Cursor values: Use
cursor: MouseCursor.Handnotcursor: Handorcursor: @Hand -
Resource paths: Use
crate_resource("self://path")notdep("crate://self/path") -
Texture declarations in shaders: Use
tex: texture_2d(float)nottex: texture2d -
Enums not exposed to script: Some Rust enums like
PopupMenuPosition::BelowInputmay not be exposed to script. If you get "not found" errors on enum variants, just remove the property and use the default -
Shader
modvsmodf: The Makepad shader language usesmodf(a, b)for float modulo, NOTmod(a, b). Similarly, useatan2(y, x)notatan(y, x)for two-argument arctangent.atan(x)(single arg) is also available.fract(x)works as expected. -
Draw shader struct field ordering: In
#[repr(C)]draw shader structs that extend another draw shader via#[deref], NEVER place#[rust]or other non-instance data AFTERDrawVarsand the instance fields. The system uses an unsafe pointer trick inDrawVars::as_slice()that reads contiguously past the end ofdyn_instancesinto the subsequent#[live]fields. Any non-instance data betweenDrawVarsand the instance fields will corrupt the GPU instance buffer. Put all extra data (like#[rust],#[live]non-instance fields such as resource handles, booleans, etc.) BEFORE the#[deref]field, and only#[live]instance fields (the ones that map to shader inputs) AFTER.// CORRECT - non-instance data before deref, instance fields after #[derive(Script, ScriptHook)] #[repr(C)] pub struct MyDrawShader { #[live] pub svg: Option<ScriptHandleRef>, // non-instance, BEFORE deref #[rust] my_state: bool, // non-instance, BEFORE deref #[deref] pub draw_super: DrawVector, // contains DrawVars + base instance fields #[live] pub tint: Vec4f, // instance field, AFTER deref - OK } // WRONG - rust data after instance fields breaks the memory layout #[derive(Script, ScriptHook)] #[repr(C)] pub struct MyDrawShader { #[deref] pub draw_super: DrawVector, #[live] pub tint: Vec4f, // instance field #[rust] my_state: bool, // BAD: sits between tint and the next shader's fields } -
Don't put comments or blank lines before the first real code in
script!/script_mod!: Rust's proc macro token stream strips comments entirely — they produce no tokens. This shifts error column/line info because the span tracking starts from the first actual token. Always start with real code (e.g.,use mod.std.assert) immediately after the opening brace. -
WARNING: Hex colors containing the letter
einscript_mod!: The Rust tokenizer interpretseorEin hex color literals as a scientific notation exponent, causing parse errors likeexpected at least one digit in exponent. For example,#2ecc71fails because2elooks like the start of2e<exponent>. Use the#xprefix to escape this: write#x2ecc71instead of#x2ecc71. This applies to any hex color where a digit is immediately followed bye/E(e.g.,#1e1e2e,#4466ee,#7799ee,#bb99ee). Colors withoute(like#ff4444,#44cc44) work fine with plain#. -
Shader enums: Prefer
matchon enum values with_ =>as the catch-all arm, notif/elsechains over integer-like values. If enummatchfails in shader compilation, treat it as a compiler bug: add or extend aplatform/script/testcase and fix the shader compiler path instead of rewriting shader logic toif/else.