mcpField
M
makepad-2.0-troubleshooting

makepad-2.0-troubleshooting

github.com/zhanghandong开发75254 次安装29 周前更新
代码仓库

关于 makepad-2.0-troubleshooting

面向使用 Makepad 2.0 和 Splash 的开发者,按症状、原因和修复方式整理常见问题,帮助排查布局、绘制、解析及控件注册错误。

主要功能

  1. 01按症状分析常见故障
  2. 02解释问题根因
  3. 03提供修正代码示例
  4. 04涵盖布局与绘制问题
  5. 05整理 Splash 解析与注册问题

适用场景

  1. 01排查容器高度导致的界面不可见
  2. 02修复文本被背景遮挡或裁切
  3. 03诊断 Splash 属性覆盖和颜色解析错误
  4. 04检查控件注册顺序

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zhanghandong/makepad-skills --skill makepad-2.0-troubleshooting

安装后在 Claude Code 中输入/makepad-2.0-troubleshooting确认已加载。

附带文件references/error-solutions.md

详细介绍

Makepad 2.0 常见问题与故障排查指南

本指南介绍使用 Makepad 2.0 和 Splash 脚本语言构建应用时的常见错误。每个问题都包含:

  • 用户看到的现象
  • 发生原因(根本原因)
  • 修复方法(正确代码)

参考文档:AGENTS.md、splash.md


问题 #1:容器高度为 0px——界面不可见

现象: 整个界面或其中某个部分没有显示。容器渲染高度为 0,导致所有子元素不可见。

根本原因: 所有基于 View 的容器(View、SolidView、RoundedView 等)默认都设置为 height: Fill。当 Fill 容器放在 Fit 父容器中(或任何可用高度由子元素决定的上下文中)时,由于循环依赖,高度会解析为 0px:父容器询问子元素有多高,子元素回答“和我的父容器一样高”,最终结果就是 0。

修复方法: 对应当紧贴内容收缩的容器,始终设置 height: Fit。

// 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"}
}

经验法则: 除非容器位于固定高度的父容器中,或者你明确希望在已知的固定尺寸祖先容器中使用 height: Fill,否则应在每个容器的左花括号后立即写上 height: Fit。

例外: 在固定高度的父容器中,可以使用 height: Fill:

View{
    height: 300
    View{
        height: Fill
        Label{text: "I fill the 300px"}
    }
}

问题 #2:有色背景上的文本不可见——缺少 new_batch

现象: 你在带有背景色的 RoundedView 或 SolidView 中添加 Label,但文本不可见。容器背景颜色显示正常,但看不到文本,即使 draw_text.color 设置为对比色也是如此。

根本原因: Makepad 会按着色器类型对绘制调用进行批处理,以提升 GPU 性能。所有使用相同文本着色器的 Label 控件会合并为一次绘制调用,所有背景也会合并为另一次绘制调用。如果没有 new_batch: true,文本绘制调用可能会在背景绘制调用之前执行,使文本在几何位置上被不透明背景遮挡。

修复方法: 对于具有可见背景(show_bg: true 或 SolidView、RoundedView 等预设样式)且包含文本子元素的任何基于 View 的容器,都添加 new_batch: true。

// 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}
}

必须使用 new_batch: true 的情况:

  • 包含文本的任何 show_bg: true 容器(或 SolidView、RoundedView 等预设样式容器)
  • 带背景动画器的可悬停项目——不添加此项,悬停时文本会消失
  • 重复项目各自带有背景时,这些项目的父容器

问题 #3:命名子元素覆盖无效——用了 : 而不是 :=

现象: 你使用 let 定义模板,并尝试按实例覆盖子元素属性,但覆盖操作被静默忽略。始终显示默认文本。

根本原因: 在 Splash 中,: 创建静态属性,而 := 创建可寻址且可覆盖的命名/动态子元素。如果你声明 label: Label{...}(使用 :),该子元素就没有可寻址名称,因此覆盖路径 label.text: 无法找到它。

修复方法: 对于之后要引用或覆盖的子元素,请使用 :=。

// 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

补充规则: 匿名容器中的命名子元素无法访问。从根容器到子元素路径上的每个容器也都必须命名:

// 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

问题 #4:带字母“e”的十六进制颜色渲染错误或导致解析错误

现象: 像 #2ecc71 这样的十六进制颜色会导致难以理解的解析错误,例如 expected at least one digit in exponent,或者颜色渲染不正确。

根本原因: script_mod!{} 中的 Rust 词法分析器会将数字后面的 e 解读为科学计数法数字的开头(例如,2e 看起来像 2 * 10^...)。这会导致包含字母 e 且它与数字相邻的十六进制颜色无法解析。

修复方法: 对于包含字母 e 或 E 的十六进制颜色,请使用 #x 前缀。

// 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

什么时候不需要 #x? 不包含字母 e 的颜色使用普通的 # 即可:

draw_bg.color: #ff4444    // OK -- no 'e'
draw_bg.color: #44cc44    // OK -- no 'e'
draw_bg.color: #333       // OK -- no 'e'

易踩坑 #5:border_radius 类型错误——必须是 float,而不是 Inset

症状: 尝试使用 Inset 设置各个角的边框圆角时,会导致解析错误或悄悄破坏布局。圆角不会显示。

根本原因: 边框圆角是一个应用于所有角的统一 f32 值。它不是一个包含各角数值、类似 Inset 的结构体。传入 Inset 或对象会悄悄破坏整个布局。

