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;