use std::collections::HashMap;
use crate::abort::AbortSignal;
use crate::dom::Dom;
use crate::error::{DomError, Result};
use crate::event::{Event, EventPhase};
use crate::node_id::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListenerId {
pub node: NodeId,
pub seq: u32,
}
pub(crate) struct Listener<Ext: 'static> {
seq: u32,
event_type: String,
capture: bool,
once: bool,
signal: Option<AbortSignal>,
handler: Option<EventHandler<Ext>>,
}
pub(crate) type EventHandler<Ext> = Box<dyn FnMut(&mut EventCtx<'_, Ext>) + 'static>;
pub struct EventCtx<'a, Ext: 'static> {
pub event: &'a mut Event,
pub dom: &'a mut Dom<Ext>,
}
impl<Ext: 'static> EventCtx<'_, Ext> {
pub fn request_redraw(&mut self) {
self.event.redraw_requested = true;
}
}
#[derive(Debug, Clone, Default)]
pub struct ListenerOptions {
pub capture: bool,
pub once: bool,
pub signal: Option<AbortSignal>,
}
impl ListenerOptions {
pub fn capture() -> Self {
Self {
capture: true,
..Default::default()
}
}
pub fn once() -> Self {
Self {
once: true,
..Default::default()
}
}
pub fn with_signal(mut self, signal: AbortSignal) -> Self {
self.signal = Some(signal);
self
}
}
#[derive(Default)]
pub(crate) struct ListenerStore<Ext: 'static> {
by_node: HashMap<NodeId, Vec<Listener<Ext>>>,
next_seq: u32,
}
impl<Ext: 'static> ListenerStore<Ext> {
fn next(&mut self) -> u32 {
let n = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
n
}
}
impl<Ext: 'static> std::fmt::Debug for ListenerStore<Ext> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListenerStore")
.field("nodes_with_listeners", &self.by_node.len())
.field("next_seq", &self.next_seq)
.finish()
}
}
impl<Ext> Dom<Ext> {
pub fn add_event_listener(
&mut self,
node: NodeId,
event_type: impl Into<String>,
options: ListenerOptions,
handler: impl FnMut(&mut EventCtx<'_, Ext>) + 'static,
) -> Result<ListenerId> {
self.node_or_err(node)?;
let seq = self.listeners.next();
let listener = Listener {
seq,
event_type: event_type.into(),
capture: options.capture,
once: options.once,
signal: options.signal,
handler: Some(Box::new(handler)),
};
self.listeners
.by_node
.entry(node)
.or_default()
.push(listener);
Ok(ListenerId { node, seq })
}
pub fn remove_event_listener(&mut self, handle: ListenerId) -> bool {
let Some(vec) = self.listeners.by_node.get_mut(&handle.node) else {
return false;
};
let before = vec.len();
vec.retain(|l| l.seq != handle.seq);
let removed = vec.len() < before;
if vec.is_empty() {
self.listeners.by_node.remove(&handle.node);
}
removed
}
pub fn listener_count(&self, node: NodeId) -> usize {
self.listeners.by_node.get(&node).map_or(0, Vec::len)
}
pub fn dispatch_event(&mut self, target: NodeId, event: &mut Event) -> Result<()> {
self.node_or_err(target)?;
event.target = Some(target);
let path = self.ancestor_path(target);
if path.is_empty() {
return Err(DomError::InvalidNode(target));
}
event.phase = EventPhase::Capturing;
for &node in path.iter().take(path.len() - 1) {
if event.propagation_stopped {
break;
}
event.current_target = Some(node);
self.fire_at(node, event, PhaseFilter::Capture);
}
if !event.propagation_stopped {
event.phase = EventPhase::AtTarget;
event.current_target = Some(target);
self.fire_at(target, event, PhaseFilter::All);
}
if event.bubbles && !event.propagation_stopped {
event.phase = EventPhase::Bubbling;
for &node in path.iter().rev().skip(1) {
if event.propagation_stopped {
break;
}
event.current_target = Some(node);
self.fire_at(node, event, PhaseFilter::Bubble);
}
}
event.phase = EventPhase::None;
event.current_target = None;
Ok(())
}
fn fire_at(&mut self, node: NodeId, event: &mut Event, filter: PhaseFilter) {
if let Some(list) = self.listeners.by_node.get_mut(&node) {
list.retain(|l| !l.signal.as_ref().is_some_and(|s| s.is_aborted()));
if list.is_empty() {
self.listeners.by_node.remove(&node);
}
}
let to_fire: Vec<(u32, bool)> = match self.listeners.by_node.get(&node) {
Some(list) => list
.iter()
.filter(|l| l.event_type == event.event_type && filter.matches(l.capture))
.map(|l| (l.seq, l.once))
.collect(),
None => return,
};
for (seq, once) in to_fire {
if event.immediate_propagation_stopped {
break;
}
let Some(list) = self.listeners.by_node.get_mut(&node) else {
return;
};
let Some(i) = list.iter().position(|l| l.seq == seq) else {
continue; };
if list[i].signal.as_ref().is_some_and(|s| s.is_aborted()) {
list.remove(i);
continue;
}
let Some(mut handler) = list[i].handler.take() else {
continue;
};
let mut ctx = EventCtx { event, dom: self };
handler(&mut ctx);
if let Some(list) = self.listeners.by_node.get_mut(&node) {
if let Some(i) = list.iter().position(|l| l.seq == seq) {
if once {
list.remove(i);
} else {
list[i].handler = Some(handler);
}
}
if list.is_empty() {
self.listeners.by_node.remove(&node);
}
}
}
}
pub(crate) fn drop_listeners(&mut self, node: NodeId) {
self.listeners.by_node.remove(&node);
}
}
#[derive(Debug, Clone, Copy)]
enum PhaseFilter {
Capture,
Bubble,
All,
}
impl PhaseFilter {
fn matches(self, listener_capture: bool) -> bool {
match self {
PhaseFilter::Capture => listener_capture,
PhaseFilter::Bubble => !listener_capture,
PhaseFilter::All => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Event;
use std::cell::Cell;
use std::rc::Rc;
fn build_chain() -> (Dom, NodeId, NodeId, NodeId, NodeId) {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(a, b).unwrap();
dom.append_child(b, c).unwrap();
dom.append_child(root, a).unwrap();
(dom, a, b, c, root)
}
#[test]
fn add_and_remove_listener() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f2 = fired.clone();
let id = dom
.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
f2.set(f2.get() + 1);
})
.unwrap();
assert_eq!(dom.listener_count(el), 1);
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(fired.get(), 1);
assert!(dom.remove_event_listener(id));
assert_eq!(dom.listener_count(el), 0);
let mut e2 = Event::new("click");
dom.dispatch_event(el, &mut e2).unwrap();
assert_eq!(fired.get(), 1); }
#[test]
fn capture_target_bubble_ordering() {
let (mut dom, a, b, c, _) = build_chain();
let order = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
let o = order.clone();
dom.add_event_listener(a, "click", ListenerOptions::capture(), move |_| {
o.borrow_mut().push("a-capture");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(b, "click", ListenerOptions::capture(), move |_| {
o.borrow_mut().push("b-capture");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(c, "click", ListenerOptions::default(), move |_| {
o.borrow_mut().push("c-target");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(b, "click", ListenerOptions::default(), move |_| {
o.borrow_mut().push("b-bubble");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |_| {
o.borrow_mut().push("a-bubble");
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(
*order.borrow(),
vec!["a-capture", "b-capture", "c-target", "b-bubble", "a-bubble"]
);
}
#[test]
fn stop_propagation_cuts_bubble() {
let (mut dom, a, b, c, _) = build_chain();
let a_fired = Rc::new(Cell::new(false));
let af = a_fired.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |_| {
af.set(true);
})
.unwrap();
dom.add_event_listener(b, "click", ListenerOptions::default(), move |ctx| {
ctx.event.stop_propagation();
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert!(!a_fired.get());
}
#[test]
fn stop_immediate_cuts_sibling_listener_on_same_node() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let hit_first = Rc::new(Cell::new(false));
let hit_second = Rc::new(Cell::new(false));
let h1 = hit_first.clone();
let h2 = hit_second.clone();
dom.add_event_listener(el, "x", ListenerOptions::default(), move |ctx| {
h1.set(true);
ctx.event.stop_immediate_propagation();
})
.unwrap();
dom.add_event_listener(el, "x", ListenerOptions::default(), move |_| {
h2.set(true);
})
.unwrap();
let mut e = Event::new("x");
dom.dispatch_event(el, &mut e).unwrap();
assert!(hit_first.get());
assert!(!hit_second.get());
}
#[test]
fn non_bubbling_skips_bubble_phase() {
let (mut dom, a, _, c, _) = build_chain();
let a_fired = Rc::new(Cell::new(false));
let af = a_fired.clone();
dom.add_event_listener(a, "x", ListenerOptions::default(), move |_| {
af.set(true);
})
.unwrap();
let mut e = Event::new("x").with_bubbles(false);
dom.dispatch_event(c, &mut e).unwrap();
assert!(!a_fired.get());
}
#[test]
fn prevent_default_sets_flag() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_event_listener(el, "click", ListenerOptions::default(), move |ctx| {
ctx.event.prevent_default();
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert!(e.default_prevented());
}
#[test]
fn once_removes_after_firing() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
dom.add_event_listener(el, "x", ListenerOptions::once(), move |_| {
f.set(f.get() + 1);
})
.unwrap();
let mut e1 = Event::new("x");
dom.dispatch_event(el, &mut e1).unwrap();
let mut e2 = Event::new("x");
dom.dispatch_event(el, &mut e2).unwrap();
assert_eq!(fired.get(), 1);
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn listener_type_is_filtered() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let click = Rc::new(Cell::new(0));
let input = Rc::new(Cell::new(0));
let cc = click.clone();
let ii = input.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
cc.set(cc.get() + 1);
})
.unwrap();
dom.add_event_listener(el, "input", ListenerOptions::default(), move |_| {
ii.set(ii.get() + 1);
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(click.get(), 1);
assert_eq!(input.get(), 0);
}
#[test]
fn handler_may_read_dom() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = dom.create_element("a");
dom.set_attribute(a, "id", "target").unwrap();
dom.append_child(root, a).unwrap();
let seen = Rc::new(Cell::new(false));
let s = seen.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |ctx| {
if ctx.dom.get_element_by_id("target").is_some() {
s.set(true);
}
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(a, &mut e).unwrap();
assert!(seen.get());
}
#[test]
fn handler_may_mutate_dom() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_event_listener(el, "click", ListenerOptions::default(), move |ctx| {
let added = ctx.dom.create_element("added");
ctx.dom.append_child(el, added).unwrap();
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(dom.node(el).child_element_count(), 1);
}
#[test]
fn dispatch_on_orphan_node_still_fires_target() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert!(fired.get());
}
#[test]
fn current_target_reflects_node_during_dispatch() {
let (mut dom, a, _b, c, _) = build_chain();
let ct_at_a = Rc::new(Cell::new(None));
let p = ct_at_a.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |ctx| {
p.set(ctx.event.current_target);
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(ct_at_a.get(), Some(a));
assert_eq!(e.current_target, None);
}
#[test]
fn removing_node_unregisters_listeners_via_drop_subtree() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let _ = dom
.add_event_listener(el, "click", ListenerOptions::default(), |_| {})
.unwrap();
let root = dom.root();
dom.append_child(root, el).unwrap();
dom.drop_subtree(el).unwrap();
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn abort_signal_removes_listener_on_next_dispatch() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
let ctrl = AbortController::new();
let sig = ctrl.signal();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig),
move |_| {
f.set(f.get() + 1);
},
)
.unwrap();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
ctrl.abort();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn abort_signal_removes_multiple_listeners_at_once() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let sig = ctrl.signal();
for _ in 0..5 {
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig.clone()),
|_| {},
)
.unwrap();
}
assert_eq!(dom.listener_count(el), 5);
ctrl.abort();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn abort_signal_independent_of_other_listeners() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let sig = ctrl.signal();
let fired_aborted = Rc::new(Cell::new(0));
let fa = fired_aborted.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig),
move |_| {
fa.set(fa.get() + 1);
},
)
.unwrap();
let fired_normal = Rc::new(Cell::new(0));
let fn_ = fired_normal.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
fn_.set(fn_.get() + 1);
})
.unwrap();
ctrl.abort();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired_aborted.get(), 0, "aborted listener must not fire");
assert_eq!(fired_normal.get(), 1, "unrelated listener still fires");
}
#[test]
fn adding_listener_with_already_aborted_signal_never_fires() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
ctrl.abort();
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(ctrl.signal()),
move |_| {
f.set(f.get() + 1);
},
)
.unwrap();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 0);
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn handler_aborting_mid_dispatch_skips_later_listeners_on_same_node() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let order = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
let order_a = order.clone();
let ctrl_a = ctrl.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
order_a.borrow_mut().push("a");
ctrl_a.abort();
})
.unwrap();
let order_b = order.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(ctrl.signal()),
move |_| {
order_b.borrow_mut().push("b");
},
)
.unwrap();
let order_c = order.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
order_c.borrow_mut().push("c");
})
.unwrap();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(*order.borrow(), vec!["a", "c"]);
}
#[test]
fn signal_clone_governs_same_listener() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let sig1 = ctrl.signal();
let sig2 = sig1.clone();
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig2),
move |_| {
f.set(f.get() + 1);
},
)
.unwrap();
assert!(!sig1.is_aborted());
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
ctrl.abort();
assert!(sig1.is_aborted());
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
}
}