重要: 属性名称因上下文而异:

  • 在 Canvas Splash(POST /splash)中: 使用 draw_bg.radius,并在末尾加点号表示浮点数
  • 在 script_mod! 宏中: 使用 draw_bg.border_radius

修复方法: 使用普通浮点数值,并采用正确的属性名称。

// 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)
}

易踩坑 #6:从 Rust 中找不到 Widget——注册顺序错误

症状: 运行时找不到某种 Widget 类型,或出现提示 Widget 未注册的脚本错误。应用可能会崩溃或什么也不显示。

根本原因: 在 Makepad 2.0 中,必须按正确顺序通过 script_mod(vm) 调用注册 Widget 模块。基础 Widget 必须先于自定义 Widget 注册,自定义 Widget 又必须先于使用它们的 UI 注册。如果顺序错误,模块就会尝试使用尚未注册的 Widget 类型。

修复方法: 在 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)
    }
}

关键规则: Widget 模块必须先于使用它们的 UI 模块注册。始终先调用 lib.rs::script_mod,再调用 app_ui::script_mod。


易踩坑 #7:Filler 会裁剪文本——与使用 width: Fill 的同级元素一起使用

症状: 水平布局中的文本被截掉一半。文本标签看起来只有一半可用宽度。

根本原因: Filler{} 定义为 View{width: Fill height: Fill}。如果它旁边的同级元素也使用 width: Fill,两者就会争用剩余的水平空间,并将其平分。文本标签只能获得一半宽度,因此文本会被裁剪。

修复方法: 如果同级元素已经使用 width: Fill,就移除 Filler{}。使用 Fill 的同级元素会自然占据所有剩余空间,并将尺寸为 Fit 的同级元素推到边缘。

// 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"}
}

易踩坑 #8:悬停时文本消失——动画 View 未设置 new_batch

症状: 列表项或按钮带有悬停效果。将鼠标悬停其上时,背景颜色会改变,但文本完全消失。移开光标后,文本会重新显示。

根本原因: 悬停动画器会将 View 的背景从透明(#0000)改为不透明或半透明颜色。如果没有设置 new_batch: true,背景和文本就会处于同一个绘制批次中。背景变为不透明后,会遮住同一批次中按相同顺序绘制的文本。

修复方法: 对于设置了 show_bg: true、带有悬停动画器且包含文本的 View,添加 new_batch: true。

// 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}
}

易踩坑 #9:Splash 中的逗号——可以使用,但不是必需的

症状: 不确定 script_mod! 代码块中的属性之间是否允许使用逗号。

根本原因: Splash 主要使用空白字符分隔。不过,Splash 分词器会将逗号视为空白字符——逗号会被悄悄忽略,不会导致解析错误。许多 Makepad 项目(包括 Robrix)在 script_mod! 代码块中大量使用逗号,这是沿袭了 Makepad 1.x live_design! 语法的习惯,该语法要求使用逗号。

建议: 两种风格都有效。请与周边代码风格保持一致:

// 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}
}

注意: 两种写法的编译和运行结果完全相同。分词器会忽略逗号。不要浪费时间移除现有代码中的逗号——这只会产生无意义的差异,而不会带来功能变化。


易踩坑 #10:使用分号——Splash 中无效

症状: 出现解析错误或意外行为。分号会被当作属性值的一部分,或导致分词器失败。

根本原因: Splash 不使用分号来结束语句。这是从 CSS、JavaScript 或 Rust 转过来的开发者常犯的错误。

修复方法: 移除所有分号。

// 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"}
}

陷阱 #11:根容器未设置 width: Fill——布局狭窄或损坏

症状: UI 在窗口一侧显示为一条狭窄的细带,或布局完全损坏。内容未填满可用的窗口宽度。

根本原因: 在最外层容器上使用固定像素宽度(例如 width: 400),意味着它无法适应可用的窗口空间。如果窗口较宽,内容只会占据一小条区域;如果窗口较窄,内容就会被裁切。

修复方法: 根容器始终使用 width: Fill。内部元素可以使用固定像素宽度。

// 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!"}
}

陷阱 #12:Label 不支持 Animator——会被静默忽略

症状: 你给 Label 添加了 animator: Animator{...} 和 cursor: MouseCursor.Hand,但悬停时没有任何反应。没有错误,也没有效果。

根本原因: Label(以及 H1-H4、P、TextBox、Image、Icon、Markdown、Html、Slider、DropDown、Splitter、Hr、Filler)都没有 animator 字段。添加该字段会被静默忽略。

修复方法: 将 Label 包装在支持 animator 的 View 中。

// 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}
}

支持 animator 的组件: View、SolidView、RoundedView、ScrollXView、ScrollYView、ScrollXYView、Button、ButtonFlat、ButtonFlatter、CheckBox、Toggle、RadioButton、LinkLabel、TextInput


陷阱 #13:默认文本颜色为白色——在浅色背景上不可见

症状: 文本存在于组件树中,但不可见。在白色或浅色背景上,文本完全看不见。选中文本或将背景改为深色后,文本可能会显现。

根本原因: 所有文本组件(Label、H1-H4、P、Button 文本等)的默认文本颜色都是白色(#fff)。在浅色背景上,白色文本不可见。

修复方法: 对浅色背景上的每个文本元素,显式将 draw_text.color 设置为深色。

// 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}
}

陷阱 #14:着色器像素函数缺少 Pal.premul()——颜色渲染错误

症状: 半透明颜色渲染后变得明亮、完全不透明或褪色。像 #ffffff08 这样微妙的色调会显示为亮白色,而不是接近透明。

