Makepad-skills makepad-2.0-events
git clone https://github.com/ZhangHanDong/makepad-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/ZhangHanDong/makepad-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/makepad-2.0-events" ~/.claude/skills/zhanghandong-makepad-skills-makepad-2-0-events && rm -rf "$T"
skills/makepad-2.0-events/SKILL.mdMakepad 2.0 Event & Action System
Overview
Makepad 2.0 uses a two-layer event system:
-
Splash Layer -- Inline event handlers written directly in
Splash code (script_mod!
,on_click
,on_render
,on_return
). These handle UI interactions declaratively inside the script, close to the widget definitions.on_startup -
Rust Layer -- The
trait withMatchEvent
,handle_actions
,handle_timer
, etc. These handle business logic, external I/O, and anything that needs full Rust power.handle_http_response
Both layers communicate through two bridge macros:
-- Execute Splash code from Rust (update state, trigger renders)script_eval!(cx, { ... })
-- Patch widget properties from Rust at runtimescript_apply_eval!(cx, widget_ref, { ... })
1. Splash Inline Event Handlers
Event handlers are attached directly to widgets inside
script_mod! blocks. They use
closure syntax with || for no arguments or |arg| for callbacks that receive a value.
on_click -- Button/widget click
Fires when the user clicks a button or clickable widget. No arguments for plain buttons, or
|checked| for CheckBox which passes the new boolean state.
// 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 -- Dynamic rendering
Fires when
.render() is called on the target view. This is the primary mechanism for
dynamic content. The body replaces the previous draw content of the view.
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{} }
Key point:
on_render is NOT called automatically. You must call ui.widget_name.render()
to trigger it. The new_batch: true property on a view tells the system to clear previous
draw content before re-rendering.
on_return -- TextInput enter key
Fires when the user presses Enter/Return inside a TextInput. Commonly used to submit forms.
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 -- App startup
Fires once when the application starts. Defined at the
Root level. Commonly used
to trigger initial renders.
ui: Root{ on_startup: ||{ ui.main_view.render() } main_window := Window{ // ... } }
Event handler capabilities
Inside event handlers you can:
- Call Splash functions:
add_todo(text, "dev") - Read widget values:
let text = ui.todo_input.text() - Set widget values:
ui.todo_input.set_text("") - Trigger re-renders:
ui.todo_list.render() - Trigger other widget clicks:
ui.add_button.on_click() - Modify state variables:
state.counter += 1 - Use array methods:
todos.push({text: "new", done: false}) - Use control flow:
if text != "" { ... }
2. Rust Event Handling -- MatchEvent Trait
The
MatchEvent trait is the Rust-side event dispatcher. It receives platform events
and widget actions through a set of handler methods.
Core trait definition (from draw/src/match_event.rs
)
draw/src/match_event.rspub 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) {} }
Standard App boilerplate (required)
Every Makepad 2.0 app needs this Rust structure:
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()); } }
CRITICAL:
handle_event must call BOTH self.match_event(cx, event) (to dispatch
to the MatchEvent handlers) AND self.ui.handle_event(cx, event, &mut Scope::empty())
(to propagate events to widgets).
3. Widget Action API
Access widgets from Rust using
self.ui.widget_type(cx, ids!(name)), then query their
action state by passing the &Actions reference.
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>
Complete example
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! Macro -- Rust to Splash Communication
script_eval! executes Splash code from within Rust handlers. It is the primary
bridge for updating Splash state and triggering UI re-renders from Rust.
Signature:
script_eval!(cx_or_vm, { splash_code })
The first argument can be
&mut Cx (inside event handlers) or &mut ScriptVm
(during initialization).
// 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) } }
Value interpolation with #(expr)
#(expr)Inside
script_eval!, use #(rust_expression) to inject Rust values into 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! Macro -- Runtime Property Patching
script_apply_eval! patches widget properties at runtime from Rust. Unlike
script_eval! which runs general Splash code, this targets a specific widget
reference and applies property changes directly.
Signature:
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. Widget Reference Access from Rust
Accessing widgets by ID
Use
self.ui.widget_type(cx, ids!(name)) to get a typed reference:
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
Nested widget access with ids!
Use dot notation to access nested widgets:
self.ui.label(cx, ids!(container.inner.child_label)) self.ui.button(cx, ids!(toolbar.save_button))
Generic widget access
self.ui.widget(cx, ids!(my_widget)) // -> WidgetRef (untyped)
Common widget setter methods (called from 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. Widget Methods from Splash
Inside
script_mod! Splash code, access widgets through the ui prefix:
// 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. Hit Events (Low-Level)
For custom widgets that need raw input handling, the
Hit enum provides low-level
events. Access them through event.hits(cx, area) in a widget's handle_event.
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, }
Usage in custom widgets
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. Timer Events
Timers are created via
Cx methods and received in handle_timer.
Creating timers
// 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);
Handling timer events
#[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 / Network Events
Making HTTP requests from Rust
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);
Handling HTTP responses
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 } }
Making HTTP requests from Splash
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. Event Flow Diagram
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. Complete Patterns
Pattern 1: Counter (button click -> state update -> re-render)
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()); } }
Pattern 2: Form (text input -> validation -> submission)
// 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: "your@email.com" on_return: || submit_form() } submit_button := Button{ text: "Submit" on_click: || submit_form() } error_label := Label{ text: "" draw_text.color: #f00 }
Pattern 3: List with per-item events
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) } } } } }
Pattern 4: Cross-layer communication (Splash state + Rust logic)
// 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() }); } } }
Source Files Reference
| File | Purpose |
|---|---|
| trait definition |
| trait (for custom widgets) |
| enum, enum, |
| Finger/pointer event types |
| macro definition |
| proc macro |
| Button action API |
| TextInput action API |
| CheckBox action API |
| DropDown action API |
| Slider action API |
| RadioButton action API |
| Counter example (minimal) |
| Todo example (comprehensive) |
| Git example (HTTP, timers) |
| Camera example (media events) |
| Text selection example |
13. New Events (March 2026)
Selection & Clipboard Events
New
Hit variants for text selection and clipboard operations:
Hit::TextCopy(TextClipboardEvent) // User copied text (Ctrl+C or native toolbar) Hit::TextCut(TextClipboardEvent) // User cut text (Ctrl+X or native toolbar)
Mobile Selection Handles: Native selection handles on iOS/Android emit:
Event::SelectionHandleDrag // User dragging a selection handle
Long-press on text selects a word and shows the native clipboard toolbar. TextFlow widget integrates clipboard actions automatically.
Popup Window Events
For popup windows (context menus, dropdowns):
Event::WindowClosed // Popup window was closed Event::PopupDismissed // Popup was dismissed by compositor (Wayland) or click-outside
WindowClosed is always emitted before PopupDismissed. Apps must handle PopupDismissed for explicit-close semantics.
IME Events
IME (Input Method Editor) support for CJK text input:
Hit::ImeAction(ImeActionEvent) // IME composition committed/cancelled Hit::TextRangeReplace(TextRangeReplaceEvent) // IME text replacement
Linux X11 IME: Full IME support added for X11. IME popup window positioning fixed to avoid appearing without TextInput focus.
Video & Media Events
// In MatchEvent trait: fn handle_video_inputs(&mut self, _cx: &mut Cx, _e: &VideoInputsEvent) {}
Video input device enumeration events from the media plugin system. Camera and video playback events route through the media plugin architecture.
Primary Selection (Linux)
Linux primary selection (middle-click paste):
- Wayland:
protocolzwp_primary_selection_device_manager_v1 - X11: PRIMARY atom handling
- Integrated with TextInput and TextFlow widgets automatically