makepad/docs/agents/splash.md
vjroger 2da550ca3d docs: the agent guides cover the designer's routes, the tween inspector, hidden windows on Windows and the surface material
tweaker.md gains "Structural edits: the designer" (the /design/* routes
and how a session's patch is persisted) and "Motion: the tween inspector"
(the Motion tab and /tweak/op?op=motion, with loop and yoyo), and its
"Persist the result" starts from a design session when one is open.
app-remote.md points at /design/* and says MAKEPAD_HIDE_WINDOWS works on
Windows as well as macOS, as the D3D11 and window code implement it.
splash.md gains a Materials note on mod.sdf.Material and the packing every
material widget shares. The code they describe is upstream already.
2026-10-02 08:52:56 +02:00

11 KiB

Splash and widget reference

Use this for current script_mod! work. Repository policy is in AGENTS.md. Check the implementation closest to your task before copying a signature; the archived live_design! tree is not the reference for current APIs.

Working sources

Task Reference
Small app and startup Counter example
Broader widget examples Splash example
Widget registration and exports widgets/core/src/lib.rs, families in widgets/src/lib.rs
Styling and animation Button
Widget lookup and updates Widget API
Virtualized list templates PortalList
Tree drawing and open state FileTree
Generated widget APIs Widget derives
Shader instance memory DrawVars
Script/compiler regression cases Script test crate

Use the widget exports and source for availability and feature gates instead of maintaining a separate widget catalogue here.

App structure

A compact pattern using the current counter example's startup and lookup APIs follows. Keep the registration style of the app you are editing.

pub use makepad_widgets;
use makepad_widgets::*;

app_main!(App);

script_mod! {
    use mod.prelude.widgets.*

    startup() do #(App::script_component(vm)){
        ui: Root{
            main_window := Window{
                window.inner_size: vec2(420, 220)
                body +: {
                    flow: Down
                    status := Label{text: "Ready"}
                    press := Button{text: "Press"}
                }
            }
        }
    }
}

#[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(cx, ids!(press)).clicked(actions) {
            self.ui.label(cx, ids!(status)).set_text(cx, "Pressed");
        }
    }
}

impl AppMain for App {
    fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
        crate::makepad_widgets::script_mod(vm);
        self::script_mod(vm)
    }

    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.match_event(cx, event);
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

For a new example crate, use an adjacent crate's Cargo configuration and update the workspace and makepad.splash. Startup helpers and app traits can change; the source links above are the reference.

Object syntax, scope, and registration

  • Properties use name: value; named child/template instances use name := Type{...}.
  • Assigning a module export uses mod.widgets.Name = .... This is an assignment, not the old property syntax.
  • App scripts import mod.prelude.widgets.*. Widget implementations use mod.prelude.widgets_internal.* and other required modules.
  • let bindings are local to the script module. Export shared definitions through mod.*, then import that module. Rust's crate.* is not a Splash module path, and Rust pub is not used on Splash assignments.
  • Register widgets with #(Type::register_widget(vm)), other components with #(Type::script_component(vm)), and draw shaders with #(Type::script_shader(vm)).
  • Register dependencies before modules whose UI instantiates them. Preserve the host's existing app/module registration sequence.
  • Derive fields and attributes depend on the type. Copy the nearest current implementation; #[source] is not a universal requirement for every Script-derived struct. Inspect the derive parser if a field type fails.

A widget's styled default commonly has this shape:

script_mod! {
    use mod.prelude.widgets_internal.*

    mod.widgets.MyButtonBase = #(MyButton::register_widget(vm))
    mod.widgets.MyButton = set_type_default() do mod.widgets.MyButtonBase{
        width: Fill
        height: Fit
        draw_bg +: {color: theme.color_bg_app}
    }
}

This assumes a matching Rust widget and draw fields are registered; it is a pattern, not a standalone widget implementation.

Values and property merging

Use +: for a partial override of an inherited typed object:

draw_bg +: {color: #f00}
padding: Inset{left: 10 top: 4}
align: Align{x: 0.5 y: 0.5}

Replacing a typed object can discard its other fields or methods. Walk and layout values should use the constructor expected by the property.

Runtime property updates interpolate Rust values with #(expr):

script_apply_eval!(cx, item, {
    height: #(height)
    draw_bg +: {color: #(color)}
});

Use theme.* for theme values. Resource references use crate_resource; copy a nearby current resource path rather than translating an old dep("crate://...") example mechanically. Cursor values use the exposed enum, such as MouseCursor.Hand. If an enum is not exposed, inspect its registration rather than silently discarding a required property.

Rust tokenizes code inside script_mod! before Splash sees it. Use escaped colors such as #x2ecc71 and #x1e1e2e where a digit followed by e or E would otherwise be parsed as an exponent.

For Rust action enums, use #[derive(Default)] and #[default] on the default variant, not the old DefaultNone derive.

In script functions, a.b ?? d reads an optional field: a missing field (or a field of nil) is d, where a.b |? d only replaces a nil value. mod(a, b) is the floored modulo of shading languages (the result takes b's sign); mod alone is still the module root, and modf(a, b) is the truncating remainder. int(x), i32, u32 and f32 values index arrays and do arithmetic as the number they hold. log(x) from mod.math is the natural log, and ln(x) is the same function (script, shaders, kernels); mod.std.log(v) prints.

Templates and lists

Script objects distinguish ordinary key: value properties in their map from named name := Type{...} items in their vector. List widgets collect the latter as templates during application.

list := PortalList{
    width: Fill
    height: Fill
    Item := View{
        height: 40
        title := Label{text: "Default"}
    }
}

The current PortalList keeps templates as rooted ScriptObjectRef values, collects them outside eval-only application, and updates instantiated items on reload. Preserve those lifetime rules in custom list widgets.

During list drawing, use the current set_item_range, next_visible_item, and item APIs. Widget lookup includes the context:

let item = list.item(cx, item_id, id!(Item));
item.label(cx, ids!(title)).set_text(cx, &format!("Item {item_id}"));
item.draw_all(cx, &mut Scope::empty());

For Dock-local templates, use a script-level let or module export when a component must be reused outside that Dock. For custom widgets, turtle drawing, FileTree traversal, and full animation state definitions, follow the linked implementations instead of maintaining duplicate tutorials.

scan: on widgets that create children from data

A web build ships only what a collect run saw the app use (see web-pack.md). A widget that creates children from data or strings (PortalList, FlatList, page and tab switchers, anything that picks a template at run time) gets a scan: list of every template it can create, written together with the templates, by default:

list := PortalList{
    scan: [@Item, @Header]
    Item := View{ title := Label{text: "Default"} }
    Header := View{ caption := Label{text: "Section"} }
}

Entries name the widget's own templates (@Item) or are template objects themselves (RowView{}). Without a scan: list the collect run spawns every template the widget holds. When a web build misses something, its log names the widget path and file:line: add the missing template to that widget's scan: list.

Shaders and animation

  • Per-draw shader values use instance(value); shared values use uniform(value). A varying hover color must not accidentally be uniform.
  • Texture declarations use texture_2d(float).
  • Shader functions use pixel: fn() {...} and method calls use ., not Rust ::. Chained color.mix(other, amount) is a useful styling idiom.
  • Use modf(a, b) for float modulo and atan2(y, x) for the two-argument arctangent. atan(x) and fract(x) are also available.
  • Animator states use id values such as default: @off. Use the current Button implementation for state definitions and transition constructors.
  • Prefer enum match with a catch-all. A failure in supported shader enum matching needs a compiler regression case and fix, not an integer-branch workaround in the widget.

Custom draw shaders need #[repr(C)]. Keep non-instance fields before the draw base and shader instance fields after it:

#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct MyDrawShader {
    #[rust] cached_state: bool,
    #[deref] draw_super: DrawQuad,
    #[live] tint: Vec4f,
}

DrawVars::as_slice() reads across the base into trailing instance fields. Putting non-instance state in that region corrupts the GPU instance buffer, including when another draw shader extends yours. Follow the base shader's current layout and registration.

Materials

mod.sdf.Material (draw/src/shader/surface.rs) lights a shape from the signed distance a pixel function already holds: face(...) shades a face from its distance, outward gradient, elevation and the packed light, relief, finish and tune vec4s; cast(...) returns what a raised face throws on its ground, to lay under it with sdf.clear; box_cov is the blurred box coverage an inner shadow is cut from. Its helpers are fn declarations in the object's own scope, so a member calls another member by its bare m_* name, never through Material..

The base shaders of RoundedView, PanelView, Button, CheckBox, Toggle, Slider and RotaryKnob read it behind a material uniform defaulting to theme.material_level, packed exactly as ReliefView packs its own (material_light, material_relief, material_finish, material_tune, material_shadow, material_inner, the three inks). Keep that packing when a widget joins: a stylesheet's tokens must mean the same thing on every surface. Gate the whole branch on the uniform, so a stock theme pays one compare per draw call and draws exactly what it always did, and put the room a shadow needs inside the control's own rect (material_margin), since makepad clips by default.

Per-frame script cost

  • Script runs interpreted, about 1-2 µs a function call. Per-frame code (a property fn(t), a Draw2D draw) should call native helpers on whole arrays, such as c.segs(flat_array, {m alpha}) and c.curve(...), not a script loop per element.
  • Work over roughly 1k elements a frame belongs in a Kernel (native code, every core; active_count: fn(t) runs only the live elements).
  • In Motion documents, memo(f, t) runs a shared per-frame helper (a camera, a beat) once per frame however many properties read it. Wrap a vector result in an array: [v].
  • Measure a Motion document with makepad-motion bench doc --realtime. The Stage guide stage-motion/performance.md has the rules and budgets.