根本原因: Makepad 使用预乘 Alpha 进行 GPU 渲染。如果从 pixel: fn() 返回颜色时没有预乘 Alpha,Alpha 混合就会产生错误结果。

修复方法: 始终使用 Pal.premul() 包装最终返回的颜色——除非返回的是 sdf.result,因为它已由 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))
    }
}

陷阱 #15:使用 CSS 属性名称——Splash 中无效

症状: 属性无法识别,导致解析错误或被静默忽略。

根本原因: Splash 不是 CSS。属性名称遵循 Makepad 自有的命名约定,通过点路径访问嵌套结构体字段。

修复方法: 使用 Makepad 属性名称。

CSS 名称Splash 名称
background-colordraw_bg.color
colordraw_text.color
font-sizedraw_text.text_style.font_size
border-radiusdraw_bg.border_radius
border-colordraw_bg.border_color
border-widthdraw_bg.border_size
paddingpadding(相同)
marginmargin(相同)
gapspacing
display: flexflow: Right 或 flow: Down
flex-direction: columnflow: Down
flex-direction: rowflow: Right
justify-content: centeralign: Center
width: 100%width: Fill
width: autowidth: 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}
}

易踩坑 #16:虚构不存在的属性——静默失效

症状: 编写的属性没有效果。不会报错,但控件的行为与预期不符。

根本原因: Splash 会静默忽略未知属性。如果猜测属性名称(例如 background:、font_size:、text_color:、border:、opacity:),这些属性会直接被丢弃。

解决方法: 仅使用文档中列出的属性。如有疑问,请查看 splash.md,或在 widgets/src/ 中搜索用法。

常见的虚构属性及其正确写法:

虚构写法(错误)正确写法
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:在颜色中使用 alpha:#fff8
on_click:在 Rust handle_actions() 中处理
class:使用 let 绑定来复用样式
id:使用 := 创建命名实例

易踩坑 #17:在脚本输出中添加 Root{} 或 Window{}——宿主级包装器

症状: 为流式传输/splash 上下文生成 UI 时,输出无法显示或导致错误。重复的 Window 包装器可能会导致意外行为。

根本原因: Root{} 和 Window{} 是宿主级包装器,在应用的 script_mod! 代码块中定义。渲染到 body +: {} 代码块中的脚本内容不应包含这些包装器——内容应放在 body 中。

解决方法: 脚本输出应直接从 UI 内容开始,不要包含 Root{} 或 Window{} 包装器。

// 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"}
}

在应用代码中(此处使用 Root 和 Window 是正确的):

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"} }
                }
            }
        }
    }
}

易踩坑 #18:未调用 on_render——必须显式调用 render()

症状: 在控件上实现了 on_render,但它从未被调用。控件的渲染逻辑没有执行。

根本原因: Makepad 不会在所有情况下自动调用 on_render。必须调用 ui.widget.render() 显式触发渲染,或确保控件属于活动绘制树的一部分。

解决方法: 确保控件的 render() 被调用,通常是在父级的绘制过程中调用。检查控件是否已正确加入控件层级,并确认 handle_event 和 draw_walk 已正确实现。

// 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 ...
    }
}

易踩坑 #19:MapView 使用 height: Fill 或 Fit——必须使用固定像素高度

症状: MapView 渲染高度为零或行为异常。地图区域不可见。

根本原因: MapView 没有固有内容高度。使用 height: Fit 会得到 0px(没有可适配的内容)。在某些上下文中,使用 height: Fill 也可能解析为 0px(与易踩坑 #1 中的循环依赖相同)。

解决方法: 始终为 MapView 指定固定的像素高度。

// 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
}

易踩坑 #20:Modal 未显示——必须从 Rust 调用 .open(cx)

症状: 脚本中定义了 Modal,但它始终不出现。看不到对话框或覆盖层。

根本原因: Modal 默认处于隐藏状态。必须在 Rust 代码中通过调用 Modal 控件引用上的 .open(cx) 以编程方式打开。

解决方法: 需要显示 Modal 时,从 Rust 事件处理程序中调用 .open(cx)。

// 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);
        }
    }
}

易踩坑 #21:apply_over 与 script_eval——在 2.0 中使用 script_apply_eval!

症状: apply_over(cx, live!{...}) 无法编译或不起作用。旧版 1.x 运行时属性更新机制已移除。

根本原因: Makepad 2.0 使用 script_apply_eval! 宏取代了 live! 宏和 apply_over。live_design! 系统中的旧宏已不存在。

解决方法: 使用 script_apply_eval! 更新运行时属性。在宏中插入 Rust 表达式时使用 #(expr)。

// 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)
    }
});

陷阱 #22:旧版派生宏——Live → Script,LiveHook → ScriptHook

症状: 出现关于未知派生宏 Live、LiveHook 或未解析 trait 的编译错误。

根本原因: Makepad 2.0 重命名了派生宏:Live 改为 Script,LiveHook 改为 ScriptHook。旧名称已不存在。

修复方法: 将所有派生宏更新为新名称。

// 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)]

也已重命名: DefaultNone 派生宏已移除——请使用标准的 #[derive(Default)],并在 None 变体上添加 #[default] 属性:

// OLD (WRONG)
#[derive(DefaultNone)]
pub enum MyAction {
    Clicked,
    None,
}

// NEW (CORRECT)
#[derive(Clone, Default)]
pub enum MyAction {
    Clicked,
    #[default]
    None,
}

陷阱 #23:THEME_COLOR 与 theme.color——旧语法已替换

