oxiui_core/dispatch.rs
1//! Event dispatch with W3C-style capture and bubble phases.
2//!
3//! [`EventDispatcher`] routes a [`DispatchEvent`] from the tree root down to a
4//! target node (the *capture* phase) and back up to the root (the *bubble*
5//! phase), invoking the handlers registered for each node along the way. A
6//! handler returns a [`Propagation`] result; once `stop_propagation` is set the
7//! dispatcher visits no further nodes.
8//!
9//! ## Handler-safe mutation during dispatch
10//!
11//! Handlers commonly want to add or remove handlers as a side effect (e.g. a
12//! "close" button that detaches its own listener). Mutating the handler list
13//! while iterating it is a classic use-after-free / skipped-element bug. The
14//! dispatcher avoids it with a **collect-then-apply** protocol: the live
15//! registry is never borrowed mutably during a dispatch. Instead, handlers push
16//! `RegistryEdit`s into a deferred queue carried by [`HandlerCtx`]; the queue
17//! is drained and applied to the registry only after the whole dispatch
18//! finishes. Adds and removes therefore take effect on the *next* event, never
19//! mid-flight.
20
21use crate::events::{KeyboardEvent, MouseEvent, Propagation, TouchEvent};
22use crate::tree::{WidgetId, WidgetTree};
23
24/// The kinds of input events the dispatcher routes.
25#[derive(Clone, Debug, PartialEq)]
26pub enum DispatchEvent {
27 /// A pointer event.
28 Mouse(MouseEvent),
29 /// A keyboard event.
30 Keyboard(KeyboardEvent),
31 /// A touch event.
32 Touch(TouchEvent),
33}
34
35/// The dispatch phase in which a handler is being invoked.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Phase {
38 /// Travelling root → target. Capture handlers fire here.
39 Capture,
40 /// At the target node itself.
41 Target,
42 /// Travelling target → root. Bubble handlers fire here.
43 Bubble,
44}
45
46/// A deferred edit to the handler registry, queued by a handler during dispatch
47/// and applied after the dispatch completes.
48enum RegistryEdit {
49 /// Add a handler to a node for a phase.
50 Add {
51 /// Node the handler is attached to.
52 id: WidgetId,
53 /// Phase the handler listens in.
54 phase: Phase,
55 /// The handler to install.
56 handler: Box<dyn EventHandler>,
57 },
58 /// Remove every handler registered on a node.
59 RemoveAll {
60 /// Node to clear.
61 id: WidgetId,
62 },
63}
64
65/// Context handed to a handler during dispatch.
66///
67/// A handler reads the event and the current node/phase, and may queue registry
68/// edits (which apply only after dispatch). It returns its desired
69/// [`Propagation`] from [`EventHandler::handle`].
70pub struct HandlerCtx<'a> {
71 /// The event being dispatched.
72 pub event: &'a DispatchEvent,
73 /// The node whose handler is currently running.
74 pub current: WidgetId,
75 /// The phase this invocation belongs to.
76 pub phase: Phase,
77 /// The eventual target node (deepest in the path).
78 pub target: WidgetId,
79 /// Deferred registry edits queued by handlers (applied post-dispatch).
80 pending: &'a mut Vec<RegistryEdit>,
81}
82
83impl HandlerCtx<'_> {
84 /// Queue a handler to be added to `id` for `phase` after dispatch finishes.
85 pub fn add_handler(&mut self, id: WidgetId, phase: Phase, handler: Box<dyn EventHandler>) {
86 self.pending.push(RegistryEdit::Add { id, phase, handler });
87 }
88
89 /// Queue removal of every handler on `id` after dispatch finishes.
90 ///
91 /// This is the *safe* way to "unregister during dispatch": the removal is
92 /// recorded now and applied once iteration is complete, so the handler list
93 /// is never mutated while it is being walked.
94 pub fn remove_handlers(&mut self, id: WidgetId) {
95 self.pending.push(RegistryEdit::RemoveAll { id });
96 }
97}
98
99/// A typed event handler attached to a node.
100pub trait EventHandler {
101 /// Handle `ctx.event` and return propagation control.
102 fn handle(&mut self, ctx: &mut HandlerCtx<'_>) -> Propagation;
103}
104
105/// Blanket impl so plain closures can be used as handlers.
106impl<F> EventHandler for F
107where
108 F: FnMut(&mut HandlerCtx<'_>) -> Propagation,
109{
110 fn handle(&mut self, ctx: &mut HandlerCtx<'_>) -> Propagation {
111 self(ctx)
112 }
113}
114
115/// Per-node handler lists, split by phase.
116#[derive(Default)]
117struct NodeHandlers {
118 capture: Vec<Box<dyn EventHandler>>,
119 bubble: Vec<Box<dyn EventHandler>>,
120}
121
122/// Routes events through capture/bubble phases over a [`WidgetTree`].
123///
124/// The dispatcher owns the handler registry but borrows the tree only
125/// immutably (to compute the ancestor path), so a caller can keep mutating the
126/// tree between dispatches.
127///
128/// ## Allocation-free fast path
129///
130/// The path buffer and the deferred-edit buffer are both held as pre-allocated
131/// `Vec`s that are cleared and reused across dispatches. This means that after
132/// the first event of each size class, dispatch is completely heap-allocation-free
133/// on the hot path (no new `Vec` allocations during capture/bubble traversal).
134pub struct EventDispatcher {
135 handlers: std::collections::HashMap<WidgetId, NodeHandlers>,
136 /// Re-used scratch buffer: the ancestor path (root → target).
137 /// Cleared and refilled on every [`dispatch`](EventDispatcher::dispatch) call.
138 path_scratch: Vec<WidgetId>,
139 /// Re-used scratch buffer: deferred registry edits queued by handlers.
140 /// Cleared and drained on every [`dispatch`](EventDispatcher::dispatch) call.
141 pending_scratch: Vec<RegistryEdit>,
142}
143
144impl Default for EventDispatcher {
145 fn default() -> Self {
146 Self {
147 handlers: std::collections::HashMap::new(),
148 // Pre-allocate for 32-node deep trees (typical UI depth is 5–15).
149 path_scratch: Vec::with_capacity(32),
150 // Pre-allocate for a handful of edits per dispatch (rarely > 2).
151 pending_scratch: Vec::with_capacity(4),
152 }
153 }
154}
155
156impl EventDispatcher {
157 /// Create an empty dispatcher.
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 /// Register a capture-phase handler on `id`.
163 pub fn on_capture(&mut self, id: WidgetId, handler: Box<dyn EventHandler>) {
164 self.handlers.entry(id).or_default().capture.push(handler);
165 }
166
167 /// Register a bubble-phase handler on `id`. Target-phase handlers are
168 /// registered here too (the target node fires its bubble handlers in the
169 /// [`Phase::Target`] step).
170 pub fn on_bubble(&mut self, id: WidgetId, handler: Box<dyn EventHandler>) {
171 self.handlers.entry(id).or_default().bubble.push(handler);
172 }
173
174 /// Remove every handler registered on `id`. Returns `true` if any existed.
175 pub fn clear_node(&mut self, id: WidgetId) -> bool {
176 self.handlers.remove(&id).is_some()
177 }
178
179 /// Total number of nodes with at least one registered handler.
180 pub fn registered_nodes(&self) -> usize {
181 self.handlers.len()
182 }
183
184 /// Compute the capture path root → target into `out` (cleared first).
185 ///
186 /// Uses the pre-allocated scratch buffer to avoid per-dispatch heap allocation.
187 fn path_to_reuse(tree: &WidgetTree, target: WidgetId, out: &mut Vec<WidgetId>) {
188 out.clear();
189 let mut cur = tree.get(target);
190 while let Some(node) = cur {
191 out.push(node.id);
192 cur = node.parent.and_then(|p| tree.get(p));
193 }
194 out.reverse(); // root → target
195 }
196
197 /// Dispatch `event` to `target`, running the capture phase (root → target),
198 /// the target phase, then the bubble phase (target → root).
199 ///
200 /// Returns the merged [`Propagation`] of every handler that ran. Dispatch
201 /// stops early as soon as a handler sets `stop_propagation`. Registry edits
202 /// queued by handlers are applied only after this call returns.
203 ///
204 /// This method is **allocation-free on the hot path** after the first call:
205 /// it reuses pre-allocated scratch buffers for both the ancestor path and
206 /// the deferred-edit queue, so no heap allocation occurs during traversal.
207 pub fn dispatch(
208 &mut self,
209 tree: &WidgetTree,
210 target: WidgetId,
211 event: DispatchEvent,
212 ) -> Propagation {
213 // Fill the path into the pre-allocated scratch buffer — no allocation.
214 // We must extract the buffer temporarily to avoid a split-borrow conflict
215 // between `self.path_scratch` (mutated) and `self.handlers` (read below).
216 let mut path = std::mem::take(&mut self.path_scratch);
217 Self::path_to_reuse(tree, target, &mut path);
218
219 if path.is_empty() {
220 self.path_scratch = path;
221 return Propagation::CONTINUE;
222 }
223
224 // Similarly extract the pending-edits buffer.
225 let mut pending = std::mem::take(&mut self.pending_scratch);
226 pending.clear();
227
228 let mut result = Propagation::CONTINUE;
229 let actual_target = path.last().copied().unwrap_or(target);
230
231 // ── Capture phase: root → target (exclusive of the target). ──────────
232 'capture: for &id in path.iter().take(path.len().saturating_sub(1)) {
233 // Take the node's capture handlers OUT of the registry for the
234 // duration of iteration, so handlers may freely queue edits that
235 // touch the same node without aliasing the list we're walking.
236 let mut taken = match self.handlers.get_mut(&id) {
237 Some(h) if !h.capture.is_empty() => std::mem::take(&mut h.capture),
238 _ => continue,
239 };
240 for handler in taken.iter_mut() {
241 let mut ctx = HandlerCtx {
242 event: &event,
243 current: id,
244 phase: Phase::Capture,
245 target: actual_target,
246 pending: &mut pending,
247 };
248 let prop = handler.handle(&mut ctx);
249 result = result.merge(prop);
250 if prop.stop_propagation {
251 self.restore_capture(id, taken);
252 break 'capture;
253 }
254 }
255 self.restore_capture(id, taken);
256 }
257
258 if !result.stop_propagation {
259 // ── Target + bubble phase: target → root. ────────────────────────
260 'bubble: for (i, &id) in path.iter().rev().enumerate() {
261 let phase = if i == 0 { Phase::Target } else { Phase::Bubble };
262 let mut taken = match self.handlers.get_mut(&id) {
263 Some(h) if !h.bubble.is_empty() => std::mem::take(&mut h.bubble),
264 _ => continue,
265 };
266 for handler in taken.iter_mut() {
267 let mut ctx = HandlerCtx {
268 event: &event,
269 current: id,
270 phase,
271 target: actual_target,
272 pending: &mut pending,
273 };
274 let prop = handler.handle(&mut ctx);
275 result = result.merge(prop);
276 if prop.stop_propagation {
277 self.restore_bubble(id, taken);
278 break 'bubble;
279 }
280 }
281 self.restore_bubble(id, taken);
282 }
283 }
284
285 self.apply_pending(&mut pending);
286
287 // Return the scratch buffers so the next call reuses the allocations.
288 // Clear pending (apply_pending already drained it) but keep capacity.
289 pending.clear();
290 self.pending_scratch = pending;
291 self.path_scratch = path;
292
293 result
294 }
295
296 /// Put captured capture-phase handlers back, preserving any added during
297 /// dispatch via the pending queue (those are applied separately).
298 fn restore_capture(&mut self, id: WidgetId, mut taken: Vec<Box<dyn EventHandler>>) {
299 let slot = self.handlers.entry(id).or_default();
300 // Anything pushed onto the (now-empty) live list while we iterated is
301 // impossible because handlers only queue edits; but be defensive and
302 // prepend the original handlers ahead of any concurrently-added ones.
303 taken.append(&mut slot.capture);
304 slot.capture = taken;
305 }
306
307 /// Put captured bubble-phase handlers back.
308 fn restore_bubble(&mut self, id: WidgetId, mut taken: Vec<Box<dyn EventHandler>>) {
309 let slot = self.handlers.entry(id).or_default();
310 taken.append(&mut slot.bubble);
311 slot.bubble = taken;
312 }
313
314 /// Apply deferred registry edits queued during dispatch.
315 ///
316 /// Drains the edits from `pending` in-place so the buffer's capacity is
317 /// preserved for the next dispatch call.
318 fn apply_pending(&mut self, pending: &mut Vec<RegistryEdit>) {
319 for edit in pending.drain(..) {
320 match edit {
321 RegistryEdit::Add { id, phase, handler } => match phase {
322 Phase::Capture => self.on_capture(id, handler),
323 Phase::Bubble | Phase::Target => self.on_bubble(id, handler),
324 },
325 RegistryEdit::RemoveAll { id } => {
326 self.handlers.remove(&id);
327 }
328 }
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::events::{Modifiers, MouseButton};
337 use crate::geometry::{Point, Rect};
338 use std::cell::RefCell;
339 use std::rc::Rc;
340
341 fn mouse_down() -> DispatchEvent {
342 DispatchEvent::Mouse(MouseEvent::Down {
343 pos: Point::new(5.0, 5.0),
344 button: MouseButton::Left,
345 modifiers: Modifiers::NONE,
346 })
347 }
348
349 /// root → a → target tree.
350 fn linear_tree() -> (WidgetTree, WidgetId, WidgetId) {
351 let mut t = WidgetTree::new(Rect::new(0.0, 0.0, 100.0, 100.0));
352 let a = t
353 .insert(WidgetId::ROOT, Rect::new(0.0, 0.0, 50.0, 50.0))
354 .expect("root");
355 let target = t.insert(a, Rect::new(0.0, 0.0, 20.0, 20.0)).expect("a");
356 (t, a, target)
357 }
358
359 #[test]
360 fn capture_then_bubble_ordering() {
361 let (tree, a, target) = linear_tree();
362 let log = Rc::new(RefCell::new(Vec::<String>::new()));
363 let mut d = EventDispatcher::new();
364
365 for (id, name) in [(WidgetId::ROOT, "root"), (a, "a"), (target, "target")] {
366 let log_c = Rc::clone(&log);
367 d.on_capture(
368 id,
369 Box::new(move |ctx: &mut HandlerCtx<'_>| {
370 log_c
371 .borrow_mut()
372 .push(format!("cap:{name}:{:?}", ctx.phase));
373 Propagation::CONTINUE
374 }),
375 );
376 let log_b = Rc::clone(&log);
377 d.on_bubble(
378 id,
379 Box::new(move |ctx: &mut HandlerCtx<'_>| {
380 log_b
381 .borrow_mut()
382 .push(format!("bub:{name}:{:?}", ctx.phase));
383 Propagation::CONTINUE
384 }),
385 );
386 }
387
388 d.dispatch(&tree, target, mouse_down());
389 let seen = log.borrow().clone();
390 assert_eq!(
391 seen,
392 vec![
393 // capture root → a (target excluded from capture loop)
394 "cap:root:Capture",
395 "cap:a:Capture",
396 // target + bubble target → root
397 "bub:target:Target",
398 "bub:a:Bubble",
399 "bub:root:Bubble",
400 ]
401 );
402 }
403
404 #[test]
405 fn stop_propagation_halts_bubble() {
406 let (tree, a, target) = linear_tree();
407 let log = Rc::new(RefCell::new(Vec::<String>::new()));
408 let mut d = EventDispatcher::new();
409
410 let log_t = Rc::clone(&log);
411 d.on_bubble(
412 target,
413 Box::new(move |_: &mut HandlerCtx<'_>| {
414 log_t.borrow_mut().push("target".to_string());
415 Propagation::stop() // stop here; `a` and root must NOT fire
416 }),
417 );
418 let log_a = Rc::clone(&log);
419 d.on_bubble(
420 a,
421 Box::new(move |_: &mut HandlerCtx<'_>| {
422 log_a.borrow_mut().push("a".to_string());
423 Propagation::CONTINUE
424 }),
425 );
426
427 let result = d.dispatch(&tree, target, mouse_down());
428 assert!(result.stop_propagation);
429 assert_eq!(*log.borrow(), vec!["target".to_string()]);
430 }
431
432 #[test]
433 fn prevent_default_is_reported() {
434 let (tree, _a, target) = linear_tree();
435 let mut d = EventDispatcher::new();
436 d.on_bubble(
437 target,
438 Box::new(|_: &mut HandlerCtx<'_>| Propagation::prevent()),
439 );
440 let result = d.dispatch(&tree, target, mouse_down());
441 assert!(result.prevent_default);
442 assert!(!result.stop_propagation);
443 }
444
445 #[test]
446 fn handler_removal_during_dispatch_is_deferred() {
447 let (tree, _a, target) = linear_tree();
448 let count = Rc::new(RefCell::new(0u32));
449 let mut d = EventDispatcher::new();
450
451 // Handler removes itself on first fire. Because removal is deferred, it
452 // still fires exactly once here; on the *second* dispatch it is gone.
453 let count_c = Rc::clone(&count);
454 d.on_bubble(
455 target,
456 Box::new(move |ctx: &mut HandlerCtx<'_>| {
457 *count_c.borrow_mut() += 1;
458 ctx.remove_handlers(target); // safe: applied after dispatch
459 Propagation::CONTINUE
460 }),
461 );
462
463 d.dispatch(&tree, target, mouse_down());
464 assert_eq!(*count.borrow(), 1);
465 assert_eq!(
466 d.registered_nodes(),
467 0,
468 "handler should be removed post-dispatch"
469 );
470
471 // Second dispatch: no handler remains, count unchanged.
472 d.dispatch(&tree, target, mouse_down());
473 assert_eq!(*count.borrow(), 1);
474 }
475
476 #[test]
477 fn handler_add_during_dispatch_is_deferred() {
478 let (tree, _a, target) = linear_tree();
479 let fired = Rc::new(RefCell::new(Vec::<&'static str>::new()));
480 let mut d = EventDispatcher::new();
481
482 let fired_outer = Rc::clone(&fired);
483 let fired_inner = Rc::clone(&fired);
484 d.on_bubble(
485 target,
486 Box::new(move |ctx: &mut HandlerCtx<'_>| {
487 fired_outer.borrow_mut().push("outer");
488 let f = Rc::clone(&fired_inner);
489 // Add a new handler mid-dispatch; must NOT fire this dispatch.
490 ctx.add_handler(
491 target,
492 Phase::Bubble,
493 Box::new(move |_: &mut HandlerCtx<'_>| {
494 f.borrow_mut().push("inner");
495 Propagation::CONTINUE
496 }),
497 );
498 Propagation::CONTINUE
499 }),
500 );
501
502 d.dispatch(&tree, target, mouse_down());
503 assert_eq!(
504 *fired.borrow(),
505 vec!["outer"],
506 "added handler must not fire same dispatch"
507 );
508 d.dispatch(&tree, target, mouse_down());
509 // Now both the original and the deferred-added handler fire.
510 assert_eq!(*fired.borrow(), vec!["outer", "outer", "inner"]);
511 }
512
513 #[test]
514 fn dispatch_to_missing_target_is_noop() {
515 let (tree, _a, _t) = linear_tree();
516 let mut d = EventDispatcher::new();
517 let prop = d.dispatch(&tree, WidgetId(9999), mouse_down());
518 assert_eq!(prop, Propagation::CONTINUE);
519 }
520}