Install any skill in seconds. Free to start, no credit card required.
Get Started Free →CRITICAL: Use for Makepad 2.0 event and action handling. Triggers on: makepad event, makepad action, MatchEvent, handle_event, handle_actions, on_click, on_render, on_return, on_startup, script_eval!, script_apply_eval!, button clicked, text changed, slider changed, checkbox toggled, Hit, FingerDown, FingerUp, KeyDown, KeyUp, Focus, ids!, TextCopy, TextCut, SelectionHandleDrag, PopupDismissed, clipboard, selection, IME, ImeAction, popup window events, video inputs, camera events, 事件, 动作, 点击, 输入,
.claude/skills/zhanghandong-makepad-2-0-events/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 276% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 315% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 640% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 312% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 256% | 0% |
Makepad 2.0 uses a two-layer event system:
script_mod! Splash code(on_click, on_render, on_return, on_startup). These handle UI interactions declaratively inside the script, close to the widget definitions.
MatchEvent trait with handle_actions, handle_timer,handle_http_response, etc. These handle business logic, external I/O, and anything that needs full Rust power.
Both layers communicate through two bridge macros:
script_eval!(cx, { ... }) -- Execute Splash code from Rust (update state, trigger renders)script_apply_eval!(cx, widget_ref, { ... }) -- Patch widget properties from Rust at runtimeEvent 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.
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.
splash// 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() } }
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.
splashmain_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.
Fires when the user presses Enter/Return inside a TextInput. Commonly used to submit forms.
splashtodo_input := TextInput{ width: Fill height: 9. * theme.space_1 empty_text: "What needs to be done?" on_return: || ui.add_button.on_click() }
Fires once when the application starts. Defined at the Root level. Commonly used to trigger initial renders.
splashui: Root{ on_startup: ||{ ui.main_view.render() } main_window := Window{ // ... } }
Inside event handlers you can:
add_todo(text, "dev")let text = ui.todo_input.text()ui.todo_input.set_text("")ui.todo_list.render()ui.add_button.on_click()state.counter += 1todos.push({text: "new", done: false})if text != "" { ... }The MatchEvent trait is the Rust-side event dispatcher. It receives platform events and widget actions through a set of handler methods.
draw/src/match_event.rs)rustpub 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) {} }
Every Makepad 2.0 app needs this Rust structure:
rustuse 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).
Access widgets from Rust using self.ui.widget_type(cx, ids!(name)), then query their action state by passing the &Actions reference.
rust// 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>
rust// 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)
rust// Access: self.ui.check_box(cx, ids!(my_check)) // Returns CheckBoxRef .changed(actions) -> Option<bool> // Toggled, returns new checked state
rust// 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)
rust// 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)
rust// 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
rust// Access: self.ui.link_label(cx, ids!(my_link)) // Returns LinkLabelRef .clicked(actions) -> bool .clicked_modifiers(actions) -> Option<KeyModifiers>
rustimpl 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); } } }
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).
rust// 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) } }
#(expr)Inside script_eval!, use #(rust_expression) to inject Rust values into Splash:
rustlet count = 42_u64; let message = "items found".to_string(); script_eval!(cx, { mod.state.count = #(count) mod.state.message = #(message) ui.results_view.render() });
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 })
rust// 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) } });
Use self.ui.widget_type(cx, ids!(name)) to get a typed reference:
rustself.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
Use dot notation to access nested widgets:
rustself.ui.label(cx, ids!(container.inner.child_label)) self.ui.button(cx, ids!(toolbar.save_button))
rustself.ui.widget(cx, ids!(my_widget)) // -> WidgetRef (untyped)
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);
Inside script_mod! Splash code, access widgets through the ui prefix:
splash// 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()
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.
rustpub 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, }
rustimpl 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 */ } _ => {} } } _ => {} } } }
Timers are created via Cx methods and received in handle_timer.
rust// 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);
rust#[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); } } }
rustlet 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);
rustimpl 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 } }
splashlet 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 } }
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 Actionsrustuse 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()); } }
splash// 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 }
splashlet 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) } } } } }
rust// 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() }); } } }
| File | Purpose | |------|---------| | draw/src/match_event.rs | MatchEvent trait definition | | widgets/src/widget_match_event.rs | WidgetMatchEvent trait (for custom widgets) | | platform/src/event/event.rs | Event enum, Hit enum, Timer | | platform/src/event/finger.rs | Finger/pointer event types | | platform/script/src/lib.rs | script_eval! macro definition | | platform/script/derive/src/lib.rs | script_apply_eval! proc macro | | widgets/src/button.rs | Button action API | | widgets/src/text_input.rs | TextInput action API | | widgets/src/check_box.rs | CheckBox action API | | widgets/src/drop_down.rs | DropDown action API | | widgets/src/slider.rs | Slider action API | | widgets/src/radio_button.rs | RadioButton action API | | examples/counter/src/app.rs | Counter example (minimal) | | examples/todo/src/app.rs | Todo example (comprehensive) | | examples/git/src/app.rs | Git example (HTTP, timers) | | examples/camera/src/ | Camera example (media events) | | examples/text_selection/src/ | Text selection example |
New Hit variants for text selection and clipboard operations:
rustHit::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:
rustEvent::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.
For popup windows (context menus, dropdowns):
rustEvent::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 (Input Method Editor) support for CJK text input:
rustHit::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.
rust// 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.
Linux primary selection (middle-click paste):
zwp_primary_selection_device_manager_v1 protocolOther measured skills in the registry, with their headline benchmark lift.