From makepad-skills
References Makepad 2.0 DSL syntax for script_mod!: colon properties, := named instances, +: merge operator, dot-path overrides, let bindings, widget registration.
npx claudepluginhub zhanghandong/makepad-skills --plugin makepad-skillsThis skill uses the workspace's default tool permissions.
Makepad 2.0 replaced the compile-time `live_design!` macro with the runtime `script_mod!` macro, powered by the Splash scripting language. This skill covers the complete DSL syntax, property system, registration patterns, and common pitfalls.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Makepad 2.0 replaced the compile-time live_design! macro with the runtime script_mod! macro, powered by the Splash scripting language. This skill covers the complete DSL syntax, property system, registration patterns, and common pitfalls.
key: value // CORRECT - colon syntax
key = value // WRONG - old 1.x syntax, no longer works
Properties are whitespace/newline separated. No commas between siblings.
View{
width: Fill
height: Fit
flow: Down
spacing: 10
padding: 15
}
:= OperatorUse := to create addressable, named widget instances:
my_button := Button{ text: "Click me" }
title := Label{ text: "Hello" }
Named instances are:
id!(my_button) or ids!(my_button)MyTemplate{ title.text: "New text" }vec (not map)Regular properties use : and go into map:
width: Fill // regular property -> map
label := Label{} // named child -> vec
+:The +: operator extends/merges with the parent instead of replacing:
draw_bg +: {
color: #f00 // Only overrides color, keeps all other draw_bg properties
}
Without +:, you REPLACE the entire property:
draw_bg: { color: #f00 } // REPLACES all of draw_bg - loses hover, border, etc.
draw_bg +: { color: #f00 } // MERGES - only changes color, keeps everything else
Dot-path is syntactic sugar for merge:
draw_bg.color: #f00
// is equivalent to:
draw_bg +: { color: #f00 }
draw_text.text_style.font_size: 14
// is equivalent to:
draw_text +: { text_style +: { font_size: 14 } }
let creates local, reusable templates within a script_mod! block:
let MyCard = RoundedView{
width: Fill height: Fit
padding: 16 flow: Down spacing: 8
draw_bg.color: #2a2a3d
draw_bg.border_radius: 8.0
title := Label{ text: "Default Title" draw_text.color: #fff }
body := Label{ text: "" draw_text.color: #aaa }
}
// Instantiate and override:
MyCard{ title.text: "Card 1" body.text: "Content here" }
MyCard{ title.text: "Card 2" body.text: "More content" }
IMPORTANT: let bindings are LOCAL to the script_mod! block. They cannot be accessed from other script_mod! blocks. To share across modules, store in mod.widgets.*.
..Inherit all properties from another definition:
set_type_default() do #(DrawMyShader::script_shader(vm)){
..mod.draw.DrawQuad // Inherit from DrawQuad
}
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
my_button := Button{ text: "Click" }
}
}
}
}
}
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm); // 1. Register base widgets
App::from_script_mod(vm, self::script_mod)
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[source] source: ScriptObjectRef, // REQUIRED for Script-derived structs
#[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());
}
}
script_mod!{
use mod.prelude.widgets_internal.* // For widget library internals
use mod.widgets.* // Access other registered widgets
// Step 1: Register the Rust struct as a widget base
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
// Step 2: Create a styled variant with default properties
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
}
}
}
For structs that implement the Widget trait:
mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm))
Rust side:
#[derive(Script, ScriptHook, Widget)]
pub struct MyWidget {
#[source] source: ScriptObjectRef, // REQUIRED
#[walk] walk: Walk,
#[layout] layout: Layout,
#[redraw] #[live] draw_bg: DrawQuad,
#[live] draw_text: DrawText,
#[rust] my_state: i32, // Runtime-only, not exposed to script
}
For non-widget structs that need script integration:
mod.widgets.MyComponentBase = #(MyComponent::script_component(vm))
For custom draw types with shader fields:
set_type_default() do #(DrawMyShader::script_shader(vm)){
..mod.draw.DrawQuad // Inherit from DrawQuad
}
Rust side:
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct DrawMyShader {
#[deref] draw_super: DrawQuad,
#[live] my_param: f32,
}
mod.widgets.MyWidget = set_type_default() do mod.widgets.MyWidgetBase{
width: Fill height: Fit
draw_bg +: { color: theme.color_bg_app }
}
Widget modules MUST be registered BEFORE UI modules that use them:
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm); // 1. Base widgets FIRST
crate::my_widgets::script_mod(vm); // 2. Custom widgets SECOND
crate::app_ui::script_mod(vm); // 3. UI using widgets THIRD
App::from_script_mod(vm, self::script_mod) // 4. App component LAST
}
}
pub fn script_mod(vm: &mut ScriptVm) {
crate::module_a::script_mod(vm);
crate::module_b::script_mod(vm);
// ... all widget modules
}
| Prelude | Use Case |
|---|---|
mod.prelude.widgets.* | App development - includes all standard widgets |
mod.prelude.widgets_internal.* | Widget library internal development |
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 (mod.theme, without theme:), the module is included but has no accessible name.
mod Object is the ONLY Way to Share// In widget_module.rs - export to mod.widgets namespace
script_mod!{
use mod.prelude.widgets_internal.*
mod.widgets.MyWidget = set_type_default() do mod.widgets.MyWidgetBase{ ... }
}
// In app_ui.rs - import via mod.widgets
script_mod!{
use mod.prelude.widgets.*
use mod.widgets.* // Now MyWidget is in scope
// ...
MyWidget{}
}
use crate.module.* does NOT work - the crate. prefix is not available in script_mod.
Use script_apply_eval! instead of the old apply_over + live!:
// Old system
item.apply_over(cx, live!{ height: (height) });
// New system - use #(expr) for Rust expression interpolation
script_apply_eval!(cx, item, {
height: #(height)
draw_bg: { is_even: #(if is_even { 1.0 } else { 0.0 }) }
});
Use ~expression to log values 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
}
#[source] source: ScriptObjectRefAll Script-derived structs MUST have this field:
#[derive(Script, ScriptHook)]
pub struct MyStruct {
#[source] source: ScriptObjectRef, // REQUIRED - will fail without it
// ...
}
height: Fit on ContainersDefault height is Fill. In a Fit parent, Fill creates a circular dependency = 0 height = invisible:
// WRONG - invisible!
View{ flow: Down
Label{ text: "You can't see me" }
}
// CORRECT
View{ height: Fit flow: Down
Label{ text: "Visible!" }
}
: vs :=key: value -- sets a property (stored in map)name := Widget{} -- creates a named, addressable child (stored in vec)label := Label{ text: "x" } -- named, overridable via Template{ label.text: "y" }label: Label{ text: "x" } -- anonymous, NOT addressable, overrides fail silently+: Merge Operator// WRONG - replaces ALL of draw_bg (loses hover, border, animations)
draw_bg: { color: #f00 }
// CORRECT - merges, only changes color
draw_bg +: { color: #f00 }
// WRONG
color: THEME_COLOR_BG // old 1.x constant syntax
color: (THEME_COLOR_BG) // old 1.x parenthesized reference
// CORRECT
color: theme.color_bg_app
padding: theme.space_2
font_size: theme.font_size_p
#x PrefixThe Rust tokenizer interprets e/E in hex literals as scientific notation exponent:
// WRONG - Rust parse error: "expected at least one digit in exponent"
color: #2ecc71
color: #1e1e2e
// CORRECT - use #x prefix
color: #x2ecc71
color: #x1e1e2e
// Colors without 'e' work fine with plain #
color: #ff4444 // OK
color: #44cc44 // OK
pub Keyword Invalid in script_mod// WRONG
pub mod.widgets.MyWidget = ...
// CORRECT - visibility is controlled by Rust module system
mod.widgets.MyWidget = ...
Inset{...} Constructor Syntax for Margins/Padding// WRONG
margin: { left: 10 }
align: { x: 0.5 y: 0.5 }
// CORRECT - use constructor syntax
margin: Inset{ left: 10 }
align: Align{ x: 0.5 y: 0.5 }
padding: Inset{ top: 5 bottom: 5 left: 10 right: 10 }
// Bare number for uniform values is OK
padding: 15
margin: 0.
#[repr(C)]Non-instance data (#[rust], non-instance #[live] fields) MUST go BEFORE #[deref]. Only instance fields (shader inputs) go AFTER:
// CORRECT
#[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: DrawQuad, // contains DrawVars + base instances
#[live] pub tint: Vec4f, // instance field, AFTER deref - OK
}
// WRONG - #[rust] after instance fields corrupts GPU buffer
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct MyDrawShader {
#[deref] pub draw_super: DrawQuad,
#[live] pub tint: Vec4f,
#[rust] my_state: bool, // BAD: between instance fields
}
Rust proc macro token stream strips comments, which shifts error positions:
// WRONG
script_mod!{
// This comment shifts error line info
use mod.prelude.widgets.*
}
// CORRECT - start with real code immediately
script_mod!{
use mod.prelude.widgets.*
// Comments after first code are fine
}
cursor: MouseCursor.Hand not cursor: Hand or cursor: @Handcrate_resource("self://path") not dep("crate://self/path")tex: texture_2d(float) not tex: texture2dmod vs modf: Use modf(a, b) for float modulo, NOT mod(a, b)default: @off with @ prefix for enum default valuesDefaultNone derive; use #[derive(Default)] with #[default] attribute.method() not ::method() (e.g., Sdf2d.viewport(...))color1.mix(color2, hover) chaining over nested mix() callscrate::makepad_widgets::script_mod(vm) in App::run() BEFORE your own modules| Old (live_design!) | New (script_mod!) |
|---|---|
<BaseWidget> | mod.widgets.BaseWidget{} or BaseWidget{} (if imported) |
{{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, {...}) |