症状: (THEME_COLOR_X) 导致解析错误或无法识别。主题值未生效。

根本原因: Makepad 2.0 将带括号的常量语法 (THEME_COLOR_X) 替换为通过 theme.color_x 进行点路径访问。类似地,<THEME_FONT> 改为 theme.font_regular,现在所有主题访问都使用 theme. 前缀。

修复方法: 将所有旧主题引用替换为带 theme. 前缀的语法。

// OLD 1.x (WRONG in 2.0)
draw_bg.color: (THEME_COLOR_BG_APP)
draw_text.text_style: <THEME_FONT_REGULAR>
padding: (THEME_SPACE_2)

// NEW 2.0 (CORRECT)
draw_bg.color: theme.color_bg_app
draw_text.text_style: theme.font_regular
padding: theme.space_2
旧语法新语法
(THEME_COLOR_X)theme.color_x
<THEME_FONT_X>theme.font_x
(THEME_SPACE_X)theme.space_x
(THEME_FONT_SIZE_X)theme.font_size_x

陷阱 #24:忘记调用 self.match_event(cx, event)——MatchEvent 不会触发

症状: MatchEvent 实现中的 handle_actions() 从未被调用。按钮点击和其他组件操作未得到处理。

根本原因: MatchEvent trait 的 handle_actions 方法由 match_event() 在内部调用。如果你没有在 AppMain::handle_event 中调用 self.match_event(cx, event),trait 分发就不会发生。

修复方法: 始终在 handle_event 中调用 self.match_event(cx, event)。

// WRONG -- match_event not called, handle_actions never fires
impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

// CORRECT -- call match_event before or after ui.handle_event
impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.match_event(cx, event);  // THIS IS REQUIRED
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

impl MatchEvent for App {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        // Now this actually gets called!
        if self.ui.button(ids!(my_button)).clicked(actions) {
            log!("Button clicked!");
        }
    }
}

陷阱 #25:渲染之间的状态未保留——使用恰当的状态管理

症状: 一次渲染过程中设置的变量在下一帧会被重置。计数器值重置为零,列表状态丢失,切换状态恢复原样。

根本原因: draw_walk 或事件处理器中的局部变量会在每次调用时重新初始化。Splash let 绑定是模板定义,而非可变的运行时状态。要保留状态,你需要在组件结构体上使用 #[rust] 字段,或在应用结构体中管理状态。

修复方法: 在组件/应用结构体上使用 #[rust] 字段来保留状态。

// WRONG -- local variable resets every frame
impl Widget for Counter {
    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
        let count = 0;  // Resets to 0 every frame!
        // ...
        DrawStep::done()
    }
}

// CORRECT -- persistent state in the struct
#[derive(Script, ScriptHook, Widget)]
pub struct Counter {
    #[source] source: ScriptObjectRef,
    #[walk] walk: Walk,
    #[layout] layout: Layout,
    #[rust] count: i32,  // Persists between frames
    #[deref] view: View,
}

impl MatchEvent for App {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        if self.ui.button(ids!(increment)).clicked(actions) {
            // State persists because it lives on the App struct
            self.count += 1;
            let label = self.ui.label(ids!(count_label));
            label.set_text(cx, &format!("{}", self.count));
        }
    }
}

快速参考:诊断决策树

Problem: UI is invisible / blank screen
  |
  +-- Are containers missing height: Fit?              --> Pitfall #1
  +-- Is text on a colored background?                 --> Pitfall #2 (new_batch)
  +-- Is text white on white?                          --> Pitfall #13
  +-- Is root container using width: Fill?             --> Pitfall #11
  +-- Did you add Root{} or Window{} in script output? --> Pitfall #17

Problem: Text disappears
  |
  +-- On hover?                                        --> Pitfall #8 (new_batch)
  +-- On colored background?                           --> Pitfall #2 (new_batch)
  +-- On light background?                             --> Pitfall #13 (text color)

Problem: Override does not work
  |
  +-- Used : instead of :=?                            --> Pitfall #3
  +-- Named child inside anonymous container?          --> Pitfall #3 (path issue)

Problem: Parse error
  |
  +-- Hex color with letter 'e'?                       --> Pitfall #4 (use #x)
  +-- Using commas?                                    --> Pitfall #9
  +-- Using semicolons?                                --> Pitfall #10
  +-- border_radius with Inset?                        --> Pitfall #5

Problem: Widget behavior not working
  |
  +-- Animator on Label?                               --> Pitfall #12
  +-- Modal not showing?                               --> Pitfall #20
  +-- MatchEvent not firing?                           --> Pitfall #24
  +-- State resetting between frames?                  --> Pitfall #25

Problem: Compilation error
  |
  +-- Derive macro not found?                          --> Pitfall #22 (Script/ScriptHook)
  +-- apply_over / live! not found?                    --> Pitfall #21 (script_apply_eval!)
  +-- Theme constant not found?                        --> Pitfall #23 (theme.xxx)
  +-- Widget type not found at runtime?                --> Pitfall #6 (registration order)

补充说明

Splash 语法快速规则
  1. 属性之间的逗号可省略(分词器会将其视为空白)
  2. 任何地方都不能使用分号
  3. 命名/可寻址子项使用 :=,静态属性使用 :
  4. 使用 +: 与父级合并,而不是替换父级
  5. 对包含字母 e 的十六进制颜色使用 #x 前缀
  6. 每个容器都要设置 height: Fit(固定高度父级内的容器除外)
  7. 根容器设置 width: Fill
  8. 任何包含文本子项的背景容器都要设置 new_batch: true
