- Visual UI programs must be launched and controlled through the Makepad Studio remote protocol.
- 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. - Do not use mount observation or runnable discovery from the bridge client. The bridge must not claim mount ownership from Studio desktop.
- Do not launch UI programs with raw
cargo run,cargo makepad, or ad hoc cargo invocation when a runnable item exists. - Do not use bridge
Cargorequests to run applications. Only launch apps from runnable items via bridgeRunItem. - For UI runnable targets, do not prebuild or precheck the app from the shell before launching it in Studio. Let the Studio
RunItembuild be the single build path so Cargo fingerprints, env vars, target dirs, and flags stay identical. - Before starting a new UI run for the same target, send
ClearBuildfor the previous build so Studio stops it and removes its run/log/profiler tabs. cargo checkorcargo buildnever counts as UI verification. After changing UI/runtime code, you must clear the old build and start a fresh Studio run before trusting screenshots, widget dumps, or interaction results.- Do not keep inspecting an older already-running app after code changes. Re-run the target and verify against the new
build_id. - Command-line-only tasks (builds, tests, linting, file ops, grep/ripgrep, etc.) can be run directly in the shell.
- Prefer studio remote control for any workflow that needs screenshots, widget queries, clicks, typing, or runtime UI inspection.
- Before using Studio protocol tools (
FindInFiles,ReadTextRange,WidgetTreeDump,WidgetQuery,Screenshot,Click,TypeText,Return), always start one persistent Studio remote bridge process and reuse it for the entire interaction.
- Studio is started manually by the user.
- Studio remote target is
ip:portonly (nohttp://, nows://), normally127.0.0.1:8001.- Use
127.0.0.1:8002only if Studio reports fallback because8001is occupied.
- Use
- Keep one persistent studio remote process for the whole interaction.
- Command:
target/release/cargo-makepad studio --studio=127.0.0.1:8001
- Send newline-delimited JSON requests on stdin.
- Read newline-delimited JSON responses on stdout.
- Protocol shape is raw
ClientToHubrequests on stdin and filteredHubToClientresponses on stdout. - Do not send
ObserveMountfrom the bridge. It can takeprimaryUI ownership for the mount and divert RunView/framebuffer traffic away from Studio desktop.
{"ListBuilds":[]}{"ClearBuild":{"build_id":[6]}}stops a running build and immediately clears its Studio UI tabs; use this before rerunning the same app.{"StopBuild":{"build_id":[6]}}stops/kills a running build but does not clear Studio tabs.{"RunItem":{"mount":"makepad","name":"makepad-example-todo"}}{"RunItem":{"mount":"makepad","name":"makepad-example-xr-quest"}}{"FindInFiles":{"mount":"makepad","pattern":"ClientToHub::","is_regex":false,"glob":null,"max_results":200}}{"FindInFiles":{"mount":"makepad","pattern":"ClientToHub::(FindInFiles|ReadTextRange)","is_regex":true,"glob":"**/*.rs","max_results":200}}{"ReadTextRange":{"path":"makepad/studio/backend/src/dispatch.rs","start_line":640,"end_line":720}}{"WidgetTreeDump":{"build_id":[6]}}{"WidgetQuery":{"build_id":[6],"query":"id:todo_input"}}{"Screenshot":{"build_id":[6],"kind_id":0}}(kind_idoptional; defaults to0){"Click":{"build_id":[6],"x":1274,"y":342}}{"TypeText":{"build_id":[6],"text":"hello"}}{"Return":{"build_id":[6],"auto_dump":false}}{"ForwardToApp":{"build_id":[6],"msg_bin":[...]}}(advanced; binary payload)
- The studio remote bridge supports raw app event passthrough via
UIToStudio::ForwardToApp. - Current
StudioToAppvariants include:Screenshot,WidgetTreeDump,KeepAlive,LiveChange,Swapchain,WindowGeomChange,TickMouseDown,MouseUp,MouseMove,ScrollKeyDown,KeyUp,TextInput,TextCopy,TextCutNone,Kill
- Use direct studio remote requests (
Screenshot,WidgetTreeDump,Click,TypeText,Return) for normal automation. - Use raw
StudioToApponly for low-level event injection/debugging.
- Bridge stdout is filtered to:
Hello,Error,TextFileRead,TextFileRange,FindFileResults,SearchFileResults,Builds,RunItems,BuildStarted,BuildStopped,BuildCleared,AppStarted,RunViewCreated,QueryLogResults,Screenshot,WidgetTreeDump,WidgetQuery,QueryCancelled. BuildClearedis a Studio frontend cleanup signal routed to the primary UI for the build's mount; bridge clients should not wait for it before starting the next run.RunViewFrameand the terminal stream are not exposed by the bridge.Screenshotresponses include file metadata (path,width,height) and not inline PNG bytes.WidgetTreeDumpresponses include text dump content keyed byrequest_id.FindInFilesresponds asSearchFileResultswith concise entries (path,line,column,line_text) anddone.FindInFilesdefaults to searching only.rs,.md,.tomlfiles unlessglobis provided.ReadTextRangeresponds asTextFileRangewithpath, requestedstart_line/end_line,total_lines, andcontent.- Query-scoped responses are lane-filtered by
query_id.client_id; only this bridge client's query results are emitted. - Build ids and query ids are
QueryIdtuple structs, so JSON encodes them as one-element arrays like[6]. FindInFiles/SearchFilesexecution is worker-pooled in backend (not main dispatch thread).
- Start studio remote process once.
- Determine the target runnable item name locally from the repo or from the user request.
- Call
ListBuildsand find any existing build for the same runnable item. - Send
ClearBuildfor that oldbuild_id; do not wait for an acknowledgment before the next launch. - Start the new UI app through
RunItem, and wait forBuildStartedandAppStarted. - After any code change that affects runtime/UI behavior, repeat steps 3-5 before doing screenshots, widget dumps, clicks, or visual conclusions.
- For code search, use
FindInFilesfirst, thenReadTextRangeto window exact regions. - Use direct shell cargo commands for non-launch tasks such as
check,build,test, orbench. - Use
WidgetQuery/WidgetTreeDumpto get click targets. - For text input, click field first, then send text, then return.
- Keep control packets compact (
auto_dump:falseon click/type/return for low latency).
RunItemexecutes a Studio-defined runnable item by name.- Use the runnable item name shown in Studio, not a Cargo package name.
RunItemdoes not implicitly replace an older build tab; agents should clear the old build themselves first withClearBuild.
- Send this as one stdin write (multiple JSON lines, no sleeps):
Click(input field center)TypeTextReturn
- Then request
WidgetTreeDumporScreenshotto confirm.
- Use coordinates from dump as-is.
W3dump uses integer pixel coordinates in the same space expected byClick.- Do not apply extra DPI math in the agent loop.
Screenshotcan arrive before visible redraw after rapid input bursts.- If screenshot looks stale, request a follow-up
WidgetTreeDump/Screenshot.
- If screenshot looks stale, request a follow-up
- If input does nothing:
- Verify
build_idwithListBuilds. - Refresh dump and retry click on input before typing.
- Verify
- If request errors with no active websocket:
- app is not connected yet; wait for startup completion and retry.
The following is the current body of CLAUDE.md included verbatim for agent guidance parity.
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.
Use the Studio bridge runnable-item flow instead of launching UI apps directly from the shell:
- Start the Studio remote bridge once.
- Determine the runnable item name locally.
- If an older instance is still running, clear it with
{"ClearBuild":{"build_id":[N]}}and launch the replacement immediately without waiting for an acknowledgment. - Launch it with
{"RunItem":{"mount":"makepad","name":"<runnable-name>"}}. - After editing UI/runtime code, do not inspect the previously running build. Always verify against the newly started build id from step 4.
Do not use ObserveMount from the bridge. That call is for mount ownership/subscription and can steal RunView/framebuffer routing away from Studio desktop.
Use direct shell cargo commands only for non-UI tasks such as library checks, tests, and file/search operations. Do not run shell cargo check, cargo build, or cargo run for UI runnable targets that will be launched via Studio.
When those non-UI tasks are used for runtime behavior or performance measurements, prefer their release variants (cargo run --release, cargo test --release, cargo build --release).
[package]
name = "makepad-example-myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
makepad-widgets = { path = "../../widgets" }The new DSL uses script_mod! macro with runtime script evaluation instead of the old live_design! compile-time macros.
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());
}
}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)
// 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_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
}
}
}| 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, {...}) |
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).
Always use theme. prefix:
color: theme.color_bg_app
padding: theme.space_2
font_size: theme.font_size_p
text_style: theme.font_regularThe +: 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
}
}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: 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}
}
}
}
}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.
// 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)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());
}
}Use := for named widget instances:
// In DSL
my_button := Button{text: "Click"}
// In Rust code
self.ui.button(ids!(my_button)).clicked(actions)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
}
}#[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) {}
}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.
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.
#[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()
}
}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()
}
}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,
}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))
}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.*
}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,
}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 {}
}
}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.
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 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.
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)
}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.