CRITICAL: Use for Makepad 2.0 troubleshooting and common mistakes. Triggers on: makepad error, makepad bug, makepad problem, makepad issue, makepad not working, text invisible, widget not showing, click not working, height zero, makepad pitfall, makepad gotcha, makepad FAQ, makepad help, script_mod error, compile error, widget not found, render not updating, hot reload not working, wasm build error, port conflict, server lock, IME popup, selection handle, popup window crash, canvas splash, POST splash loop, 100% CPU, set_visible not working, on_render empty, event bridge unreliable, float time display, fn tick not called, on_audio not called, button click through, 常见错误, 问题排查, 故障排除, 不显示, 不工作, 看不见, 热重载, 编译错误
This skill covers common mistakes when building with Makepad 2.0 and the Splash scripting language. Each pitfall includes:
Reference documents: AGENTS.md, splash.md
Symptom: Your entire UI or a section of it does not appear. The container renders with zero height, making all children invisible.
Root Cause: All View-based containers (View, SolidView, RoundedView, etc.) default to height: Fill. When a Fill container is placed inside a Fit parent (or any context where the available height is determined by children), the height resolves to 0px due to circular dependency: the parent asks the child how tall it is, the child says "as tall as my parent", and the result is zero.
Fix: Always set height: Fit on containers that should shrink-wrap their content.
// WRONG -- height defaults to Fill, resolves to 0px in a Fit context
View{
flow: Down
Label{text: "Hello"}
}
// CORRECT -- height: Fit makes the container wrap its children
View{
height: Fit
flow: Down
Label{text: "Hello"}
}
Rule of thumb: Write height: Fit immediately after the opening brace of every container unless you have a fixed-height parent or you explicitly want height: Fill inside a known fixed-size ancestor.
Exception: Inside a fixed-height parent, height: Fill is valid:
View{
height: 300
View{
height: Fill
Label{text: "I fill the 300px"}
}
}
Symptom: You add a Label inside a RoundedView or SolidView with a background color, but the text is invisible. The container appears correctly colored but the text cannot be seen, even though draw_text.color is set to a contrasting color.
Root Cause: Makepad batches draw calls by shader type for GPU performance. All Label widgets using the same text shader get batched into one draw call, and all backgrounds into another. Without new_batch: true, the text draw call may execute before the background draw call, placing the text geometrically behind the opaque background.
Fix: Add new_batch: true to any View-based container that has a visible background (show_bg: true or pre-styled views like SolidView, RoundedView) and contains text children.
// WRONG -- text is drawn behind the background due to batching
RoundedView{
height: Fit
draw_bg.color: #333
Label{text: "Can't see me"}
}
// CORRECT -- new_batch forces background to draw before children's text
RoundedView{
height: Fit
new_batch: true
draw_bg.color: #333
Label{text: "Now visible" draw_text.color: #fff}
}
When you MUST use new_batch: true:
show_bg: true (or pre-styled like SolidView, RoundedView) that contains text: instead of :=Symptom: You define a template with let and try to override a child property per-instance, but the override is silently ignored. The default text always shows.
Root Cause: In Splash, : creates a static property, while := creates a named/dynamic child that is addressable and overridable. If you declare label: Label{...} (with :), the child has no addressable name and the override path label.text: cannot find it.
Fix: Use := for any child you want to reference or override later.
// WRONG -- static child, override fails silently
let Card = View{
height: Fit
title: Label{text: "default"}
}
Card{title.text: "new text"} // Fails! title is not addressable
// CORRECT -- named child with :=, override works
let Card = View{
height: Fit
title := Label{text: "default"}
}
Card{title.text: "new text"} // Works! title is a named child
Additional rule: Named children inside anonymous containers are UNREACHABLE. Every container in the path from root to child must also be named:
// WRONG -- label is inside an anonymous View, unreachable
let Item = View{
height: Fit
View{
flow: Down
label := Label{text: "default"}
}
}
Item{label.text: "new"} // Fails! No path to label through anonymous View
// CORRECT -- full named path
let Item = View{
height: Fit
texts := View{
flow: Down
label := Label{text: "default"}
}
}
Item{texts.label.text: "new"} // Works! Full dot-path through named containers
Symptom: A hex color like #2ecc71 causes a cryptic parse error such as expected at least one digit in exponent, or the color renders incorrectly.
Root Cause: The Rust tokenizer inside script_mod!{} interprets a digit followed by e as the start of a scientific notation number (e.g., 2e looks like 2 * 10^...). This breaks parsing of hex colors that contain the letter e adjacent to digits.
Fix: Use the #x prefix for any hex color containing the letter e or E.
// WRONG -- parser reads '2e' as scientific notation exponent
draw_bg.color: #2ecc71
draw_bg.color: #1e1e2e
draw_bg.color: #4466ee
// CORRECT -- #x prefix escapes the hex literal
draw_bg.color: #x2ecc71
draw_bg.color: #x1e1e2e
draw_bg.color: #x4466ee
When is #x NOT needed? Colors without the letter e work fine with plain #:
draw_bg.color: #ff4444 // OK -- no 'e'
draw_bg.color: #44cc44 // OK -- no 'e'
draw_bg.color: #333 // OK -- no 'e'
Symptom: Attempting to set per-corner border radii with Inset causes a parse error or silently breaks the layout. The rounded corners do not appear.
Root Cause: Border radius is a single f32 uniform value applied uniformly to all corners. It is NOT an Inset-like struct with per-corner values. Passing an Inset or object silently breaks the entire layout.
CRITICAL: The property name differs by context:
draw_bg.radius with trailing-dot floatdraw_bg.border_radiusFix: Use a plain float value with the correct property name.
// WRONG -- border_radius is not an Inset
draw_bg.border_radius: Inset{top_left: 10 top_right: 10}
// WRONG -- not an object
draw_bg.border_radius: {top: 10 bottom: 0}
// CORRECT (Canvas Splash context) -- use draw_bg.radius with trailing dot
draw_bg.radius: 10.
// CORRECT (script_mod! context) -- use draw_bg.border_radius
draw_bg.border_radius: 10.0
// For per-corner radii, use RoundedAllView with a vec4
// (top-left, top-right, right-bottom, left-bottom)
RoundedAllView{
height: Fit
draw_bg.border_radius: vec4(10.0 10.0 0.0 0.0)
}
Symptom: At runtime, a widget type is not found or a script error occurs saying a widget is not registered. The app may panic or display nothing.
Root Cause: In Makepad 2.0, widget modules must be registered via script_mod(vm) calls in the correct order. Base widgets must be registered before custom widgets, and custom widgets before the UI that uses them. If the order is wrong, a module tries to use a widget type that has not been registered yet.
Fix: Follow the correct registration order in App::run().
impl App {
fn run(vm: &mut ScriptVm) -> Self {
// 1. Register base widget library (theme + all standard widgets)
crate::makepad_widgets::script_mod(vm);
// 2. Register your custom widget modules (if any)
crate::my_custom_widgets::script_mod(vm);
// 3. Register your app UI module (uses widgets from steps 1 and 2)
crate::app_ui::script_mod(vm);
// 4. Create the app from its own script_mod
App::from_script_mod(vm, self::script_mod)
}
}
Key rule: Widget modules must be registered BEFORE UI modules that use them. Always call lib.rs::script_mod before app_ui::script_mod.
Symptom: Text in a horizontal layout is cut off halfway. The text label appears to have only half the available width.
Root Cause: Filler{} is defined as View{width: Fill height: Fill}. When placed next to a sibling that also has width: Fill, both compete for the remaining horizontal space and split it 50/50. The text label only gets half the width and text is clipped.
Fix: Remove Filler{} when a sibling already uses width: Fill. The Fill sibling naturally takes all remaining space, pushing Fit-sized siblings to the edge.
// WRONG -- Filler splits space with Fill sibling, text is clipped
View{
flow: Right height: Fit
Label{width: Fill text: "Long text that gets clipped"}
Filler{}
Button{text: "OK"}
}
// CORRECT -- width: Fill on label pushes button to the right edge
View{
flow: Right height: Fit
Label{width: Fill text: "Long text now has full space"}
Button{text: "OK"}
}
// CORRECT use of Filler -- between Fit-sized siblings
View{
flow: Right height: Fit
Label{text: "left"}
Filler{}
Label{text: "right"}
}
Symptom: A list item or button has hover effects. When you hover over it, the background color changes but the text vanishes completely. Moving the cursor away brings the text back.
Root Cause: The hover animator changes the View's background from transparent (#0000) to an opaque or semi-opaque color. Without new_batch: true, the background and text are in the same draw batch. When the background becomes opaque, it covers the text that was drawn in the same batch order.
Fix: Add new_batch: true to any View with show_bg: true that has a hover animator and contains text.
// WRONG -- text disappears when hover activates the background
View{
width: Fill height: Fit
show_bg: true
draw_bg +: {
color: uniform(#0000)
color_hover: uniform(#fff2)
hover: instance(0.0)
pixel: fn(){
return Pal.premul(self.color.mix(self.color_hover, self.hover))
}
}
animator: Animator{
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 0.0}}
}
on: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 1.0}}
}
}
}
Label{text: "Vanishes on hover!" draw_text.color: #fff}
}
// CORRECT -- add new_batch: true
View{
width: Fill height: Fit
new_batch: true
show_bg: true
draw_bg +: {
color: uniform(#0000)
color_hover: uniform(#fff2)
hover: instance(0.0)
pixel: fn(){
return Pal.premul(self.color.mix(self.color_hover, self.hover))
}
}
animator: Animator{
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 0.0}}
}
on: AnimatorState{
from: {all: Forward {duration: 0.15}}
apply: {draw_bg: {hover: 1.0}}
}
}
}
Label{text: "Stays visible on hover!" draw_text.color: #fff}
}
Symptom: Confusion about whether commas are allowed between properties in script_mod! blocks.
Root Cause: Splash is primarily whitespace-delimited. However, the Splash tokenizer treats commas as whitespace — they are silently consumed and do not cause parse errors. Many Makepad projects (including Robrix) use commas extensively in script_mod! blocks, inherited from Makepad 1.x live_design! syntax which required commas.
Guidance: Both styles are valid. Match the surrounding code style:
// Style A: with commas (common in codebases migrated from 1.x)
View{
flow: Down,
height: Fit,
spacing: 10,
padding: Inset{top: 5, bottom: 5, left: 10, right: 10}
}
// Style B: without commas (pure Splash style)
View{
flow: Down
height: Fit
spacing: 10
padding: Inset{top: 5 bottom: 5 left: 10 right: 10}
}
Note: Both compile and run identically. The tokenizer discards commas. Do NOT waste time removing commas from existing code — it creates noisy diffs with no functional change.
Symptom: Parse errors or unexpected behavior. The semicolons are treated as part of property values or cause tokenizer failures.
Root Cause: Splash does not use semicolons to terminate statements. This is a common mistake for developers coming from CSS, JavaScript, or Rust backgrounds.
Fix: Remove all semicolons.
// WRONG -- semicolons are not valid
View{
flow: Down;
height: Fit;
Label{text: "Hello";};
}
// CORRECT -- no semicolons needed
View{
flow: Down
height: Fit
Label{text: "Hello"}
}
Symptom: The UI appears as a narrow sliver on one side of the window, or the layout is entirely broken. Content does not fill the available window width.
Root Cause: Using a fixed pixel width (e.g., width: 400) on the outermost container means it does not adapt to the available window space. If the window is wider, the content is a small strip. If narrower, content is clipped.
Fix: Always use width: Fill on the root container. Fixed pixel widths are fine for inner elements.
// WRONG -- fixed width on root, does not adapt to window
RoundedView{
width: 400
height: Fit
flow: Down
Label{text: "Narrow!"}
}
// CORRECT -- root fills available width
RoundedView{
width: Fill
height: Fit
flow: Down
Label{text: "Full width!"}
}
Symptom: You add animator: Animator{...} and cursor: MouseCursor.Hand to a Label, but nothing happens on hover. No error, no effect.
Root Cause: Label (and H1-H4, P, TextBox, Image, Icon, Markdown, Html, Slider, DropDown, Splitter, Hr, Filler) do NOT have an animator field. Adding one is silently ignored.
Fix: Wrap the Label in a View that does support animator.
// WRONG -- animator on Label is silently ignored
Label{
animator: Animator{
hover: {
default: @off
off: AnimatorState{ from: {all: Forward{duration: 0.15}} apply: {draw_text: {hover: 0.0}} }
on: AnimatorState{ from: {all: Forward{duration: 0.15}} apply: {draw_text: {hover: 1.0}} }
}
}
text: "Hover me"
}
// CORRECT -- animate the wrapping View
View{
width: Fill height: Fit
new_batch: true
cursor: MouseCursor.Hand
show_bg: true
draw_bg +: {
color: uniform(#0000)
color_hover: uniform(#fff2)
hover: instance(0.0)
pixel: fn(){ return Pal.premul(self.color.mix(self.color_hover, self.hover)) }
}
animator: Animator{
hover: {
default: @off
off: AnimatorState{ from: {all: Forward{duration: 0.15}} apply: {draw_bg: {hover: 0.0}} }
on: AnimatorState{ from: {all: Forward{duration: 0.15}} apply: {draw_bg: {hover: 1.0}} }
}
}
Label{text: "Hover me" draw_text.color: #fff}
}
Widgets that SUPPORT animator: View, SolidView, RoundedView, ScrollXView, ScrollYView, ScrollXYView, Button, ButtonFlat, ButtonFlatter, CheckBox, Toggle, RadioButton, LinkLabel, TextInput
Symptom: Text is present in the widget tree but invisible. On white or light-colored backgrounds, the text simply cannot be seen. It may appear if you select the text or change the background to a dark color.
Root Cause: All text widgets (Label, H1-H4, P, Button text, etc.) default to white (#fff) text color. On a light background, white text is invisible.
Fix: Explicitly set draw_text.color to a dark color for every text element on light backgrounds.
// WRONG -- white text on white background
View{
height: Fit
show_bg: true
draw_bg.color: #fff
Label{text: "Can't see this"}
}
// CORRECT -- explicit dark text color
View{
height: Fit
new_batch: true
show_bg: true
draw_bg.color: #fff
Label{text: "Visible!" draw_text.color: #333}
}
Symptom: Semi-transparent colors render as bright, fully opaque, or washed out. A subtle tint like #ffffff08 appears as bright white instead of nearly transparent.
Root Cause: Makepad uses premultiplied alpha for GPU rendering. When you return a color from a pixel: fn() without premultiplying the alpha, the alpha blending produces incorrect results.
Fix: Always wrap your final color return in Pal.premul() -- unless returning sdf.result, which is already premultiplied by sdf.fill() / sdf.stroke().
// WRONG -- alpha not premultiplied, renders incorrectly
draw_bg +: {
pixel: fn() {
return vec4(1.0 0.0 0.0 0.5)
}
}
// CORRECT -- premultiply alpha
draw_bg +: {
pixel: fn() {
return Pal.premul(vec4(1.0 0.0 0.0 0.5))
}
}
// ALSO CORRECT -- sdf.result is already premultiplied
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(#f00)
return sdf.result
}
}
// CORRECT -- color mixing with premultiply
draw_bg +: {
pixel: fn() {
return Pal.premul(self.color.mix(self.color_hover, self.hover))
}
}
Symptom: Properties are not recognized, causing parse errors or the property being silently ignored.
Root Cause: Splash is not CSS. Property names use Makepad's own naming convention with dot-path access to nested struct fields.
Fix: Use Makepad property names.
| CSS Name | Splash Name |
|---|---|
background-color | draw_bg.color |
color | draw_text.color |
font-size | draw_text.text_style.font_size |
border-radius | draw_bg.border_radius |
border-color | draw_bg.border_color |
border-width | draw_bg.border_size |
padding | padding (same) |
margin | margin (same) |
gap | spacing |
display: flex | flow: Right or flow: Down |
flex-direction: column | flow: Down |
flex-direction: row | flow: Right |
justify-content: center | align: Center |
width: 100% | width: Fill |
width: auto | width: Fit |
// WRONG -- CSS property names
View{
background-color: #333
font-size: 14
border-radius: 8px
gap: 10
}
// CORRECT -- Splash property names
RoundedView{
height: Fit
draw_bg.color: #333
draw_bg.border_radius: 8.0
spacing: 10
Label{text: "Hello" draw_text.text_style.font_size: 14}
}
Symptom: A property you wrote has no effect. No error is raised, but the widget does not behave as expected.
Root Cause: Splash silently ignores unknown properties. If you guess at a property name (e.g., background:, font_size:, text_color:, border:, opacity:), it is simply discarded.
Fix: Only use documented properties. If unsure, check splash.md or grep for usage in widgets/src/.
Common invented properties and their correct equivalents:
| Invented (WRONG) | Correct |
|---|---|
background: | draw_bg.color: |
font_size: | draw_text.text_style.font_size: |
text_color: | draw_text.color: |
border: | draw_bg.border_size: + draw_bg.border_color: |
opacity: | Use alpha in color: #fff8 |
on_click: | Handle in Rust handle_actions() |
class: | Use let bindings for reusable styles |
id: | Use := for named instances |
Symptom: When generating UI for streaming/splash context, the output does not display or causes errors. Duplicate Window wrappers may cause unexpected behavior.
Root Cause: Root{} and Window{} are host-level wrappers that are defined in the app's script_mod! block. Script content that renders inside a body +: {} block should NOT include these wrappers -- the content goes inside the body.
Fix: Your script output should start directly with the UI content, without Root{} or Window{} wrappers.
// WRONG -- do not add host-level wrappers in script output
Root{
Window{
body +: {
View{ height: Fit Label{text: "Hello"} }
}
}
}
// CORRECT -- just the content
View{
height: Fit
Label{text: "Hello"}
}
In app code (where Root and Window ARE correct):
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 goes HERE
View{ height: Fit Label{text: "Hello"} }
}
}
}
}
}
Symptom: You implement on_render on a widget but it is never called. The widget's render logic does not execute.
Root Cause: Makepad does not automatically call on_render in all situations. You must explicitly trigger rendering by calling ui.widget.render() or ensuring the widget is part of the active draw tree.
Fix: Ensure render() is called on the widget, typically from the parent's draw pass. Check that the widget is properly included in the widget hierarchy and that handle_event and draw_walk are correctly implemented.
// Make sure your widget is in the draw tree and draw_walk is implemented
impl Widget for MyWidget {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
cx.begin_turtle(walk, self.layout);
// ... drawing code ...
cx.end_turtle_with_area(&mut self.area);
DrawStep::done()
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
// ... event handling ...
}
}
Symptom: A MapView renders with zero height or behaves erratically. The map area is invisible.
Root Cause: MapView has no intrinsic content height. Using height: Fit gives 0px (no content to fit). Using height: Fill may also resolve to 0px in certain contexts (same circular dependency as Pitfall #1).
Fix: Always give MapView a fixed pixel height.
// WRONG -- no intrinsic height, resolves to 0px
MapView{
width: Fill
height: Fit
}
// WRONG -- may resolve to 0px in a Fit parent
MapView{
width: Fill
height: Fill
}
// CORRECT -- fixed pixel height
MapView{
width: Fill
height: 500
}
Symptom: A Modal is defined in the script but never appears. There is no visible dialog or overlay.
Root Cause: Modals are hidden by default. They must be opened programmatically from Rust code by calling .open(cx) on the modal widget reference.
Fix: Call .open(cx) from your Rust event handler when you want to show the modal.
// In script_mod! -- define the modal
my_modal := Modal{
content +: {
width: 300 height: Fit
RoundedView{
height: Fit
new_batch: true
padding: 20 flow: Down spacing: 10
draw_bg.color: #333
draw_bg.border_radius: 8.0
Label{text: "Are you sure?" draw_text.color: #fff}
confirm_btn := Button{text: "Confirm"}
cancel_btn := ButtonFlat{text: "Cancel"}
}
}
}
// In Rust -- open the modal when a button is clicked
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
if self.ui.button(ids!(open_modal_btn)).clicked(actions) {
self.ui.modal(ids!(my_modal)).open(cx);
}
if self.ui.button(ids!(cancel_btn)).clicked(actions) {
self.ui.modal(ids!(my_modal)).close(cx);
}
}
}
Symptom: apply_over(cx, live!{...}) does not compile or does not work. The old 1.x runtime property update mechanism is gone.
Root Cause: Makepad 2.0 replaced the live! macro and apply_over with the script_apply_eval! macro. The old macros from the live_design! system no longer exist.
Fix: Use script_apply_eval! for runtime property updates. Use #(expr) for Rust expression interpolation inside the macro.
// OLD 1.x way (WRONG in 2.0)
item.apply_over(cx, live!{
height: (height)
draw_bg: {is_even: (if is_even {1.0} else {0.0})}
});
// NEW 2.0 way (CORRECT)
script_apply_eval!(cx, item, {
height: #(height)
draw_bg: {is_even: #(if is_even {1.0} else {0.0})}
});
// For colors
let color = self.color_focus;
script_apply_eval!(cx, item, {
draw_bg: {
color: #(color)
}
});
Symptom: Compilation errors about unknown derive macros Live, LiveHook, or unresolved traits.
Root Cause: Makepad 2.0 renamed the derive macros: Live became Script, LiveHook became ScriptHook. The old names no longer exist.
Fix: Update all derive macros to the new names.
// OLD 1.x (WRONG in 2.0)
#[derive(Live, LiveHook)]
pub struct App {
#[live] ui: WidgetRef,
}
// NEW 2.0 (CORRECT)
#[derive(Script, ScriptHook)]
pub struct App {
#[live] ui: WidgetRef,
}
// For widgets:
// OLD
#[derive(Live, LiveHook, Widget)]
// NEW
#[derive(Script, ScriptHook, Widget)]
// For animated widgets:
// OLD
#[derive(Live, LiveHook, Widget, Animator)]
// NEW
#[derive(Script, ScriptHook, Widget, Animator)]
Also renamed: DefaultNone derive is gone -- use standard #[derive(Default)] with #[default] attribute on the None variant:
// OLD (WRONG)
#[derive(DefaultNone)]
pub enum MyAction {
Clicked,
None,
}
// NEW (CORRECT)
#[derive(Clone, Default)]
pub enum MyAction {
Clicked,
#[default]
None,
}
Symptom: (THEME_COLOR_X) causes a parse error or is not recognized. Theme values are not applied.
Root Cause: Makepad 2.0 replaced the parenthesized constant syntax (THEME_COLOR_X) with dot-path access via theme.color_x. Similarly, <THEME_FONT> became theme.font_regular, and all theme access now uses the theme. prefix.
Fix: Replace all old theme references with theme. prefix syntax.
// OLD 1.x (WRONG in 2.0)
draw_bg.color: (THEME_COLOR_BG_APP)
draw_text.text_style: <THEME_FONT_REGULAR>