App::run() 中的注册顺序
  1. crate::makepad_widgets::script_mod(vm)——基础组件
  2. 自定义组件模块
  3. 应用 UI 模块
  4. App::from_script_mod(vm, self::script_mod)
支持 Animator 的组件

View、SolidView、RoundedView、ScrollXView、ScrollYView、ScrollXYView、Button、ButtonFlat、ButtonFlatter、CheckBox、Toggle、RadioButton、LinkLabel、TextInput

不支持 Animator 的组件

Label、H1–H4、P、TextBox、Image、Icon、Markdown、Html、Slider、DropDown、Splitter、Hr、Filler


陷阱 #26:热重载不起作用——缺少 --hot 标志或文件监视器问题

**症状:**你编辑了 Rust 源文件中的 script_mod! 代码,但运行中的应用没有更新。热重载没有发生。

**根本原因:**热重载需要使用 --hot 标志启动文件监视器。否则,应用会正常运行,但不会监视文件更改。此外,监视器会规范化路径并对内容去重——如果更改后提取出的 script_mod 内容与之前相同,就不会触发重新加载。

解决方法:

# Run with hot reload enabled
cargo run -p my-app -- --hot

其他检查项:

  • 确保 script_mod! 代码块格式正确(语法错误会导致提取失败)
  • 检查 script_mod! 中的原始字符串、字节字符串和注释是否正确闭合
  • 监视器会跟踪 #(...) Rust 占位符的数量——添加或移除占位符需要完整重新构建
  • 文件路径会在不同平台间进行规范化(处理 Windows 反斜杠和符号链接)

陷阱 #27:WASM 服务端口冲突——过时的锁文件

症状:cargo makepad wasm run 因端口被占用而失败,或在启动过程中卡住。多个 WASM 服务端可能发生冲突。

**根本原因:**WASM 服务端管理器使用按工作区划分的锁文件来跟踪 PID 和端口。崩溃进程留下的过时锁文件可能会阻止新服务端启动。

解决方法:

  • 服务端管理器会自动检测并清理过时的锁(检查 PID 是否存活,以及端口归属)
  • 如果自动检测失败,请手动移除工作区中的锁文件
  • 启动互斥锁可防止并发启动 wasm run,并支持配置超时

**诊断:**服务端管理器会使用 lsof(Unix)或 netstat(Windows)报告详细的端口占用信息。


陷阱 #28:没有 TextInput 焦点时出现 IME 弹窗

**症状:**没有 TextInput 获得焦点时,或 IME(输入法编辑器)弹窗出现在错误位置时,弹窗仍会出现。

**根本原因:**在 Linux X11 上,IME 事件可能会在未正确跟踪焦点的情况下分发。此问题已修复,现在只有 TextInput 控件获得键盘焦点时才会显示 IME 弹窗。

**解决方法:**确保 TextInput 在 FingerDown 时调用 cx.set_key_focus(area)。平台层现在会根据焦点状态正确控制 IME 弹窗的显示。


陷阱 #29:Wayland 上下文菜单崩溃——反复打开和关闭

**症状:**在 Wayland 上,快速打开和关闭上下文菜单(弹出窗口)会导致崩溃或协议错误。

根本原因:xdg_popup 协议要求正确的 grab 语义。反复打开和关闭弹出窗口,而不等待合成器确认,会触发协议违规。

**解决方法:**平台层已修复此问题。弹出窗口现在使用带有合成器驱动 grab 的 xdg_popup,并发出 PopupDismissed 事件。应用必须处理 PopupDismissed,以实现显式关闭语义(不要在关闭处理程序中立即重新打开)。


更新的快速参考:诊断决策树

Problem: Hot reload not working
  |
  +-- Missing --hot flag?                               --> Pitfall #26
  +-- script_mod! has syntax errors?                    --> Pitfall #26
  +-- Content unchanged (same extracted script)?        --> Pitfall #26

Problem: WASM build/server issues
  |
  +-- Port conflict?                                    --> Pitfall #27
  +-- Server hangs on startup?                          --> Pitfall #27
  +-- Binary too large?                                 --> Use --wasm-opt, --split-functions

Problem: Platform-specific issues
  |
  +-- IME popup at wrong time?                          --> Pitfall #28
  +-- Wayland popup crash?                              --> Pitfall #29
  +-- Selection handles not showing (mobile)?           --> Check iOS 16+ / Android JNI bridge

Problem: Splash eval issues (POST /splash, set_text)
  |
  +-- ScrollYView renders blank in eval?                --> Pitfall #30
  +-- Code block has no syntax highlighting?            --> Pitfall #31
  +-- CJK characters overlap in CodeView?              --> Pitfall #32
  +-- Type default properties not inherited?            --> Pitfall #33
  +-- on_after_apply not called for eval widgets?      --> Pitfall #33

陷阱 #30:ScrollYView 在 Splash 求值中呈现为空白

**症状:**通过 POST /splash 发送 ScrollYView{...} 会呈现空白面板。同样的代码在编译后的 script_mod! 中运行正常。

根本原因:ScrollYView 需要完成内部初始化,但在 Splash 运行时求值期间无法正确完成。滚动视图虽然创建成功,但其视口尺寸在求值上下文中没有正确设置。

**解决方法:**改用带 height: Fit 的普通 View。Canvas 会将 Splash 面板放入自己的滚动容器中。

// WRONG -- blank in Splash eval
ScrollYView{ width: Fill height: Fill flow: Down
    Label{ text: "content" }
}

