robius-event-action
SKILL.md
Robius Event and Action Patterns Skill
Best practices for event handling and action patterns in Makepad applications based on Robrix and Moly codebases.
Source codebases:
- Robrix: Matrix chat client - MessageAction, RoomsListAction, AppStateAction
- Moly: AI chat application - StoreAction, ChatAction, NavigationAction, Timer patterns
Triggers
Use this skill when:
- Implementing custom actions in Makepad
- Handling events in widgets
- Centralizing action handling in App
- Widget-to-widget communication
- Keywords: makepad action, makepad event, widget action, handle_actions, cx.widget_action
Custom Action Pattern
Defining Domain-Specific Actions
use makepad_widgets::*;
/// Actions emitted by the Message widget
#[derive(Clone, DefaultNone, Debug)]
pub enum MessageAction {
/// User wants to react to a message
React { details: MessageDetails, reaction: String },
/// User wants to reply to a message
Reply(MessageDetails),
/// User wants to edit a message
Edit(MessageDetails),
/// User wants to delete a message
Delete(MessageDetails),
/// User requested to open context menu
OpenContextMenu { details: MessageDetails, abs_pos: DVec2 },
/// Required default variant
None,
}
/// Data associated with a message action
#[derive(Clone, Debug)]
pub struct MessageDetails {
pub room_id: OwnedRoomId,
pub event_id: OwnedEventId,
pub content: String,
pub sender_id: OwnedUserId,
}
Emitting Actions from Widgets
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() {
// Emit widget action
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,
},
);
}
_ => {}
}
}
}
Centralized Action Handling in App
Using MatchEvent Trait
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
// Called once on app startup
self.initialize(cx);
}
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
for action in actions {
// Pattern 1: Direct downcast for non-widget actions
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; // Action handled
}
// Pattern 2: Widget action cast
if let MessageAction::OpenContextMenu { details, abs_pos } =
action.as_widget_action().cast()
{
self.show_context_menu(cx, details, abs_pos);
continue;
}
// Pattern 3: Match on downcast_ref for enum variants
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;
}
_ => {}
}
// Pattern 4: Modal actions
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) {
// Forward to MatchEvent
self.match_event(cx, event);
// Pass events to widget tree
let scope = &mut Scope::with_data(&mut self.app_state);
self.ui.handle_event(cx, event, scope);
}
}
Action Types
Widget Actions (UI Thread)
Emitted by widgets, handled in the same frame:
// Emitting
cx.widget_action(
self.widget_uid(),
&scope.path,
MyAction::Something,
);
// Handling (two patterns)
// Pattern A: Direct cast for widget actions
if let MyAction::Something = action.as_widget_action().cast() {
// handle...
}
// Pattern B: With widget UID matching
if let Some(uid) = action.as_widget_action().widget_uid() {
if uid == my_expected_uid {
if let MyAction::Something = action.as_widget_action().cast() {
// handle...
}
}
}
Posted Actions (From Async)
Posted from async tasks, received in next event cycle:
// In async task
Cx::post_action(DataFetchedAction { data });
SignalToUI::set_ui_signal(); // Wake UI thread
// Handling in App (NOT widget actions)
if let Some(action) = action.downcast_ref::<DataFetchedAction>() {
self.process_data(&action.data);
}
Global Actions
For app-wide state changes:
// Using cx.action() for global actions
cx.action(NavigationAction::GoBack);
// Handling
if let Some(NavigationAction::GoBack) = action.downcast_ref() {
self.navigate_back(cx);
}
Event Handling Patterns
Hit Testing
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);
// Start drag, capture, etc.
}
Hit::FingerUp(fe) => {
if fe.is_over && fe.is_primary_hit() {
if fe.was_tap() {
// Single tap
}
if fe.was_long_press() {
// Long press
}
}
}
Hit::FingerMove(fe) => {
// Drag handling
}
Hit::FingerHoverIn(_) => {
self.animator_play(cx, id!(hover.on));
}
Hit::FingerHoverOut(_) => {
self.animator_play(cx, id!(hover.off));
}
Hit::FingerScroll(se) => {
// Scroll handling
}
_ => {}
}
}
}
Keyboard Events
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);
}
_ => {}
}
}
}
Signal Events
For handling async updates:
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if let Event::Signal = event {
// Poll update queues
while let Some(update) = PENDING_UPDATES.pop() {
self.apply_update(cx, update);
}
}
}
Action Chaining Pattern
Widget emits action → Parent catches and re-emits with more context:
// In child widget
cx.widget_action(
self.widget_uid(),
&scope.path,
ItemAction::Selected(item_id),
);
// In parent widget's handle_event
if let ItemAction::Selected(item_id) = action.as_widget_action().cast() {
// Add context and forward to App
cx.widget_action(
self.widget_uid(),
&scope.path,
ListAction::ItemSelected {
list_id: self.list_id.clone(),
item_id,
},
);
}
Best Practices
- Use
DefaultNonederive: All action enums must have aNonevariant - Use
continueafter handling: Prevents unnecessary processing - Downcast pattern for async actions: Posted actions are not widget actions
- Widget action cast for UI actions: Use
as_widget_action().cast() - Always call
SignalToUI::set_ui_signal(): After posting actions from async - Centralize in App::handle_actions: Keep action handling in one place
- Use descriptive action names:
MessageAction::ReplynotMessageAction::Action1
Reference Files
references/action-patterns.md- Additional action patterns (Robrix)references/event-handling.md- Event handling reference (Robrix)references/moly-action-patterns.md- Moly-specific patterns- Store-based action forwarding
- Timer-based retry pattern
- Radio button navigation
- External link handling
- Platform-conditional actions (#[cfg])
- UiRunner event handling
Weekly Installs
3
Repository
zhanghandong/makepad-skillsInstalled on
opencode3
codex3
claude-code3
antigravity3
gemini-cli3
windsurf2