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.
RUST_BACKTRACE=1 cargo run -p makepad-example-splash --release & PID=$!; sleep 15; kill $PID 2>/dev/null; echo "Process $PID killed"[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.