// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use std::sync::Arc; use crate::types::{ Length, Point, Rect }; use super::{ slider, text }; /// Per-leaf interaction snapshot captured during layout. One variant per /// interactive widget kind; the layout pass clones the relevant callbacks / /// values from the [`Element`](super::Element) tree into here so input handlers can dispatch /// in O(1) without re-walking the tree. /// /// `None` is used for focusable widgets that emit no message (e.g. a disabled /// button or a focusable container) — the entry still appears in /// `widget_rects` for hit testing and focus traversal. pub enum WidgetHandlers { None, Button { on_press: Option, on_long_press: Option, on_drag_start: Option, /// Keyboard `Escape`-key message — the runtime scans every laid-out /// `Button` snapshot in reverse and fires the first non-`None` /// `on_escape` it finds before the default ESC fallthrough chain. /// Currently sourced from /// [`crate::widget::pressable::Pressable::on_escape`]; native /// [`crate::widget::button::Button`] always sets this to `None`. on_escape: Option, repeating: bool, }, Toggle { on_toggle: Option, value: bool }, Checkbox { on_toggle: Option, value: bool }, Radio { on_select: Option, selected: bool }, ListItem { on_press: Option }, WindowButton { on_press: Option }, TextEdit { value: String, on_change: Option Msg>>, on_submit: Option, on_focus: Option, /// `true` when the source `TextEdit` was built with /// `.secure( true )`. Propagates the wipe-on-drop behaviour to /// every per-frame handler snapshot so credential text does not /// linger across multiple cloned `WidgetHandlers` allocations. secure: bool, purpose: crate::osk::Purpose, /// `true` when the source `TextEdit` was built with /// `.multiline( true )`. The keyboard dispatch reads this so /// pressing Enter inserts a `\n` instead of firing /// [`Self::submit_msg`]. Mutually exclusive with `secure`. multiline: bool, /// Horizontal alignment snapshot — needed by the runtime's /// hit-testing path so a click on a centred / right-aligned /// field lands on the correct glyph. align: text::TextAlign, /// Font size snapshot — needed by the hit-testing path so /// the runtime measures glyphs at the same size the renderer /// drew them. `None` for fields that do not call /// `.font_size( … )` (they follow the widget-scaling mode). font_size: Option, /// Horizontal inner padding override snapshot, for the same hit-testing. pad_h: Option, /// `true` when the source field opted into select-all-on- /// focus. The runtime reads this in `set_focus` to decide /// whether the new selection should anchor at `0` (replace /// on next keystroke) or at the cursor (insert). select_on_focus: bool, /// `true` when edits keep the cursor at the end of the displayed value. pin_cursor_to_end: bool, /// Snapshot of the /// [`crate::widget::text_edit::TextEdit::password_toggle`] /// callback. When `Some`, pointer / touch dispatch checks /// the eye-icon hit zone (via /// [`crate::widget::text_edit::password_toggle_hit_zone`]) /// before falling through to cursor placement and fires /// this message instead. password_toggle_msg: Option, }, Slider { on_change: Option Msg>>, /// Fired once when the drag ends, with the final value. Lets an app /// keep an expensive commit (a subprocess, a D-Bus round trip, a /// compositor reconfigure) off the per-motion path. on_release: Option Msg>>, axis: slider::SliderAxis, value: f32, /// Thumb size resolved through the widget-scaling mode at layout /// time. Feeds `value_from_pos_in_rect` so a click maps against the /// same thumb the renderer drew. Unused by the vertical axis. thumb_px: f32, }, /// A drag target like a slider, but reporting the node under the /// pointer instead of a value. PatternGrid { on_node: Option Msg>>, on_release: Option, /// Snap radius, resolved through the widget-scaling mode at layout time. snap_px: f32, }, } impl Drop for WidgetHandlers { /// Mirror the wipe-on-drop behaviour of [`super::text_edit::TextEdit`] for the /// per-frame handler snapshots the runtime keeps. When the snapshot /// was built from a secure text edit we scrub the value bytes here so /// the heap allocation that backs the cloned `String` is overwritten /// before it is returned to the allocator. Non-secure variants run an /// inert path; the field-level drops that fire after this fn handle /// the rest of the data. fn drop( &mut self ) { if let WidgetHandlers::TextEdit { value, secure: true, .. } = self { // SAFETY: identical reasoning to `TextEdit::drop` — we only // write zeros into the underlying byte buffer, leaving valid // UTF-8 (NUL bytes) in place until the auto-drop frees it. let bytes = unsafe { value.as_mut_vec() }; crate::secure_mem::secure_zero( bytes ); } } } impl Clone for WidgetHandlers { fn clone( &self ) -> Self { match self { WidgetHandlers::None => WidgetHandlers::None, WidgetHandlers::Button { on_press, on_long_press, on_drag_start, on_escape, repeating } => WidgetHandlers::Button { on_press: on_press.clone(), on_long_press: on_long_press.clone(), on_drag_start: on_drag_start.clone(), on_escape: on_escape.clone(), repeating: *repeating, }, WidgetHandlers::Toggle { on_toggle, value } => WidgetHandlers::Toggle { on_toggle: on_toggle.clone(), value: *value }, WidgetHandlers::Checkbox { on_toggle, value } => WidgetHandlers::Checkbox { on_toggle: on_toggle.clone(), value: *value }, WidgetHandlers::Radio { on_select, selected } => WidgetHandlers::Radio { on_select: on_select.clone(), selected: *selected }, WidgetHandlers::ListItem { on_press } => WidgetHandlers::ListItem { on_press: on_press.clone() }, WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() }, WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, pad_h, select_on_focus, pin_cursor_to_end, password_toggle_msg } => { WidgetHandlers::TextEdit { value: value.clone(), on_change: on_change.clone(), on_submit: on_submit.clone(), on_focus: on_focus.clone(), secure: *secure, purpose: *purpose, multiline: *multiline, align: *align, font_size: *font_size, pad_h: *pad_h, select_on_focus: *select_on_focus, pin_cursor_to_end: *pin_cursor_to_end, password_toggle_msg: password_toggle_msg.clone(), } } WidgetHandlers::Slider { on_change, on_release, axis, value, thumb_px } => { WidgetHandlers::Slider { on_change: on_change.clone(), on_release: on_release.clone(), axis: *axis, value: *value, thumb_px: *thumb_px, } } WidgetHandlers::PatternGrid { on_node, on_release, snap_px } => { WidgetHandlers::PatternGrid { on_node: on_node.clone(), on_release: on_release.clone(), snap_px: *snap_px, } } } } } impl WidgetHandlers { pub fn is_text_input( &self ) -> bool { matches!( self, WidgetHandlers::TextEdit { .. } ) } /// What a text field is for; `Normal` for anything else. pub fn input_purpose( &self ) -> crate::osk::Purpose { match self { WidgetHandlers::TextEdit { purpose, .. } => *purpose, _ => crate::osk::Purpose::Normal, } } pub fn is_disabled( &self ) -> bool { match self { WidgetHandlers::Button { on_press, on_long_press, on_drag_start, on_escape, .. } => on_press.is_none() && on_long_press.is_none() && on_drag_start.is_none() && on_escape.is_none(), WidgetHandlers::Toggle { on_toggle, .. } => on_toggle.is_none(), WidgetHandlers::Checkbox { on_toggle, .. } => on_toggle.is_none(), WidgetHandlers::Radio { on_select, .. } => on_select.is_none(), WidgetHandlers::ListItem { on_press } => on_press.is_none(), WidgetHandlers::WindowButton { on_press } => on_press.is_none(), WidgetHandlers::Slider { on_change, .. } => on_change.is_none(), WidgetHandlers::PatternGrid { on_node, .. } => on_node.is_none(), WidgetHandlers::TextEdit { on_change, on_submit, .. } => on_change.is_none() && on_submit.is_none(), WidgetHandlers::None => true, } } /// Sliders and pattern grids both own the pointer for the whole drag. pub fn is_drag_target( &self ) -> bool { matches!( self, WidgetHandlers::Slider { .. } | WidgetHandlers::PatternGrid { .. } ) } /// The pattern-grid node under `pos`, within the snap radius. pub fn pattern_node_at( &self, rect: Rect, pos: Point ) -> Option { match self { WidgetHandlers::PatternGrid { snap_px, .. } => super::pattern_grid::node_at( rect, pos, *snap_px ), _ => None, } } /// The message a pattern grid fires for `node`, if it has an `on_node`. pub fn pattern_msg( &self, node: u8 ) -> Option { match self { WidgetHandlers::PatternGrid { on_node: Some( f ), .. } => Some( f( node ) ), _ => None, } } /// The message a pattern grid fires for the node under `pos`, if any. pub fn pattern_node_msg( &self, rect: Rect, pos: Point ) -> Option { self.pattern_node_at( rect, pos ).and_then( |node| self.pattern_msg( node ) ) } pub fn pattern_release_msg( &self ) -> Option { match self { WidgetHandlers::PatternGrid { on_release, .. } => on_release.clone(), _ => None, } } pub( crate ) fn set_pattern_snap_px( &mut self, px: f32 ) { if let WidgetHandlers::PatternGrid { snap_px, .. } = self { *snap_px = px; } } /// `true` when this is a [`WidgetHandlers::TextEdit`] whose source /// widget was built with `.multiline( true )`. The keyboard /// dispatch reads this so pressing Enter inserts a `\n` instead of /// submitting. pub fn is_multiline_text_input( &self ) -> bool { matches!( self, WidgetHandlers::TextEdit { multiline: true, .. } ) } pub fn is_slider( &self ) -> bool { matches!( self, WidgetHandlers::Slider { .. } ) } /// `true` when this widget is a row inside a scrollable list that /// keyboard arrow navigation should treat as a stepping point. /// Currently restricted to [`ListItem`](crate::ListItem); buttons, /// toggles, sliders, etc. are not stepped over by Arrow Up/Down so /// they do not interfere with the row-by-row navigation pattern in /// combo popups, settings menus and similar lists. pub fn is_navigable_list_item( &self ) -> bool { matches!( self, WidgetHandlers::ListItem { .. } ) } /// Convenience: extract the press / activation message for the variants /// that have one (Button, Toggle, Checkbox, Radio, ListItem). Returns /// `None` for sliders / text edits / `None` / disabled widgets. pub fn press_msg( &self ) -> Option { match self { WidgetHandlers::Button { on_press, .. } => on_press.clone(), WidgetHandlers::Toggle { on_toggle, .. } => on_toggle.clone(), WidgetHandlers::Checkbox { on_toggle, .. } => on_toggle.clone(), WidgetHandlers::Radio { on_select, .. } => on_select.clone(), WidgetHandlers::ListItem { on_press } => on_press.clone(), WidgetHandlers::WindowButton { on_press } => on_press.clone(), _ => None, } } /// Whether a tap on this widget fires something. pub fn has_press( &self ) -> bool { self.press_msg().is_some() } /// Submit message (Enter on a focused TextEdit). `None` for every other /// variant. pub fn submit_msg( &self ) -> Option { match self { WidgetHandlers::TextEdit { on_submit, .. } => on_submit.clone(), _ => None, } } /// Build the `on_change` message for a TextEdit given the new value. pub fn text_change_msg( &self, new_value: &str ) -> Option { match self { WidgetHandlers::TextEdit { on_change: Some( f ), .. } => Some( f( new_value.to_string() ) ), _ => None, } } /// Build the `on_change` message for a Slider given a value in `[0,1]`. pub fn slider_change_msg( &self, value: f32 ) -> Option { match self { WidgetHandlers::Slider { on_change: Some( f ), .. } => Some( f( value ) ), _ => None, } } /// Build the `on_release` message for a Slider given its final value. pub fn slider_release_msg( &self, value: f32 ) -> Option { match self { WidgetHandlers::Slider { on_release: Some( f ), .. } => Some( f( value ) ), _ => None, } } /// Compute the `[0.0, 1.0]` value for the slider this handler belongs to, /// given a pointer position inside its layout rect. Dispatches on the /// stored [`slider::SliderAxis`] so the same call site in `input.rs` drives /// both horizontal [`Slider`](slider::Slider) and vertical /// [`VSlider`](super::vslider::VSlider). /// /// Returns `0.0` for non-slider variants — callers combine this with /// [`Self::slider_change_msg`], which also gates on the variant, so the /// zero is never consumed in practice. pub fn slider_value_from_pos( &self, rect: Rect, pos: Point ) -> f32 { match self { WidgetHandlers::Slider { axis, thumb_px, .. } => { slider::value_from_pos_in_rect( rect, pos, *axis, *thumb_px ) } _ => 0.0, } } /// Store the widget-scaling-resolved thumb size into a horizontal /// [`WidgetHandlers::Slider`] snapshot. Called by the layout pass (which /// carries a canvas) so the input path maps clicks against the same /// thumb the renderer drew. A no-op for every other variant. pub( crate ) fn set_slider_thumb_px( &mut self, px: f32 ) { if let WidgetHandlers::Slider { thumb_px, .. } = self { *thumb_px = px; } } /// `true` when this is a [`WidgetHandlers::Button`] whose source /// widget opted into press-and-hold repeat. The runtime reads /// this on press to decide whether to fire `press_msg` /// immediately + arm a calloop repeat timer, and on release to /// suppress the regular tap-on-release fire. pub fn is_repeating( &self ) -> bool { matches!( self, WidgetHandlers::Button { repeating: true, .. } ) } /// Long-press / right-click message for this widget, or `None` if /// none configured. Currently only [`WidgetHandlers::Button`] /// carries one. Firing this does not by itself put the press into /// drag mode — see [`Self::drag_start_msg`] for that. pub fn long_press_msg( &self ) -> Option { match self { WidgetHandlers::Button { on_long_press, .. } => on_long_press.clone(), _ => None, } } /// Drag-arm message for this widget, or `None` if none configured. /// Fired by the touch hold-timer alongside `long_press_msg`, and /// by mouse left-button motion past the drag-promotion threshold /// (without firing the menu). Promotes the gesture into drag mode. pub fn drag_start_msg( &self ) -> Option { match self { WidgetHandlers::Button { on_drag_start, .. } => on_drag_start.clone(), _ => None, } } /// Keyboard `Escape` message for this widget, or `None` if none /// configured. Used by the keyboard ESC handler to scan /// `widget_rects` for a [`crate::widget::dialog::Dialog`] (or other /// `Pressable::on_escape`-bearing wrapper) and fire its cancel /// message before the default ESC fallthrough chain. pub fn escape_msg( &self ) -> Option { match self { WidgetHandlers::Button { on_escape, .. } => on_escape.clone(), _ => None, } } /// Current text-edit value (for cursor placement on focus, backspace /// rebuild, etc.). `None` for non-text-edit variants. pub fn current_value( &self ) -> Option<&str> { match self { WidgetHandlers::TextEdit { value, .. } => Some( value.as_str() ), _ => None, } } }