Skip to main content

kozan_core/dom/
document.rs

1//! Document — owns all node data through parallel arenas.
2//!
3//! Single owner of all DOM state. `DocumentCell` wraps a raw pointer for
4//! internal subsystems that need unchecked access.
5
6use core::any::TypeId;
7use core::ptr::NonNull;
8
9use crate::data_storage::DataStorage;
10use crate::dom::document_cell::DocumentCell;
11use crate::dom::element_data::ElementData;
12use crate::dom::handle::Handle;
13use crate::dom::node::{NodeFlags, NodeMeta};
14use crate::dom::traits::Element;
15use crate::dom::traits::HasHandle;
16use crate::events::EventListenerMap;
17use crate::events::listener::RegisteredListener;
18use crate::events::store::EventStore;
19use crate::id::{INVALID, IdAllocator, RawId};
20use crate::layout::node_data::LayoutNodeData;
21use crate::styling::StyleEngine;
22use crate::tree;
23use crate::tree::TreeData;
24use crate::{Text, TextData};
25use kozan_primitives::arena::Storage;
26
27/// A document — the top-level owner of a node tree.
28///
29/// Owns all node data through parallel arenas. Internal subsystems
30/// (layout, style, events) access data through `DocumentCell`.
31///
32/// The owner must keep Document at a stable address. Handles store
33/// raw pointers back to this Document through `DocumentCell`.
34pub struct Document {
35    // ---- Allocation ----
36    pub(crate) ids: IdAllocator,
37
38    // ---- Parallel storages ----
39    pub(crate) meta: Storage<NodeMeta>,
40    pub(crate) tree: Storage<TreeData>,
41    pub(crate) element_data: Storage<ElementData>,
42    pub(crate) data: DataStorage,
43
44    // ---- Layout data (per-node Taffy style, cache, layout results) ----
45    // DOM IS the layout tree — no separate LayoutTree needed.
46    pub(crate) layout: Storage<LayoutNodeData>,
47
48    // ---- Style engine (owns computed/inline styles + Stylo state) ----
49    pub(crate) style_engine: StyleEngine,
50
51    // ---- Event listeners ----
52    pub(crate) event_store: EventStore,
53
54    // ---- Root + Body ----
55    pub(crate) root: u32,
56    pub(crate) body: u32,
57
58    // ---- Dirty flags (read and cleared by FrameWidget) ----
59    /// DOM structure changed (append, remove, detach) — requires full tree rebuild.
60    pub(crate) needs_tree_rebuild: bool,
61
62    /// DOM node indices whose content changed (text update) — incremental relayout.
63    pub(crate) dirty_layout_nodes: Vec<u32>,
64
65    /// Style changed via inline styles or state change — needs restyle + relayout.
66    /// Set by `mark_for_restyle()`, cleared by `update_lifecycle()`.
67    pub(crate) needs_style_recalc: bool,
68
69    // ---- Debug ----
70    #[cfg(debug_assertions)]
71    pub(crate) alive: bool,
72}
73
74impl Document {
75    /// Create a new empty document with a root node.
76    #[must_use]
77    pub fn new() -> Self {
78        let mut doc = Self {
79            ids: IdAllocator::new(),
80            meta: Storage::new(),
81            tree: Storage::new(),
82            element_data: Storage::new(),
83            data: DataStorage::new(),
84            layout: Storage::new(),
85            style_engine: StyleEngine::new(),
86            event_store: EventStore::new(),
87            root: 0,
88            body: 0,
89            needs_tree_rebuild: true,
90            dirty_layout_nodes: Vec::new(),
91            needs_style_recalc: false,
92            #[cfg(debug_assertions)]
93            alive: true,
94        };
95
96        // Root (document node — like <html>)
97        let (root_index, _gen) = doc.alloc_node(NodeFlags::document(), TypeId::of::<()>());
98        doc.root = root_index;
99
100        doc
101    }
102
103    /// Initialize the `<body>` element. Called after Document is pinned
104    /// in memory (needs stable address for Handle operations).
105    ///
106    /// Creates a real `<body>` element — UA stylesheet provides:
107    /// `body { display: block; margin: 8px; }`
108    pub fn init_body(&mut self) {
109        if self.body != 0 {
110            return; // already initialized
111        }
112        let body = self.create::<crate::html::HtmlBodyElement>();
113        self.root().append(body);
114        self.body = body.handle().raw().index();
115    }
116
117    /// Get a `DocumentCell` for internal subsystem access.
118    ///
119    /// The caller must ensure `self` stays at a stable address.
120    pub(crate) fn cell(&self) -> DocumentCell {
121        DocumentCell::new(NonNull::from(self))
122    }
123
124    /// Allocate a new node with all parallel storages initialized.
125    pub(crate) fn alloc_node(&mut self, flags: NodeFlags, data_type_id: TypeId) -> (u32, u32) {
126        let raw = self.ids.alloc();
127        let (index, generation) = (raw.index(), raw.generation());
128        self.meta.set(
129            index,
130            NodeMeta {
131                flags,
132                data_type_id,
133            },
134        );
135        self.tree.set(index, TreeData::detached());
136        self.layout.set(index, LayoutNodeData::new());
137        self.event_store.ensure_slot(index);
138
139        unsafe {
140            self.meta.get_unchecked_mut(index).flags.mark_style_dirty();
141            self.meta.get_unchecked_mut(index).flags.mark_tree_dirty();
142        }
143
144        (index, generation)
145    }
146
147    // ── Internal subsystem methods (used via DocumentCell::read/write) ──
148
149    /// Check if a node is alive by `RawId`.
150    pub(crate) fn is_alive_id(&self, id: RawId) -> bool {
151        self.ids.is_alive(id)
152    }
153
154    /// Get the current generation for an index.
155    pub(crate) fn generation(&self, index: u32) -> Option<u32> {
156        if (index as usize) < self.ids.capacity() {
157            Some(unsafe { self.ids.generation_unchecked(index) })
158        } else {
159            None
160        }
161    }
162
163    /// Build a `RawId` from a raw index by looking up its current generation.
164    pub(crate) fn raw_id(&self, index: u32) -> Option<RawId> {
165        if (index as usize) >= self.ids.capacity() {
166            return None;
167        }
168        let generation = unsafe { self.ids.generation_unchecked(index) };
169        let id = RawId::new(index, generation);
170        if self.ids.is_alive(id) {
171            Some(id)
172        } else {
173            None
174        }
175    }
176
177    /// Get node metadata by `RawId` (with liveness check).
178    pub(crate) fn node_meta(&self, id: RawId) -> Option<crate::dom::node::NodeMeta> {
179        if !self.ids.is_alive(id) {
180            return None;
181        }
182        self.meta.get(id.index()).copied()
183    }
184
185    /// Get node type by `RawId`.
186    pub(crate) fn node_kind(&self, id: RawId) -> Option<crate::dom::node::NodeType> {
187        self.node_meta(id).map(|m| m.flags.node_type())
188    }
189
190    /// Get tree data by `RawId` (with liveness check).
191    pub(crate) fn tree_data(&self, id: RawId) -> Option<crate::tree::TreeData> {
192        if !self.ids.is_alive(id) {
193            return None;
194        }
195        self.tree.get(id.index()).copied()
196    }
197
198    /// Get children as `RawIds`.
199    pub(crate) fn children_ids(&self, id: RawId) -> Vec<RawId> {
200        if !self.ids.is_alive(id) {
201            return Vec::new();
202        }
203        let indices = unsafe { tree::ops::children(&self.tree, id.index()) };
204        indices
205            .into_iter()
206            .map(|idx| {
207                let child_gen = unsafe { self.ids.generation_unchecked(idx) };
208                RawId::new(idx, child_gen)
209            })
210            .collect()
211    }
212
213    /// Read element-type-specific data (`ButtonData`, `TextInputData`, etc.).
214    pub(crate) fn read_data<D: 'static, R: 'static>(
215        &self,
216        id: RawId,
217        f: impl FnOnce(&D) -> R,
218    ) -> Option<R> {
219        if !self.ids.is_alive(id) {
220            return None;
221        }
222        let meta = self.meta.get(id.index())?;
223        if meta.data_type_id != TypeId::of::<D>() {
224            return None;
225        }
226        let data = unsafe { self.data.get::<D>(id.index()) };
227        Some(f(data))
228    }
229
230    /// Write element-type-specific data. Marks the node dirty.
231    pub(crate) fn write_data<D: 'static, R: 'static>(
232        &mut self,
233        id: RawId,
234        f: impl FnOnce(&mut D) -> R,
235    ) -> Option<R> {
236        if !self.ids.is_alive(id) {
237            return None;
238        }
239        let meta = self.meta.get(id.index())?;
240        if meta.data_type_id != TypeId::of::<D>() {
241            return None;
242        }
243        let data = unsafe { self.data.get_mut::<D>(id.index()) };
244        let result = f(data);
245        unsafe {
246            self.meta
247                .get_unchecked_mut(id.index())
248                .flags
249                .mark_paint_dirty();
250        }
251        Some(result)
252    }
253
254    /// Read shared element data (attributes, id, class, focus state).
255    pub(crate) fn read_element_data<R: 'static>(
256        &self,
257        id: RawId,
258        f: impl FnOnce(&ElementData) -> R,
259    ) -> Option<R> {
260        if !self.ids.is_alive(id) {
261            return None;
262        }
263        let meta = self.meta.get(id.index())?;
264        if !meta.flags.is_element() {
265            return None;
266        }
267        let ed = self.element_data.get(id.index())?;
268        Some(f(ed))
269    }
270
271    /// Write shared element data. Marks the node dirty.
272    pub(crate) fn write_element_data<R: 'static>(
273        &mut self,
274        id: RawId,
275        f: impl FnOnce(&mut ElementData) -> R,
276    ) -> Option<R> {
277        if !self.ids.is_alive(id) {
278            return None;
279        }
280        let meta = self.meta.get(id.index())?;
281        if !meta.flags.is_element() {
282            return None;
283        }
284        let ed = self.element_data.get_mut(id.index())?;
285        let result = f(ed);
286        unsafe {
287            self.meta
288                .get_unchecked_mut(id.index())
289                .flags
290                .mark_style_dirty();
291        }
292        // Propagate dirty_descendants up to root for Stylo's pre_traverse.
293        self.propagate_dirty_ancestors(id.index());
294        Some(result)
295    }
296
297    /// Walk up the tree setting `dirty_descendants` on each ancestor's `ElementData`.
298    /// Stops when an ancestor already has the flag set (already propagated).
299    pub(crate) fn propagate_dirty_ancestors(&mut self, index: u32) {
300        let mut current = self
301            .tree
302            .get(index)
303            .map_or(crate::id::INVALID, |t| t.parent);
304        while current != crate::id::INVALID {
305            if let Some(ed) = self.element_data.get(current) {
306                if ed.dirty_descendants.get() {
307                    break; // Already propagated.
308                }
309                ed.dirty_descendants.set(true);
310            }
311            current = self
312                .tree
313                .get(current)
314                .map_or(crate::id::INVALID, |t| t.parent);
315        }
316    }
317
318    // ── Tree mutations ──
319
320    /// Append `child` as the last child of `parent`.
321    pub(crate) fn append_child(&mut self, parent: RawId, child: RawId) {
322        if !self.ids.is_alive(parent) {
323            return;
324        }
325        if !self.ids.is_alive(child) {
326            return;
327        }
328        if let Some(meta) = self.meta.get(parent.index()) {
329            if !meta.flags.is_container() {
330                return;
331            }
332        } else {
333            return;
334        }
335        if unsafe { tree::ops::is_ancestor(&self.tree, child.index(), parent.index()) } {
336            return;
337        }
338        unsafe {
339            tree::ops::append(&mut self.tree, parent.index(), child.index());
340            self.meta
341                .get_unchecked_mut(parent.index())
342                .flags
343                .mark_tree_dirty();
344            self.meta
345                .get_unchecked_mut(child.index())
346                .flags
347                .mark_tree_dirty();
348        }
349        self.needs_tree_rebuild = true;
350    }
351
352    /// Insert `child` before `ref_id` in the child list.
353    pub(crate) fn insert_before(&mut self, ref_id: RawId, child: RawId) {
354        if !self.ids.is_alive(ref_id) {
355            return;
356        }
357        if !self.ids.is_alive(child) {
358            return;
359        }
360        if let Some(tree) = self.tree.get(ref_id.index()) {
361            if !tree.has_parent() {
362                return;
363            }
364        } else {
365            return;
366        }
367        if unsafe { tree::ops::is_ancestor(&self.tree, child.index(), ref_id.index()) } {
368            return;
369        }
370        unsafe {
371            tree::ops::insert_before(&mut self.tree, ref_id.index(), child.index());
372            self.meta
373                .get_unchecked_mut(child.index())
374                .flags
375                .mark_tree_dirty();
376        }
377        self.needs_tree_rebuild = true;
378    }
379
380    /// Detach a node from the tree.
381    pub(crate) fn detach_node(&mut self, id: RawId) {
382        if !self.ids.is_alive(id) {
383            return;
384        }
385        let parent = self
386            .tree
387            .get(id.index())
388            .map_or(crate::id::INVALID, |t| t.parent);
389        unsafe {
390            tree::ops::detach(&mut self.tree, id.index());
391        }
392        if parent != crate::id::INVALID {
393            unsafe {
394                self.meta.get_unchecked_mut(parent).flags.mark_tree_dirty();
395            }
396            self.needs_tree_rebuild = true;
397        }
398    }
399
400    /// Destroy a node: detach, drop data, free slot.
401    pub(crate) fn destroy_node(&mut self, id: RawId) {
402        if !self.ids.is_alive(id) {
403            return;
404        }
405        self.detach_node(id);
406        // Drop element-specific data.
407        if let Some(meta) = self.meta.get(id.index()).copied() {
408            if meta.data_type_id != TypeId::of::<()>() {
409                self.data.remove(meta.data_type_id, id.index());
410            }
411        }
412        // Reset Stylo data on ElementData (prevents stale computed styles).
413        if let Some(ed) = self.element_data.get_mut(id.index()) {
414            ed.stylo_data = style::data::ElementDataWrapper::default();
415        }
416        // Clear layout data.
417        self.layout.clear_slot(id.index());
418        // Remove event listeners.
419        self.event_store.remove_node(id.index());
420        // Free slot (bumps generation, invalidates all handles).
421        self.ids.free(id);
422    }
423
424    // ── Internal helpers (by raw index, for styling/layout tree walking) ──
425
426    /// Get node metadata by raw index (no generation check).
427    pub(crate) fn node_meta_by_index(&self, index: u32) -> Option<crate::dom::node::NodeMeta> {
428        self.meta.get(index).copied()
429    }
430
431    /// Tag name by raw index.
432    pub(crate) fn tag_name(&self, index: u32) -> Option<&'static str> {
433        Some(self.element_data.get(index)?.tag_name)
434    }
435
436    /// Get tree data by raw index.
437    pub(crate) fn tree_data_by_index(&self, index: u32) -> Option<TreeData> {
438        self.tree.get(index).copied()
439    }
440
441    /// Attribute value by raw index.
442    #[allow(dead_code)]
443    pub(crate) fn attribute(&self, index: u32, name: &str) -> Option<String> {
444        self.element_data
445            .get(index)?
446            .attributes
447            .get(name)
448            .map(|v| v.to_string())
449    }
450
451    /// Get child indices as raw u32.
452    #[allow(dead_code)]
453    pub(crate) fn children_indices_raw(&self, index: u32) -> Vec<u32> {
454        if !self.tree.is_initialized(index) {
455            return Vec::new();
456        }
457        unsafe { tree::ops::children(&self.tree, index) }
458    }
459
460    /// Text content by raw index.
461    #[allow(dead_code)]
462    pub(crate) fn text_content(&self, index: u32) -> Option<String> {
463        let meta = self.meta.get(index)?;
464        if meta.data_type_id != TypeId::of::<TextData>() {
465            return None;
466        }
467        let data = unsafe { self.data.get::<TextData>(index) };
468        Some(data.content.clone())
469    }
470
471    /// Get the computed style for a node (from Stylo's `ElementData`).
472    ///
473    /// Returns `None` if styles haven't been computed yet or for non-element nodes.
474    pub fn computed_style(
475        &self,
476        index: u32,
477    ) -> Option<servo_arc::Arc<style::properties::ComputedValues>> {
478        let ed = self.element_data.get(index)?;
479        let data = ed.stylo_data.borrow();
480        if data.has_styles() {
481            Some(data.styles.primary().clone())
482        } else {
483            None
484        }
485    }
486
487    /// Intrinsic sizing information for a replaced element.
488    #[allow(dead_code)] // Used when replaced element rendering is enabled
489    pub(crate) fn intrinsic_sizing(&self, _index: u32) -> Option<crate::layout::IntrinsicSizes> {
490        None
491    }
492
493    // ── Event listener access ──
494
495    /// Get or create the per-node `EventListenerMap` (lazy allocation).
496    pub(crate) fn ensure_event_listeners(&mut self, index: u32) -> &mut EventListenerMap {
497        self.event_store.ensure_listeners(index)
498    }
499
500    /// Get the per-node `EventListenerMap` mutably, if it has been allocated.
501    pub(crate) fn event_listeners_mut(&mut self, index: u32) -> Option<&mut EventListenerMap> {
502        self.event_store.get_mut(index)
503    }
504
505    /// Take listeners for dispatch (take-call-put pattern).
506    pub(crate) fn take_event_listeners(
507        &mut self,
508        index: u32,
509        type_id: TypeId,
510    ) -> Option<Vec<RegisteredListener>> {
511        self.event_store.take(index, type_id)
512    }
513
514    /// Put listeners back after dispatch.
515    pub(crate) fn put_event_listeners(
516        &mut self,
517        index: u32,
518        type_id: TypeId,
519        listeners: Vec<RegisteredListener>,
520    ) {
521        self.event_store.put(index, type_id, listeners);
522    }
523
524    /// Initialize typed data for a slot.
525    pub(crate) fn init_typed_data<T: Default + 'static>(&mut self, index: u32) {
526        self.data.init::<T>(index);
527    }
528
529    /// Set element data for a freshly allocated element.
530    pub(crate) fn set_element_data_new(&mut self, index: u32, data: ElementData) {
531        self.element_data.set(index, data);
532    }
533
534    /// Set text content for a freshly allocated text node.
535    pub(crate) fn set_text_content_new(&mut self, index: u32, content: &str) {
536        unsafe {
537            self.data.get_mut::<TextData>(index).content = content.to_string();
538        }
539    }
540
541    // ── Handle creation ──
542
543    fn make_handle(&self, index: u32, generation: u32) -> Handle {
544        Handle::new(RawId::new(index, generation), self.cell())
545    }
546
547    // ── Shorthand element creation ──
548
549    pub fn div(&self) -> crate::html::HtmlDivElement {
550        self.create::<crate::html::HtmlDivElement>()
551    }
552    pub fn span(&self) -> crate::html::HtmlSpanElement {
553        self.create::<crate::html::HtmlSpanElement>()
554    }
555    pub fn p(&self) -> crate::html::HtmlParagraphElement {
556        self.create::<crate::html::HtmlParagraphElement>()
557    }
558    pub fn button(&self) -> crate::html::HtmlButtonElement {
559        self.create::<crate::html::HtmlButtonElement>()
560    }
561    pub fn img(&self) -> crate::html::HtmlImageElement {
562        self.create::<crate::html::HtmlImageElement>()
563    }
564    pub fn input(&self) -> crate::html::HtmlInputElement {
565        self.create::<crate::html::HtmlInputElement>()
566    }
567    pub fn label(&self) -> crate::html::HtmlLabelElement {
568        self.create::<crate::html::HtmlLabelElement>()
569    }
570    pub fn h1(&self) -> crate::html::HtmlHeadingElement {
571        self.create_heading(1)
572    }
573    pub fn h2(&self) -> crate::html::HtmlHeadingElement {
574        self.create_heading(2)
575    }
576    pub fn h3(&self) -> crate::html::HtmlHeadingElement {
577        self.create_heading(3)
578    }
579    pub fn text_node(&self, content: &str) -> Text {
580        self.create_text(content)
581    }
582
583    pub fn div_in(&self, parent: impl Into<Handle>) -> crate::html::HtmlDivElement {
584        let el = self.div();
585        parent.into().append(el);
586        el
587    }
588
589    pub fn span_in(&self, parent: impl Into<Handle>) -> crate::html::HtmlSpanElement {
590        let el = self.span();
591        parent.into().append(el);
592        el
593    }
594
595    pub fn text_in(&self, parent: impl Into<Handle>, content: &str) -> Text {
596        let t = self.text_node(content);
597        parent.into().append(t);
598        t
599    }
600
601    // ── Node creation (public API) ──
602
603    pub fn create<T: Element>(&self) -> T {
604        assert!(
605            !T::TAG_NAME.is_empty(),
606            "Element type has no fixed tag — use create_with_tag() instead"
607        );
608        self.create_with_tag::<T>(T::TAG_NAME)
609    }
610
611    pub fn create_with_tag<T: Element>(&self, tag: &'static str) -> T {
612        let cell = self.cell();
613        let (index, generation) = cell.write(|doc| {
614            let (index, generation) =
615                doc.alloc_node(NodeFlags::element(T::IS_FOCUSABLE), TypeId::of::<T::Data>());
616            doc.set_element_data_new(index, ElementData::new(tag, T::IS_FOCUSABLE));
617            if TypeId::of::<T::Data>() != TypeId::of::<()>() {
618                doc.init_typed_data::<T::Data>(index);
619            }
620            (index, generation)
621        });
622
623        T::from_handle(self.make_handle(index, generation))
624    }
625
626    pub fn create_heading(&self, level: u8) -> crate::html::HtmlHeadingElement {
627        assert!(
628            (1..=6).contains(&level),
629            "heading level must be 1-6, got {level}"
630        );
631        let tag: &'static str = match level {
632            1 => "h1",
633            2 => "h2",
634            3 => "h3",
635            4 => "h4",
636            5 => "h5",
637            6 => "h6",
638            _ => unreachable!(),
639        };
640        let elem = self.create_with_tag::<crate::html::HtmlHeadingElement>(tag);
641        elem.set_level(level);
642        elem
643    }
644
645    pub fn create_text(&self, content: &str) -> Text {
646        let cell = self.cell();
647        let content_owned = content.to_string();
648        let (index, generation) = cell.write(|doc| {
649            let (index, generation) = doc.alloc_node(NodeFlags::text(), TypeId::of::<TextData>());
650            doc.init_typed_data::<TextData>(index);
651            doc.set_text_content_new(index, &content_owned);
652            (index, generation)
653        });
654
655        Text::from_raw(self.make_handle(index, generation))
656    }
657
658    // ── Queries ──
659
660    pub fn root(&self) -> Handle {
661        Handle::new(RawId::new(self.root, 0), self.cell())
662    }
663
664    /// The `<body>` element — main content container.
665    ///
666    /// Like Chrome's `document.body`. Created by `init_body()` after the
667    /// Document is pinned in memory. Styles come from the UA stylesheet:
668    /// `body { display: block; margin: 8px; }`.
669    ///
670    /// All user content goes in `body()`, not `root()`.
671    pub fn body(&self) -> Handle {
672        debug_assert!(self.body != 0, "body() called before init_body()");
673        Handle::new(RawId::new(self.body, 0), self.cell())
674    }
675
676    pub fn resolve(&self, raw: RawId) -> Option<Handle> {
677        if self.ids.is_alive(raw) {
678            Some(Handle::new(raw, self.cell()))
679        } else {
680            None
681        }
682    }
683
684    pub fn node_count(&self) -> u32 {
685        self.ids.count()
686    }
687
688    pub fn root_index(&self) -> u32 {
689        self.root
690    }
691
692    /// Whether any pending changes need a visual update.
693    ///
694    /// Chrome equivalent: `Document::NeedsStyleRecalc()` + layout dirty checks.
695    /// Used by the scheduler to request a frame after spawned tasks mutate the DOM.
696    pub fn needs_visual_update(&self) -> bool {
697        self.needs_style_recalc || self.needs_tree_rebuild || !self.dirty_layout_nodes.is_empty()
698    }
699
700    /// Atomically read and clear the layout-dirty state for the current frame.
701    ///
702    /// Returns `true` if any structural or style change requires a full cache
703    /// clear (tree rebuild, style recalc). Text-only changes (incremental node
704    /// dirtying) are also cleared here but do not set the return flag.
705    pub(crate) fn take_layout_dirty(&mut self) -> bool {
706        let dirty = self.needs_style_recalc || self.needs_tree_rebuild;
707        self.needs_style_recalc = false;
708        self.needs_tree_rebuild = false;
709        self.dirty_layout_nodes.clear();
710        dirty
711    }
712
713    /// Get a Handle for a node by its arena index.
714    ///
715    /// Used by hit testing and event dispatch: the fragment tree stores only
716    /// the node index (`u32`), this recovers the full Handle needed for
717    /// event dispatch. Returns `None` if the index is dead or out of bounds.
718    ///
719    /// Chrome equivalent: resolving a `LayoutObject`'s `GetNode()` pointer.
720    pub fn handle_for_index(&self, index: u32) -> Option<Handle> {
721        let generation = self.ids.live_generation(index)?;
722        Some(Handle::new(RawId::new(index, generation), self.cell()))
723    }
724
725    /// Set or clear the HOVER element state flag on a node.
726    ///
727    /// Chrome equivalent: `Element::SetHovered()` → `SetNeedsStyleRecalc()`.
728    /// Marks the element for restyle and propagates `dirty_descendants` up
729    /// the ancestor chain so Stylo's traversal reaches and restyles it.
730    #[allow(dead_code)] // Called via DispatchSurface trait; used by upcoming input dispatch
731    pub(crate) fn set_hover_state(&self, index: u32, hovered: bool) {
732        let Some(generation) = self.ids.live_generation(index) else {
733            return;
734        };
735        let id = RawId::new(index, generation);
736        self.cell().write(|doc| {
737            doc.write_element_data(id, |ed| {
738                if hovered {
739                    ed.element_state.insert(style_dom::ElementState::HOVER);
740                } else {
741                    ed.element_state.remove(style_dom::ElementState::HOVER);
742                }
743            });
744            // Tell Stylo this element needs restyling + mark ancestors dirty.
745            // Without this, Stylo sees clean cached data and skips the traversal.
746            doc.mark_for_restyle(index);
747        });
748    }
749
750    /// Set or clear HOVER on a node AND all its ancestors.
751    ///
752    /// Chrome equivalent: `Document::SetHoveredElement()` — when you hover
753    /// a child, the parent also gets `:hover`. CSS `.card:hover` stays active
754    /// when cursor moves to `.card-icon` (a child element).
755    pub(crate) fn set_hover_chain(&self, index: u32, hovered: bool) {
756        self.set_state_chain(index, style_dom::ElementState::HOVER, hovered);
757    }
758
759    /// Set or clear ACTIVE on a node AND all its ancestors.
760    ///
761    /// Like `set_hover_chain` — CSS `:active` propagates up.
762    /// Clicking a child element activates the parent too.
763    pub(crate) fn set_active_chain(&self, index: u32, active: bool) {
764        self.set_state_chain(index, style_dom::ElementState::ACTIVE, active);
765    }
766
767    /// Set or clear an `ElementState` flag on a node and all its ancestors,
768    /// marking each for restyle. Used by hover and active chain propagation.
769    fn set_state_chain(&self, index: u32, flag: style_dom::ElementState, active: bool) {
770        self.cell().write(|doc| {
771            let mut current = index;
772            loop {
773                if let Some(ed) = doc.element_data.get_mut(current) {
774                    if active {
775                        ed.element_state.insert(flag);
776                    } else {
777                        ed.element_state.remove(flag);
778                    }
779                }
780                doc.mark_for_restyle(current);
781
782                match doc.tree.get(current) {
783                    Some(td) if td.parent != INVALID => current = td.parent,
784                    _ => break,
785                }
786            }
787        });
788    }
789
790    /// Mark an element as needing restyle and propagate dirty flags up.
791    ///
792    /// Chrome equivalent: `Element::SetNeedsStyleRecalc()` +
793    /// `Element::MarkAncestorsWithChildNeedsStyleRecalc()`.
794    ///
795    /// Sets `RestyleHint::RESTYLE_SELF` on the element's Stylo data, then
796    /// walks up the parent chain setting `dirty_descendants = true` on each
797    /// ancestor. This ensures Stylo's `pre_traverse` sees dirty flags and
798    /// decides to traverse the tree.
799    pub(crate) fn mark_for_restyle(&mut self, index: u32) {
800        self.needs_style_recalc = true;
801
802        if let Some(ed) = self.element_data.get(index) {
803            ed.mark_for_restyle();
804        }
805
806        self.propagate_dirty_ancestors(index);
807    }
808
809    // ── Layout data access ──
810
811    /// Get layout data for a node by raw index.
812    #[allow(dead_code)] // API for upcoming renderer/platform integration
813    pub(crate) fn layout_data(&self, index: u32) -> Option<&LayoutNodeData> {
814        self.layout.get(index)
815    }
816
817    /// Get mutable layout data for a node by raw index.
818    #[allow(dead_code)] // API for upcoming renderer/platform integration
819    pub(crate) fn layout_data_mut(&mut self, index: u32) -> Option<&mut LayoutNodeData> {
820        self.layout.get_mut(index)
821    }
822
823    /// Clear layout cache on a node and propagate up the layout ancestor chain.
824    ///
825    /// When a child's content or style changes, parent caches are stale
826    /// (parent size depends on child size). Walk up clearing each ancestor's
827    /// cache until we reach the root or an already-cleared node.
828    ///
829    /// Chrome equivalent: `LayoutObject::SetNeedsLayout()` propagation.
830    pub(crate) fn mark_layout_dirty(&mut self, index: u32) {
831        let mut current = index;
832        while let Some(data) = self.layout.get_mut(current) {
833            data.clear_cache();
834            let Some(parent) = data.layout_parent() else {
835                break;
836            };
837            current = parent;
838        }
839    }
840
841    #[allow(dead_code)]
842    pub(crate) fn cell_for_layout(&self) -> DocumentCell {
843        self.cell()
844    }
845
846    // ── Style engine (delegates to concrete StyleEngine) ──
847
848    pub fn recalc_styles(&mut self) {
849        // Style engine needs DocumentCell for tree walking.
850        // This is safe: the engine only accesses through read/write.
851        let cell = self.cell();
852        let root = self.root;
853        self.style_engine.recalc_styles(cell, root);
854    }
855
856    /// Add a CSS stylesheet with full selector support.
857    ///
858    /// Chrome equivalent: `<style>` element or `document.adoptedStyleSheets`.
859    /// Supports all CSS selectors: `.class`, `#id`, `tag`, `[attr]`,
860    /// descendant, child, pseudo-classes — everything Stylo/Chrome supports.
861    ///
862    /// ```ignore
863    /// doc.add_stylesheet(".card { background: red; border-radius: 8px; }");
864    /// doc.add_stylesheet(include_str!("../assets/dashboard.css"));
865    /// ```
866    pub fn add_stylesheet(&self, css: &str) {
867        let cell = self.cell();
868        let css_owned = css.to_string();
869        cell.write(|doc| {
870            doc.style_engine.add_stylesheet(&css_owned);
871        });
872    }
873
874    pub(crate) fn set_viewport(&mut self, width: f32, height: f32) {
875        self.style_engine.set_viewport(width, height);
876    }
877}
878
879impl Default for Document {
880    fn default() -> Self {
881        Self::new()
882    }
883}
884
885impl Drop for Document {
886    fn drop(&mut self) {
887        #[cfg(debug_assertions)]
888        {
889            self.alive = false;
890        }
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::dom::traits::HasHandle;
898
899    #[test]
900    fn new_creates_exactly_one_root_node() {
901        let doc = Document::new();
902        assert_eq!(doc.node_count(), 1);
903    }
904
905    #[test]
906    fn created_element_handle_is_alive() {
907        let doc = Document::new();
908        let div = doc.div();
909        assert!(div.handle().is_alive());
910    }
911
912    #[test]
913    fn destroy_makes_handle_dead() {
914        let doc = Document::new();
915        let div = doc.div();
916        let handle = div.handle();
917        handle.destroy();
918        assert!(!handle.is_alive());
919    }
920
921    #[test]
922    fn handle_raw_index_matches_arena_slot() {
923        let doc = Document::new();
924        // Root occupies slot 0; first allocated element goes to slot 1.
925        let div = doc.div();
926        let raw = div.handle().raw();
927        assert!(doc.ids.is_alive(raw));
928    }
929}