Skip to main content

rdom_core/
dom.rs

1//! `Dom<Ext>` — the arena.
2//!
3//! Owns all nodes in a flat `Vec<Option<Node<Ext>>>`. Tree structure is
4//! expressed via `NodeId` fields inside each `Node`. Slots freed by
5//! `remove_child` are recycled LIFO via a free list.
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::num::NonZeroU32;
9
10use crate::accessor::NodeRef;
11use crate::dispatch::ListenerStore;
12use crate::error::{DomError, Result};
13use crate::indexes::Indexes;
14use crate::node::{Node, NodeData, NodeType};
15use crate::node_id::NodeId;
16use crate::node_list::NodeList;
17use crate::observer::{InteractionKind, Mutation, ObserverStore};
18use crate::selection::{Position, Range};
19
20/// The root of a DOM tree. One instance per document.
21///
22/// Not `Clone`: event listeners are stored as `Box<dyn FnMut>` closures
23/// which cannot be cloned. Use `clone_node(root, true)` if you need a
24/// structural copy of the tree (attrs/classes/children — listeners are
25/// explicitly excluded, matching browser semantics).
26///
27/// `Ext: 'static` is required because event listeners and mutation
28/// observers are stored as `Box<dyn … + 'static>` trait objects —
29/// non-'static Ext types couldn't be boxed that way.
30/// Generation every slot starts at.
31const FIRST_GENERATION: NonZeroU32 = NonZeroU32::MIN;
32
33/// One arena slot: the node (or `None` while the slot is free) and the
34/// generation it was last issued under. A `NodeId` resolves only while
35/// its generation matches the slot's, so a handle to a dropped node
36/// never aliases the slot's next occupant. Colocated so the hottest
37/// lookup (`get_node`) touches one `Vec` entry, not two.
38#[derive(Debug)]
39pub(crate) struct Slot<Ext> {
40    pub(crate) generation: NonZeroU32,
41    pub(crate) node: Option<Node<Ext>>,
42}
43
44impl<Ext> Slot<Ext> {
45    fn fresh(node: Node<Ext>) -> Self {
46        Self {
47            generation: FIRST_GENERATION,
48            node: Some(node),
49        }
50    }
51}
52
53#[derive(Debug)]
54pub struct Dom<Ext: 'static = ()> {
55    /// Arena storage, indexed by `NodeId::index`.
56    pub(crate) nodes: Vec<Slot<Ext>>,
57    /// Free-slot indices, LIFO (cache-friendly reuse).
58    pub(crate) free: Vec<u32>,
59    /// The root node. Created at `Dom::new`; identity is stable for the
60    /// lifetime of the `Dom`.
61    pub(crate) root: NodeId,
62    /// DOM §2.9 "activation behavior" hook, installed by a backend
63    /// (rdom-tui's builtins). `dispatch_event` calls it before any
64    /// listener runs (`ActivationPhase::Pre`, the legacy-pre-activation
65    /// step: a checkbox flips here) and again after dispatch
66    /// (`Post { canceled }`, where a canceled event reverts and an
67    /// uncanceled one fires `input` / `change`). Runs regardless of
68    /// `stopPropagation()`, which only affects listeners. `None` = no
69    /// element in this Dom has activation behavior.
70    pub(crate) activation_hook: crate::dispatch::ActivationSlot<Ext>,
71    /// The backend's constraint check behind `:valid` / `:invalid`
72    /// (`Dom::set_validity_hook`). `None` = every candidate is valid.
73    pub(crate) validity_hook: crate::constraint::ValiditySlot<Ext>,
74    /// O(1) indexes for id / tag / class lookups. Kept in sync with every
75    /// mutation via `hook_register` / `hook_unregister` and the attr/class
76    /// accessors in `attrs.rs`.
77    pub(crate) indexes: Indexes,
78    /// Event listener side-storage. Only nodes with at least one listener
79    /// appear in the map — sparse trees pay nothing.
80    pub(crate) listeners: ListenerStore<Ext>,
81    /// Current interaction state — consulted by the selector matcher for
82    /// `:hover` and `:focus` pseudo-classes. `None` means "nothing
83    /// hovered" / "nothing focused". Mutators (`set_hovered` /
84    /// `set_focused`) are where Phase 7.4's MutationObserver will hook
85    /// in to fire `InteractionChanged` records.
86    pub(crate) hovered: Option<NodeId>,
87    pub(crate) focused: Option<NodeId>,
88    /// The element being activated (`:active`, Selectors 4 §9.4) —
89    /// the backend sets it while the primary pointer button is held.
90    pub(crate) active: Option<NodeId>,
91    /// Whether the focused element's focus should be made evident
92    /// (`:focus-visible`, Selectors 4 §13.2). The backend applies the
93    /// UA heuristics (keyboard vs. pointer); `true` until it says
94    /// otherwise.
95    pub(crate) focus_visible: bool,
96    /// The element that currently owns the pointer, via
97    /// `set_pointer_capture`. While set, the runtime routes every
98    /// `mousemove`, drag, and `mouseup` to this element regardless of
99    /// which element the cursor is over. Auto-released on the next
100    /// `mouseup` (browser-faithful). No mutation record fires — this
101    /// state doesn't affect cascade / selectors.
102    pub(crate) pointer_capture: Option<NodeId>,
103    /// Opt-in flag for the active captured drag: when `true`, the backend
104    /// autoscrolls the nearest scroll container while the pointer dwells at its
105    /// edge (DRAG-AUTOSCROLL). A generic bool here keeps rdom-core
106    /// renderer-agnostic — the backend (rdom-tui) interprets it. Reset to
107    /// `false` whenever the capture changes or releases, so a plain capture (a
108    /// slider, a scrollbar drag) never autoscrolls unless it opts in.
109    pub(crate) drag_autoscroll: bool,
110    /// Document-level text selection. `None` = nothing selected.
111    /// Mutations fire `Mutation::SelectionChanged`. Paint observers
112    /// use those records to refresh the `::selection` overlay.
113    pub(crate) selection: Option<crate::Selection>,
114    /// Count of actual selection changes (see [`Dom::selection_serial`]).
115    pub(crate) selection_serial: crate::SelectionSerial,
116    /// Mutation observers. Fires `Mutation` records on every DOM change.
117    pub(crate) observers: ObserverStore<Ext>,
118    /// Re-entrancy guard: true while an observer callback is running.
119    /// Mutations attempted during that window panic with a clear message.
120    pub(crate) is_observing: bool,
121}
122
123impl<Ext: Default> Default for Dom<Ext> {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl<Ext: Default> Dom<Ext> {
130    /// Create a new arena with a `<document-fragment>` root. Use
131    /// `with_root_tag` if you want the root to be a specific element tag.
132    pub fn new() -> Self {
133        let root_node: Node<Ext> = Node::new(NodeData::Fragment);
134        let nodes = vec![Slot::fresh(root_node)];
135        let root = NodeId::from_parts(0, FIRST_GENERATION);
136        Self {
137            nodes,
138            free: Vec::new(),
139            root,
140            indexes: Indexes::default(),
141            listeners: ListenerStore::default(),
142            hovered: None,
143            focused: None,
144            active: None,
145            focus_visible: true,
146            pointer_capture: None,
147            drag_autoscroll: false,
148            selection: None,
149            selection_serial: crate::SelectionSerial::new(0),
150            observers: ObserverStore::default(),
151            is_observing: false,
152            activation_hook: crate::dispatch::ActivationSlot(None),
153            validity_hook: crate::constraint::ValiditySlot(None),
154        }
155    }
156}
157
158impl<Ext> Dom<Ext> {
159    /// The node currently flagged as hovered (see `:hover`). `None` when
160    /// nothing is hovered. Matching consults this field directly.
161    pub fn hovered(&self) -> Option<NodeId> {
162        self.hovered
163    }
164
165    /// The node currently flagged as focused (see `:focus`).
166    pub fn focused(&self) -> Option<NodeId> {
167        self.focused
168    }
169
170    /// The element currently being activated (see `:active`): the
171    /// backend sets it for the duration of a primary-button press.
172    /// `None` when nothing is being activated.
173    pub fn active(&self) -> Option<NodeId> {
174        self.active
175    }
176
177    /// Whether the focused element's focus should be made evident —
178    /// the UA's judgement behind `:focus-visible` (Selectors 4 §13.2),
179    /// which matches the focused element exactly while this is `true`.
180    /// Starts `true` (focus before any pointer interaction is evident,
181    /// as browsers treat script focus on a fresh page); a backend flips
182    /// it with [`set_focus_visible`](Self::set_focus_visible) from its
183    /// input-modality heuristics. It persists across focus changes, so
184    /// focus moved by script keeps the previous element's visibility,
185    /// as the spec asks.
186    pub fn focus_visible(&self) -> bool {
187        self.focus_visible
188    }
189
190    /// The node that currently owns the pointer via
191    /// [`set_pointer_capture`](Self::set_pointer_capture). `None`
192    /// (the default) means routing uses hit-testing as usual.
193    pub fn pointer_capture(&self) -> Option<NodeId> {
194        self.pointer_capture
195    }
196
197    /// The document's text selection, if any. `None` means no
198    /// selection (not even a caret). Mutated via
199    /// [`set_selection`](Self::set_selection) which fires
200    /// `Mutation::SelectionChanged` for paint observers.
201    pub fn selection(&self) -> Option<&crate::Selection> {
202        self.selection.as_ref()
203    }
204
205    /// The current selection normalized to a document-ordered
206    /// `Range` — `start` precedes `end` per
207    /// [`compare_boundary_points`](Self::compare_boundary_points).
208    /// Useful for paint + copy walks that need ordered traversal.
209    ///
210    /// Returns `None` when nothing is selected OR when the
211    /// anchor/focus nodes are disconnected (shouldn't happen in
212    /// practice, but handled defensively).
213    pub fn selection_range(&self) -> Option<crate::Range> {
214        let sel = self.selection.as_ref()?;
215        let a = sel.anchor;
216        let f = sel.focus;
217        let (start, end) = match self.compare_boundary_points(a, f)? {
218            std::cmp::Ordering::Greater => (f, a),
219            _ => (a, f),
220        };
221        Some(crate::Range::ordered_unchecked(start, end))
222    }
223}
224
225impl<Ext: 'static> Dom<Ext> {
226    /// Set or clear the hovered node. Fires an `InteractionChanged`
227    /// mutation record when the value actually changes; no-op when
228    /// setting to the current value.
229    pub fn set_hovered(&mut self, id: Option<NodeId>) {
230        if self.hovered == id {
231            return;
232        }
233        let prev = self.hovered;
234        self.hovered = id;
235        self.fire_mutation(Mutation::InteractionChanged {
236            prev,
237            next: id,
238            kind: InteractionKind::Hover,
239        });
240    }
241
242    /// Set or clear the element being activated (`:active`). Fires an
243    /// `InteractionChanged { kind: Active }` record when the value
244    /// changes; no-op otherwise. Detaching the element clears it, like
245    /// hover and focus.
246    pub fn set_active(&mut self, id: Option<NodeId>) {
247        if self.active == id {
248            return;
249        }
250        let prev = self.active;
251        self.active = id;
252        self.fire_mutation(Mutation::InteractionChanged {
253            prev,
254            next: id,
255            kind: InteractionKind::Active,
256        });
257    }
258
259    /// Set or clear the focused node. Fires an `InteractionChanged`
260    /// record on change.
261    pub fn set_focused(&mut self, id: Option<NodeId>) {
262        if self.focused == id {
263            return;
264        }
265        let prev = self.focused;
266        self.focused = id;
267        self.fire_mutation(Mutation::InteractionChanged {
268            prev,
269            next: id,
270            kind: InteractionKind::Focus,
271        });
272    }
273
274    /// Set whether the focused element's focus should be made evident
275    /// (see [`focus_visible`](Self::focus_visible)). Fires an
276    /// `InteractionChanged { kind: FocusVisible }` record naming the
277    /// focused element as both `prev` and `next` when the value
278    /// changes; no-op otherwise.
279    pub fn set_focus_visible(&mut self, visible: bool) {
280        if self.focus_visible == visible {
281            return;
282        }
283        self.focus_visible = visible;
284        self.fire_mutation(Mutation::InteractionChanged {
285            prev: self.focused,
286            next: self.focused,
287            kind: InteractionKind::FocusVisible,
288        });
289    }
290
291    /// Claim the pointer for `id`. While set, the runtime routes
292    /// `mousemove` / drag / `mouseup` to `id` regardless of where
293    /// the cursor lands — critical for drag-select, resize
294    /// handles, scrubbing.
295    ///
296    /// Typical usage from a `mousedown` listener:
297    ///
298    /// ```ignore
299    /// dom.add_event_listener(handle, "mousedown",
300    ///     ListenerOptions::default(), |ctx| {
301    ///         let target = ctx.event.target.unwrap();
302    ///         ctx.dom.set_pointer_capture(target).unwrap();
303    ///     })?;
304    /// ```
305    ///
306    /// The capture releases automatically on `mouseup`, or can be
307    /// released explicitly via [`release_pointer_capture`].
308    ///
309    /// Returns `Err(DomError::InvalidNode)` if `id` doesn't exist.
310    /// Does **not** fire a mutation record — pointer capture
311    /// doesn't affect cascade / selectors (no `:pointer-captured`
312    /// pseudo in v1).
313    ///
314    /// [`release_pointer_capture`]: Self::release_pointer_capture
315    pub fn set_pointer_capture(&mut self, id: NodeId) -> crate::Result<()> {
316        self.node_or_err(id)?;
317        self.pointer_capture = Some(id);
318        // A fresh capture starts without autoscroll; the owner opts in
319        // explicitly via `set_drag_autoscroll(true)`. Resetting here means a
320        // scrollbar / slider capture never inherits a stale autoscroll flag.
321        self.drag_autoscroll = false;
322        Ok(())
323    }
324
325    /// Release any active pointer capture. Idempotent — no-op when
326    /// nothing was captured. Also clears the drag-autoscroll opt-in.
327    pub fn release_pointer_capture(&mut self) {
328        self.pointer_capture = None;
329        self.drag_autoscroll = false;
330    }
331
332    /// Opt the active captured drag into edge autoscroll (DRAG-AUTOSCROLL):
333    /// while set, the backend scrolls the nearest scroll container as the
334    /// pointer dwells at its edge. Pair with
335    /// [`set_pointer_capture`](Self::set_pointer_capture) from a drag's
336    /// `mousedown` handler (the same handler should `prevent_default` so the
337    /// runtime's own text-selection/scrollbar defaults don't claim the drag).
338    /// Auto-cleared when the capture releases. No-op without an active capture.
339    pub fn set_drag_autoscroll(&mut self, on: bool) {
340        if self.pointer_capture.is_some() {
341            self.drag_autoscroll = on;
342        }
343    }
344
345    /// Whether the active captured drag opted into edge autoscroll. Read by the
346    /// backend each frame to decide whether to run the autoscroll phase.
347    pub fn drag_autoscroll(&self) -> bool {
348        self.drag_autoscroll
349    }
350
351    /// Set the document selection. `None` clears it.
352    ///
353    /// Fires `Mutation::SelectionChanged { prev, next }` on change
354    /// so paint observers can refresh the `::selection` overlay.
355    /// No-op when `next == current selection`.
356    pub fn set_selection(&mut self, next: Option<crate::Selection>) {
357        if self.selection == next {
358            return;
359        }
360        let prev = self.selection.take();
361        self.selection = next;
362        self.selection_serial = self.selection_serial.next();
363        self.fire_mutation(Mutation::SelectionChanged { prev, next });
364    }
365
366    /// A counter that advances on every actual selection change —
367    /// every [`set_selection`](Self::set_selection) that fires
368    /// `Mutation::SelectionChanged`, including the clear when the
369    /// selected nodes leave the tree. Equal readings mean the selection
370    /// was not touched in between, even if it moved away and back.
371    ///
372    /// Backends use it to tell their own caret updates from foreign
373    /// ones without observing mutations — rdom-tui's undo grouping
374    /// keeps a typing run open only while the selection is the one its
375    /// last edit left (Blink closes the typing command on any other
376    /// selection change). No web API exposes this; it is bookkeeping.
377    pub fn selection_serial(&self) -> crate::SelectionSerial {
378        self.selection_serial
379    }
380
381    // ── Dom-level shortcuts ──────────────────────────────────────
382
383    /// Find the first element in the document matching `selector`,
384    /// in document order. DOM `Document.querySelector`.
385    ///
386    /// Document-rooted shortcut for the more general
387    /// [`Self::query_selector_in`]. Malformed selectors return
388    /// `None` (browser-DOM throws; rdom diverges per §9.1 spec
389    /// table).
390    pub fn query_selector(&self, selector: &str) -> Option<NodeRef<'_, Ext>> {
391        self.query_selector_in(self.root, selector)
392            .ok()
393            .flatten()
394            .map(|id| self.node(id))
395    }
396
397    /// All elements in the document matching `selector`, in
398    /// document order. DOM `Document.querySelectorAll`.
399    ///
400    /// Document-rooted shortcut for
401    /// [`Self::query_selector_all_in`]. Malformed selectors yield
402    /// an empty list.
403    pub fn query_selector_all(&self, selector: &str) -> NodeList<'_, Ext> {
404        let ids = self
405            .query_selector_all_in(self.root, selector)
406            .unwrap_or_default();
407        NodeList::from_ids(self, ids)
408    }
409
410    /// All elements in the document with the given tag name, in
411    /// document order. DOM `Document.getElementsByTagName`.
412    ///
413    /// Returns a snapshot — unlike browser's live HTMLCollection
414    /// (Lock #2, parity ledger §25). Tag comparison is case-
415    /// sensitive (rdom tags are lowercased at parse time, so
416    /// authors pass lowercase).
417    pub fn elements_by_tag(&self, tag: &str) -> NodeList<'_, Ext> {
418        let mut out: Vec<NodeId> = Vec::new();
419        self.walk_descendants(self.root, &mut |id, data| {
420            if let NodeData::Element { tag: t, .. } = data
421                && t == tag
422            {
423                out.push(id);
424            }
425        });
426        NodeList::from_ids(self, out)
427    }
428
429    /// The document element. DOM `Document.documentElement`.
430    ///
431    /// When the root is an element (typically `<html>` via
432    /// `Dom::with_root_tag("html")`), this is the root itself.
433    /// When the root is a Fragment (the default), returns the
434    /// first element child of the fragment, or the fragment
435    /// itself if it has no element children.
436    pub fn document_element(&self) -> NodeRef<'_, Ext> {
437        let root = self.root;
438        if matches!(
439            self.get_node(root).map(|n| &n.data),
440            Some(NodeData::Fragment)
441        ) && let Some(first_el) = self.node(root).first_element_child()
442        {
443            return first_el;
444        }
445        self.node(root)
446    }
447
448    /// The currently focused element. DOM `Document.activeElement`.
449    /// Alias for [`Self::focused`] returning a `NodeRef`.
450    pub fn active_element(&self) -> Option<NodeRef<'_, Ext>> {
451        self.focused.map(|id| self.node(id))
452    }
453
454    /// `true` iff the document has a focused element. DOM
455    /// `Document.hasFocus`. (rdom has a single document; the
456    /// browser semantics of "focused window" don't apply.)
457    pub fn has_focus(&self) -> bool {
458        self.focused.is_some()
459    }
460
461    /// Construct an empty `Range` collapsed at the document root,
462    /// offset 0. DOM `Document.createRange()`.
463    ///
464    /// The returned range can be re-anchored via struct-literal
465    /// assignment or [`Range::ordered_unchecked`]. Range boundary
466    /// setters (`setStart` / `setEnd`) are polish.
467    pub fn create_range(&self) -> Range {
468        let pos = Position::new(self.root, 0);
469        Range::ordered_unchecked(pos, pos)
470    }
471}
472
473impl<Ext: Default> Dom<Ext> {
474    /// Create a new arena with a named element as the root.
475    pub fn with_root_tag(tag: &str) -> Self {
476        let root_node: Node<Ext> = Node::new(NodeData::Element {
477            tag: tag.to_string(),
478            attrs: BTreeMap::new(),
479            classes: BTreeSet::new(),
480            ext: Ext::default(),
481        });
482        let nodes = vec![Slot::fresh(root_node)];
483        let root = NodeId::from_parts(0, FIRST_GENERATION);
484        let mut dom = Self {
485            nodes,
486            free: Vec::new(),
487            root,
488            indexes: Indexes::default(),
489            listeners: ListenerStore::default(),
490            hovered: None,
491            focused: None,
492            active: None,
493            focus_visible: true,
494            pointer_capture: None,
495            drag_autoscroll: false,
496            selection: None,
497            selection_serial: crate::SelectionSerial::new(0),
498            observers: ObserverStore::default(),
499            is_observing: false,
500            activation_hook: crate::dispatch::ActivationSlot(None),
501            validity_hook: crate::constraint::ValiditySlot(None),
502        };
503        dom.hook_register(root);
504        dom
505    }
506
507    /// Create a new Element node. Orphan — not attached to any parent.
508    /// Use `append_child` to attach it.
509    pub fn create_element(&mut self, tag: &str) -> NodeId {
510        self.alloc(Node::new(NodeData::Element {
511            tag: tag.to_string(),
512            attrs: BTreeMap::new(),
513            classes: BTreeSet::new(),
514            ext: Ext::default(),
515        }))
516    }
517}
518
519impl<Ext> Dom<Ext> {
520    /// Create an Element with explicit extension data (useful when `Ext`
521    /// doesn't implement `Default` or you want to pre-populate state).
522    pub fn create_element_with_ext(&mut self, tag: &str, ext: Ext) -> NodeId {
523        self.alloc(Node::new(NodeData::Element {
524            tag: tag.to_string(),
525            attrs: BTreeMap::new(),
526            classes: BTreeSet::new(),
527            ext,
528        }))
529    }
530
531    /// Create a Text node (content of a text child).
532    pub fn create_text_node(&mut self, data: &str) -> NodeId {
533        self.alloc(Node::new(NodeData::Text {
534            data: data.to_string(),
535        }))
536    }
537
538    /// Create a Comment node.
539    pub fn create_comment(&mut self, data: &str) -> NodeId {
540        self.alloc(Node::new(NodeData::Comment {
541            data: data.to_string(),
542        }))
543    }
544
545    /// Create a DocumentFragment. Used as a detachable subtree container;
546    /// inserting a fragment unwraps it.
547    pub fn create_document_fragment(&mut self) -> NodeId {
548        self.alloc(Node::new(NodeData::Fragment))
549    }
550
551    /// The root node of this arena.
552    pub fn root(&self) -> NodeId {
553        self.root
554    }
555
556    /// Does the arena currently hold this id? `false` for a freed node
557    /// even after its slot has been recycled (the generation differs).
558    pub fn contains(&self, id: NodeId) -> bool {
559        self.get_node(id).is_some()
560    }
561
562    /// How many live nodes are in the arena (excludes freed slots).
563    pub fn len(&self) -> usize {
564        self.nodes.len() - self.free.len()
565    }
566
567    pub fn is_empty(&self) -> bool {
568        self.len() == 0
569    }
570
571    // ── Internal ─────────────────────────────────────────────────────
572
573    /// Allocate a slot for `node`. Reuses a freed slot if available.
574    /// Registers the node in the id/tag/class indexes if it's an Element.
575    pub(crate) fn alloc(&mut self, node: Node<Ext>) -> NodeId {
576        let new_id = if let Some(idx) = self.free.pop() {
577            let idx = idx as usize;
578            let slot = &mut self.nodes[idx];
579            debug_assert!(slot.node.is_none(), "free list held a live slot");
580            slot.node = Some(node);
581            NodeId::from_parts(idx, slot.generation)
582        } else {
583            let idx = self.nodes.len();
584            self.nodes.push(Slot::fresh(node));
585            NodeId::from_parts(idx, FIRST_GENERATION)
586        };
587        self.hook_register(new_id);
588        new_id
589    }
590
591    /// Mark a slot as freed. Caller must ensure the node is already
592    /// detached (unlinked from parent/siblings). Unregisters from every
593    /// index and drops any attached listeners before the slot is wiped.
594    pub(crate) fn free(&mut self, id: NodeId) {
595        if self.get_node(id).is_none() {
596            return;
597        }
598        let idx = id.index();
599        self.hook_unregister(id);
600        self.drop_listeners(id);
601        let slot = &mut self.nodes[idx];
602        slot.node = None;
603        // Retire every outstanding handle to this slot. On the (4-billion
604        // recycles) wrap we restart at 1 rather than panic; a handle that
605        // old aliasing is accepted.
606        slot.generation = slot.generation.checked_add(1).unwrap_or(FIRST_GENERATION);
607        self.free.push(idx as u32);
608    }
609
610    /// Shared-ref node access; `None` if the slot is freed, out of
611    /// bounds, or has been recycled since `id` was issued.
612    #[inline]
613    pub(crate) fn get_node(&self, id: NodeId) -> Option<&Node<Ext>> {
614        let slot = self.nodes.get(id.index())?;
615        if slot.generation != id.generation_raw() {
616            return None;
617        }
618        slot.node.as_ref()
619    }
620
621    /// Mutable node access; same liveness rule as `get_node`.
622    #[inline]
623    pub(crate) fn get_node_mut(&mut self, id: NodeId) -> Option<&mut Node<Ext>> {
624        let slot = self.nodes.get_mut(id.index())?;
625        if slot.generation != id.generation_raw() {
626            return None;
627        }
628        slot.node.as_mut()
629    }
630
631    /// Node access that errors on invalid id (use when the caller expects
632    /// it to exist — e.g. caller just created it, or it's a parent/child
633    /// pointer we trust).
634    pub(crate) fn node_or_err(&self, id: NodeId) -> Result<&Node<Ext>> {
635        self.get_node(id).ok_or(DomError::InvalidNode(id))
636    }
637
638    pub(crate) fn node_mut_or_err(&mut self, id: NodeId) -> Result<&mut Node<Ext>> {
639        self.get_node_mut(id).ok_or(DomError::InvalidNode(id))
640    }
641
642    /// Check whether `ancestor` is an ancestor of `descendant` (or the
643    /// same node). Used for cycle detection in tree mutations.
644    pub(crate) fn is_ancestor(&self, ancestor: NodeId, descendant: NodeId) -> bool {
645        let mut current = Some(descendant);
646        while let Some(id) = current {
647            if id == ancestor {
648                return true;
649            }
650            current = self.get_node(id).and_then(|n| n.parent);
651        }
652        false
653    }
654
655    /// Node type — handy enough to hoist to Dom level.
656    pub fn node_type(&self, id: NodeId) -> Option<NodeType> {
657        self.get_node(id).map(|n| n.node_type())
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn new_dom_has_fragment_root() {
667        let dom: Dom = Dom::new();
668        let root = dom.root();
669        assert!(dom.contains(root));
670        assert_eq!(dom.node_type(root), Some(NodeType::Fragment));
671        assert_eq!(dom.len(), 1);
672    }
673
674    #[test]
675    fn with_root_tag_creates_element_root() {
676        let dom: Dom = Dom::with_root_tag("body");
677        let root = dom.root();
678        let n = dom.get_node(root).unwrap();
679        assert_eq!(n.node_type(), NodeType::Element);
680        assert_eq!(n.tag_name(), Some("body"));
681    }
682
683    #[test]
684    fn create_element_is_orphan() {
685        let mut dom: Dom = Dom::new();
686        let el = dom.create_element("div");
687        let n = dom.get_node(el).unwrap();
688        assert!(n.parent.is_none());
689        assert!(n.first_child.is_none());
690        assert_eq!(n.node_type(), NodeType::Element);
691    }
692
693    #[test]
694    fn create_allocates_distinct_ids() {
695        let mut dom: Dom = Dom::new();
696        let a = dom.create_element("a");
697        let b = dom.create_element("b");
698        let c = dom.create_text_node("hi");
699        assert_ne!(a, b);
700        assert_ne!(b, c);
701        assert_ne!(a, c);
702        assert_eq!(dom.len(), 4); // root + 3
703    }
704
705    #[test]
706    fn freed_slot_gets_reused() {
707        let mut dom: Dom = Dom::new();
708        let a = dom.create_element("a");
709        assert!(dom.contains(a));
710        dom.free(a);
711        assert!(!dom.contains(a));
712
713        // Next allocation reuses the same slot.
714        let b = dom.create_element("b");
715        assert_eq!(a.index(), b.index());
716    }
717
718    /// Slot reuse must never make a stale id look live: the recycled slot
719    /// gets a new generation, so the old handle fails every lookup and
720    /// every mutation path instead of silently aliasing the new node.
721    #[test]
722    fn stale_id_after_slot_reuse_is_rejected() {
723        let mut dom: Dom = Dom::new();
724        let root = dom.root();
725        let a = dom.create_element("a");
726        dom.append_child(root, a).unwrap();
727        dom.remove_child_dropping(root, a).unwrap();
728        assert!(!dom.contains(a));
729
730        let b = dom.create_element("b");
731        assert_eq!(a.index(), b.index(), "slot is recycled");
732        assert_ne!(a, b, "but the id is not");
733        assert!(dom.contains(b));
734        assert!(!dom.contains(a), "the stale id is still dead");
735
736        assert!(matches!(
737            dom.node_or_err(a).unwrap_err(),
738            DomError::InvalidNode(_)
739        ));
740        assert!(matches!(
741            dom.set_attribute(a, "id", "x").unwrap_err(),
742            DomError::InvalidNode(_)
743        ));
744        assert!(matches!(
745            dom.append_child(a, b).unwrap_err(),
746            DomError::InvalidNode(_)
747        ));
748        // The new node was not touched through the stale handle.
749        assert_eq!(dom.get_attribute(b, "id"), None);
750        assert!(dom.validate().is_empty());
751    }
752
753    /// Generations survive repeated recycling of the same slot.
754    #[test]
755    fn each_recycle_of_a_slot_bumps_the_generation() {
756        let mut dom: Dom = Dom::new();
757        let mut prev = dom.create_element("x");
758        for _ in 0..5 {
759            dom.free(prev);
760            let next = dom.create_element("x");
761            assert_eq!(prev.index(), next.index());
762            assert_eq!(next.generation(), prev.generation() + 1);
763            assert!(!dom.contains(prev));
764            prev = next;
765        }
766    }
767
768    #[test]
769    fn invalid_id_returns_error() {
770        let dom: Dom = Dom::new();
771        let ghost = NodeId::from_parts(999, std::num::NonZeroU32::MIN);
772        assert!(matches!(
773            dom.node_or_err(ghost).unwrap_err(),
774            DomError::InvalidNode(_)
775        ));
776    }
777
778    #[test]
779    fn is_ancestor_detects_self() {
780        let mut dom: Dom = Dom::new();
781        let a = dom.create_element("a");
782        // Without attachment, the node is its own ancestor (descending
783        // from itself) — walker starts at `descendant` and matches.
784        assert!(dom.is_ancestor(a, a));
785    }
786
787    // ── pointer_capture ──────────────────────────────────────────────
788
789    #[test]
790    fn pointer_capture_none_by_default() {
791        let dom: Dom = Dom::new();
792        assert_eq!(dom.pointer_capture(), None);
793    }
794
795    #[test]
796    fn set_pointer_capture_records_node() {
797        let mut dom: Dom = Dom::new();
798        let el = dom.create_element("handle");
799        dom.set_pointer_capture(el).unwrap();
800        assert_eq!(dom.pointer_capture(), Some(el));
801    }
802
803    #[test]
804    fn set_pointer_capture_invalid_node_errors() {
805        let mut dom: Dom = Dom::new();
806        let el = dom.create_element("x");
807        dom.free(el);
808        assert!(dom.set_pointer_capture(el).is_err());
809        assert_eq!(dom.pointer_capture(), None);
810    }
811
812    #[test]
813    fn release_pointer_capture_clears_state() {
814        let mut dom: Dom = Dom::new();
815        let el = dom.create_element("handle");
816        dom.set_pointer_capture(el).unwrap();
817        dom.release_pointer_capture();
818        assert_eq!(dom.pointer_capture(), None);
819    }
820
821    #[test]
822    fn release_pointer_capture_is_idempotent() {
823        let mut dom: Dom = Dom::new();
824        // Releasing without a prior capture is a no-op, not an error.
825        dom.release_pointer_capture();
826        dom.release_pointer_capture();
827        assert_eq!(dom.pointer_capture(), None);
828    }
829
830    #[test]
831    fn set_pointer_capture_replaces_previous() {
832        let mut dom: Dom = Dom::new();
833        let a = dom.create_element("a");
834        let b = dom.create_element("b");
835        dom.set_pointer_capture(a).unwrap();
836        dom.set_pointer_capture(b).unwrap();
837        assert_eq!(dom.pointer_capture(), Some(b));
838    }
839
840    #[test]
841    fn drag_autoscroll_defaults_off_and_needs_a_capture() {
842        let mut dom: Dom = Dom::new();
843        let a = dom.create_element("a");
844        assert!(!dom.drag_autoscroll(), "off by default");
845        // No-op without an active capture.
846        dom.set_drag_autoscroll(true);
847        assert!(!dom.drag_autoscroll(), "ignored with no capture");
848        // Opts in once captured.
849        dom.set_pointer_capture(a).unwrap();
850        dom.set_drag_autoscroll(true);
851        assert!(dom.drag_autoscroll());
852    }
853
854    #[test]
855    fn drag_autoscroll_clears_on_release_and_recapture() {
856        let mut dom: Dom = Dom::new();
857        let a = dom.create_element("a");
858        let b = dom.create_element("b");
859        dom.set_pointer_capture(a).unwrap();
860        dom.set_drag_autoscroll(true);
861        dom.release_pointer_capture();
862        assert!(!dom.drag_autoscroll(), "release clears the opt-in");
863        // A fresh capture starts without autoscroll (no stale flag).
864        dom.set_pointer_capture(a).unwrap();
865        dom.set_drag_autoscroll(true);
866        dom.set_pointer_capture(b).unwrap(); // replace → reset
867        assert!(!dom.drag_autoscroll(), "re-capture resets the opt-in");
868    }
869
870    // ── selection ────────────────────────────────────────────────────
871
872    #[test]
873    fn selection_none_by_default() {
874        let dom: Dom = Dom::new();
875        assert_eq!(dom.selection(), None);
876        assert_eq!(dom.selection_range(), None);
877    }
878
879    #[test]
880    fn set_selection_stores_value() {
881        use crate::{Position, Selection};
882        let mut dom: Dom = Dom::new();
883        let t = dom.create_text_node("hello");
884        let sel = Selection::new(Position::new(t, 1), Position::new(t, 4));
885        dom.set_selection(Some(sel));
886        assert_eq!(dom.selection().copied(), Some(sel));
887    }
888
889    #[test]
890    fn set_selection_none_clears() {
891        use crate::{Position, Selection};
892        let mut dom: Dom = Dom::new();
893        let t = dom.create_text_node("hello");
894        dom.set_selection(Some(Selection::caret(Position::new(t, 0))));
895        dom.set_selection(None);
896        assert_eq!(dom.selection(), None);
897    }
898
899    #[test]
900    fn selection_serial_advances_on_every_actual_change() {
901        use crate::{Position, Selection};
902        let mut dom: Dom = Dom::new();
903        let t = dom.create_text_node("hello");
904        let s0 = dom.selection_serial();
905        dom.set_selection(Some(Selection::caret(Position::new(t, 1))));
906        let s1 = dom.selection_serial();
907        assert_ne!(s0, s1);
908        dom.set_selection(Some(Selection::caret(Position::new(t, 1))));
909        assert_eq!(dom.selection_serial(), s1, "a no-op set does not advance");
910        dom.set_selection(Some(Selection::caret(Position::new(t, 2))));
911        dom.set_selection(Some(Selection::caret(Position::new(t, 1))));
912        assert_ne!(
913            dom.selection_serial(),
914            s1,
915            "moving away and back is two changes"
916        );
917        let later: crate::SelectionSerial = dom.selection_serial();
918        assert!(later > s1, "serials are ordered: {later:?} after {s1:?}");
919    }
920
921    #[test]
922    fn selection_range_same_node_orders_by_offset() {
923        use crate::{Position, Range, Selection};
924        let mut dom: Dom = Dom::new();
925        let t = dom.create_text_node("hello");
926        // Inverted selection (anchor after focus in byte order).
927        dom.set_selection(Some(Selection::new(
928            Position::new(t, 4),
929            Position::new(t, 1),
930        )));
931        let r = dom.selection_range().unwrap();
932        assert_eq!(
933            r,
934            Range::ordered_unchecked(Position::new(t, 1), Position::new(t, 4))
935        );
936    }
937
938    #[test]
939    fn selection_range_different_nodes_orders_by_document_position() {
940        use crate::{Position, Selection};
941        let mut dom: Dom = Dom::new();
942        let root = dom.root();
943        let t1 = dom.create_text_node("first");
944        let t2 = dom.create_text_node("second");
945        dom.append_child(root, t1).unwrap();
946        dom.append_child(root, t2).unwrap();
947
948        // Selection from t2 → t1 (inverted).
949        dom.set_selection(Some(Selection::new(
950            Position::new(t2, 2),
951            Position::new(t1, 3),
952        )));
953        let r = dom.selection_range().unwrap();
954        assert_eq!(r.start.node, t1);
955        assert_eq!(r.end.node, t2);
956    }
957
958    /// Anchor inside a child, focus on the ancestor: the ancestor point
959    /// is ordered by its offset against the child's index (DOM §5.2), not
960    /// by which node "contains" the other.
961    #[test]
962    fn selection_range_ancestor_position_orders_by_child_index() {
963        use crate::{Position, Selection};
964        let mut dom: Dom = Dom::new();
965        let root = dom.root();
966        let p = dom.create_element("p");
967        let t = dom.create_text_node("hello");
968        dom.append_child(root, p).unwrap();
969        dom.append_child(p, t).unwrap();
970
971        // (root, 1) is *after* everything inside p (p is root's child 0).
972        dom.set_selection(Some(Selection::new(
973            Position::new(t, 2),
974            Position::new(root, 1),
975        )));
976        let r = dom.selection_range().unwrap();
977        assert_eq!(r.start, Position::new(t, 2));
978        assert_eq!(r.end, Position::new(root, 1));
979
980        // (root, 0) is *before* everything inside p.
981        dom.set_selection(Some(Selection::new(
982            Position::new(t, 2),
983            Position::new(root, 0),
984        )));
985        let r = dom.selection_range().unwrap();
986        assert_eq!(r.start, Position::new(root, 0));
987        assert_eq!(r.end, Position::new(t, 2));
988    }
989
990    // ── M4b step 18: Dom-level accessor additions ─────────────────────
991
992    #[test]
993    fn query_selector_one_arg_runs_from_root() {
994        let mut dom: Dom = Dom::new();
995        let root = dom.root();
996        let a = dom.create_element("p");
997        dom.node_mut(a).add_class("hit").unwrap();
998        let b = dom.create_element("p");
999        dom.node_mut(b).add_class("hit").unwrap();
1000        dom.append_child(root, a).unwrap();
1001        dom.append_child(root, b).unwrap();
1002        let hit = dom.query_selector(".hit").unwrap();
1003        assert_eq!(hit.id(), a);
1004    }
1005
1006    #[test]
1007    fn query_selector_one_arg_returns_none_on_invalid_selector() {
1008        let dom: Dom = Dom::new();
1009        assert!(dom.query_selector("!!!").is_none());
1010    }
1011
1012    #[test]
1013    fn query_selector_all_one_arg_returns_doc_order_node_list() {
1014        let mut dom: Dom = Dom::new();
1015        let root = dom.root();
1016        let a = dom.create_element("p");
1017        let b = dom.create_element("p");
1018        let c = dom.create_element("p");
1019        dom.append_child(root, a).unwrap();
1020        dom.append_child(root, b).unwrap();
1021        dom.append_child(root, c).unwrap();
1022        let list = dom.query_selector_all("p");
1023        assert_eq!(list.len(), 3);
1024        let ids: Vec<NodeId> = list.iter().map(|n| n.id()).collect();
1025        assert_eq!(ids, vec![a, b, c]);
1026    }
1027
1028    #[test]
1029    fn elements_by_tag_returns_node_list_in_doc_order() {
1030        let mut dom: Dom = Dom::new();
1031        let root = dom.root();
1032        let div = dom.create_element("div");
1033        let span1 = dom.create_element("span");
1034        let span2 = dom.create_element("span");
1035        let p = dom.create_element("p");
1036        dom.append_child(div, span1).unwrap();
1037        dom.append_child(div, p).unwrap();
1038        dom.append_child(div, span2).unwrap();
1039        dom.append_child(root, div).unwrap();
1040        let list = dom.elements_by_tag("span");
1041        let ids: Vec<NodeId> = list.iter().map(|n| n.id()).collect();
1042        assert_eq!(ids, vec![span1, span2]);
1043    }
1044
1045    #[test]
1046    fn document_element_returns_root_when_element() {
1047        let dom: Dom<()> = Dom::with_root_tag("html");
1048        let html_id = dom.root();
1049        assert_eq!(dom.document_element().id(), html_id);
1050    }
1051
1052    #[test]
1053    fn document_element_returns_first_element_child_when_root_is_fragment() {
1054        let mut dom: Dom = Dom::new();
1055        let root = dom.root();
1056        // Default root is a Fragment; first element child wins.
1057        let html = dom.create_element("html");
1058        let comment = dom.create_comment("note");
1059        dom.append_child(root, comment).unwrap();
1060        dom.append_child(root, html).unwrap();
1061        assert_eq!(dom.document_element().id(), html);
1062    }
1063
1064    #[test]
1065    fn document_element_returns_root_fragment_when_no_element_child() {
1066        // Edge case: empty fragment root → return the root itself.
1067        let dom: Dom = Dom::new();
1068        let root = dom.root();
1069        assert_eq!(dom.document_element().id(), root);
1070    }
1071
1072    #[test]
1073    fn active_element_and_has_focus_track_focused() {
1074        let mut dom: Dom = Dom::new();
1075        let root = dom.root();
1076        let el = dom.create_element("input");
1077        dom.append_child(root, el).unwrap();
1078        assert!(!dom.has_focus());
1079        assert!(dom.active_element().is_none());
1080        dom.set_focused(Some(el));
1081        assert!(dom.has_focus());
1082        assert_eq!(dom.active_element().map(|n| n.id()), Some(el));
1083        dom.set_focused(None);
1084        assert!(!dom.has_focus());
1085        assert!(dom.active_element().is_none());
1086    }
1087
1088    #[test]
1089    fn create_range_returns_collapsed_at_root() {
1090        use crate::Position;
1091        let dom: Dom = Dom::new();
1092        let r = dom.create_range();
1093        assert!(r.is_collapsed());
1094        assert_eq!(r.start, Position::new(dom.root(), 0));
1095    }
1096}