名称: robius-event-action 描述: | 关键:用于Robius事件和动作模式。触发条件: 自定义动作,MatchEvent,post_action,cx.widget_action, handle_actions,DefaultNone,小部件动作,事件处理, 事件处理,自定义动作
Robius事件与动作模式技能
基于Robrix和Moly代码库的Makepad应用程序中事件处理和动作模式的最佳实践。
源代码库:
- Robrix:Matrix聊天客户端 - MessageAction,RoomsListAction,AppStateAction
- Moly:AI聊天应用程序 - StoreAction,ChatAction,NavigationAction,计时器模式
触发条件
在以下情况下使用此技能:
- 在Makepad中实现自定义动作
- 在小部件中处理事件
- 在应用程序中集中处理动作
- 小部件间通信
- 关键词:makepad action, makepad event, widget action, handle_actions, cx.widget_action
自定义动作模式
定义领域特定动作
use makepad_widgets::*;
/// 由Message小部件发出的动作
#[derive(Clone, DefaultNone, Debug)]
pub enum MessageAction {
/// 用户想对消息做出反应
React { details: MessageDetails, reaction: String },
/// 用户想回复消息
Reply(MessageDetails),
/// 用户想编辑消息
Edit(MessageDetails),
/// 用户想删除消息
Delete(MessageDetails),
/// 用户请求打开上下文菜单
OpenContextMenu { details: MessageDetails, abs_pos: DVec2 },
/// 必需的默认变体
None,
}
/// 与消息动作关联的数据
#[derive(Clone, Debug)]
pub struct MessageDetails {
pub room_id: OwnedRoomId,
pub event_id: OwnedEventId,
pub content: String,
pub sender_id: OwnedUserId,
}
从小部件发出动作
impl Widget for Message {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
self.view.handle_event(cx, event, scope);
let area = self.view.area();
match event.hits(cx, area) {
Hit::FingerDown(_fe) => {
cx.set_key_focus(area);
}
Hit::FingerUp(fe) => {
if fe.is_over && fe.is_primary_hit() && fe.was_tap() {
// 发出小部件动作
cx.widget_action(
self.widget_uid(),
&scope.path,
MessageAction::Reply(self.get_details()),
);
}
}
Hit::FingerLongPress(lpe) => {
cx.widget_action(
self.widget_uid(),
&scope.path,
MessageAction::OpenContextMenu {
details: self.get_details(),
abs_pos: lpe.abs,
},
);
}
_ => {}
}
}
}
应用程序中集中动作处理
使用MatchEvent特性
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
// 在应用启动时调用一次
self.initialize(cx);
}
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
for action in actions {
// 模式1:非小部件动作的直接向下转型
if let Some(action) = action.downcast_ref::<LoginAction>() {
match action {
LoginAction::LoginSuccess => {
self.app_state.logged_in = true;
self.update_ui_visibility(cx);
}
LoginAction::LoginFailure(error) => {
self.show_error(cx, error);
}
}
continue; // 动作已处理
}
// 模式2:小部件动作转换
if let MessageAction::OpenContextMenu { details, abs_pos } =
action.as_widget_action().cast()
{
self.show_context_menu(cx, details, abs_pos);
continue;
}
// 模式3:对枚举变体进行向下转型匹配
match action.downcast_ref() {
Some(AppStateAction::RoomFocused(room)) => {
self.app_state.selected_room = Some(room.clone());
continue;
}
Some(AppStateAction::NavigateToRoom { destination }) => {
self.navigate_to_room(cx, destination);
continue;
}
_ => {}
}
// 模式4:模态动作
match action.downcast_ref() {
Some(ModalAction::Open { kind }) => {
self.ui.modal(ids!(my_modal)).open(cx);
continue;
}
Some(ModalAction::Close { was_internal }) => {
if *was_internal {
self.ui.modal(ids!(my_modal)).close(cx);
}
continue;
}
_ => {}
}
}
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
// 转发到MatchEvent
self.match_event(cx, event);
// 将事件传递给小部件树
let scope = &mut Scope::with_data(&mut self.app_state);
self.ui.handle_event(cx, event, scope);
}
}
动作类型
小部件动作(UI线程)
由小部件发出,在同一帧中处理:
// 发出
cx.widget_action(
self.widget_uid(),
&scope.path,
MyAction::Something,
);
// 处理(两种模式)
// 模式A:对小部件动作的直接转换
if let MyAction::Something = action.as_widget_action().cast() {
// 处理...
}
// 模式B:带有小部件UID匹配
if let Some(uid) = action.as_widget_action().widget_uid() {
if uid == my_expected_uid {
if let MyAction::Something = action.as_widget_action().cast() {
// 处理...
}
}
}
发布动作(来自异步)
从异步任务发布,在下一次事件周期中接收:
// 在异步任务中
Cx::post_action(DataFetchedAction { data });
SignalToUI::set_ui_signal(); // 唤醒UI线程
// 在应用程序中处理(不是小部件动作)
if let Some(action) = action.downcast_ref::<DataFetchedAction>() {
self.process_data(&action.data);
}
全局动作
用于应用范围的状态变更:
// 使用cx.action()进行全局动作
cx.action(NavigationAction::GoBack);
// 处理
if let Some(NavigationAction::GoBack) = action.downcast_ref() {
self.navigate_back(cx);
}
事件处理模式
命中测试
impl Widget for MyWidget {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
let area = self.view.area();
match event.hits(cx, area) {
Hit::FingerDown(fe) => {
cx.set_key_focus(area);
// 开始拖拽,捕获等
}
Hit::FingerUp(fe) => {
if fe.is_over && fe.is_primary_hit() {
if fe.was_tap() {
// 单点触摸
}
if fe.was_long_press() {
// 长按
}
}
}
Hit::FingerMove(fe) => {
// 拖拽处理
}
Hit::FingerHoverIn(_) => {
self.animator_play(cx, id!(hover.on));
}
Hit::FingerHoverOut(_) => {
self.animator_play(cx, id!(hover.off));
}
Hit::FingerScroll(se) => {
// 滚动处理
}
_ => {}
}
}
}
键盘事件
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if let Event::KeyDown(ke) = event {
match ke.key_code {
KeyCode::Return if !ke.modifiers.shift => {
self.submit(cx);
}
KeyCode::Escape => {
self.cancel(cx);
}
KeyCode::KeyC if ke.modifiers.control || ke.modifiers.logo => {
self.copy_to_clipboard(cx);
}
_ => {}
}
}
}
信号事件
用于处理异步更新:
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if let Event::Signal = event {
// 轮询更新队列
while let Some(update) = PENDING_UPDATES.pop() {
self.apply_update(cx, update);
}
}
}
动作链模式
小部件发出动作 → 父组件捕获并重新发出带更多上下文:
// 在子小部件中
cx.widget_action(
self.widget_uid(),
&scope.path,
ItemAction::Selected(item_id),
);
// 在父小部件的handle_event中
if let ItemAction::Selected(item_id) = action.as_widget_action().cast() {
// 添加上下文并转发到应用程序
cx.widget_action(
self.widget_uid(),
&scope.path,
ListAction::ItemSelected {
list_id: self.list_id.clone(),
item_id,
},
);
}
最佳实践
- 使用
DefaultNone派生:所有动作枚举必须有一个None变体 - 处理后使用
continue:防止不必要的处理 - 异步动作的向下转型模式:发布动作不是小部件动作
- UI动作的小部件动作转换:使用
as_widget_action().cast() - 始终调用
SignalToUI::set_ui_signal():从异步发布动作后 - 集中在App::handle_actions:将动作处理集中在一个地方
- 使用描述性动作名称:
MessageAction::Reply而不是MessageAction::Action1
参考文件
references/action-patterns.md- 其他动作模式(Robrix)references/event-handling.md- 事件处理参考(Robrix)references/moly-action-patterns.md- Moly特定模式- 基于存储的动作转发
- 基于计时器的重试模式
- 单选按钮导航
- 外部链接处理
- 平台条件动作(#[cfg])
- UiRunner事件处理