// CORRECT -- works in eval
View{ width: Fill height: Fit flow: Down
    Label{ text: "content" }
}

陷阱 #31:Splash eval 中的 Markdown 代码块没有语法高亮

**症状:**通过 POST /splash 发送的 Markdown{body: "...```rust\n...\n```..."} 会将代码块显示为无语法着色的纯等宽文本。同样的 Markdown 在已编译的 script_mod! 中会通过 CodeView 实现完整的语法高亮。

**根本原因:**Canvas 使用 set_type_default() 覆盖了 mod.widgets.Markdown,添加了 code_block := View{CodeView{...}} 和 use_code_block_widget: true。但通过 Splash eval 创建的实例不会继承这些设置,原因是:(1) eval 路径不会调用 on_after_apply;(2) 类型默认值 vec 条目(命名子项)不会完整复制到实例。

修复(2026-03-23 已应用):widgets/src/markdown.rs 现在会在 draw_walk 中通过 vm.bx.heap.type_default_for_object() 延迟查找类型默认值,并注册缺失的模板。此外,找到 code_block 模板时会自动启用 use_code_block_widget。


陷阱 #32:CodeView / CodeEditor 中的中日韩字符重叠

**症状:**代码块中的中文、日文或韩文字符会与相邻字符重叠。等宽网格未考虑中日韩字符的双宽字形。

根本原因:code_editor/src/char.rs 中的 column_count() 对所有字符都返回 1。中日韩字符在等宽网格中需要占用 2 列。

修复(2026-03-23 已应用):char.rs 现在会对中日韩统一表意文字、平假名、片假名、韩文、全角拉丁字符及相关 Unicode 范围返回 2。此外,还在全部三个主题文件的 font_code 中添加了 LXGWWenKaiRegular.ttf 作为中文回退字体。


陷阱 #33:Splash eval 中不会继承类型默认值覆盖项

**症状:**通过 set_type_default() 覆盖某个组件类型后(例如 Canvas 为 Markdown 添加额外模板),通过 Splash eval(POST /splash)创建的实例不会获取覆盖项中的属性或命名子项。

**根本原因:**Splash eval 路径只会应用 eval 值(例如 {body: "..."})来创建组件,不会触发 ScriptHook::on_after_apply。因此会使用 #[live] 字段默认值,而不是类型默认值。类型默认值中的 vec 条目(例如命名子项模板)也可能在原型复制链中丢失。

**解决方法:**在 draw_walk 中实现延迟初始化,检查是否缺少状态,并通过 vm.bx.heap.type_default_for_object(self.source.as_object()) 查找类型默认值。Markdown 的 code_block 模板采用了这种模式。


陷阱 #34:在 draw_bg +: 中使用 instance 会导致崩溃——“cannot push to frozen vec”

**症状:**在 draw_bg +: { } 中添加 instance my_var: 0.5 时,运行时崩溃并报错:“cannot push to frozen vec”。

**根本原因:**Splash +: 块可以覆盖 pixel: fn() 并设置现有变量,但不能添加新的实例/统一变量字段。GPU 布局是在编译时确定的。

**修复:**使用内置变量(self.draw_pass.time、self.pos、self.rect_size),或创建自定义 Rust DrawQuad 类型。


陷阱 #35:透明窗口仍显示不透明背景

**症状:**设置了 window.transparent: true,但窗口仍然显示灰色/深色背景。

根本原因:window.transparent 只会告知操作系统窗口是透明的。默认情况下,渲染通道仍会清除为不透明颜色。

**修复:**必须同时设置:

window.transparent: true
pass.clear_color: #x00000000    // CRITICAL: fully transparent clear color

并且不要在 body +: {} 上设置 draw_bg.color,否则会覆盖透明设置。

参考:Makepad 源码中的 tools/canvas/src/app.rs。


陷阱 #36:sdf.box() 使用较大的 border_radius 会产生菱形/尖刺形状

**症状:**在高 56px 的 RoundedView 上使用 border_radius: 28.0,并设置 width: Fit,会产生尖锐的菱形端部,而不是圆润的胶囊形端部。

**根本原因:**当 r >= min(w,h)/2 时,sdf.box() 公式 size.xy - vec2(2*r, 2*r) 会变成负数,从而破坏 SDF。

**修复:**使用自定义胶囊 SDF:let cx = clamp(px, r, max(r, w-r))——参见 makepad-2.0-shaders 技能中的 “SDF Capsule Pattern”。

陷阱 #37:子控件溢出父级 SDF 遮罩

症状: LoadingSpinner 或其他子控件渲染到父级 View 的自定义 SDF 胶囊形状之外,导致边缘出现可见瑕疵。

根本原因: 每个控件都有自己的绘制调用。子控件不会受父级 SDF 着色器的裁剪,而是在自己的矩形区域中独立绘制。

修复方法: 将动画直接嵌入父级的 pixel: fn() 着色器中,而不是使用子控件。或者使用 clip_x: true + clip_y: true(对圆角形状可能无法完全解决问题)。


陷阱 #38:show_in_dock(false) 会隐藏 NSStatusBar 项

症状: 调用 cx.show_in_dock(false) 会使应用在 Dock 中不可见,同时也会隐藏所有 NSStatusBar 项(菜单栏图标)。

根本原因: show_in_dock(false) 会设置 NSApplicationActivationPolicyAccessory,导致应用无法拥有状态栏项。

修复方法: 不要在运行时调用 show_in_dock(false)。对于 .app 包,请改用 Info.plist 中的 LSUIElement=true。执行 cargo run 时,请接受显示 Dock 图标。


