关于 makepad-2.0-troubleshooting
面向使用 Makepad 2.0 和 Splash 的开发者,按症状、原因和修复方式整理常见问题,帮助排查布局、绘制、解析及控件注册错误。
主要功能
- 01按症状分析常见故障
- 02解释问题根因
- 03提供修正代码示例
- 04涵盖布局与绘制问题
- 05整理 Splash 解析与注册问题
适用场景
- 01排查容器高度导致的界面不可见
- 02修复文本被背景遮挡或裁切
- 03诊断 Splash 属性覆盖和颜色解析错误
- 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-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(相同) |
margin | margin(相同) |
gap | spacing |
display: flex | flow: Right 或 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}
}
易踩坑 #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 语法快速规则
- 属性之间的逗号可省略(分词器会将其视为空白)
- 任何地方都不能使用分号
- 命名/可寻址子项使用
:=,静态属性使用: - 使用
+:与父级合并,而不是替换父级 - 对包含字母
e的十六进制颜色使用#x前缀 - 每个容器都要设置
height: Fit(固定高度父级内的容器除外) - 根容器设置
width: Fill - 任何包含文本子项的背景容器都要设置
new_batch: true
App::run() 中的注册顺序
crate::makepad_widgets::script_mod(vm)——基础组件- 自定义组件模块
- 应用 UI 模块
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 状态和着色器实例变量:
- 在 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 } }
}
}
}
}
- 从 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)