{"id":"fff6ca3b-e365-4b2b-a741-a459fea6f4fa","shortId":"XxHQB5","kind":"skill","title":"robius-event-action","tagline":"CRITICAL: Use for Robius event and action patterns. Triggers on:\ncustom action, MatchEvent, post_action, cx.widget_action,\nhandle_actions, DefaultNone, widget action, event handling,\n事件处理, 自定义动作","description":"# Robius Event and Action Patterns Skill\n\nBest practices for event handling and action patterns in Makepad applications based on Robrix and Moly codebases.\n\n**Source codebases:**\n- **Robrix**: Matrix chat client - MessageAction, RoomsListAction, AppStateAction\n- **Moly**: AI chat application - StoreAction, ChatAction, NavigationAction, Timer patterns\n\n## When to Use\nUse this skill when:\n- Implementing custom actions in Makepad\n- Handling events in widgets\n- Centralizing action handling in App\n- Widget-to-widget communication\n- Keywords: makepad action, makepad event, widget action, handle_actions, cx.widget_action\n\n## Custom Action Pattern\n\n### Defining Domain-Specific Actions\n\n```rust\nuse makepad_widgets::*;\n\n/// Actions emitted by the Message widget\n#[derive(Clone, DefaultNone, Debug)]\npub enum MessageAction {\n    /// User wants to react to a message\n    React { details: MessageDetails, reaction: String },\n    /// User wants to reply to a message\n    Reply(MessageDetails),\n    /// User wants to edit a message\n    Edit(MessageDetails),\n    /// User wants to delete a message\n    Delete(MessageDetails),\n    /// User requested to open context menu\n    OpenContextMenu { details: MessageDetails, abs_pos: DVec2 },\n    /// Required default variant\n    None,\n}\n\n/// Data associated with a message action\n#[derive(Clone, Debug)]\npub struct MessageDetails {\n    pub room_id: OwnedRoomId,\n    pub event_id: OwnedEventId,\n    pub content: String,\n    pub sender_id: OwnedUserId,\n}\n```\n\n### Emitting Actions from Widgets\n\n```rust\nimpl Widget for Message {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        self.view.handle_event(cx, event, scope);\n\n        let area = self.view.area();\n        match event.hits(cx, area) {\n            Hit::FingerDown(_fe) => {\n                cx.set_key_focus(area);\n            }\n            Hit::FingerUp(fe) => {\n                if fe.is_over && fe.is_primary_hit() && fe.was_tap() {\n                    // Emit widget action\n                    cx.widget_action(\n                        self.widget_uid(),\n                        &scope.path,\n                        MessageAction::Reply(self.get_details()),\n                    );\n                }\n            }\n            Hit::FingerLongPress(lpe) => {\n                cx.widget_action(\n                    self.widget_uid(),\n                    &scope.path,\n                    MessageAction::OpenContextMenu {\n                        details: self.get_details(),\n                        abs_pos: lpe.abs,\n                    },\n                );\n            }\n            _ => {}\n        }\n    }\n}\n```\n\n## Centralized Action Handling in App\n\n### Using MatchEvent Trait\n\n```rust\nimpl MatchEvent for App {\n    fn handle_startup(&mut self, cx: &mut Cx) {\n        // Called once on app startup\n        self.initialize(cx);\n    }\n\n    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {\n        for action in actions {\n            // Pattern 1: Direct downcast for non-widget actions\n            if let Some(action) = action.downcast_ref::<LoginAction>() {\n                match action {\n                    LoginAction::LoginSuccess => {\n                        self.app_state.logged_in = true;\n                        self.update_ui_visibility(cx);\n                    }\n                    LoginAction::LoginFailure(error) => {\n                        self.show_error(cx, error);\n                    }\n                }\n                continue;  // Action handled\n            }\n\n            // Pattern 2: Widget action cast\n            if let MessageAction::OpenContextMenu { details, abs_pos } =\n                action.as_widget_action().cast()\n            {\n                self.show_context_menu(cx, details, abs_pos);\n                continue;\n            }\n\n            // Pattern 3: Match on downcast_ref for enum variants\n            match action.downcast_ref() {\n                Some(AppStateAction::RoomFocused(room)) => {\n                    self.app_state.selected_room = Some(room.clone());\n                    continue;\n                }\n                Some(AppStateAction::NavigateToRoom { destination }) => {\n                    self.navigate_to_room(cx, destination);\n                    continue;\n                }\n                _ => {}\n            }\n\n            // Pattern 4: Modal actions\n            match action.downcast_ref() {\n                Some(ModalAction::Open { kind }) => {\n                    self.ui.modal(ids!(my_modal)).open(cx);\n                    continue;\n                }\n                Some(ModalAction::Close { was_internal }) => {\n                    if *was_internal {\n                        self.ui.modal(ids!(my_modal)).close(cx);\n                    }\n                    continue;\n                }\n                _ => {}\n            }\n        }\n    }\n}\n\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        // Forward to MatchEvent\n        self.match_event(cx, event);\n\n        // Pass events to widget tree\n        let scope = &mut Scope::with_data(&mut self.app_state);\n        self.ui.handle_event(cx, event, scope);\n    }\n}\n```\n\n## Action Types\n\n### Widget Actions (UI Thread)\n\nEmitted by widgets, handled in the same frame:\n\n```rust\n// Emitting\ncx.widget_action(\n    self.widget_uid(),\n    &scope.path,\n    MyAction::Something,\n);\n\n// Handling (two patterns)\n// Pattern A: Direct cast for widget actions\nif let MyAction::Something = action.as_widget_action().cast() {\n    // handle...\n}\n\n// Pattern B: With widget UID matching\nif let Some(uid) = action.as_widget_action().widget_uid() {\n    if uid == my_expected_uid {\n        if let MyAction::Something = action.as_widget_action().cast() {\n            // handle...\n        }\n    }\n}\n```\n\n### Posted Actions (From Async)\n\nPosted from async tasks, received in next event cycle:\n\n```rust\n// In async task\nCx::post_action(DataFetchedAction { data });\nSignalToUI::set_ui_signal();  // Wake UI thread\n\n// Handling in App (NOT widget actions)\nif let Some(action) = action.downcast_ref::<DataFetchedAction>() {\n    self.process_data(&action.data);\n}\n```\n\n### Global Actions\n\nFor app-wide state changes:\n\n```rust\n// Using cx.action() for global actions\ncx.action(NavigationAction::GoBack);\n\n// Handling\nif let Some(NavigationAction::GoBack) = action.downcast_ref() {\n    self.navigate_back(cx);\n}\n```\n\n## Event Handling Patterns\n\n### Hit Testing\n\n```rust\nimpl Widget for MyWidget {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        let area = self.view.area();\n        match event.hits(cx, area) {\n            Hit::FingerDown(fe) => {\n                cx.set_key_focus(area);\n                // Start drag, capture, etc.\n            }\n            Hit::FingerUp(fe) => {\n                if fe.is_over && fe.is_primary_hit() {\n                    if fe.was_tap() {\n                        // Single tap\n                    }\n                    if fe.was_long_press() {\n                        // Long press\n                    }\n                }\n            }\n            Hit::FingerMove(fe) => {\n                // Drag handling\n            }\n            Hit::FingerHoverIn(_) => {\n                self.animator_play(cx, id!(hover.on));\n            }\n            Hit::FingerHoverOut(_) => {\n                self.animator_play(cx, id!(hover.off));\n            }\n            Hit::FingerScroll(se) => {\n                // Scroll handling\n            }\n            _ => {}\n        }\n    }\n}\n```\n\n### Keyboard Events\n\n```rust\nfn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n    if let Event::KeyDown(ke) = event {\n        match ke.key_code {\n            KeyCode::Return if !ke.modifiers.shift => {\n                self.submit(cx);\n            }\n            KeyCode::Escape => {\n                self.cancel(cx);\n            }\n            KeyCode::KeyC if ke.modifiers.control || ke.modifiers.logo => {\n                self.copy_to_clipboard(cx);\n            }\n            _ => {}\n        }\n    }\n}\n```\n\n### Signal Events\n\nFor handling async updates:\n\n```rust\nfn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n    if let Event::Signal = event {\n        // Poll update queues\n        while let Some(update) = PENDING_UPDATES.pop() {\n            self.apply_update(cx, update);\n        }\n    }\n}\n```\n\n## Action Chaining Pattern\n\nWidget emits action → Parent catches and re-emits with more context:\n\n```rust\n// In child widget\ncx.widget_action(\n    self.widget_uid(),\n    &scope.path,\n    ItemAction::Selected(item_id),\n);\n\n// In parent widget's handle_event\nif let ItemAction::Selected(item_id) = action.as_widget_action().cast() {\n    // Add context and forward to App\n    cx.widget_action(\n        self.widget_uid(),\n        &scope.path,\n        ListAction::ItemSelected {\n            list_id: self.list_id.clone(),\n            item_id,\n        },\n    );\n}\n```\n\n## Best Practices\n\n1. **Use `DefaultNone` derive**: All action enums must have a `None` variant\n2. **Use `continue` after handling**: Prevents unnecessary processing\n3. **Downcast pattern for async actions**: Posted actions are not widget actions\n4. **Widget action cast for UI actions**: Use `as_widget_action().cast()`\n5. **Always call `SignalToUI::set_ui_signal()`**: After posting actions from async\n6. **Centralize in App::handle_actions**: Keep action handling in one place\n7. **Use descriptive action names**: `MessageAction::Reply` not `MessageAction::Action1`\n\n## Reference Files\n\n- `references/action-patterns.md` - Additional action patterns (Robrix)\n- `references/event-handling.md` - Event handling reference (Robrix)\n- `references/moly-action-patterns.md` - Moly-specific patterns\n  - Store-based action forwarding\n  - Timer-based retry pattern\n  - Radio button navigation\n  - External link handling\n  - Platform-conditional actions (#[cfg])\n  - UiRunner event handling\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["robius","event","action","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding"],"capabilities":["skill","source-sickn33","skill-robius-event-action","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/robius-event-action","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34583 github stars · SKILL.md body (10,241 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-22T18:52:08.976Z","embedding":null,"createdAt":"2026-04-18T21:43:39.211Z","updatedAt":"2026-04-22T18:52:08.976Z","lastSeenAt":"2026-04-22T18:52:08.976Z","tsv":"'1':337,873 '2':373,885 '3':397,893 '4':428,905 '5':917 '6':929 '7':941 'ab':180,291,382,393 'action':4,11,16,19,21,23,26,34,43,81,89,100,104,106,108,110,116,121,192,215,268,270,282,295,324,330,331,333,335,344,348,352,370,375,386,430,500,503,517,532,539,554,568,572,590,605,609,616,628,809,814,829,851,860,878,898,900,904,907,911,915,926,934,936,944,955,971,987 'action.as':384,537,552,566,849 'action.data':614 'action.downcast':349,406,432,610,638 'action1':950 'add':853 'addit':954 'ai':64 'alway':918 'app':92,298,306,318,463,602,619,858,932 'app-wid':618 'applic':47,66 'appmain':461 'appstateact':62,409,418 'area':242,247,254,667,672,679 'ask':1025 'associ':188 'async':574,577,586,776,897,928 'b':543 'back':641 'base':48,970,975 'best':37,871 'boundari':1033 'button':979 'call':315,919 'captur':682 'cast':376,387,529,540,569,852,908,916 'catch':816 'central':88,294,930 'cfg':988 'chain':810 'chang':622 'chat':58,65 'chatact':68 'child':826 'clarif':1027 'clear':1000 'client':59 'clipboard':770 'clone':128,194 'close':447,457 'code':752 'codebas':53,55 'communic':97 'condit':986 'content':208 'context':175,389,823,854 'continu':369,395,416,426,444,459,887 'criteria':1036 'critic':5 'custom':15,80,109 'cx':228,230,238,246,312,314,321,327,329,361,367,391,424,443,458,469,471,479,497,588,642,658,660,671,713,720,736,738,758,762,771,784,786,807 'cx.action':625,629 'cx.set':251,676 'cx.widget':20,107,269,281,516,828,859 'cycl':583 'data':187,491,592,613 'datafetchedact':591 'debug':130,195 'default':184 'defaultnon':24,129,875 'defin':112 'delet':166,169 'deriv':127,193,876 'describ':1004 'descript':943 'destin':420,425 'detail':142,178,277,288,290,381,392 'direct':338,528 'domain':114 'domain-specif':113 'downcast':339,400,894 'drag':681,707 'dvec2':182 'edit':158,161 'emit':122,214,266,506,515,813,820 'enum':132,403,879 'environ':1016 'environment-specif':1015 'error':364,366,368 'escap':760 'etc':683 'event':3,9,27,32,40,85,102,204,225,231,232,237,239,466,472,473,478,480,482,496,498,582,643,655,661,662,729,733,739,740,746,749,773,781,787,788,794,796,842,959,990 'event.hits':245,670 'expect':560 'expert':1021 'extern':981 'fe':250,257,675,686,706 'fe.is':259,261,688,690 'fe.was':264,694,699 'file':952 'fingerdown':249,674 'fingerhoverin':710 'fingerhoverout':717 'fingerlongpress':279 'fingermov':705 'fingerscrol':724 'fingerup':256,685 'fn':223,307,322,464,653,731,779 'focus':253,678 'forward':474,856,972 'frame':513 'global':615,627 'goback':631,637 'handl':22,28,41,84,90,105,224,296,308,323,371,465,509,523,541,570,600,632,644,654,708,727,732,775,780,841,889,933,937,960,983,991 'hit':248,255,263,278,646,673,684,692,704,709,716,723 'hover.off':722 'hover.on':715 'id':201,205,212,439,454,714,721,836,848,867,870 'impl':219,303,460,649 'implement':79 'input':1030 'intern':449,452 'item':835,847,869 'itemact':833,845 'itemselect':865 'ke':748 'ke.key':751 'ke.modifiers.control':766 'ke.modifiers.logo':767 'ke.modifiers.shift':756 'keep':935 'key':252,677 'keyboard':728 'keyc':764 'keycod':753,759,763 'keydown':747 'keyword':98 'kind':437 'let':241,346,378,486,534,549,563,607,634,666,745,793,801,844 'limit':992 'link':982 'list':866 'listact':864 'loginact':353,362 'loginfailur':363 'loginsuccess':354 'long':700,702 'lpe':280 'lpe.abs':293 'makepad':46,83,99,101,119 'match':244,351,398,405,431,547,669,750,1001 'matchev':17,300,304,476 'matrix':57 'menu':176,390 'messag':125,140,152,160,168,191,222 'messageact':60,133,274,286,379,946,949 'messagedetail':143,154,162,170,179,198 'miss':1038 'modal':429,441,456 'modalact':435,446 'moli':52,63,965 'moly-specif':964 'must':880 'mut':226,229,234,310,313,325,328,467,470,488,492,656,659,664,734,737,742,782,785,790 'myaction':521,535,564 'mywidget':652 'name':945 'navig':980 'navigatetoroom':419 'navigationact':69,630,636 'next':581 'non':342 'non-widget':341 'none':186,883 'one':939 'open':174,436,442 'opencontextmenu':177,287,380 'output':1010 'ownedeventid':206 'ownedroomid':202 'owneduserid':213 'parent':815,838 'pass':481 'pattern':12,35,44,71,111,336,372,396,427,525,526,542,645,811,895,956,967,977 'pending_updates.pop':804 'permiss':1031 'place':940 'platform':985 'platform-condit':984 'play':712,719 'poll':797 'pos':181,292,383,394 'post':18,571,575,589,899,925 'practic':38,872 'press':701,703 'prevent':890 'primari':262,691 'process':892 'pub':131,196,199,203,207,210 'queue':799 'radio':978 're':819 're-emit':818 'react':137,141 'reaction':144 'receiv':579 'ref':350,401,407,433,611,639 'refer':951,961 'references/action-patterns.md':953 'references/event-handling.md':958 'references/moly-action-patterns.md':963 'repli':149,153,275,947 'request':172 'requir':183,1029 'retri':976 'return':754 'review':1022 'robius':2,8,31 'robius-event-act':1 'robrix':50,56,957,962 'room':200,411,413,423 'room.clone':415 'roomfocus':410 'roomslistact':61 'rust':117,218,302,514,584,623,648,730,778,824 'safeti':1032 'scope':233,235,240,487,489,499,663,665,741,743,789,791,1003 'scope.path':273,285,520,832,863 'scroll':726 'se':725 'select':834,846 'self':227,311,326,468,657,735,783 'self.animator':711,718 'self.app':493 'self.app_state.logged':355 'self.app_state.selected':412 'self.apply':805 'self.cancel':761 'self.copy':768 'self.get':276,289 'self.initialize':320 'self.list_id.clone':868 'self.match':477 'self.navigate':421,640 'self.process':612 'self.show':365,388 'self.submit':757 'self.ui.handle':495 'self.ui.modal':438,453 'self.update':358 'self.view.area':243,668 'self.view.handle':236 'self.widget':271,283,518,830,861 'sender':211 'set':594,921 'signal':596,772,795,923 'signaltoui':593,920 'singl':696 'skill':36,77,995 'skill-robius-event-action' 'someth':522,536,565 'sourc':54 'source-sickn33' 'specif':115,966,1017 'start':680 'startup':309,319 'state':494,621 'stop':1023 'store':969 'store-bas':968 'storeact':67 'string':145,209 'struct':197 'substitut':1013 'success':1035 'tap':265,695,697 'task':578,587,999 'test':647,1019 'thread':505,599 'timer':70,974 'timer-bas':973 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'trait':301 'treat':1008 'tree':485 'trigger':13 'true':357 'two':524 'type':501 'ui':359,504,595,598,910,922 'uid':272,284,519,546,551,556,558,561,831,862 'uirunn':989 'unnecessari':891 'updat':777,798,803,806,808 'use':6,74,75,118,299,624,874,886,912,942,993 'user':134,146,155,163,171 'valid':1018 'variant':185,404,884 'visibl':360 'wake':597 'want':135,147,156,164 'wide':620 'widget':25,87,94,96,103,120,126,217,220,267,343,374,385,484,502,508,531,538,545,553,555,567,604,650,812,827,839,850,903,906,914 'widget-to-widget':93 '事件处理':29 '自定义动作':30","prices":[{"id":"d3a240fd-5531-484e-9513-3c0c49927277","listingId":"fff6ca3b-e365-4b2b-a741-a459fea6f4fa","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:43:39.211Z"}],"sources":[{"listingId":"fff6ca3b-e365-4b2b-a741-a459fea6f4fa","source":"github","sourceId":"sickn33/antigravity-awesome-skills/robius-event-action","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/robius-event-action","isPrimary":false,"firstSeenAt":"2026-04-18T21:43:39.211Z","lastSeenAt":"2026-04-22T18:52:08.976Z"}],"details":{"listingId":"fff6ca3b-e365-4b2b-a741-a459fea6f4fa","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"robius-event-action","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34583,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-22T06:40:00Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"19e385cbd15725fa36f1d1681dca0d8f016487bd","skill_md_path":"skills/robius-event-action/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/robius-event-action"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"robius-event-action","description":"CRITICAL: Use for Robius event and action patterns. Triggers on:\ncustom action, MatchEvent, post_action, cx.widget_action,\nhandle_actions, DefaultNone, widget action, event handling,\n事件处理, 自定义动作"},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/robius-event-action"},"updatedAt":"2026-04-22T18:52:08.976Z"}}