陷阱 #39:Emoji 显示为乱码或方框字符

症状: 某些 emoji(✨ ⏳)在 Label 文本中显示为方框或乱码字符。

根本原因: Makepad 的字体图集不包含所有 emoji 字形,只支持部分基本 emoji。

修复方法: 逐个测试 emoji。已知可用:🎙 🔍 🔄。已知不可用:✨ ⏳。请准备 ASCII 替代字符作为后备方案。


陷阱 #40:script_apply_eval! 对动态创建的控件无效

症状: 你通过 widget_ref_from_live_ptr(cx, Some(ptr))(或 cx.with_vm(|vm| WidgetRef::script_from_value(vm, value)))创建控件,然后使用 script_apply_eval!(cx, item, { draw_bg.color: #(color) }) 在运行时更改其属性。调用会执行且不报错,但控件外观没有变化。

根本原因: script_apply_eval! 内部会调用 self.script_source() 获取控件的 ScriptObject,然后针对它执行脚本代码。对于从 LivePtr/ScriptValue 模板动态创建的控件,script_source() 会返回 ScriptObject::ZERO(默认值)。这意味着 eval 作用域为空——由于没有有效的目标对象,属性赋值会静默失败。

以下所有方法都会受到影响:

  • script_apply_eval!(cx, item, { draw_bg.color: #(color) }) — 无效
  • script_apply_eval!(cx, item, { draw_bg +: { color: #(color) } }) — 无效
  • script_apply_eval!(cx, item, { show_bg: true }) — 无效
  • ViewRef.set_uniform(cx, live_id!(color), &[r,g,b,a]) — 同样无效

修复方法: 改用Animator 状态和着色器实例变量:

  1. 在 DSL 模板中定义实例变量和像素着色器:
mod.widgets.MyItem = View {
    show_bg: true
    draw_bg +: {
        color: (COLOR_PRIMARY)
        selected: instance(0.0)

        pixel: fn() {
            let sdf = Sdf2d.viewport(self.pos * self.rect_size)
            sdf.box(0. 0. self.rect_size.x self.rect_size.y self.border_radius)
            let highlight = #x1E90FF30
            sdf.fill(Pal.premul(self.color.mix(highlight self.selected)))
            return sdf.result
        }
    }

    animator: Animator {
        highlight: {
            default: @off
            off: AnimatorState {
                from: { all: Forward { duration: 0.12 } }
                apply: { draw_bg: { selected: 0.0 } }
            }
            on: AnimatorState {
                from: { all: Forward { duration: 0.08 } }
                apply: { draw_bg: { selected: 1.0 } }
            }
        }
    }
}
  1. 从 Rust 中切换状态:
let view = item.as_view();
if is_highlighted {
    view.animator_cut(cx, ids!(highlight.on));
} else {
    view.animator_cut(cx, ids!(highlight.off));
}

关键要点: Animator 状态之所以有效,是因为它们通过 Makepad 的动画系统运行,直接修改 GPU 缓冲区中的着色器实例变量——这完全绕过了 ScriptObject/eval 机制。


易踩坑 #41:script_apply_eval! 运行时无法使用 DSL 常量

症状: 使用 script_apply_eval! 时出现 variable Right not found in scope、variable Fit not found in scope、variable Align not found in scope、variable MouseCursor not found in scope 等运行时错误。

根本原因: script_apply_eval! 会在受限的运行时作用域中评估代码,其中不包含 Right、Down、Fit、Fill、Align、Inset、MouseCursor 等 DSL 常量。这些常量仅在 script_mod! 评估期间(编译/初始化时)可用,而在运行时不可用。

修复方法: 对于布局更改,请在定义时于 DSL 模板中设置属性。如果必须在运行时更改布局,请直接使用 Rust API:

// WRONG — DSL constants not available at runtime
script_apply_eval!(cx, item, {
    flow: Right
    height: Fit
    align: Align{y: 0.5}
});

// CORRECT — Use Rust values with #(expr) interpolation
let height = 36.0_f64;
script_apply_eval!(cx, item, {
    height: #(height)
});

// CORRECT — For colors (hex literals work)
let color = vec4(0.1, 0.2, 0.3, 1.0);
script_apply_eval!(cx, item, {
    draw_bg +: { color: #(color) }
});

// CORRECT — Bake layout into DSL template instead
mod.widgets.MyItem = View {
    flow: Right
    height: 36
    align: Align{y: 0.5}
}

script_apply_eval! 中确实可用的内容: 数值、十六进制颜色(#fff)、#(rust_expr) 插值,以及属性路径(draw_bg +: { ... })。

不可用的内容: 类型构造器(Align{}、Inset{})、枚举变体(Right、Down、Fit、Fill)以及控件类型引用(MouseCursor.Hand)。


易踩坑 #42:Dock.load_state() 会破坏 DrawList 引用,导致渲染空白

症状: 从持久化状态恢复 Dock 布局后(例如应用重启时),整个 Dock 区域渲染为一片空白/灰色。控制台会显示大量 Drawlist id generation wrong index: N current gen:1 in pointer:0 错误。

根本原因: Dock.load_state() 会在事件处理期间调用 self.tab_bars.clear(),这会丢弃包含 DrawList2d 的 TabBarWrap 实例,从而释放 DrawList 池中的条目(并递增其 generation)。但是,Makepad 的渲染管线仍保留着上一帧绘制中缓存的 DrawListId 引用,其 generation 为 0。下一帧访问这些过期引用时,就会造成 generation 不匹配。

修复方法: 不要调用 dock.load_state() 来恢复持久化状态,而应以编程方式重新创建标签页:

// WRONG — corrupts DrawList references
dock.borrow_mut().load_state(cx, saved_dock_items.clone());

// CORRECT — use normal Dock API
self.close_all_tabs(cx);
for room in &saved_room_order {
    self.focus_or_create_tab(cx, room.clone());
}

权衡: 这种方法无法恢复自定义分隔条位置或多窗格布局。要实现这些功能,需要在 Makepad 上游修复 Dock 控件。


易踩坑 #43:命名子项在 1.0 中使用 =,在 2.0 DSL 模板中使用 :=

症状: 将 live_design! 迁移到 script_mod! 时,用 name = Widget { } 定义的命名子项会导致 variable name not found in scope 等运行时错误。子项虽然存在于控件树中,但无法通过 ids!(name) 从 Rust 访问。

根本原因: 在 Makepad 1.0 的 live_design! 中,= 和 := 都会创建命名子项。在 Makepad 2.0 的 script_mod! 中,只有 := 才会创建可寻址的命名子项。在 2.0 中使用 = 会进行局部变量赋值,而不会创建命名子控件。

修复方法:

// 1.0 (live_design!) — both work
avatar = <Avatar> { width: 24, height: 24 }
avatar := <Avatar> { width: 24, height: 24 }

// 2.0 (script_mod!) — ONLY := works for named children
avatar := Avatar { width: 24 height: 24 }  // CORRECT
avatar = Avatar { width: 24 height: 24 }   // WRONG — local variable, not child

易踩坑 #44:DSL 和 script_apply_eval! 中,draw_bg: 会替换,draw_bg +: 会合并

症状: 在 DSL 模板或 script_apply_eval! 中设置 draw_bg: { color: #f00 },会导致其他所有 draw_bg 属性(border_radius、border_color、悬停状态、着色器代码)丢失。

根本原因: : 运算符会替换整个属性值。+: 运算符会与现有值合并,只覆盖指定的子属性。

修复方法: 如果要修改 draw_bg、draw_text 或任何嵌套结构体中的子属性,请始终使用 +::

// WRONG — replaces ALL of draw_bg, losing border_radius, shader, etc.
draw_bg: { color: #f00 }

// CORRECT — only changes color, keeps everything else
draw_bg +: { color: #f00 }

// In script_apply_eval!, same rule applies:
// WRONG
script_apply_eval!(cx, item, { draw_bg: { color: #(color) } });

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

// Dot-path shorthand also works (equivalent to +: merge):
draw_bg.color: #f00
draw_bg.border_radius: 8.0

陷阱 #45:弹出菜单/弹窗出现在错误位置——把布局间距当作覆盖层定位

症状: 弹出菜单或语言选择器本应显示在按钮上方,却总是出现在按钮下方、固定在错误的角落,或看起来 Y 轴位置计算完全不起作用。

根本原因: 你把弹窗构建为局部布局树中的普通子元素,并尝试使用 margin.top / margin.left 或其他布局偏移量来定位。在 Makepad 中,这仍然是布局协商,而不是真正的覆盖层锚定。要实现真正的弹出行为,弹窗通常应由顶层覆盖层所有者(Modal、工具提示层、弹出层所有者)持有,并通过 walk.abs_pos 定位。

修复方法: 将弹窗的所有权移到公共祖先或覆盖层所有者。使用 button.area().clipped_rect(cx) 获取触发按钮的位置,计算弹窗在覆盖层坐标空间中的绝对位置,并将其写入 popup.walk.abs_pos。

// WRONG -- local child + layout spacing fights the turtle
script_apply_eval!(cx, popup, {
    margin.left: #(x)
    margin.top: #(y)
});

// CORRECT -- top-level overlay popup with explicit absolute anchor
let button_rect = button.area().clipped_rect(cx);
let popup_pos = dvec2(button_rect.pos.x, button_rect.pos.y - 294.0);

if let Some(mut popup) = self.view(cx, ids!(popup)).borrow_mut() {
    popup.walk.abs_pos = Some(popup_pos);
}

重要区别:

  • clip_x: false / clip_y: false 只允许局部子元素绘制到其父元素之外。
  • 它们不会让该子元素表现得像真正的覆盖层。

经验法则: 如果弹窗必须脱离触发它的组件,就将弹窗状态提升到公共祖先,并由该祖先持有覆盖层。


更新后的诊断决策树(新增内容)

Problem: script_apply_eval! has no effect on widget
  |
  +-- Widget created via widget_ref_from_live_ptr()?    --> Pitfall #40 (use Animator)
  +-- Using DSL constants (Right, Fit, Align)?          --> Pitfall #41 (use Rust values)
  +-- Using draw_bg: instead of draw_bg +: ?            --> Pitfall #44 (use merge operator)

Problem: Dock renders blank after state restore
  |
  +-- Called Dock.load_state() in event handler?         --> Pitfall #42 (use tab recreation)

Problem: Named child not addressable from Rust
  |
  +-- Used = instead of := in script_mod!?               --> Pitfall #43 (use :=)

Problem: Popup/menu anchored to button but renders in wrong place
  |
  +-- Built as normal child and moved with margin?       --> Pitfall #45 (use top-level overlay + walk.abs_pos)
  +-- Only disabled clip_x/clip_y on local parent?       --> Pitfall #45 (overflow is not a true overlay)

评论

支持 Markdown