Skip to main content

kozan_core/events/
path.rs

1//! Event propagation path.
2//!
3//! Chrome's `EventPath`: a snapshot of the ancestor chain from target to root,
4//! built once at dispatch start. Uses `NodeEventContext` entries.
5//!
6//! Kozan: uses raw `u32` indices (node slot indices) since all tree data
7//! is accessed through `DocumentCell`.
8
9use crate::dom::document_cell::DocumentCell;
10use crate::id::{INVALID, RawId};
11
12/// The propagation path for an event.
13///
14/// Built from target node up to the document root.
15/// Stored as `(index, generation)` pairs for liveness validation.
16///
17/// Index 0 = target, Index N = root (same order as Chrome).
18/// Capture iterates in reverse. Bubble iterates forward.
19pub struct EventPath {
20    /// (`node_index`, `node_generation`) pairs. Target-first order.
21    entries: Vec<(u32, u32)>,
22}
23
24impl EventPath {
25    /// Build the propagation path from target up to root.
26    ///
27    /// Takes a snapshot of the ancestor chain at dispatch time.
28    /// Changes to the tree during dispatch do not affect the path.
29    pub(crate) fn build(cell: DocumentCell, target: RawId) -> Self {
30        let mut entries = Vec::new();
31
32        if !cell.read(|doc| doc.is_alive_id(target)) {
33            return Self { entries };
34        }
35
36        entries.push((target.index(), target.generation()));
37
38        // Walk up the tree via parent pointers.
39        let mut current = target.index();
40        loop {
41            let Some(cur_gen) = cell.read(|doc| doc.generation(current)) else {
42                break;
43            };
44            let Some(tree) = cell.read(|doc| doc.tree_data(RawId::new(current, cur_gen))) else {
45                break;
46            };
47
48            if tree.parent == INVALID {
49                break;
50            }
51
52            let Some(parent_gen) = cell.read(|doc| doc.generation(tree.parent)) else {
53                break;
54            };
55
56            entries.push((tree.parent, parent_gen));
57            current = tree.parent;
58        }
59
60        Self { entries }
61    }
62
63    /// Number of nodes in the path.
64    #[must_use]
65    pub fn len(&self) -> usize {
66        self.entries.len()
67    }
68
69    /// Is the path empty? (target was dead)
70    #[must_use]
71    pub fn is_empty(&self) -> bool {
72        self.entries.is_empty()
73    }
74
75    /// The target node (first entry).
76    #[must_use]
77    pub fn target(&self) -> Option<(u32, u32)> {
78        self.entries.first().copied()
79    }
80
81    /// Index of the target in the path (always 0 if non-empty).
82    #[must_use]
83    pub fn target_index(&self) -> usize {
84        0
85    }
86
87    /// Iterate in capture order (root -> target).
88    pub fn capture_order(&self) -> impl Iterator<Item = (usize, u32, u32)> + '_ {
89        self.entries
90            .iter()
91            .enumerate()
92            .rev()
93            .map(|(i, &(idx, generation))| (i, idx, generation))
94    }
95
96    /// Iterate in bubble order (target -> root).
97    pub fn bubble_order(&self) -> impl Iterator<Item = (usize, u32, u32)> + '_ {
98        self.entries
99            .iter()
100            .enumerate()
101            .map(|(i, &(idx, generation))| (i, idx, generation))
102    }
103
104    /// Get entry at position.
105    #[must_use]
106    pub fn get(&self, pos: usize) -> Option<(u32, u32)> {
107        self.entries.get(pos).copied()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::dom::document::Document;
115    use crate::dom::traits::{ContainerNode, HasHandle};
116    use crate::html::{HtmlButtonElement, HtmlDivElement};
117
118    #[test]
119    fn path_target_to_root_ordering() {
120        let doc = Document::new();
121        let div = doc.create::<HtmlDivElement>();
122        let btn = doc.create::<HtmlButtonElement>();
123        doc.root().append(div);
124        div.append(btn);
125
126        let cell = doc.cell();
127        let path = EventPath::build(cell, btn.handle().raw());
128
129        // Path: target (btn) → div → root. Length = 3.
130        assert_eq!(path.len(), 3);
131        assert_eq!(
132            path.target().expect("non-empty path").0,
133            btn.handle().raw().index()
134        );
135        // Last entry is the root.
136        let (root_idx, _) = path.get(2).expect("root entry");
137        assert_eq!(root_idx, doc.root().raw().index());
138    }
139
140    #[test]
141    fn path_single_node_at_root() {
142        let doc = Document::new();
143        let cell = doc.cell();
144        let root_id = doc.root().raw();
145        let path = EventPath::build(cell, root_id);
146
147        // Root has no parent, so path is just the root itself.
148        assert_eq!(path.len(), 1);
149        assert_eq!(path.target().expect("non-empty").0, root_id.index());
150    }
151
152    #[test]
153    fn path_dead_target_produces_empty_path() {
154        let doc = Document::new();
155        let btn = doc.create::<HtmlButtonElement>();
156        let raw = btn.handle().raw();
157        btn.handle().destroy();
158
159        let cell = doc.cell();
160        let path = EventPath::build(cell, raw);
161        assert!(path.is_empty());
162    }
163
164    #[test]
165    fn capture_order_is_root_to_target() {
166        let doc = Document::new();
167        let div = doc.create::<HtmlDivElement>();
168        let btn = doc.create::<HtmlButtonElement>();
169        doc.root().append(div);
170        div.append(btn);
171
172        let cell = doc.cell();
173        let path = EventPath::build(cell, btn.handle().raw());
174
175        let capture: Vec<u32> = path.capture_order().map(|(_, idx, _)| idx).collect();
176        // Capture goes root → div → btn.
177        assert_eq!(capture[0], doc.root().raw().index());
178        assert_eq!(capture[1], div.handle().raw().index());
179        assert_eq!(capture[2], btn.handle().raw().index());
180    }
181
182    #[test]
183    fn bubble_order_is_target_to_root() {
184        let doc = Document::new();
185        let div = doc.create::<HtmlDivElement>();
186        let btn = doc.create::<HtmlButtonElement>();
187        doc.root().append(div);
188        div.append(btn);
189
190        let cell = doc.cell();
191        let path = EventPath::build(cell, btn.handle().raw());
192
193        let bubble: Vec<u32> = path.bubble_order().map(|(_, idx, _)| idx).collect();
194        // Bubble goes btn → div → root.
195        assert_eq!(bubble[0], btn.handle().raw().index());
196        assert_eq!(bubble[1], div.handle().raw().index());
197        assert_eq!(bubble[2], doc.root().raw().index());
198    }
199
200    #[test]
201    fn detached_node_path_contains_only_target() {
202        let doc = Document::new();
203        let btn = doc.create::<HtmlButtonElement>();
204        // Not attached to tree — no parent.
205        let cell = doc.cell();
206        let path = EventPath::build(cell, btn.handle().raw());
207
208        assert_eq!(path.len(), 1);
209        assert_eq!(
210            path.target().expect("has target").0,
211            btn.handle().raw().index()
212        );
213    }
214}