关于 makepad-2.0-events
面向 Makepad 2.0 应用开发者的事件系统参考,说明 Splash 处理器与 Rust 事件分发如何协作,并介绍两者间的界面和状态桥接方式。
主要功能
- 01Splash 内联事件处理器
- 02Rust MatchEvent 事件分发
- 03控件动作与引用 API
- 04Rust 与 Splash 通信宏
- 05定时器和 HTTP 事件处理
适用场景
- 01处理点击和表单提交
- 02从 Rust 更新状态并触发渲染
- 03在应用代码中读取控件动作
- 04添加定时器或 HTTP 响应处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zhanghandong/ makepad-skills --skill makepad-2.0-events安装后在 Claude Code 中输入/makepad-2.0-events确认已加载。
附带文件references/event-patterns.md
详细介绍
Makepad 2.0 事件与操作系统
概述
Makepad 2.0 使用双层事件系统:
-
Splash 层 —— 直接写在
script_mod!Splash 代码中的内联事件处理器 (on_click、on_render、on_return、on_startup)。这些处理器以声明式方式在脚本内部处理 UI 交互, 紧邻控件定义。 -
Rust 层 —— 使用
MatchEventtrait,以及handle_actions、handle_timer、handle_http_response等方法。这些方法处理业务逻辑、外部 I/O,以及其他需要充分发挥 Rust 能力的任务。
两层通过两个桥接宏通信:
script_eval!(cx, { ... })—— 从 Rust 执行 Splash 代码(更新状态、触发渲染)script_apply_eval!(cx, widget_ref, { ... })—— 在运行时从 Rust 修补控件属性
1. Splash 内联事件处理器
事件处理器直接附加到 script_mod! 代码块内的控件上。它们使用闭包语法:无参数时用 ||,接收值的回调则用 |arg|。
on_click —— 按钮/控件点击
当用户点击按钮或可点击控件时触发。普通按钮不接收参数,
CheckBox 则使用 |checked|,传入新的布尔状态。
// Plain button click
add_button := Button{
text: "Add"
on_click: ||{
let text = ui.todo_input.text()
if text != "" {
add_todo(text, "")
ui.todo_input.set_text("")
}
}
}
// CheckBox click with checked state argument
check.on_click: |checked| toggle_todo(i, checked)
// Inline delete with closure capturing loop variable
delete.on_click: || delete_todo(i)
// Calling another widget's click programmatically
clear_done := ButtonFlatter{
text: "Clear completed"
on_click: ||{
todos.retain(|todo| !todo.done)
ui.todo_list.render()
}
}
on_render —— 动态渲染
对目标视图调用 .render() 时触发。这是动态内容的主要机制。处理器主体会替换该视图之前绘制的内容。
main_view := View{
width: Fill
height: Fill
on_render: ||{
counter_label := Label{
text: "Count: " + state.counter
draw_text.text_style.font_size: 24
}
}
}
// List rendering with for loop and per-item event handlers
todo_list := ScrollYView{
width: Fill height: Fill
new_batch: true
on_render: ||{
if todos.len() == 0
EmptyState{}
else for i, todo in todos {
TodoItem{
label.text: todo.text
check.active: todo.done
check.on_click: |checked| toggle_todo(i, checked)
delete.on_click: || delete_todo(i)
}
}
}
EmptyState{}
}
要点:on_render 不会自动调用。必须调用 ui.widget_name.render()
来触发它。视图上的 new_batch: true 属性会告诉系统在重新渲染前清除之前绘制的内容。
on_return —— TextInput 回车键
当用户在 TextInput 内按下 Enter/Return 时触发。通常用于提交表单。
todo_input := TextInput{
width: Fill height: 9. * theme.space_1
empty_text: "What needs to be done?"
on_return: || ui.add_button.on_click()
}
on_startup —— 应用启动
应用启动时触发一次。在 Root 级别定义。通常用于触发初始渲染。
ui: Root{
on_startup: ||{
ui.main_view.render()
}
main_window := Window{
// ...
}
}
事件处理器的功能
在事件处理器中,你可以:
- 调用 Splash 函数:
add_todo(text, "dev") - 读取控件值:
let text = ui.todo_input.text() - 设置控件值:
ui.todo_input.set_text("") - 触发重新渲染:
ui.todo_list.render() - 触发其他控件的点击:
ui.add_button.on_click() - 修改状态变量:
state.counter += 1 - 使用数组方法:
todos.push({text: "new", done: false}) - 使用控制流:
if text != "" { ... }
2. Rust 事件处理 —— MatchEvent Trait
MatchEvent trait 是 Rust 端的事件分发器。它通过一组处理器方法接收平台事件和控件操作。
核心 trait 定义(来自 draw/src/match_event.rs)
pub trait MatchEvent {
// Lifecycle
fn handle_startup(&mut self, _cx: &mut Cx) {}
fn handle_shutdown(&mut self, _cx: &mut Cx) {}
fn handle_foreground(&mut self, _cx: &mut Cx) {}
fn handle_background(&mut self, _cx: &mut Cx) {}
fn handle_pause(&mut self, _cx: &mut Cx) {}
fn handle_resume(&mut self, _cx: &mut Cx) {}
// Window focus
fn handle_window_got_focus(&mut self, _cx: &mut Cx, _window_id: &WindowId) {}
fn handle_window_lost_focus(&mut self, _cx: &mut Cx, _window_id: &WindowId) {}
// Frame
fn handle_next_frame(&mut self, _cx: &mut Cx, _e: &NextFrameEvent) {}
// Widget actions (most commonly used)
fn handle_action(&mut self, _cx: &mut Cx, _e: &Action) {}
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
for action in actions {
self.handle_action(cx, action);
}
}
// Input
fn handle_key_down(&mut self, _cx: &mut Cx, _e: &KeyEvent) {}
fn handle_key_up(&mut self, _cx: &mut Cx, _e: &KeyEvent) {}
fn handle_back_pressed(&mut self, _cx: &mut Cx) -> bool { false }
// Timer
fn handle_timer(&mut self, _cx: &mut Cx, _e: &TimerEvent) {}
// Drawing
fn handle_draw(&mut self, _cx: &mut Cx, _e: &DrawEvent) {}
fn handle_draw_2d(&mut self, _cx: &mut Cx2d) {}
// Network
fn handle_http_response(&mut self, _cx: &mut Cx, _request_id: LiveId, _response: &HttpResponse) {}
fn handle_http_request_error(&mut self, _cx: &mut Cx, _request_id: LiveId, _err: &HttpError) {}
fn handle_http_progress(&mut self, _cx: &mut Cx, _request_id: LiveId, _progress: &HttpProgress) {}
fn handle_http_stream(&mut self, _cx: &mut Cx, _request_id: LiveId, _data: &HttpResponse) {}
fn handle_http_stream_complete(&mut self, _cx: &mut Cx, _request_id: LiveId, _data: &HttpResponse) {}
// Signals
fn handle_signal(&mut self, _cx: &mut Cx) {}
// Media devices
fn handle_audio_devices(&mut self, _cx: &mut Cx, _e: &AudioDevicesEvent) {}
fn handle_midi_ports(&mut self, _cx: &mut Cx, _e: &MidiPortsEvent) {}
fn handle_video_inputs(&mut self, _cx: &mut Cx, _e: &VideoInputsEvent) {}
}
标准 App 样板代码(必需)
每个 Makepad 2.0 应用都需要以下 Rust 结构:
use makepad_widgets::*;
app_main!(App);
script_mod! {
// ... Splash UI code ...
}
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm);
App::from_script_mod(vm, self::script_mod)
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
}
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
// Handle widget actions here
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}
重要:handle_event 必须同时调用 self.match_event(cx, event)(将事件分发给 MatchEvent 处理器)和 self.ui.handle_event(cx, event, &mut Scope::empty())
(将事件传递给控件)。
3. 控件操作 API
在 Rust 中使用 self.ui.widget_type(cx, ids!(name)) 访问控件,然后传入 &Actions 引用以查询其操作状态。
Button
// Access: self.ui.button(cx, ids!(my_button))
// Returns ButtonRef
.clicked(actions) -> bool // Was clicked (finger down + up inside)
.pressed(actions) -> bool // Was pressed down
.long_pressed(actions) -> bool // Was long-pressed (not yet released)
.released(actions) -> bool // Was released (NOT a click)
.clicked_modifiers(actions) -> Option<KeyModifiers> // Clicked with modifier keys
.pressed_modifiers(actions) -> Option<KeyModifiers>
.released_modifiers(actions) -> Option<KeyModifiers>
TextInput
// Access: self.ui.text_input(cx, ids!(my_input))
// Returns TextInputRef
.changed(actions) -> Option<String> // Text changed, returns new text
.returned(actions) -> Option<(String, KeyModifiers)> // Enter pressed, returns text + mods
.escaped(actions) -> bool // Escape pressed
.key_down_unhandled(actions) -> Option<KeyEvent> // Unhandled key event
.selected_text() -> String // Current selection (no actions needed)
CheckBox
// Access: self.ui.check_box(cx, ids!(my_check))
// Returns CheckBoxRef
.changed(actions) -> Option<bool> // Toggled, returns new checked state
DropDown
// Access: self.ui.drop_down(cx, ids!(my_dropdown))
// Returns DropDownRef
.selected(actions) -> Option<usize> // Item selected, returns index
.changed(actions) -> Option<usize> // Same as selected
.changed_label(actions) -> Option<String> // Item selected, returns label string
.selected_item() -> usize // Current selection (no actions needed)
.selected_label() -> String // Current label (no actions needed)
Slider
// Access: self.ui.slider(cx, ids!(my_slider))
// Returns SliderRef
.slided(actions) -> Option<f64> // Value changed during slide or text edit
.end_slide(actions) -> Option<f64> // Slide ended or text committed
.value() -> Option<f64> // Current value (no actions needed)
RadioButton/RadioButtonGroup
// Access: self.ui.radio_button(cx, ids!(my_radio))
// Returns RadioButtonRef
.clicked(actions) -> bool // Was clicked
// For groups:
.selected(cx, actions) -> Option<usize> // Selected index in group
LinkLabel
// Access: self.ui.link_label(cx, ids!(my_link))
// Returns LinkLabelRef
.clicked(actions) -> bool
.clicked_modifiers(actions) -> Option<KeyModifiers>
完整示例
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
if self.ui.button(cx, ids!(increment_button)).clicked(actions) {
script_eval!(cx, {
mod.state.counter += 1
ui.main_view.render()
});
}
if let Some(text) = self.ui.text_input(cx, ids!(search_input)).changed(actions) {
self.perform_search(cx, &text);
}
if let Some(checked) = self.ui.check_box(cx, ids!(dark_mode)).changed(actions) {
self.set_theme(cx, checked);
}
if let Some(index) = self.ui.drop_down(cx, ids!(language)).selected(actions) {
self.change_language(cx, index);
}
}
}
4. script_eval! 宏 —— Rust 与 Splash 通信
script_eval! 在 Rust 处理器中执行 Splash 代码。它是从 Rust 更新 Splash 状态并触发 UI 重新渲染的主要桥梁。
签名:script_eval!(cx_or_vm, { splash_code })
第一个参数可以是 &mut Cx(在事件处理器内),也可以是 &mut ScriptVm(在初始化期间)。
// Update Splash state and re-render
if self.ui.button(cx, ids!(increment_button)).clicked(actions) {
script_eval!(cx, {
mod.state.counter += 1
ui.main_view.render()
});
}
// Pass Rust values into Splash using #(expr) interpolation
let rust_string = "Hello from Rust";
script_eval!(cx, {
mod.value = #(rust_string)
});
// During init (inside App::run), use vm instead of cx
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::theme_mod(vm);
script_eval!(vm, {
mod.theme = mod.themes.light
});
crate::makepad_widgets::widgets_mod(vm);
App::from_script_mod(vm, self::script_mod)
}
}
使用 #(expr) 插入值
在 script_eval! 内,使用 #(rust_expression) 将 Rust 值插入 Splash:
let count = 42_u64;
let message = "items found".to_string();
script_eval!(cx, {
mod.state.count = #(count)
mod.state.message = #(message)
ui.results_view.render()
});
5. script_apply_eval! 宏——运行时属性修补
script_apply_eval! 可在运行时从 Rust 修补 widget 属性。与运行通用 Splash 代码的
script_eval! 不同,此宏面向特定的 widget 引用,并直接应用属性更改。
签名:script_apply_eval!(cx, widget_ref, { property_patches })
// Patch a single property
let height = 500.0_f64;
script_apply_eval!(cx, item, {
page_view: { height: #(height) }
});
// Patch margin
let margin = Inset { top: 10.0, bottom: 10.0, left: 5.0, right: 5.0 };
script_apply_eval!(cx, content_view, {
margin: #(margin)
});
// Patch width with a Makepad type
script_apply_eval!(cx, content_view, {
width: #(Size::fit())
});
// Patch nested draw shader properties
let bg_color = vec4(0.2, 0.3, 0.4, 1.0);
let triangle_height = 8.0_f64;
script_apply_eval!(cx, content, {
draw_bg +: {
triangle_height: #(triangle_height)
background_color: #(bg_color)
}
});
6. 从 Rust 访问 Widget 引用
按 ID 访问 widget
使用 self.ui.widget_type(cx, ids!(name)) 获取类型化引用:
self.ui.button(cx, ids!(increment_button)) // -> ButtonRef
self.ui.label(cx, ids!(status_label)) // -> LabelRef
self.ui.text_input(cx, ids!(search_input)) // -> TextInputRef
self.ui.check_box(cx, ids!(dark_mode)) // -> CheckBoxRef
self.ui.drop_down(cx, ids!(language)) // -> DropDownRef
self.ui.slider(cx, ids!(volume)) // -> SliderRef
self.ui.view(cx, ids!(content)) // -> ViewRef
self.ui.radio_button(cx, ids!(option_a)) // -> RadioButtonRef
使用 ids! 访问嵌套 widget
使用点号表示法访问嵌套 widget:
self.ui.label(cx, ids!(container.inner.child_label))
self.ui.button(cx, ids!(toolbar.save_button))
通用 widget 访问
self.ui.widget(cx, ids!(my_widget)) // -> WidgetRef (untyped)
常见的 widget setter 方法(从 Rust 调用)
// Label
self.ui.label(cx, ids!(my_label)).set_text(cx, "Hello");
// TextInput
self.ui.text_input(cx, ids!(my_input)).set_text(cx, "default value");
// CheckBox
if let Some(mut inner) = self.ui.check_box(cx, ids!(my_check)).borrow_mut() {
inner.set_active(cx, true);
}
// DropDown
self.ui.drop_down(cx, ids!(my_dd)).set_selected_item(cx, 2);
self.ui.drop_down(cx, ids!(my_dd)).set_labels(cx, vec!["A".into(), "B".into()]);
// Slider
self.ui.slider(cx, ids!(my_slider)).set_value(cx, 0.5);
// Redraw any widget
self.ui.widget(cx, ids!(my_widget)).redraw(cx);
// Visibility
self.ui.button(cx, ids!(my_button)).set_visible(cx, false);
7. 从 Splash 调用 Widget 方法
在 script_mod! 的 Splash 代码中,通过 ui 前缀访问 widget:
// Trigger a re-render of a view's on_render handler
ui.main_view.render()
// Get text from a TextInput
let text = ui.todo_input.text()
// Set text on a TextInput
ui.todo_input.set_text("")
// Trigger a click on another widget
ui.add_button.on_click()
8. 命中事件(底层)
对于需要处理原始输入的自定义 widget,Hit 枚举提供底层事件。可在 widget 的 handle_event
中通过 event.hits(cx, area) 访问这些事件。
pub enum Hit {
// Keyboard focus
KeyFocus(KeyFocusEvent),
KeyFocusLost(KeyFocusEvent),
// Keyboard input
KeyDown(KeyEvent),
KeyUp(KeyEvent),
// Text input
TextInput(TextInputEvent),
TextRangeReplace(TextRangeReplaceEvent),
TextCopy(TextClipboardEvent),
TextCut(TextClipboardEvent),
ImeAction(ImeActionEvent),
// Pointer/finger events
FingerDown(FingerDownEvent),
FingerMove(FingerMoveEvent),
FingerUp(FingerUpEvent),
FingerScroll(FingerScrollEvent),
FingerLongPress(FingerLongPressEvent),
// Hover events
FingerHoverIn(FingerHoverEvent),
FingerHoverOver(FingerHoverEvent),
FingerHoverOut(FingerHoverEvent),
// Triggers
Trigger(TriggerHitEvent),
Nothing,
}
在自定义 widget 中使用
impl Widget for MyCustomWidget {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
match event.hits(cx, self.draw_bg.area()) {
Hit::FingerDown(fd) => {
cx.set_key_focus(self.draw_bg.area());
// fd.abs -- absolute position
// fd.rel -- relative position within widget
// fd.modifiers -- KeyModifiers
}
Hit::FingerUp(fu) => {
if fu.is_over {
// Finger was released inside the widget = click
let uid = self.widget_uid();
cx.widget_action(uid, MyWidgetAction::Clicked);
}
}
Hit::FingerMove(fm) => {
// fm.abs, fm.rel -- current position
}
Hit::FingerHoverIn(_) => {
self.animator_play(cx, ids!(hover.on));
}
Hit::FingerHoverOut(_) => {
self.animator_play(cx, ids!(hover.off));
}
Hit::KeyDown(ke) => {
match ke.key_code {
KeyCode::ReturnKey => { /* handle enter */ }
KeyCode::Escape => { /* handle escape */ }
_ => {}
}
}
_ => {}
}
}
}
9. 定时器事件
通过 Cx 方法创建定时器,并在 handle_timer 中接收。
创建定时器
// One-shot timer (fires once after delay in seconds)
let timer: Timer = cx.start_timeout(2.0); // 2 seconds
// Repeating timer (fires every interval seconds)
let timer: Timer = cx.start_interval(0.5); // every 500ms
// Stop a timer
cx.stop_timer(timer);
处理定时器事件
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
#[rust]
poll_timer: Timer,
}
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
self.poll_timer = cx.start_interval(30.0);
}
fn handle_timer(&mut self, cx: &mut Cx, event: &TimerEvent) {
if self.poll_timer.is_timer(event).is_some() {
self.do_periodic_work(cx);
}
}
}
10. HTTP / 网络事件
从 Rust 发起 HTTP 请求
let request_id = live_id!(my_request);
let mut req = HttpRequest::new(url, HttpMethod::Get);
req.set_header("Content-Type", "application/json");
req.set_body(body_bytes);
cx.http_request(request_id, req);
处理 HTTP 响应
impl MatchEvent for App {
fn handle_http_response(&mut self, cx: &mut Cx, request_id: LiveId, response: &HttpResponse) {
if request_id == live_id!(my_request) {
let body = &response.body;
// Process response...
}
}
fn handle_http_request_error(&mut self, cx: &mut Cx, request_id: LiveId, err: &HttpError) {
if request_id == live_id!(my_request) {
log!("Request failed: {}", err.message);
}
}
fn handle_http_progress(&mut self, cx: &mut Cx, request_id: LiveId, progress: &HttpProgress) {
if request_id == live_id!(my_request) {
let pct = progress.loaded as f64 / progress.total as f64;
// Update progress UI...
}
}
// For streaming responses
fn handle_http_stream(&mut self, cx: &mut Cx, request_id: LiveId, data: &HttpResponse) {
// Receive incremental chunks
}
fn handle_http_stream_complete(&mut self, cx: &mut Cx, request_id: LiveId, data: &HttpResponse) {
// Stream finished
}
}
从 Splash 发起 HTTP 请求
let req = net.HttpRequest{
url: "https://api.example.com/data"
method: net.HttpMethod.GET
headers: {"Content-Type": "application/json"}
}
net.http_request(req) do net.HttpEvents{
on_response: |res| {
// handle response
let data = res.body.to_string().parse_json()
}
on_error: |e| {
// handle error
}
}
// Streaming HTTP request
let req = net.HttpRequest{
url: "https://api.openai.com/v1/chat/completions"
method: net.HttpMethod.POST
headers: {"Content-Type": "application/json"}
is_streaming: true
body: {model: "gpt-4" messages: [{role: "user" content: "Hello"}]}
}
net.http_request(req) do net.HttpEvents{
on_stream: fn(res){
// Process each streaming chunk
let chunk = res.body.to_string()
}
on_complete: fn(res){
// Stream finished
}
}
11. 事件流图
User Input (click/key/mouse)
|
v
Platform Event Loop (Cx)
|
v
AppMain::handle_event(&mut self, cx, event)
|
+--> self.match_event(cx, event)
| |
| +--> MatchEvent::handle_startup / handle_shutdown / ...
| +--> MatchEvent::handle_actions(cx, actions)
| | |
| | +--> self.ui.button(cx, ids!(name)).clicked(actions)
| | +--> script_eval!(cx, { ... })
| | |
| | v
| | ScriptVm::eval -> updates Splash state
| | |
| | +--> ui.view.render() -> schedules re-draw
| |
| +--> MatchEvent::handle_timer(cx, event)
| +--> MatchEvent::handle_http_response(cx, id, response)
| +--> MatchEvent::handle_key_down(cx, event)
|
+--> self.ui.handle_event(cx, event, scope)
|
+--> Widget tree event propagation
+--> Hit detection (FingerDown/Up/Move)
+--> Splash on_click / on_return handlers execute
+--> on_render handlers execute during draw
+--> Widget actions emitted -> feed back into Actions
12. 完整模式
模式 1:计数器(按钮点击 -> 状态更新 -> 重新渲染)
use makepad_widgets::*;
app_main!(App);
script_mod! {
use mod.prelude.widgets.*
let state = {
counter: 0
}
mod.state = state
startup() do #(App::script_component(vm)){
ui: Root{
on_startup: ||{
ui.main_view.render()
}
main_window := Window{
window.inner_size: vec2(420, 220)
body +: {
main_view := View{
width: Fill
height: Fill
flow: Down
spacing: 12
align: Center
on_render: ||{
counter_label := Label{
text: "Count: " + state.counter
draw_text.text_style.font_size: 24
}
}
}
increment_button := Button{
text: "Increment"
}
}
}
}
}
}
impl App {
fn run(vm: &mut ScriptVm) -> Self {
crate::makepad_widgets::script_mod(vm);
App::from_script_mod(vm, self::script_mod)
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
}
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
if self.ui.button(cx, ids!(increment_button)).clicked(actions) {
script_eval!(cx, {
mod.state.counter += 1
ui.main_view.render()
});
}
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}
模式 2:表单(文本输入 -> 验证 -> 提交)
// Inside script_mod!
fn submit_form(){
let name = ui.name_input.text()
let email = ui.email_input.text()
if name == "" {
ui.error_label.set_text("Name is required")
} else if email == "" {
ui.error_label.set_text("Email is required")
} else {
// Process form...
ui.error_label.set_text("")
ui.name_input.set_text("")
ui.email_input.set_text("")
}
}
// In the UI tree:
name_input := TextInput{
empty_text: "Your name"
}
email_input := TextInput{
empty_text: "[email protected]"
on_return: || submit_form()
}
submit_button := Button{
text: "Submit"
on_click: || submit_form()
}
error_label := Label{
text: ""
draw_text.color: #f00
}
模式 3:带有逐项事件的列表
let items = []
items.push({name: "Item A", selected: false})
items.push({name: "Item B", selected: false})
fn toggle_item(index, checked){
items[index].selected = checked
}
fn delete_item(index){
items.remove(index)
ui.item_list.render()
}
// In UI:
item_list := ScrollYView{
width: Fill height: Fill
new_batch: true
on_render: ||{
for i, item in items {
RoundedView{
width: Fill height: Fit
flow: Right
check := CheckBox{
active: item.selected
on_click: |checked| toggle_item(i, checked)
}
Label{text: item.name}
Button{
text: "Delete"
on_click: || delete_item(i)
}
}
}
}
}
模式 4:跨层通信(Splash 状态 + Rust 逻辑)
// Rust side: handle complex logic, then update Splash state
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
if self.ui.button(cx, ids!(fetch_button)).clicked(actions) {
// Rust handles the HTTP request
let request_id = live_id!(fetch_data);
let req = HttpRequest::new("https://api.example.com/data", HttpMethod::Get);
cx.http_request(request_id, req);
}
}
fn handle_http_response(&mut self, cx: &mut Cx, request_id: LiveId, response: &HttpResponse) {
if request_id == live_id!(fetch_data) {
let data = String::from_utf8_lossy(&response.body).to_string();
// Bridge back to Splash to update UI
script_eval!(cx, {
mod.state.data = #(data)
mod.state.loading = false
ui.results_view.render()
});
}
}
}
源文件参考
| 文件 | 用途 |
|---|---|
draw/src/match_event.rs | MatchEvent trait 定义 |
widgets/src/widget_match_event.rs | WidgetMatchEvent trait(用于自定义 widget) |
platform/src/event/event.rs | Event 枚举、Hit 枚举、Timer |
platform/src/event/finger.rs | 手指/指针事件类型 |
platform/script/src/lib.rs | script_eval! 宏定义 |
platform/script/derive/src/lib.rs | script_apply_eval! 过程宏 |
widgets/src/button.rs | Button 操作 API |
widgets/src/text_input.rs | TextInput 操作 API |
widgets/src/check_box.rs | CheckBox 操作 API |
widgets/src/drop_down.rs | DropDown 操作 API |
widgets/src/slider.rs | Slider 操作 API |
widgets/src/radio_button.rs | RadioButton 操作 API |
examples/counter/src/app.rs | 计数器示例(精简版) |
examples/todo/src/app.rs | Todo 示例(综合版) |
examples/git/src/app.rs | Git 示例(HTTP、定时器) |
examples/camera/src/ | 相机示例(媒体事件) |
examples/text_selection/src/ | 文本选择示例 |
13. 新增事件(2026 年 3 月)
选择与剪贴板事件
新增用于文本选择和剪贴板操作的 Hit 变体:
Hit::TextCopy(TextClipboardEvent) // User copied text (Ctrl+C or native toolbar)
Hit::TextCut(TextClipboardEvent) // User cut text (Ctrl+X or native toolbar)
移动端选择手柄: iOS/Android 上的原生选择手柄会触发:
Event::SelectionHandleDrag // User dragging a selection handle
长按文本可选择一个词,并显示原生剪贴板工具栏。TextFlow widget 会自动集成剪贴板操作。
弹出窗口事件
用于弹出窗口(上下文菜单、下拉菜单):
Event::WindowClosed // Popup window was closed
Event::PopupDismissed // Popup was dismissed by compositor (Wayland) or click-outside
WindowClosed 始终先于 PopupDismissed 触发。应用必须处理 PopupDismissed,以实现显式关闭语义。
IME 事件
用于 CJK 文本输入的 IME(输入法编辑器)支持:
Hit::ImeAction(ImeActionEvent) // IME composition committed/cancelled
Hit::TextRangeReplace(TextRangeReplaceEvent) // IME text replacement
Linux X11 IME: 已为 X11 添加完整的 IME 支持。已修复 IME 弹出窗口定位问题,避免其在 TextInput 未获得焦点时出现。
视频与媒体事件
// In MatchEvent trait:
fn handle_video_inputs(&mut self, _cx: &mut Cx, _e: &VideoInputsEvent) {}
来自媒体插件系统的视频输入设备枚举事件。摄像头和视频播放事件通过媒体插件架构路由。
主要选区(Linux)
Linux 主要选区(鼠标中键粘贴):
- Wayland:
zwp_primary_selection_device_manager_v1协议 - X11: PRIMARY 原子处理
- 自动集成到 TextInput 和 TextFlow 控件中