Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第17章:自定义 Widget

为什么这很重要

到目前为止,我们使用的都是 Makepad 内置的 Widget——View、Label、Button、PortalList。但实际应用中你总会遇到内置组件无法满足的需求:自定义图表、特殊交互模式、业务专属的 UI 组件。

Makepad 2.0 的 Widget 系统是可扩展的——你可以用 Rust 创建新的 Widget,然后在 Splash 中像内置组件一样使用它。第5章的 TodoList 就是一个自定义 Widget 的实例。

本章讲解创建自定义 Widget 的完整流程。

flowchart TD
    A["#[derive(Script, ScriptHook, Widget)]"] --> B["实现 Widget trait"]
    B --> C["draw_walk() — 渲染逻辑"]
    B --> D["handle_event() — 事件处理"]
    A --> E["在 script_mod! 中注册"]
    E --> F["Splash 中使用 MyWidget{}"]

Widget trait 的核心方法

每个自定义 Widget 都需要实现 Widget trait 的两个核心方法:

#![allow(unused)]
fn main() {
impl Widget for TodoList {
    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
        // 渲染逻辑:在这里绘制 Widget 的视觉内容
    }

    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
        // 事件处理:在这里响应鼠标、键盘等事件
    }
}
}

来源:examples/todo/src/main.rs:210-246(简化)

draw_walk:渲染

draw_walk 在每帧被调用(如果 Widget 需要重绘)。它接收:

  • cx: &mut Cx2d——2D 绘图上下文
  • scope: &mut Scope——作用域数据(可传递父组件的数据)
  • walk: Walk——布局信息(位置和尺寸)

返回 DrawStep::done() 表示渲染完成。

handle_event:事件

handle_event 在每个事件循环迭代中被调用。通常转发给内部的 View:

#![allow(unused)]
fn main() {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
    self.view.handle_event(cx, event, scope);
}
}

创建自定义 Widget 的步骤

步骤一:定义 Rust 结构体

#![allow(unused)]
fn main() {
#[derive(Script, ScriptHook, Widget)]
struct MyChart {
    #[deref]
    view: View,
    // 自定义字段
    #[rust]
    data: Vec<f64>,
}
}
  • #[derive(Script, ScriptHook, Widget)]——让结构体既能作为 Widget 工作,也能被 Splash 运行时注册和驱动
  • #[deref] view: View——委托基础的布局和绘制给内部的 View
  • #[rust]——标记非 Splash 字段(不会出现在 Splash DSL 中)

步骤二:实现 Widget trait

#![allow(unused)]
fn main() {
impl Widget for MyChart {
    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
        // 使用 self.data 绘制图表
        while let Some(step) = self.view.draw_walk(cx, scope, walk).step() {
            // 自定义渲染逻辑
        }
        DrawStep::done()
    }

    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
        self.view.handle_event(cx, event, scope);
    }
}
}

步骤三:在 Splash 中注册

mod.widgets.MyChartBase = #(MyChart::register_widget(vm))
mod.widgets.MyChart = set_type_default() do mod.widgets.MyChartBase{
    width: Fill height: 200
}

#(MyChart::register_widget(vm)) 告诉 Splash VM 有一个叫 MyChart 的新 Widget 类型。

步骤四:在 Splash 中使用

chart := mod.widgets.MyChart{
    width: Fill height: 300
}

注册后,自定义 Widget 和内置 Widget 使用方式完全相同——支持属性设置、:= 命名、模板定义。


实例:TodoList 的 Widget 结构

回顾第5章的 TodoList——它是一个完整的自定义 Widget 实例:

#![allow(unused)]
fn main() {
#[derive(Script, ScriptHook, Widget)]
struct TodoList {
    #[deref]
    view: View,
}
}

来源:examples/todo/src/main.rs:204-208

TodoList 的 draw_walkTODOS 数据源读取数据,遍历 PortalList 的可见项,为每一项设置文字和样式(详见第5章的完整分析)。

它在 Splash 中被注册和使用:

mod.widgets.TodoListBase = #(TodoList::register_widget(vm))
mod.widgets.TodoList = set_type_default() do mod.widgets.TodoListBase{
    list := PortalList{...}
}

// 使用
todo_list := mod.widgets.TodoList{}

来源:examples/todo/src/main.rs:96-109,174


Scope:父子组件通信

Scope 是 Makepad 中父组件向子组件传递数据的机制:

#![allow(unused)]
fn main() {
// 父组件传递数据
let scope = Scope::with_data(&mut my_data);
child.draw_walk(cx, &mut scope, walk);

// 子组件接收数据
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
    if let Some(data) = scope.data.get::<MyData>() {
        // 使用父组件传递的数据
    }
}
}

来源:platform/script/src/apply.rs:41-47

Scope 是类型擦除的——通过 Any trait 实现。子组件需要知道父组件传递的具体类型才能 downcast_ref


模式提炼

模式:Deref 委托

#![allow(unused)]
fn main() {
#[derive(Script, ScriptHook, Widget)]
struct MyWidget {
    #[deref]
    view: View,
}
}

几乎所有自定义 Widget 都使用 #[deref] view: View 模式——将基础的布局、绘制、事件处理委托给内部的 View。你只需要在 draw_walkhandle_event 中添加自定义逻辑。

模式:数据驱动渲染

#![allow(unused)]
fn main() {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
    let data = DATA.read().unwrap();  // 读取全局数据
    while let Some(step) = self.view.draw_walk(cx, scope, walk).step() {
        // 用 data 驱动渲染
    }
    DrawStep::done()
}
}

Widget 在 draw_walk 中读取数据并渲染。数据修改后调用 redraw(cx) 触发重绘。这和第4章的"状态-更新-渲染"循环是相同的模式,只是发生在 Widget 内部。


本章小结

概念说明
#[derive(Script, ScriptHook, Widget)]让 Rust 结构体既成为 Widget,又能被 Splash 注册与驱动
draw_walk()Widget 的渲染入口
handle_event()Widget 的事件入口
#[deref] view: View委托基础功能给内部 View
register_widget(vm)在 Splash VM 中注册自定义 Widget
Scope父子 Widget 间的数据传递

Part III(Widget 体系篇)到此完成。下一步进入 Part IV(渲染与 Shader 篇),讲解 Makepad 的 GPU 渲染管线和 Sdf2d Shader 编程。