- 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.
931 lines
34 KiB
Markdown
931 lines
34 KiB
Markdown
# Makepad Agent Runbook
|
||
|
||
> **Driving a running app: use `--remote`.** Every makepad app started with
|
||
> `--remote` serves a tiny localhost HTTP control surface: window list, PNG
|
||
> grabs, real mouse/key/text injection, widget rects, log tail, graceful quit.
|
||
> It replaces `screencapture -l`, `winid.swift`, CGEvent scripting and the
|
||
> studio websocket bridge for all agent work. Full spec: [App Remote Control](#app-remote-control---remote).
|
||
|
||
## Execution Policy
|
||
- Launch UI programs as standalone release binaries from this checkout. Do
|
||
not use the Studio remote bridge, `ObserveMount`, `RunItem`, or any
|
||
`cargo-makepad studio` websocket client.
|
||
- Launch with `--remote` whenever you intend to look at or drive the app,
|
||
and finish with `GET /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 `--release`
|
||
unless 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 raw
|
||
`cargo run` / `cargo makepad` to 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 check` or `cargo build` never 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
|
||
1. `cargo build --release -p <package>` from this checkout.
|
||
2. Kill any older process of that same executable.
|
||
3. Run `target/release/<bin> --remote` from the repo root (so resource paths
|
||
resolve), parse the port from the startup line, drive it over HTTP.
|
||
4. After code changes, repeat 1–3 before drawing conclusions.
|
||
5. `GET /gq` when 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
|
||
|
||
```bash
|
||
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 `/close` each window, then `/quit`). Never leave test windows
|
||
on the user's screen, and never `pkill` when 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
|
||
`/log` ring. While the app lives, `/s?w=1` on such a window answers
|
||
`{"err":"window 1 closed by user"}` rather than "no window 1".
|
||
- **`--remote` windows 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=NAME` changes the tag; `--remote-title-tag=off` removes
|
||
it.
|
||
|
||
### Semantics worth knowing
|
||
|
||
- **Coordinates** are layout points, window-local, y down — the same space
|
||
`MouseDownEvent.abs` uses, and the same space `/snap` reports rects in. No
|
||
dpi maths: a rect from `/snap` goes straight into `/click`.
|
||
- **Window ids** are stable `usize` slots (`/s` `"i"`). Every window-targeting
|
||
route takes `w=`; 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 same `fingers` bookkeeping — so hits, capture, tap counts and gestures
|
||
behave exactly as they do for a human. `/click` sends 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 `/g` there 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.
|
||
|
||
```bash
|
||
# 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.
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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`.
|
||
|
||
```bash
|
||
./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
|
||
|
||
```toml
|
||
[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
|
||
|
||
```rust
|
||
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
|
||
// 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
|
||
|
||
```rust
|
||
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:
|
||
```rust
|
||
// 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:
|
||
```rust
|
||
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:
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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)
|
||
|
||
```rust
|
||
// 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
|
||
|
||
```rust
|
||
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:
|
||
```rust
|
||
// 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:
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
#[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`**: Contains `key: value` pairs (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`.
|
||
|
||
```rust
|
||
// 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`:
|
||
```rust
|
||
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:
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
#[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
|
||
|
||
```rust
|
||
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`:
|
||
|
||
```rust
|
||
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:
|
||
|
||
```rust
|
||
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 development
|
||
- `mod.prelude.widgets.*` - For app development (includes all widgets)
|
||
|
||
```rust
|
||
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:
|
||
|
||
```rust
|
||
// 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!`:
|
||
|
||
1. **Each widget module** defines its own `script_mod!` that registers to `mod.widgets.*`:
|
||
```rust
|
||
// 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 {}
|
||
}
|
||
}
|
||
```
|
||
|
||
2. **The lib.rs** aggregates all widget script_mods:
|
||
```rust
|
||
pub fn script_mod(vm: &mut ScriptVm) {
|
||
crate::module1::script_mod(vm);
|
||
crate::module2::script_mod(vm);
|
||
// ... all widget modules
|
||
}
|
||
```
|
||
|
||
3. **The app.rs** calls them in correct order:
|
||
```rust
|
||
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)
|
||
}
|
||
}
|
||
```
|
||
|
||
4. **The app_ui.rs** can then use registered widgets:
|
||
```rust
|
||
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:
|
||
|
||
```rust
|
||
// 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:
|
||
```rust
|
||
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 +: MyLetBinding` won'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:
|
||
```rust
|
||
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 uses `id!(code_block)`
|
||
- DSL defines `my_button := Button { ... }` → Rust uses `ids!(my_button)`
|
||
|
||
1. **Missing `#[source]`**: All Script-derived structs need `#[source] source: ScriptObjectRef`
|
||
|
||
2. **Template scope**: Templates defined inside Dock aren't available outside; use `let` at script level
|
||
|
||
3. **Uniform vs Instance**: Use `instance()` for per-widget varying colors (like hover states on backgrounds)
|
||
|
||
4. **Forgot `+:`**: Without `+:`, you replace the entire property instead of merging
|
||
|
||
5. **Theme access**: Always `theme.color_x`, never `THEME_COLOR_X` or `(theme.color_x)`
|
||
|
||
6. **Missing widget registration**: Call `crate::makepad_widgets::script_mod(vm)` in `App::run()` before your own `script_mod`. Note: the old `live_design!` system and its crates are archived under `old/`
|
||
|
||
7. **Draw shader repr**: Custom draw shaders need `#[repr(C)]` for correct memory layout
|
||
|
||
8. **DefaultNone derive**: Don't use `DefaultNone` derive - use standard `#[derive(Default)]` with `#[default]` attribute on the `None` variant
|
||
|
||
9. **Script_mod call order**: Widget modules must be registered BEFORE UI modules that use them. Always call `lib.rs::script_mod` before `app_ui::script_mod`
|
||
|
||
10. **`pub` keyword invalid in script_mod**: Don't use `pub mod.widgets.X = ...`, just use `mod.widgets.X = ...`. Visibility is controlled by the Rust module system, not script_mod.
|
||
|
||
11. **Syntax for Inset/Align/Walk**: Use constructor syntax - `margin: Inset{left: 10}` not `margin: {left: 10}`, `align: Align{x: 0.5 y: 0.5}` not `align: {x: 0.5, y: 0.5}`
|
||
|
||
12. **Cursor values**: Use `cursor: MouseCursor.Hand` not `cursor: Hand` or `cursor: @Hand`
|
||
|
||
13. **Resource paths**: Use `crate_resource("self://path")` not `dep("crate://self/path")`
|
||
|
||
14. **Texture declarations in shaders**: Use `tex: texture_2d(float)` not `tex: texture2d`
|
||
|
||
15. **Enums not exposed to script**: Some Rust enums like `PopupMenuPosition::BelowInput` may not be exposed to script. If you get "not found" errors on enum variants, just remove the property and use the default
|
||
|
||
17. **Shader `mod` vs `modf`**: The Makepad shader language uses `modf(a, b)` for float modulo, NOT `mod(a, b)`. Similarly, use `atan2(y, x)` not `atan(y, x)` for two-argument arctangent. `atan(x)` (single arg) is also available. `fract(x)` works as expected.
|
||
|
||
16. **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 AFTER `DrawVars` and the instance fields. The system uses an unsafe pointer trick in `DrawVars::as_slice()` that reads contiguously past the end of `dyn_instances` into the subsequent `#[live]` fields. Any non-instance data between `DrawVars` and 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.
|
||
```rust
|
||
// 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
|
||
}
|
||
```
|
||
|
||
18. **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.
|
||
|
||
19. **WARNING: Hex colors containing the letter `e` in `script_mod!`**: The Rust tokenizer interprets `e` or `E` in hex color literals as a scientific notation exponent, causing parse errors like `expected at least one digit in exponent`. For example, `#2ecc71` fails because `2e` looks like the start of `2e<exponent>`. **Use the `#x` prefix** to escape this: write `#x2ecc71` instead of `#x2ecc71`. This applies to any hex color where a digit is immediately followed by `e`/`E` (e.g., `#1e1e2e`, `#4466ee`, `#7799ee`, `#bb99ee`). Colors without `e` (like `#ff4444`, `#44cc44`) work fine with plain `#`.
|
||
|
||
20. **Shader enums**: Prefer `match` on enum values with `_ =>` as the catch-all arm, not `if/else` chains over integer-like values. If enum `match` fails in shader compilation, treat it as a compiler bug: add or extend a `platform/script/test` case and fix the shader compiler path instead of rewriting shader logic to `if/else`.
|