Files
Al-Garadi ef285b5c97 fix: sync upstream main with Windows validation and skill guidance cleanup (#457)
* fix: stabilize validation and tests on Windows

* test: add Windows smoke coverage for skill activation

* refactor: make setup_web script CommonJS

* fix: repair aegisops-ai frontmatter

* docs: add when-to-use guidance to core skills

* docs: add when-to-use guidance to Apify skills

* docs: add when-to-use guidance to Google and Expo skills

* docs: add when-to-use guidance to Makepad skills

* docs: add when-to-use guidance to git workflow skills

* docs: add when-to-use guidance to fp-ts skills

* docs: add when-to-use guidance to Three.js skills

* docs: add when-to-use guidance to n8n skills

* docs: add when-to-use guidance to health analysis skills

* docs: add when-to-use guidance to writing and review skills

* meta: sync generated catalog metadata

* docs: add when-to-use guidance to Robius skills

* docs: add when-to-use guidance to review and workflow skills

* docs: add when-to-use guidance to science and data skills

* docs: add when-to-use guidance to tooling and automation skills

* docs: add when-to-use guidance to remaining skills

* fix: gate bundle helper execution in Windows activation

* chore: drop generated artifacts from contributor PR

* docs(maintenance): Record PR 457 sweep

Document the open issue triage, PR supersedence decision, local verification, and source-only cleanup that prepared PR #457 for re-running CI.

---------

Co-authored-by: sickn33 <sickn33@users.noreply.github.com>
2026-04-05 21:04:39 +02:00

10 KiB

name, description, risk, source
name description risk source
robius-event-action CRITICAL: Use for Robius event and action patterns. Triggers on: custom action, MatchEvent, post_action, cx.widget_action, handle_actions, DefaultNone, widget action, event handling, 事件处理, 自定义动作 unknown community

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

When to Use

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

  1. Use DefaultNone derive: All action enums must have a None variant
  2. Use continue after handling: Prevents unnecessary processing
  3. Downcast pattern for async actions: Posted actions are not widget actions
  4. Widget action cast for UI actions: Use as_widget_action().cast()
  5. Always call SignalToUI::set_ui_signal(): After posting actions from async
  6. Centralize in App::handle_actions: Keep action handling in one place
  7. Use descriptive action names: MessageAction::Reply not MessageAction::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