//! Pointer input: hit testing, hover tracking with enter/leave synthesis, //! and capture/bubble dispatch to per-widget handlers. //! //! Handlers are stored on widgets but cannot borrow the tree while running; //! they receive an [`EventCtx`] whose command queue defers tree mutations to //! the next frame, and they read or write signals directly. Reactive state //! plus commands is the whole mutation surface — the same one bindings use. use guiduck_scene::geom::Point; use crate::widget::{Commands, Widget, WidgetId, WidgetTree}; /// A pointer button. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PointerButton { Left, Right, Middle, Other(u16), } /// Keyboard keys, reduced to the vocabulary widgets act on. Anything else /// arrives as [`Key::Other`] and is ignorable. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Key { /// Text produced by the key press, layout-applied. Character(String), Backspace, Delete, Left, Right, Up, Down, Home, End, PageUp, PageDown, Enter, Tab, Escape, Other, } impl Key { /// The canonical spelling, or `None` for a key that has none. /// /// This is the *accelerator* spelling — `guiduck_component_core::accel` /// owns the grammar, and a test in this module pins every key both /// vocabularies name to one string, so `"Ctrl+S"` means the same thing /// declared in an `:accel` and observed through `event.key`. /// /// [`Key::Other`] has none, and that is the honest answer rather than an /// oversight: it is precisely the key we did not model, so there is no /// name to give it and nothing an application could match against. pub fn name(&self) -> Option { Some(match self { // Uppercased, as an accelerator stores it: `Ctrl+s` and `Ctrl+S` // are one keystroke, so they must be one spelling. Self::Character(c) => c.to_ascii_uppercase(), Self::Backspace => "Backspace".to_owned(), Self::Delete => "Delete".to_owned(), Self::Left => "Left".to_owned(), Self::Right => "Right".to_owned(), Self::Up => "Up".to_owned(), Self::Down => "Down".to_owned(), Self::Home => "Home".to_owned(), Self::End => "End".to_owned(), Self::PageUp => "PageUp".to_owned(), Self::PageDown => "PageDown".to_owned(), Self::Enter => "Enter".to_owned(), Self::Tab => "Tab".to_owned(), Self::Escape => "Escape".to_owned(), Self::Other => return None, }) } } #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Modifiers { pub ctrl: bool, pub shift: bool, pub alt: bool, pub logo: bool, } /// A key-plus-modifiers pattern for application shortcuts (Ctrl+S and /// friends), registered via /// [`WidgetTree::on_shortcut`](crate::WidgetTree::on_shortcut). #[derive(Clone, Debug, PartialEq)] pub struct Keystroke { pub key: Key, pub modifiers: Modifiers, } impl Keystroke { /// A character key with Ctrl held — the common accelerator shape. pub fn ctrl(character: &str) -> Self { Self { key: Key::Character(character.into()), modifiers: Modifiers { ctrl: true, ..Default::default() }, } } /// Whether a key event matches this pattern. Characters compare /// case-insensitively so a held Shift (or caps lock) does not change /// which accelerator fires; modifiers must match exactly. pub fn matches(&self, input: &KeyInput) -> bool { if self.modifiers != input.modifiers { return false; } match (&self.key, &input.key) { (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b), (a, b) => a == b, } } } /// A keyboard event in the tree's vocabulary. #[derive(Clone, Debug, PartialEq)] pub struct KeyInput { pub key: Key, pub modifiers: Modifiers, /// False for key releases (widgets generally ignore those). pub pressed: bool, } impl KeyInput { /// The keystroke's canonical spelling — held modifiers in a fixed order, /// then the key — or `None` if the key has no name ([`Key::Other`]). /// /// The order matches an accelerator declaration's, because it is the same /// notation: what `:accel "Ctrl+Shift+S"` writes is what `event.key` /// reads. pub fn spelling(&self) -> Option { let mut out = String::new(); for (held, name) in [ (self.modifiers.ctrl, "Ctrl"), (self.modifiers.alt, "Alt"), (self.modifiers.shift, "Shift"), (self.modifiers.logo, "Super"), ] { if held { out.push_str(name); out.push('+'); } } out.push_str(&self.key.name()?); Some(out) } } /// Input-method events, forwarded to the focused widget. #[derive(Clone, Debug, PartialEq)] pub enum ImeInput { Enabled, /// In-progress composition text with an optional cursor byte range. Preedit { text: String, cursor: Option<(usize, usize)>, }, /// Finished composition to insert. Commit(String), Disabled, } /// Raw pointer input as delivered by the platform shell, in logical window /// coordinates. #[derive(Copy, Clone, Debug, PartialEq)] pub enum PointerInput { Moved(Point), Down { pos: Point, button: PointerButton, /// When the press happened; drives multi-click detection. The shell /// supplies it so the tree has no clock of its own and tests can /// fabricate timings. time: std::time::Instant, }, Up { pos: Point, button: PointerButton, }, /// Wheel or touchpad scrolling at a position. `delta` is in logical /// pixels with the platform's orientation: positive y is a scroll /// *up/away* (a scrollable widget moves its offset by `-delta`). Scroll { pos: Point, delta: guiduck_scene::geom::Vec2, }, /// The pointer left the window. Left, } /// The event kinds a handler can subscribe to. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum EventKind { PointerDown, PointerUp, PointerMove, /// The pointer entered this widget or one of its descendants. Delivered /// directly (no capture/bubble). PointerEnter, /// The counterpart of [`PointerEnter`](EventKind::PointerEnter). PointerLeave, /// A press and release that both hit this widget (or a descendant). /// Dispatched immediately on release, carrying the running click count. Click, /// The settled outcome of a click *burst* — one dispatch per burst, once /// the ≤200ms window from the first press has closed, carrying the final /// click count. Unlike [`Click`](EventKind::Click) it never fires on the /// first press of a double, so a handler can act on a *confirmed* single /// (`count == 1`) or a double (`count == 2`) without a following press /// retracting it. A widget opts a press's path into the accumulate-and- /// resolve pipeline by handling this or reporting /// [`Widget::handles_counted_click`]; nothing else pays the latency. /// /// [`Widget::handles_counted_click`]: crate::widget::Widget::handles_counted_click CountedClick, /// The widget's value changed through user input (e.g. a text edit). /// Handlers receive [`EventData::Changed`] with the new value. /// Programmatic setters do not raise this — only the user does. Changed, /// The user asked to dismiss a dialog. Handlers receive no payload. Close, /// A menu row was chosen. Handlers receive no payload: what was chosen /// is the wire's own arguments (`:on-select (open f.id)`). Select, /// A toggle control flipped through user input. Handlers receive /// [`EventData::Toggled`] with the new checked state. Programmatic /// setters do not raise this — only the user does. Toggled, /// A `(link …)` span inside a paragraph was invoked. Handlers receive /// [`EventData::Link`] with the span's target. Link, /// A control whose value is a number moved through user input. Handlers /// receive [`EventData::ValueChanged`] with the new value. Programmatic /// setters do not raise this — only the user does. ValueChanged, /// A scroll container's position moved through user input — the wheel, /// the keyboard, a thumb drag. Handlers read the new offset through /// `event.offset-x` / `event.offset-y`. Programmatic setters do not raise /// this — only the user does, which is what lets an application hold the /// position as its own state without its binding fighting the reader. Scrolled, /// A file was dropped onto the window. Handlers receive /// [`EventData::FileDrop`] with its path; the framework carries the path /// and the app interprets it, exactly as it does a link target. One /// dispatch per file. FileDrop, /// This widget gained or lost focus. Handlers receive /// [`EventData::FocusChanged`] with the new state. /// /// Delivered to the widget itself and *not* bubbled: the payload answers /// "am I focused", which is not a fact about an ancestor. It reports every /// transition whatever caused it — a click, Tab, the focus invariant, a /// disabled widget being evicted — which is what lets an app drive /// `:focused` and still keep its own state true. FocusChange, /// A key the focused widget did not consume, offered to its `:on-key` /// wire and then to its ancestors'. Handlers receive [`EventData::Key`] /// with the keystroke's canonical spelling; consuming the event (through /// [`EventCtx::stop_propagation`]) consumes the key. Key, /// An event a widget declared in a `.gdw` manifest. Handlers receive /// [`EventData::User`], whose `name` says *which* — one kind for the /// whole open half of the vocabulary, because the closed set is what this /// enum is for, and a `Copy` kind cannot carry an owned name. User, } /// The payload delivered to an external handler, matching the kind it /// subscribed to. #[derive(Clone, Debug, PartialEq)] pub enum EventData { /// The pointer kinds (Down/Up/Move/Enter/Leave/Click). Pointer(PointerEvent), /// [`EventKind::Changed`]: the widget's new value. Changed(String), /// [`EventKind::Toggled`]: the widget's new checked state. Toggled(bool), /// [`EventKind::Link`]: the target of the link span that was invoked, /// verbatim as the `(link "…" …)` wrote it. What it *means* is the /// application's business — the framework never resolves it. Link(String), /// [`EventKind::FileDrop`]: the path of a file dropped on the window, as /// the platform gave it. What to do with it is the application's business. FileDrop(String), /// [`EventKind::FocusChange`]: whether the widget now has focus. FocusChanged(bool), /// [`EventKind::ValueChanged`]: the control's new value. ValueChanged(f64), /// [`EventKind::Scrolled`]: the scroll container's new offset. Scrolled { x: f64, y: f64 }, /// [`EventKind::Key`]: the keystroke's canonical spelling — the same one /// an accelerator is written in, so `"Ctrl+S"` names one thing whether it /// is declared or observed. Key(String), /// The focused widget was activated by keyboard (Enter/Space on a /// button). Never dispatched to handlers: the tree converts it into a /// synthesized click at the widget's center, so keyboard activation and /// pointer clicks share one wire. Activated, /// A menu title or submenu row was invoked and wants its popup open. /// Never dispatched to handlers: a widget cannot reach the overlay /// stack, so it reports and the tree opens the popup, building the /// menu's deferred content into it. MenuOpen, /// An open menu panel got a navigation key. Never dispatched to /// handlers: moving the highlight needs the panel's rows, which is the /// tree's knowledge, not the panel's. MenuNav(crate::widget::MenuNav), /// The user asked to dismiss a dialog (Escape). A *request*: the app /// answers by clearing `:open`, so the binding stays the truth. CloseRequested, /// A menu row was chosen, by pointer, by Enter on the highlighted row, or /// by its accelerator. The tree turns it into the `:on-select` dispatch /// and closes the menu stack, so all three routes are one wire — as /// [`Activated`](Self::Activated) makes them one for a button. Selected, /// [`EventKind::User`]: an event a widget declared in a `.gdw` manifest. /// The name is the manifest's (`on-link`), and the payload is whatever it /// declared `:payload` — the widget's half of the contract the compiler /// checked the `.gdc` against. User { name: String, payload: Option, }, } /// The value a manifest-declared event delivers: the `.gdc` scalar type set, /// at its widest carrier per family. /// /// A manifest may declare `i32` or `i64`, `f32` or `f64`; a widget emits /// through the one variant of its family and the consumer — generated code or /// the interpreter — narrows to the declared width. That keeps the emission /// side of the contract as small as the enum, rather than making the widget /// author pick a carrier that has to match the manifest exactly. #[derive(Clone, Debug, PartialEq)] pub enum UserValue { Int(i64), Float(f64), Bool(bool), Str(String), } impl UserValue { /// The value as an integer, if it is one. pub fn as_int(&self) -> Option { match self { Self::Int(v) => Some(*v), _ => None, } } /// The value as a float, if it is one. pub fn as_float(&self) -> Option { match self { Self::Float(v) => Some(*v), _ => None, } } /// The value as a bool, if it is one. pub fn as_bool(&self) -> Option { match self { Self::Bool(v) => Some(*v), _ => None, } } /// The value as a string, if it is one. pub fn as_str(&self) -> Option<&str> { match self { Self::Str(v) => Some(v), _ => None, } } } impl EventData { /// The pointer details, when this is a pointer event. pub fn pointer(&self) -> Option<&PointerEvent> { match self { EventData::Pointer(event) => Some(event), _ => None, } } /// The new value, when this is a change event. pub fn changed(&self) -> Option<&str> { match self { EventData::Changed(value) => Some(value), _ => None, } } /// The new checked state, when this is a toggle event. pub fn toggled(&self) -> Option { match self { EventData::Toggled(value) => Some(*value), _ => None, } } /// The link's target, when this is a link event. pub fn link(&self) -> Option<&str> { match self { EventData::Link(target) => Some(target), _ => None, } } /// The dropped file's path, when this is a file-drop event. pub fn file_drop(&self) -> Option<&str> { match self { EventData::FileDrop(path) => Some(path), _ => None, } } /// The new scroll offset, when this is a scroll event. pub fn scrolled(&self) -> Option<(f64, f64)> { match self { EventData::Scrolled { x, y } => Some((*x, *y)), _ => None, } } /// The new value, when this is a value-change event. pub fn value_changed(&self) -> Option { match self { EventData::ValueChanged(value) => Some(*value), _ => None, } } /// The payload of any event that delivers a number — the numeric /// counterpart of [`string_payload`](Self::string_payload). pub fn number_payload(&self) -> Option { self.value_changed() } /// The new focus state, when this is a focus-change event. pub fn focus_changed(&self) -> Option { match self { EventData::FocusChanged(focused) => Some(*focused), _ => None, } } /// The keystroke's canonical spelling, when this is a key event. Read by /// `event.key` in a `:on-key` wire's arguments. pub fn key(&self) -> Option<&str> { match self { EventData::Key(spelling) => Some(spelling), _ => None, } } /// The payload of any event that delivers a string, whichever one it is. /// /// The generic accessor the compiler's two back ends read through: they /// key on the [`PayloadKind`] the registry declares, not on which event /// they are wiring, so a new string-carrying event costs them no arm. It /// is derived from the exact accessors above rather than restating them, /// which is what keeps this from becoming a second opinion about what a /// `Changed` carries. /// /// [`PayloadKind`]: guiduck_component_core::registry::PayloadKind pub fn string_payload(&self) -> Option<&str> { self.changed() .or_else(|| self.link()) .or_else(|| self.file_drop()) } /// The payload of any event that delivers a bool — the counterpart of /// [`string_payload`](Self::string_payload). pub fn bool_payload(&self) -> Option { self.toggled().or_else(|| self.focus_changed()) } /// The event's name and payload, when this is a manifest-declared event. /// Every `:on-…` wire on a user widget subscribes to the one /// [`EventKind::User`], so a wire matches the name here before it fires. pub fn user(&self) -> Option<(&str, Option<&UserValue>)> { match self { EventData::User { name, payload } => Some((name, payload.as_ref())), _ => None, } } } /// A pointer event as seen by a handler. #[derive(Copy, Clone, Debug, PartialEq)] pub struct PointerEvent { /// Position in logical window coordinates. pub window: Point, /// Position relative to the widget the handler is attached to. pub local: Point, /// The button involved, for Down/Up/Click. pub button: Option, /// How many rapid presses this event belongs to (1 = single click, /// 2 = double, 3 = triple, …), for Down/Up/Click; 0 otherwise. pub click_count: u8, } /// Dispatch phase a handler participates in. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Phase { /// Root-to-target, before the target sees the event. Capture, /// Target-to-root (the default). Bubble, } /// Context handed to event handlers. pub struct EventCtx { commands: Commands, /// The widget the event was originally dispatched to. pub target: WidgetId, /// The widget whose handler is currently running. pub current: WidgetId, /// The phase the running handler registered for. pub phase: Phase, stop: bool, } impl EventCtx { /// Queue a mutation of a widget for the next frame. pub fn mutate(&self, id: WidgetId, f: impl FnOnce(&mut T) + 'static) { self.commands.mutate(id, f); } /// Queue an arbitrary tree mutation for the next frame. pub fn push_command(&self, f: impl FnOnce(&mut WidgetTree) + 'static) { self.commands.push(f); } /// Prevent handlers later in the dispatch (deeper captures, remaining /// bubble ancestors) from seeing this event. pub fn stop_propagation(&mut self) { self.stop = true; } } pub(crate) type Handler = Box; pub(crate) struct HandlerEntry { pub kind: EventKind, pub phase: Phase, pub f: Handler, } /// A press continues the current click burst when it lands within this long /// of the burst's *first* press… /// /// Anchoring the window at the first press, not the previous one, bounds the /// whole burst — and therefore the latency before a [`CountedClick`] resolves /// — at this value however many clicks it holds. Presses slower than this /// apart read as separate bursts. /// /// [`CountedClick`]: EventKind::CountedClick const MULTI_CLICK_WINDOW: std::time::Duration = std::time::Duration::from_millis(200); /// …and within this many logical pixels of it, on the same button. const MULTI_CLICK_SLOP: f64 = 4.0; /// A click completed and, because its path distinguishes click counts, owes a /// [`CountedClick`](EventKind::CountedClick) once the burst window closes. #[derive(Copy, Clone)] struct PendingCounted { /// When the burst is settled and the CountedClick dispatches — the burst's /// first-press time plus [`MULTI_CLICK_WINDOW`]. deadline: std::time::Instant, /// The widget the burst's most recent click completed on. target: WidgetId, /// Window position of that click, for the dispatched pointer payload. pos: Point, button: PointerButton, } /// Pointer-derived state the tree tracks between inputs. #[derive(Default)] pub(crate) struct PointerState { /// Hover chain, root first, innermost (hit target) last. pub(crate) hover_chain: Vec, /// Target of the most recent unreleased press. pub(crate) pressed: Option, /// Widget that consumed the press internally and receives moves until /// release (drag selection). capture: Option, /// Last known pointer position. Read by `cursor_shape`, which has to ask /// the hovered widget about a place rather than about itself. pub(crate) position: Option, /// The first press of the current click burst (time, position, button). /// A later press within [`MULTI_CLICK_WINDOW`] and [`MULTI_CLICK_SLOP`] of /// it, on the same button, continues the burst; anything else begins one. burst: Option<(std::time::Instant, Point, PointerButton)>, /// Click count of the current (or most recent) burst: 1 for a single /// click, 2 for a double, … Stamped onto Down/Up/Click events. click_count: u8, /// A completed click whose CountedClick dispatch is owed once the burst /// window closes — set only when the press's path opted in. pending_counted: Option, } impl WidgetTree { /// Attach a bubble-phase event handler to a widget. pub fn on_event( &mut self, id: WidgetId, kind: EventKind, f: impl FnMut(&mut EventCtx, &EventData) + 'static, ) { self.add_handler(id, kind, Phase::Bubble, f); } /// Attach a handler for an explicit phase. pub fn add_handler( &mut self, id: WidgetId, kind: EventKind, phase: Phase, f: impl FnMut(&mut EventCtx, &EventData) + 'static, ) { if let Some(handlers) = self.handlers_mut(id) { handlers.push(HandlerEntry { kind, phase, f: Box::new(f), }); } } /// The topmost widget at `pos` (logical window coordinates), if any. /// Overlays are tested topmost-first; a modal overlay is a barrier — /// nothing beneath it can be hit. pub fn hit_test(&self, pos: Point) -> Option { for overlay in self.overlays.iter().rev() { // A note the pointer passes through (a tooltip) is not a layer // the pointer can land on. if !overlay.hit_testable { continue; } let rect = self.layout(overlay.root) + overlay.position.to_vec2(); if rect.contains(pos) { return Some(self.hit_test_within(overlay.root, pos - rect.origin().to_vec2())); } if overlay.modal { return None; } } let root = self.root()?; let rect = self.layout(root); if !rect.contains(pos) { return None; } Some(self.hit_test_within(root, pos - rect.origin().to_vec2())) } /// `pos` is relative to `id`'s origin; `id` is known to contain it. fn hit_test_within(&self, id: WidgetId, pos: Point) -> WidgetId { // Children sit at their layout position shifted by the parent's // content offset (scroll) — the same shift the paint walk applies, // so pixels and pointer targets cannot disagree. A child scrolled // out of view has its shifted rect outside the parent's bounds, // and `pos` (inside those bounds) can no longer land on it, which // makes clipped content unhittable with no extra check. let offset = self.content_offset(id); for child in self.children(id).iter().rev() { let rect = self.layout(*child) + offset; if rect.contains(pos) { return self.hit_test_within(*child, pos - rect.origin().to_vec2()); } } id } /// Feed one pointer input through hover tracking and dispatch. The /// clipboard is part of the pointer vocabulary because of the primary /// selection: drags populate it, middle-click pastes from it. pub fn dispatch_pointer( &mut self, input: PointerInput, clipboard: &mut dyn crate::clipboard::Clipboard, ) { match input { PointerInput::Moved(pos) => { self.pointer.position = Some(pos); if let Some(captured) = self.pointer.capture { self.deliver_internal_to( captured, EventKind::PointerMove, pos, None, clipboard, ); } let target = self.update_hover(pos); if let Some(target) = target { self.dispatch(target, EventKind::PointerMove, pos, None); } } PointerInput::Down { pos, button, time } => { self.pointer.position = Some(pos); // Multi-click detection: a press within the window and slop of // the burst's first press raises the count; anything else // starts a new burst — and settles the previous one, whose // CountedClick (if it owed one) is now final, before this press // is handled. let continues = matches!( self.pointer.burst, Some((first_time, first_pos, first_button)) if first_button == button && time.duration_since(first_time) <= MULTI_CLICK_WINDOW && (pos - first_pos).hypot() <= MULTI_CLICK_SLOP ); if continues { self.pointer.click_count = self.pointer.click_count.saturating_add(1); } else { self.resolve_counted_click(clipboard); self.pointer.click_count = 1; self.pointer.burst = Some((time, pos, button)); } let target = self.update_hover(pos); // Light dismissal: a press outside an overlay's stack // closes it and is consumed — it must not also activate // whatever it landed on. if self.dismiss_for_press(target) { self.pointer.pressed = None; self.update_hover(pos); return; } // A right-press opens the context menu attached at or above // what it landed on, at the pointer — which is why a context // menu needs no handler and no coordinates: the framework // already knows where the press was. It is consumed, so the // press does not also act on what is underneath. if button == PointerButton::Right && let Some(target) = target && self.open_context_menu(target, pos) { self.pointer.pressed = None; return; } self.pointer.pressed = target; if let Some(target) = target { // Click-to-focus: the innermost focusable widget in the // chain takes focus. Clicking where nothing is focusable // keeps the current focus — the focus invariant says // focus never drops while focusables exist. if let Some(focusable) = self.innermost_focusable(target) { self.set_focus(Some(focusable)); } // The pressed chain's :active state just switched on. self.mark_style_dirty_chain(target); // Internal behavior first (cursor placement, drag // start); a consuming widget captures the pointer. When // an *ancestor* consumed (an overlay scrollbar thumb // over content), the press belongs to it — retargeting // `pressed` keeps a Click from also firing on whatever // sat underneath the thumb. if let Some(consumer) = self.deliver_internal( target, EventKind::PointerDown, pos, Some(button), clipboard, ) { self.pointer.capture = Some(consumer); self.pointer.pressed = Some(consumer); } self.dispatch(target, EventKind::PointerDown, pos, Some(button)); } } PointerInput::Up { pos, button } => { self.pointer.position = Some(pos); if let Some(captured) = self.pointer.capture.take() { self.deliver_internal_to( captured, EventKind::PointerUp, pos, Some(button), clipboard, ); } let target = self.update_hover(pos); let pressed = self.pointer.pressed.take(); if let Some(pressed) = pressed { // …and off again. self.mark_style_dirty_chain(pressed); } if let Some(target) = target { self.dispatch(target, EventKind::PointerUp, pos, Some(button)); // A click is a press and release over the same widget. if pressed == Some(target) { // Click is immediate: widgets with click behavior of // their own (a checkbox toggling) see it first, then // handlers. self.deliver_internal( target, EventKind::Click, pos, Some(button), clipboard, ); self.dispatch(target, EventKind::Click, pos, Some(button)); // If anything on the path distinguishes click counts, // this completed click joins the burst's pending // CountedClick, which dispatches once the window closes // (in `tick`, or when the next burst begins). if self.path_counts_clicks(target) && let Some((first_time, _, _)) = self.pointer.burst { self.pointer.pending_counted = Some(PendingCounted { deadline: first_time + MULTI_CLICK_WINDOW, target, pos, button, }); } } } } PointerInput::Scroll { pos, delta } => { self.pointer.position = Some(pos); let target = self.update_hover(pos); if let Some(target) = target { // Innermost widget that actually scrolls consumes; a // scroll area at its end lets an ancestor take over. let mut current = Some(target); while let Some(id) = current { let consumed = self .with_widget_and_text(id, |widget, _| widget.on_scroll(delta)) .unwrap_or(false); if consumed { break; } current = self.parent(id); } } // The content may have moved under the cursor; hover // follows the new geometry. self.update_hover(pos); } PointerInput::Left => { self.pointer.position = None; self.set_hover_chain(Vec::new()); } } } /// Deliver a file dropped on the window. It dispatches a /// [`FileDrop`](EventKind::FileDrop) — carrying the path for a /// `:on-file-drop` handler to interpret — to the widget under the last /// pointer position (so the whole chain up to it sees it, and a drop zone /// can be a child), falling back to the root when the pointer's whereabouts /// are unknown. One call per file. pub fn drop_file(&mut self, path: String) { let target = self .pointer .position .and_then(|pos| self.hit_test(pos)) .or_else(|| self.root()); if let Some(target) = target { self.dispatch_semantic(target, EventKind::FileDrop, EventData::FileDrop(path)); } } /// Recompute the hover chain for `pos`, synthesizing enter/leave events /// for the difference, and return the hit target. /// /// A disabled hit target retargets to its nearest enabled ancestor: /// disabled widgets occlude what is beneath them but are inert — they /// take no hover state and receive no events, while their enabled /// container behaves as if the pointer were on its own padding. fn update_hover(&mut self, pos: Point) -> Option { let target = self.hit_test(pos).and_then(|id| self.enabled_target(id)); let chain = match target { Some(target) => self.ancestor_chain(target), None => Vec::new(), }; self.set_hover_chain(chain); target } /// Root-first chain of ancestors ending at `id` itself, and empty for a /// widget the tree no longer has. /// /// A widget can be gone by the time an event that named it is delivered: a /// click burst settles a whole [`MULTI_CLICK_WINDOW`] after the press that /// began it, and the frames in between may have retired what it landed on. /// A widget that is not in the tree has no ancestors, and answering so is /// what keeps every path walk from asking about a widget the tree cannot /// answer for — its geometry, in [`absolute_origin`], which every dispatch /// localizes its payload with. /// /// [`absolute_origin`]: WidgetTree::absolute_origin fn ancestor_chain(&self, id: WidgetId) -> Vec { if !self.node_exists(id) { return Vec::new(); } let mut chain = vec![id]; let mut current = id; while let Some(parent) = self.parent(current) { chain.push(parent); current = parent; } chain.reverse(); chain } /// The widgets under the pointer, outermost first. pub(crate) fn hover_chain_for_tooltip(&self) -> &[WidgetId] { &self.pointer.hover_chain } fn set_hover_chain(&mut self, new_chain: Vec) { let old_chain = std::mem::take(&mut self.pointer.hover_chain); let common = old_chain .iter() .zip(new_chain.iter()) .take_while(|(a, b)| a == b) .count(); // Widgets entering or leaving the chain change their :hover state. self.style_dirty.extend_from_slice(&old_chain[common..]); self.style_dirty.extend_from_slice(&new_chain[common..]); let pos = self.pointer.position.unwrap_or(Point::ORIGIN); // Leaves fire innermost-first, enters outermost-first, like the DOM. for id in old_chain[common..].iter().rev() { self.deliver_direct(*id, EventKind::PointerLeave, pos); } for id in &new_chain[common..] { self.deliver_direct(*id, EventKind::PointerEnter, pos); } self.pointer.hover_chain = new_chain; // Pointing at a menu row makes it the current one, so the pointer and // the arrows move the *same* highlight and cannot disagree about // what is current. It lives here, beside `:hover`, because it is the // same kind of fact — what the pointer is on — and because enter and // leave reach only external handlers, never the widget's own hooks. // A no-op unless the innermost hovered widget really is a menu row. if let Some(innermost) = self.pointer.hover_chain.last().copied() { self.highlight_menu_row(innermost); } // And what the pointer is *resting* on is the same question again. self.update_tooltip(); } /// Mark a widget and its ancestors for restyle (their `:active` state /// depends on the pressed chain). fn mark_style_dirty_chain(&mut self, target: WidgetId) { let mut current = Some(target); while let Some(id) = current { self.style_dirty.push(id); current = self.parent(id); } } /// Deliver an event to exactly one widget (enter/leave semantics). fn deliver_direct(&mut self, id: WidgetId, kind: EventKind, window: Point) { // The hover chain can hold ids removed by a hot reload; they get no // farewell events. if self.handlers_mut(id).is_none() { return; } let origin = self.absolute_origin(id); let event = EventData::Pointer(PointerEvent { window, local: window - origin.to_vec2(), button: None, click_count: 0, }); let mut ctx = EventCtx { commands: self.commands(), target: id, current: id, phase: Phase::Bubble, stop: false, }; self.run_handlers(id, kind, Phase::Bubble, &mut ctx, &event); // Enter/leave also run capture-phase handlers, in the same single // delivery; there is no propagation to stop. ctx.phase = Phase::Capture; self.run_handlers(id, kind, Phase::Capture, &mut ctx, &event); } /// Capture from the root down to `target`, then bubble back up, with /// pointer payloads localized per widget. fn dispatch( &mut self, target: WidgetId, kind: EventKind, window: Point, button: Option, ) { self.dispatch_along_path(target, kind, |tree, id| { EventData::Pointer(tree.event_for(id, window, button)) }); } /// Settle the pending click burst: dispatch its `CountedClick` carrying the /// final count — the widget's own behavior first (a link emitting /// `:on-link` on a confirmed single) then external `:on-counted-click` /// handlers, the same order a `Click` takes. A no-op when no click owed /// one. Runs from `tick` at the deadline and when the next burst begins. pub(crate) fn resolve_counted_click( &mut self, clipboard: &mut dyn crate::clipboard::Clipboard, ) { let Some(pending) = self.pointer.pending_counted.take() else { return; }; // The final count still stands in pointer state — a new burst has not // reset it — so the dispatched pointer payload carries it. self.deliver_internal( pending.target, EventKind::CountedClick, pending.pos, Some(pending.button), clipboard, ); self.dispatch( pending.target, EventKind::CountedClick, pending.pos, Some(pending.button), ); // Resolution ends the burst: the next press, however soon it lands, // begins a fresh one rather than escalating a count already reported. self.pointer.burst = None; } /// The deadline at which a pending `CountedClick` becomes due, for the /// tree's wake scheduling. pub(crate) fn counted_click_deadline(&self) -> Option { self.pointer.pending_counted.map(|p| p.deadline) } /// Whether the pending `CountedClick` is due at `now`. pub(crate) fn counted_click_due(&self, now: std::time::Instant) -> bool { self.pointer .pending_counted .is_some_and(|p| p.deadline <= now) } /// Whether a press landing on `target` should accumulate into a burst — /// true when any widget on its path handles `CountedClick` externally or /// reports [`Widget::handles_counted_click`]. When nothing does, the click /// is never held and nothing pays the resolution latency. /// /// [`Widget::handles_counted_click`]: crate::widget::Widget::handles_counted_click fn path_counts_clicks(&mut self, target: WidgetId) -> bool { for id in self.ancestor_chain(target) { let has_handler = self .handlers_mut(id) .is_some_and(|hs| hs.iter().any(|h| h.kind == EventKind::CountedClick)); if has_handler { return true; } if self .with_widget_and_text(id, |widget, _| widget.handles_counted_click()) .unwrap_or(false) { return true; } } false } /// Dispatch a widget-emitted semantic event (e.g. Changed) along the /// same capture/bubble path; the payload is position-independent, so /// every widget on the path sees the same data. pub(crate) fn dispatch_semantic(&mut self, target: WidgetId, kind: EventKind, data: EventData) { self.dispatch_along_path(target, kind, |_, _| data.clone()); } /// The one capture/bubble traversal both pointer and semantic dispatch /// share; `data_for` produces each widget's view of the payload. fn dispatch_along_path( &mut self, target: WidgetId, kind: EventKind, data_for: impl Fn(&Self, WidgetId) -> EventData, ) { let path = self.ancestor_chain(target); let mut ctx = EventCtx { commands: self.commands(), target, current: target, phase: Phase::Capture, stop: false, }; for id in &path { ctx.current = *id; ctx.phase = Phase::Capture; let event = data_for(self, *id); self.run_handlers(*id, kind, Phase::Capture, &mut ctx, &event); if ctx.stop { return; } } for id in path.iter().rev() { ctx.current = *id; ctx.phase = Phase::Bubble; let event = data_for(self, *id); self.run_handlers(*id, kind, Phase::Bubble, &mut ctx, &event); if ctx.stop { return; } // A button or checkbox consumes its click: its own handlers have // run, but the click stops here rather than bubbling to an // enclosing container's `:on-click`. if kind == EventKind::Click && self .with_widget(*id, |widget| widget.consumes_click()) .unwrap_or(false) { return; } } } fn event_for( &self, id: WidgetId, window: Point, button: Option, ) -> PointerEvent { PointerEvent { window, local: window - self.absolute_origin(id).to_vec2(), button, // Button events (Down/Up/Click) belong to the current press and // carry its click count; moves carry none. click_count: if button.is_some() { self.pointer.click_count } else { 0 }, } } /// Run the matching handlers of one widget. Handlers are moved out for /// the duration so they can queue commands and touch signals freely; /// handlers added to this widget during the call are preserved. fn run_handlers( &mut self, id: WidgetId, kind: EventKind, phase: Phase, ctx: &mut EventCtx, event: &EventData, ) { let Some(handlers) = self.handlers_mut(id) else { return; }; let mut taken = std::mem::take(handlers); for entry in &mut taken { if entry.kind == kind && entry.phase == phase { (entry.f)(ctx, event); } } if let Some(handlers) = self.handlers_mut(id) { let added = std::mem::take(handlers); taken.extend(added); *handlers = taken; } } /// Dispatch an event to one widget and no further. /// /// For events whose payload is a fact about *that* widget and nobody else: /// [`EventKind::FocusChange`] says "am I focused", which an ancestor /// cannot answer for itself, so there is nothing for a capture or bubble /// phase to mean. pub(crate) fn dispatch_direct(&mut self, target: WidgetId, kind: EventKind, event: EventData) { if self.handlers_mut(target).is_none() { return; } let mut ctx = EventCtx { commands: self.commands(), target, current: target, phase: Phase::Bubble, stop: false, }; self.run_handlers(target, kind, Phase::Bubble, &mut ctx, &event); } /// Dispatch an event from `target` outwards, and return whether a handler /// consumed it. /// /// Bubble-only, unlike [`Self::dispatch_along_path`]: a key belongs to the /// focused widget first and to its ancestors only if it declines, so a /// capture phase would let an enclosing container answer a keystroke /// before the control it was typed into — which is the one thing the /// routing order exists to prevent. pub(crate) fn dispatch_bubbling( &mut self, target: WidgetId, kind: EventKind, event: EventData, ) -> bool { let path = self.ancestor_chain(target); let mut ctx = EventCtx { commands: self.commands(), target, current: target, phase: Phase::Bubble, stop: false, }; for id in path.iter().rev() { ctx.current = *id; self.run_handlers(*id, kind, Phase::Bubble, &mut ctx, &event); if ctx.stop { return true; } } false } /// Dispatch a synthesized click (capture + bubble) to a widget, used by /// accessibility actions. pub(crate) fn dispatch_click_to(&mut self, target: WidgetId, pos: Point) { // Same shape as a real pointer click: the widget's own behavior // first, then external handlers. Synthesized clicks carry no // clipboard access. self.deliver_internal( target, EventKind::Click, pos, Some(PointerButton::Left), &mut crate::clipboard::NoClipboard, ); self.dispatch(target, EventKind::Click, pos, Some(PointerButton::Left)); } /// The innermost focusable widget at or above `target`. fn innermost_focusable(&self, target: WidgetId) -> Option { let mut current = Some(target); while let Some(id) = current { let focusable = self .with_widget(id, |widget| widget.focusable()) .unwrap_or(false); if focusable { return Some(id); } current = self.parent(id); } None } /// Offer an internal pointer event along the ancestor chain, innermost /// first, stopping at the first widget that consumes it. fn deliver_internal( &mut self, target: WidgetId, kind: EventKind, window: Point, button: Option, clipboard: &mut dyn crate::clipboard::Clipboard, ) -> Option { let mut current = Some(target); while let Some(id) = current { if self.deliver_internal_to(id, kind, window, button, clipboard) { return Some(id); } current = self.parent(id); } None } /// Deliver an internal pointer event to exactly one widget, with local /// coordinates. Returns whether the widget consumed it. fn deliver_internal_to( &mut self, id: WidgetId, kind: EventKind, window: Point, button: Option, clipboard: &mut dyn crate::clipboard::Clipboard, ) -> bool { if self.handlers_mut(id).is_none() { return false; } let event = self.event_for(id, window, button); self.with_widget_and_text(id, |widget, text| { widget.on_pointer(kind, &event, text, clipboard) }) .unwrap_or(false) } /// A widget's origin in window coordinates, accounting for every /// ancestor's scroll offset and — for widgets inside an overlay — the /// overlay's placement. /// A widget's origin in window coordinates: its layout position, plus /// every ancestor's, plus any scroll offsets and overlay placement along /// the way. The one answer to "where is this on screen" — an overlay's /// placement is resolved after layout, so summing layout rectangles alone /// gets a popup's contents wrong. pub fn absolute_origin(&self, id: WidgetId) -> Point { let mut origin = self.layout(id).origin(); let mut current = id; while let Some(parent) = self.parent(current) { origin += self.layout(parent).origin().to_vec2() + self.content_offset(parent); current = parent; } origin + crate::widget::overlay::layer_offset(self, id) } } #[cfg(test)] mod tests;