Skip to main content

kozan_core/dom/
handle.rs

1//! Handle — the 16-byte universal node reference.
2//!
3//! # Thread safety
4//!
5//! `Handle` is `Send` but NOT `Sync`.
6//!
7//! **Send**: A `Handle` can be moved into a `WakeSender` callback that
8//! runs on the view thread. This is sound because:
9//!   1. `WakeSender` callbacks are guaranteed to execute on the same view
10//!      thread that owns the `Document`.
11//!   2. The `Document` is alive for the lifetime of the view thread.
12//!   3. There is never concurrent access — the handle is moved, not shared.
13//!
14//! **!Sync**: Sharing `&Handle` across threads is NOT allowed. Mutations go
15//! through `DocumentCell` which has no internal locking — concurrent `&mut`
16//! from two threads would be UB. `!Sync` prevents this at compile time.
17//!
18//! This mirrors Chrome's `WeakPtr<Node>` used in cross-thread task posting:
19//! the pointer is only dereferenced on the owning thread, but the closure
20//! carrying it is sent through the IPC/task channel.
21
22use core::marker::PhantomData;
23
24use crate::dom::document_cell::DocumentCell;
25use crate::dom::node::NodeType;
26use crate::id::{INVALID, RawId};
27
28/// A universal node handle. 16 bytes, `Copy`, `Send`, `!Sync`.
29///
30/// Internally: `RawId` (8 bytes) + `DocumentCell` (8 bytes).
31/// All node types are newtypes around this.
32///
33/// See module-level doc for the thread-safety invariants.
34#[derive(Copy, Clone)]
35pub struct Handle {
36    pub(crate) id: RawId,
37    pub(crate) cell: DocumentCell,
38    /// Suppresses `Sync` (shared-reference access from multiple threads)
39    /// while allowing `Send` (moving ownership to another thread).
40    _no_sync: PhantomData<*const ()>,
41}
42
43// SAFETY: See module-level doc. Callbacks carrying a Handle always run on
44// the view thread that owns the Document — no concurrent access occurs.
45unsafe impl Send for Handle {}
46
47impl Handle {
48    #[inline]
49    pub(crate) fn new(id: RawId, cell: DocumentCell) -> Self {
50        Self {
51            id,
52            cell,
53            _no_sync: PhantomData,
54        }
55    }
56
57    /// The raw node ID (index + generation). Can be sent across threads.
58    #[inline]
59    #[must_use]
60    pub fn raw(&self) -> RawId {
61        self.id
62    }
63
64    /// Check if this node is still alive.
65    #[inline]
66    #[must_use]
67    pub fn is_alive(&self) -> bool {
68        self.cell.read(|doc| doc.is_alive_id(self.id))
69    }
70
71    // ---- Data access ----
72
73    /// Read element-specific data through a scoped closure.
74    #[inline]
75    pub fn read_data<D: 'static, R: 'static>(&self, f: impl FnOnce(&D) -> R) -> Option<R> {
76        self.cell.check_alive();
77        self.cell.read(|doc| doc.read_data(self.id, f))
78    }
79
80    /// Write element-specific data. Marks dirty.
81    #[inline]
82    pub fn write_data<D: 'static, R: 'static>(&self, f: impl FnOnce(&mut D) -> R) -> Option<R> {
83        self.cell.check_alive();
84        self.cell.write(|doc| doc.write_data(self.id, f))
85    }
86
87    /// Mark this node's parent element for layout only (not restyle).
88    ///
89    /// Used by text nodes: text content changes affect the parent's layout
90    /// (text sizing). Chrome: `CharacterData::DidModifyData()` →
91    /// `ContainerNode::ChildrenChanged()` → `SetNeedsStyleRecalc()`.
92    pub(crate) fn mark_parent_needs_layout(&self) {
93        let index = self.id.index();
94        self.cell.write(|doc| {
95            let parent_idx = doc
96                .tree
97                .get(index)
98                .map(|td| td.parent)
99                .filter(|&p| p != crate::id::INVALID);
100            if let Some(_parent) = parent_idx {
101                // Text content changed — needs relayout, NOT restyle.
102                // DON'T set needs_style_recalc (would force-clear ALL caches).
103                // Just clear this node's cache + ancestors (via layout_parent chain).
104                doc.dirty_layout_nodes.push(index);
105                doc.mark_layout_dirty(index);
106            }
107        });
108    }
109
110    // ---- Element data (attributes) ----
111
112    /// Tag name (e.g., "div", "button"). None for non-element nodes.
113    #[must_use]
114    pub fn tag_name(&self) -> Option<&'static str> {
115        self.cell
116            .read(|doc| doc.read_element_data(self.id, |ed| ed.tag_name))
117    }
118
119    /// Get the `id` attribute.
120    #[must_use]
121    pub fn id(&self) -> String {
122        self.attribute("id").unwrap_or_default()
123    }
124
125    /// Set the `id` attribute.
126    pub fn set_id(&self, id: impl Into<String>) {
127        self.set_attribute("id", id);
128    }
129
130    /// Get the `class` attribute.
131    #[must_use]
132    pub fn class_name(&self) -> String {
133        self.attribute("class").unwrap_or_default()
134    }
135
136    /// Set the `class` attribute.
137    pub fn set_class_name(&self, class: impl Into<String>) {
138        self.set_attribute("class", class);
139    }
140
141    /// Returns the attribute value for `name`, or `None` if absent.
142    #[must_use]
143    pub fn attribute(&self, name: &str) -> Option<String> {
144        self.cell
145            .read(|doc| {
146                doc.read_element_data(self.id, |ed| ed.attributes.get(name).map(|v| v.to_string()))
147            })
148            .flatten()
149    }
150
151    /// Set an attribute.
152    pub fn set_attribute(&self, name: &str, value: impl Into<String>) {
153        let value = value.into();
154        let index = self.id.index();
155        self.cell.write(|doc| {
156            let guard = doc.style_engine.shared_lock().clone();
157            doc.write_element_data(self.id, |ed| {
158                ed.on_attribute_set(name, &value, &guard);
159                ed.attributes.set(name, value);
160            });
161            doc.mark_for_restyle(index);
162        });
163    }
164
165    /// Remove an attribute.
166    #[must_use]
167    pub fn remove_attribute(&self, name: &str) -> Option<String> {
168        let index = self.id.index();
169        self.cell.write(|doc| {
170            let removed = doc
171                .write_element_data(self.id, |ed| {
172                    ed.on_attribute_removed(name);
173                    ed.attributes.remove(name)
174                })
175                .flatten();
176            doc.mark_for_restyle(index);
177            removed
178        })
179    }
180
181    // ---- ClassList — Chrome: element.classList (DOMTokenList) ----
182
183    /// Add a CSS class. No-op if already present.
184    /// Chrome equivalent: `element.classList.add("name")`.
185    pub fn class_add(&self, name: &str) {
186        let index = self.id.index();
187        self.cell.write(|doc| {
188            let changed = doc
189                .write_element_data(self.id, |ed| ed.class_add(name))
190                .unwrap_or(false);
191            if changed {
192                doc.mark_for_restyle(index);
193            }
194        });
195    }
196
197    /// Remove a CSS class. No-op if not present.
198    /// Chrome equivalent: `element.classList.remove("name")`.
199    pub fn class_remove(&self, name: &str) {
200        let index = self.id.index();
201        self.cell.write(|doc| {
202            let changed = doc
203                .write_element_data(self.id, |ed| ed.class_remove(name))
204                .unwrap_or(false);
205            if changed {
206                doc.mark_for_restyle(index);
207            }
208        });
209    }
210
211    /// Toggle a CSS class. Returns true if now present.
212    /// Chrome equivalent: `element.classList.toggle("name")`.
213    #[must_use]
214    pub fn class_toggle(&self, name: &str) -> bool {
215        let index = self.id.index();
216        self.cell.write(|doc| {
217            let present = doc
218                .write_element_data(self.id, |ed| ed.class_toggle(name))
219                .unwrap_or(false);
220            doc.mark_for_restyle(index);
221            present
222        })
223    }
224
225    /// Check if an element has a CSS class.
226    /// Chrome equivalent: `element.classList.contains("name")`.
227    #[must_use]
228    pub fn class_contains(&self, name: &str) -> bool {
229        self.cell
230            .read(|doc| doc.read_element_data(self.id, |ed| ed.class_contains(name)))
231            .unwrap_or(false)
232    }
233
234    // ---- Tree operations ----
235
236    /// Append `child` as the last child.
237    ///
238    /// Accepts any node type (`HtmlDivElement`, `Text`, `Handle`, etc.).
239    pub fn append(&self, child: impl Into<Handle>) -> &Self {
240        let child = child.into();
241        self.cell.check_alive();
242        self.cell.write(|doc| doc.append_child(self.id, child.id));
243        self
244    }
245
246    /// Append a child and return self (Copy) for chaining.
247    ///
248    /// ```ignore
249    /// doc.root()
250    ///     .child(doc.div())
251    ///     .child(doc.div());
252    /// ```
253    pub fn child(self, child: impl Into<Handle>) -> Self {
254        self.append(child);
255        self
256    }
257
258    /// Append multiple children at once.
259    pub fn add_children<I, C>(self, items: I) -> Self
260    where
261        I: IntoIterator<Item = C>,
262        C: Into<Handle>,
263    {
264        self.cell.check_alive();
265        self.cell.write(|doc| {
266            for child in items {
267                doc.append_child(self.id, child.into().id);
268            }
269        });
270        self
271    }
272
273    /// Insert `child` before this node.
274    pub fn insert_before(&self, child: impl Into<Handle>) {
275        let child = child.into();
276        self.cell.check_alive();
277        self.cell.write(|doc| doc.insert_before(self.id, child.id));
278    }
279
280    /// Remove from parent.
281    pub fn detach(&self) {
282        self.cell.check_alive();
283        self.cell.write(|doc| doc.detach_node(self.id));
284    }
285
286    /// Destroy this node. All handles become stale.
287    pub fn destroy(&self) {
288        self.cell.check_alive();
289        self.cell.write(|doc| doc.destroy_node(self.id));
290    }
291
292    // ---- Tree queries ----
293
294    /// Parent node.
295    #[must_use]
296    pub fn parent(&self) -> Option<Handle> {
297        self.cell
298            .read(|doc| {
299                let tree = doc.tree_data(self.id)?;
300                if tree.parent == INVALID {
301                    return None;
302                }
303                let id = doc.raw_id(tree.parent)?;
304                Some(id)
305            })
306            .map(|id| Handle::new(id, self.cell))
307    }
308
309    /// First child.
310    #[must_use]
311    pub fn first_child(&self) -> Option<Handle> {
312        self.cell
313            .read(|doc| {
314                let tree = doc.tree_data(self.id)?;
315                if tree.first_child == INVALID {
316                    return None;
317                }
318                doc.raw_id(tree.first_child)
319            })
320            .map(|id| Handle::new(id, self.cell))
321    }
322
323    /// Last child.
324    #[must_use]
325    pub fn last_child(&self) -> Option<Handle> {
326        self.cell
327            .read(|doc| {
328                let tree = doc.tree_data(self.id)?;
329                if tree.last_child == INVALID {
330                    return None;
331                }
332                doc.raw_id(tree.last_child)
333            })
334            .map(|id| Handle::new(id, self.cell))
335    }
336
337    /// Next sibling.
338    #[must_use]
339    pub fn next_sibling(&self) -> Option<Handle> {
340        self.cell
341            .read(|doc| {
342                let tree = doc.tree_data(self.id)?;
343                if tree.next_sibling == INVALID {
344                    return None;
345                }
346                doc.raw_id(tree.next_sibling)
347            })
348            .map(|id| Handle::new(id, self.cell))
349    }
350
351    /// Previous sibling.
352    #[must_use]
353    pub fn prev_sibling(&self) -> Option<Handle> {
354        self.cell
355            .read(|doc| {
356                let tree = doc.tree_data(self.id)?;
357                if tree.prev_sibling == INVALID {
358                    return None;
359                }
360                doc.raw_id(tree.prev_sibling)
361            })
362            .map(|id| Handle::new(id, self.cell))
363    }
364
365    /// All children.
366    #[must_use]
367    pub fn children(&self) -> Vec<Handle> {
368        let ids = self.cell.read(|doc| doc.children_ids(self.id));
369        ids.into_iter()
370            .map(|id| Handle::new(id, self.cell))
371            .collect()
372    }
373
374    // ---- Node kind ----
375
376    #[must_use]
377    pub fn node_kind(&self) -> Option<NodeType> {
378        self.cell.read(|doc| doc.node_kind(self.id))
379    }
380    #[must_use]
381    pub fn is_element(&self) -> bool {
382        self.node_kind() == Some(NodeType::Element)
383    }
384    #[must_use]
385    pub fn is_text(&self) -> bool {
386        self.node_kind() == Some(NodeType::Text)
387    }
388    #[must_use]
389    pub fn is_document(&self) -> bool {
390        self.node_kind() == Some(NodeType::Document)
391    }
392
393    // ---- Style ----
394
395    /// Per-property style access — like JavaScript's `element.style`.
396    ///
397    /// ```ignore
398    /// div.style().color(AbsoluteColor::RED);
399    /// div.style().display(Display::Flex);
400    /// ```
401    #[must_use]
402    pub fn style(&self) -> crate::styling::builder::StyleAccess {
403        crate::styling::builder::StyleAccess::new(self.cell, self.id)
404    }
405
406    // ---- Event dispatch (for untyped handles from hit testing) ----
407
408    /// Dispatch an event to this node.
409    ///
410    /// Runs the full capture -> target -> bubble pipeline.
411    /// Returns `true` if the default action was NOT prevented.
412    ///
413    /// This mirrors `EventTarget::dispatch_event()` but works on raw `Handle`
414    /// without requiring the `HasHandle` trait (which would conflict with the
415    /// blanket `From<T: HasHandle>` impl). Used by `EventHandler` after hit
416    /// testing returns a node index resolved to a Handle.
417    pub fn dispatch_event(&self, event: &dyn crate::events::Event) -> bool {
418        if !self.is_alive() {
419            return false;
420        }
421        let mut store = crate::events::dispatcher::EventStoreAccess::new(self.cell);
422        crate::events::dispatch(self.cell, self.id, event, &mut store)
423    }
424}
425
426/// Any type with `HasHandle` can convert to `Handle`.
427/// Enables `doc.root().append(btn)` instead of `doc.root().append(btn.handle())`.
428impl<T: crate::dom::traits::HasHandle> From<T> for Handle {
429    #[inline]
430    fn from(value: T) -> Self {
431        value.handle()
432    }
433}
434
435impl core::fmt::Debug for Handle {
436    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
437        write!(f, "Handle({:?})", self.id)
438    }
439}