event.rs raw

//! 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<String> {
        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<String> {
        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<UserValue>,
    },
}

/// 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<i64> {
        match self {
            Self::Int(v) => Some(*v),
            _ => None,
        }
    }

    /// The value as a float, if it is one.
    pub fn as_float(&self) -> Option<f64> {
        match self {
            Self::Float(v) => Some(*v),
            _ => None,
        }
    }

    /// The value as a bool, if it is one.
    pub fn as_bool(&self) -> Option<bool> {
        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<bool> {
        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<f64> {
        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<f64> {
        self.value_changed()
    }

    /// The new focus state, when this is a focus-change event.
    pub fn focus_changed(&self) -> Option<bool> {
        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<bool> {
        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<PointerButton>,
    /// 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<T: Widget>(&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<dyn FnMut(&mut EventCtx, &EventData)>;

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<WidgetId>,
    /// Target of the most recent unreleased press.
    pub(crate) pressed: Option<WidgetId>,
    /// Widget that consumed the press internally and receives moves until
    /// release (drag selection).
    capture: Option<WidgetId>,
    /// 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<Point>,
    /// 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<PendingCounted>,
}

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<WidgetId> {
        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<WidgetId> {
        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<WidgetId> {
        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<WidgetId>) {
        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<PointerButton>,
    ) {
        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<std::time::Instant> {
        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<PointerButton>,
    ) -> 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<WidgetId> {
        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<PointerButton>,
        clipboard: &mut dyn crate::clipboard::Clipboard,
    ) -> Option<WidgetId> {
        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<PointerButton>,
        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;