Skip to main content

apace_dom/node/
node.rs

1use crate::Document;
2use crate::layout::paint_tree::{HoistedPaintChild, StackingContext};
3use apace_traits::events::{
4    ApacePointerEvent, ApacePointerId, DomEventData, HitResult, PointerCoords,
5};
6use apace_traits::node_id::NodeId;
7use apace_traits::shell::ShellProvider;
8use bitflags::bitflags;
9use euclid::{Point2D, Rect, Size2D};
10use keyboard_types::Modifiers;
11use kurbo::{Affine, Rect as KurboRect};
12use markup5ever::{LocalName, local_name};
13use parley::{BreakReason, Cluster, ClusterSide};
14use selectors::matching::ElementSelectorFlags;
15use std::cell::{Cell, RefCell};
16use std::fmt::Write;
17use std::ops::Deref;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicBool, Ordering};
20use style::computed_values::flex_direction::T as FlexDirection;
21use style::computed_values::isolation::T as Isolation;
22use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
23use style::invalidation::element::restyle_hints::RestyleHint;
24use style::properties::ComputedValues;
25use style::properties::generated::longhands::position::computed_value::T as Position;
26use style::selector_parser::RestyleDamage;
27use style::servo_arc::Arc as ServoArc;
28use style::shared_lock::SharedRwLock;
29use style::stylesheets::UrlExtraData;
30use style::values::computed::CSSPixelLength;
31use style::values::computed::Display as StyloDisplay;
32use style::values::computed::Rotate;
33use style::values::generics::transform::{Scale, Translate};
34use style::values::specified::box_::{DisplayInside, DisplayOutside};
35use style_dom::ElementState;
36use style_traits::values::ToCss;
37use stylo_takia::CopyStyleGetters;
38use stylo_takia::WritingModeExt;
39use takia::{Cache, prelude::Layout};
40use thin_vec::ThinVec;
41
42use super::stylo_data::{ComputedStyleRef, StyloData};
43use super::{Attribute, DocumentData, ElementData, LayoutData};
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum DisplayOuter {
47    Block,
48    Inline,
49    None,
50}
51
52bitflags! {
53    #[derive(Clone, Copy, PartialEq)]
54    pub struct NodeFlags: u32 {
55        /// Whether the node is the root node of an Inline Formatting Context
56        const IS_INLINE_ROOT = 0b00000001;
57        /// Whether the node is the root node of an Table formatting context
58        const IS_TABLE_ROOT = 0b00000010;
59        /// Whether the node is "in the document" (~= has a parent and isn't a template node)
60        const IS_IN_DOCUMENT = 0b00000100;
61        /// A document-owned viewport layout/paint root. DOM and style ancestry stay intact.
62        const IS_TOP_LAYER = 0b00001000;
63        const CONTAIN_TOP_LAYER_SCROLL = 0b00010000;
64        /// Whether the node is an inline box whose in-flow content is block-level only
65        const IS_INLINE_WITH_ONLY_BLOCK_CHILDREN = 0b00100000;
66    }
67}
68
69impl NodeFlags {
70    pub fn is_top_layer(&self) -> bool {
71        self.contains(Self::IS_TOP_LAYER)
72    }
73    #[inline(always)]
74    pub fn is_inline_root(&self) -> bool {
75        self.contains(Self::IS_INLINE_ROOT)
76    }
77
78    #[inline(always)]
79    pub fn is_table_root(&self) -> bool {
80        self.contains(Self::IS_TABLE_ROOT)
81    }
82
83    #[inline(always)]
84    pub fn is_in_document(&self) -> bool {
85        self.contains(Self::IS_IN_DOCUMENT)
86    }
87
88    #[inline(always)]
89    pub fn reset_construction_flags(&mut self) {
90        self.remove(Self::IS_INLINE_ROOT);
91        self.remove(Self::IS_TABLE_ROOT);
92        self.remove(Self::IS_INLINE_WITH_ONLY_BLOCK_CHILDREN);
93    }
94}
95
96pub struct Node {
97    // The actual tree we belong to. This is unsafe!!
98    tree: *mut crate::NodeTree,
99
100    /// Our Id
101    pub id: NodeId,
102    /// Our parent's ID
103    pub parent: Option<NodeId>,
104    // What are our children?
105    pub children: ThinVec<NodeId>,
106    /// Our parent in the layout hierachy: a separate list that includes anonymous collections of inline elements
107    pub layout_parent: Cell<Option<NodeId>>,
108    /// A separate child list that includes anonymous collections of inline elements
109    pub layout_children: RefCell<Option<ThinVec<NodeId>>>,
110    /// Out-of-flow (absolutely/fixed positioned) boxes for which this node is the
111    /// containing block. Recorded by Takia's out-of-flow positioning pass. The
112    /// `Layout.location` of these boxes is relative to this node's border box.
113    pub hoisted_children: RefCell<ThinVec<NodeId>>,
114    /// For an out-of-flow box that was hoisted by Takia's out-of-flow positioning
115    /// pass, the node whose `hoisted_children` list contains it (its containing
116    /// block). The box's `Layout.location` is relative to this node rather than
117    /// to its `layout_parent`. `None` for in-flow boxes.
118    pub oof_containing_block: Cell<Option<NodeId>>,
119    /// Anonymous block boxes created for this node during layout construction.
120    ///
121    /// Anonymous blocks live only in the slab (they are not part of the DOM
122    /// `children` list), so we track the ones we own here to be able to
123    /// deallocate them when this node is reconstructed.
124    pub anonymous_blocks: ThinVec<NodeId>,
125    /// The same as layout_children, but sorted by z-index
126    pub paint_children: RefCell<Option<ThinVec<NodeId>>>,
127    pub stacking_context: Option<Box<StackingContext>>,
128    /// The hoisted entries this node's subtree pushed into the enclosing
129    /// stacking context on its last (non-skipped) paint-tree build. Replayed
130    /// when the subtree is skipped as clean. Empty for stacking-context roots.
131    pub sc_contribution_cache: RefCell<ThinVec<HoistedPaintChild>>,
132
133    // Flags
134    pub flags: NodeFlags,
135    /// Whether the node's box is out of flow (absolutely positioned and not `display: none`).
136    /// Written by the style traversal from the node's new primary style, so that layout
137    /// passes can test it without reading styles.
138    pub out_of_flow: AtomicBool,
139
140    /// Node type (Element, TextNode, etc) specific data.
141    ///
142    /// For element nodes this holds the [`ElementData`], which stores most of
143    /// the per-node style/layout state. For the document node it holds the
144    /// [`DocumentData`]. Access the moved fields through the forwarding methods
145    /// on [`Node`] (e.g. [`Node::style`], [`Node::final_layout`]).
146    pub data: NodeData,
147}
148
149unsafe impl Send for Node {}
150unsafe impl Sync for Node {}
151
152/// Generates forwarding accessors for fields that live on both [`ElementData`]
153/// (element / anonymous block nodes) and [`DocumentData`] (the document node).
154macro_rules! universal_accessors {
155    ($($(#[$meta:meta])* $field:ident / $field_mut:ident : $ty:ty),* $(,)?) => {
156        impl Node {
157            $(
158                $(#[$meta])*
159                #[inline]
160                pub fn $field(&self) -> &$ty {
161                    match &self.data {
162                        NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.$field,
163                        NodeData::Document(data) => &data.$field,
164                        _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
165                    }
166                }
167
168                $(#[$meta])*
169                #[inline]
170                pub fn $field_mut(&mut self) -> &mut $ty {
171                    match &mut self.data {
172                        NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.$field,
173                        NodeData::Document(data) => &mut data.$field,
174                        _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
175                    }
176                }
177            )*
178        }
179    };
180}
181
182universal_accessors! {
183    stylo_element_data / stylo_element_data_mut: StyloData,
184    transform / transform_mut: Option<Box<Affine>>,
185    display_constructed_as / display_constructed_as_mut: StyloDisplay,
186    // The document node is styled/snapshotted like an element, so it also
187    // carries these:
188    element_state / element_state_mut: ElementState,
189    snapshot_handled / snapshot_handled_mut: AtomicBool,
190    // `apply_selector_flags` deposits `for_parent()` flags on the parent node,
191    // and the parent of the root <html> element is the document -- so the
192    // document has to be able to hold selector flags too.
193    selector_flags / selector_flags_mut: Cell<ElementSelectorFlags>,
194}
195
196impl Node {
197    /// This node's layout output state, or a shared default if layout has
198    /// never written to this node.
199    ///
200    /// Panics for node kinds which do not participate in layout (text and
201    /// comment nodes).
202    #[inline]
203    pub fn layout_data(&self) -> &LayoutData {
204        match &self.data {
205            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.layout_data(),
206            NodeData::Document(data) => data.layout_data(),
207            _ => panic!("`layout_data` is not available on this node kind"),
208        }
209    }
210
211    /// Mutable access to this node's layout output state, allocating it if it
212    /// does not yet exist.
213    ///
214    /// Panics for node kinds which do not participate in layout (text and
215    /// comment nodes).
216    #[inline]
217    pub fn layout_data_mut(&mut self) -> &mut LayoutData {
218        match &mut self.data {
219            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.layout_data_mut(),
220            NodeData::Document(data) => data.layout_data_mut(),
221            _ => panic!("`layout_data` is not available on this node kind"),
222        }
223    }
224
225    /// Mutable access to this node's layout output state, if it has been
226    /// allocated. Never allocates. Returns `None` for node kinds which do not
227    /// participate in layout.
228    #[inline]
229    pub fn try_layout_data_mut(&mut self) -> Option<&mut LayoutData> {
230        match &mut self.data {
231            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
232                data.layout_data.as_deref_mut()
233            }
234            NodeData::Document(data) => data.layout_data.as_deref_mut(),
235            _ => None,
236        }
237    }
238
239    /// Clear this node's takia layout cache without allocating `LayoutData`
240    /// for nodes that have never been laid out.
241    #[inline]
242    pub fn clear_layout_cache(&mut self) {
243        if let Some(layout_data) = self.try_layout_data_mut() {
244            layout_data.cache.clear();
245        }
246    }
247
248    /// Clear this node's takia layout cache and any cached inline
249    /// `content_widths`, forcing a full relayout of it on the next pass.
250    pub fn invalidate_layout_cache(&mut self) {
251        self.clear_layout_cache();
252        if let Some(inline_layout) = self
253            .data
254            .downcast_element_mut()
255            .and_then(|el| el.inline_layout_data.as_mut())
256        {
257            inline_layout.content_widths = None;
258        }
259    }
260
261    #[inline]
262    pub fn cache(&self) -> &Cache {
263        &self.layout_data().cache
264    }
265
266    #[inline]
267    pub fn cache_mut(&mut self) -> &mut Cache {
268        &mut self.layout_data_mut().cache
269    }
270
271    #[inline]
272    pub fn unrounded_layout(&self) -> &Layout {
273        &self.layout_data().unrounded_layout
274    }
275
276    #[inline]
277    pub fn unrounded_layout_mut(&mut self) -> &mut Layout {
278        &mut self.layout_data_mut().unrounded_layout
279    }
280
281    #[inline]
282    pub fn final_layout(&self) -> &Layout {
283        &self.layout_data().final_layout
284    }
285
286    #[inline]
287    pub fn final_layout_mut(&mut self) -> &mut Layout {
288        &mut self.layout_data_mut().final_layout
289    }
290
291    #[inline]
292    pub fn scroll_offset(&self) -> &crate::Point<f64> {
293        &self.layout_data().scroll_offset
294    }
295
296    #[inline]
297    pub fn scroll_offset_mut(&mut self) -> &mut crate::Point<f64> {
298        &mut self.layout_data_mut().scroll_offset
299    }
300
301    #[inline]
302    pub fn scrollable_overflow(&self) -> &KurboRect {
303        &self.layout_data().scrollable_overflow
304    }
305
306    #[inline]
307    pub fn scrollable_overflow_mut(&mut self) -> &mut KurboRect {
308        &mut self.layout_data_mut().scrollable_overflow
309    }
310
311    /// Style data from stylo, if this node kind carries it (element or document
312    /// nodes). Returns `None` for text/comment nodes.
313    #[inline]
314    pub fn try_stylo_element_data(&self) -> Option<&StyloData> {
315        match &self.data {
316            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
317                Some(&data.stylo_element_data)
318            }
319            NodeData::Document(data) => Some(&data.stylo_element_data),
320            _ => None,
321        }
322    }
323
324    #[inline]
325    pub fn try_stylo_element_data_mut(&mut self) -> Option<&mut StyloData> {
326        match &mut self.data {
327            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
328                Some(&mut data.stylo_element_data)
329            }
330            NodeData::Document(data) => Some(&mut data.stylo_element_data),
331            _ => None,
332        }
333    }
334
335    /// The `dirty_descendants` flag, if this node kind carries it (element or
336    /// document nodes). Returns `None` for text/comment nodes.
337    #[inline]
338    fn dirty_descendants_flag(&self) -> Option<&AtomicBool> {
339        match &self.data {
340            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
341                Some(&data.dirty_descendants)
342            }
343            NodeData::Document(data) => Some(&data.dirty_descendants),
344            _ => None,
345        }
346    }
347
348    /// The `damaged_descendants` flag, if this node kind carries it (element or
349    /// document nodes). Returns `None` for text/comment nodes.
350    #[inline]
351    fn damaged_descendants_flag(&self) -> Option<&AtomicBool> {
352        match &self.data {
353            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
354                Some(&data.damaged_descendants)
355            }
356            NodeData::Document(data) => Some(&data.damaged_descendants),
357            _ => None,
358        }
359    }
360
361    /// The document's shared style lock. Only available on element and
362    /// document nodes.
363    #[inline]
364    pub fn guard(&self) -> &SharedRwLock {
365        let guard = match &self.data {
366            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.guard.as_ref(),
367            NodeData::Document(data) => data.guard.as_ref(),
368            _ => None,
369        };
370        guard.expect("`guard` is not available on this node kind")
371    }
372
373    #[inline]
374    pub fn has_snapshot(&self) -> bool {
375        match &self.data {
376            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot,
377            NodeData::Document(data) => data.has_snapshot,
378            _ => false,
379        }
380    }
381
382    #[inline]
383    pub fn set_has_snapshot(&mut self, value: bool) {
384        match &mut self.data {
385            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot = value,
386            NodeData::Document(data) => data.has_snapshot = value,
387            _ => {}
388        }
389    }
390
391    #[inline]
392    pub fn before(&self) -> Option<NodeId> {
393        self.element_data().and_then(|data| data.before)
394    }
395
396    #[inline]
397    pub fn after(&self) -> Option<NodeId> {
398        self.element_data().and_then(|data| data.after)
399    }
400}
401
402impl Node {
403    pub(crate) fn new(
404        tree: *mut crate::NodeTree,
405        id: NodeId,
406        guard: SharedRwLock,
407        mut data: NodeData,
408    ) -> Self {
409        // Store a handle to the document's shared style lock on the node data.
410        // Both element and document nodes are styled by stylo and so need it.
411        match &mut data {
412            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
413                data.guard = Some(guard);
414            }
415            NodeData::Document(data) => data.guard = Some(guard),
416            _ => {}
417        }
418
419        Self {
420            tree,
421
422            id,
423            parent: None,
424            children: ThinVec::new(),
425            layout_parent: Cell::new(None),
426            oof_containing_block: Cell::new(None),
427            layout_children: RefCell::new(None),
428            hoisted_children: RefCell::new(ThinVec::new()),
429            anonymous_blocks: ThinVec::new(),
430            paint_children: RefCell::new(None),
431            stacking_context: None,
432            sc_contribution_cache: RefCell::new(ThinVec::new()),
433
434            flags: NodeFlags::empty(),
435            out_of_flow: AtomicBool::new(false),
436            data,
437        }
438    }
439
440    pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
441        let transform = self.primary_styles().and_then(|s| {
442            let size = self.final_layout().size;
443            let reference_box = Rect::new(
444                Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
445                Size2D::new(
446                    CSSPixelLength::new(size.width),
447                    CSSPixelLength::new(size.height),
448                ),
449            );
450            // Resolve the transform in CSS pixels, then convert it to device-pixel space
451            // (S * T * S^-1): translation components are scaled, linear components are not.
452            crate::resolve_2d_transform(s.get_box(), reference_box).map(|t| {
453                let scale = scale as f64;
454                let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
455                Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
456            })
457        });
458
459        let slot = self.transform_mut();
460        match (slot.as_deref_mut(), transform) {
461            (Some(existing), Some(new)) => *existing = new,
462            (None, Some(new)) => *slot = Some(Box::new(new)),
463            (_, None) => *slot = None,
464        }
465        transform
466    }
467
468    pub fn pe_by_index(&self, index: usize) -> Option<NodeId> {
469        match index {
470            0 => self.after(),
471            1 => self.before(),
472            _ => panic!("Invalid pseudo element index"),
473        }
474    }
475
476    pub fn set_pe_by_index(&mut self, index: usize, value: Option<NodeId>) {
477        let Some(data) = self.element_data_mut() else {
478            return;
479        };
480        match index {
481            0 => data.after = value,
482            1 => data.before = value,
483            _ => panic!("Invalid pseudo element index"),
484        }
485    }
486
487    pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
488        let display = self.primary_styles().as_ref()?.clone_display();
489        Some(
490            if self.element_data().is_some_and(|el| el.is_button_input()) {
491                stylo_takia::convert::button_input_display(display)
492            } else {
493                display
494            },
495        )
496    }
497
498    pub fn is_or_contains_block(&self) -> bool {
499        let style = self.primary_styles();
500        let style = style.as_ref();
501
502        // Ignore out-of-flow items
503        let position = style
504            .map(|s| s.clone_position())
505            .unwrap_or(Position::Relative);
506        let is_in_flow = matches!(
507            position,
508            Position::Static | Position::Relative | Position::Sticky
509        );
510        if !is_in_flow {
511            return false;
512        }
513        // Floated boxes do not break up the inline flow: they participate in the
514        // inline formatting context as out-of-flow inline boxes
515        let is_floating = style
516            .map(|s| s.clone_float().is_floating())
517            .unwrap_or(false);
518        if is_floating {
519            return false;
520        }
521        let display = self.display_style().unwrap_or(StyloDisplay::inline());
522        match display.outside() {
523            DisplayOutside::None => false,
524            DisplayOutside::Block => true,
525            _ => {
526                if display.inside() == DisplayInside::Flow {
527                    self.children
528                        .iter()
529                        .copied()
530                        .any(|child_id| self.tree()[child_id].is_or_contains_block())
531                } else {
532                    false
533                }
534            }
535        }
536    }
537
538    /// Whether box-tree construction found this to be an inline box with block-level in-flow content only.
539    /// It is laid out as a block box, so its own background and borders must not be painted
540    /// over that content. Inline text or inline children keep the painting.
541    pub fn is_inline_with_only_block_children(&self) -> bool {
542        self.flags
543            .contains(NodeFlags::IS_INLINE_WITH_ONLY_BLOCK_CHILDREN)
544    }
545
546    pub fn is_whitespace_node(&self) -> bool {
547        match &self.data {
548            NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
549            _ => false,
550        }
551    }
552
553    /// Whether this is an empty or whitespace-only text node whose whitespace may be collapsed
554    /// away under the `white-space-collapse` it inherits from its parent.
555    pub fn is_collapsible_whitespace_node(&self) -> bool {
556        let NodeData::Text(data) = &self.data else {
557            return false;
558        };
559        if data.content.is_empty() {
560            return true;
561        }
562        if !data.content.chars().all(|c| c.is_ascii_whitespace()) {
563            return false;
564        }
565        let white_space_collapse = self
566            .parent
567            .and_then(|parent_id| self.with(parent_id).primary_styles())
568            .map(|style| style.clone_white_space_collapse());
569        match white_space_collapse {
570            Some(
571                WhiteSpaceCollapse::Preserve
572                | WhiteSpaceCollapse::BreakSpaces
573                | WhiteSpaceCollapse::PreserveSpaces,
574            ) => false,
575            Some(WhiteSpaceCollapse::PreserveBreaks) => !data.content.contains('\n'),
576            Some(WhiteSpaceCollapse::Collapse) | None => true,
577        }
578    }
579
580    pub fn is_focussable(&self) -> bool {
581        self.data
582            .downcast_element()
583            .map(|el| el.is_focussable)
584            .unwrap_or(false)
585    }
586
587    pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
588        if let Some(stylo_element_data) = self.try_stylo_element_data_mut() {
589            if let Some(mut element_data) = stylo_element_data.get_mut() {
590                element_data.hint.insert(hint);
591            }
592        }
593        // Mark all ancestors as having dirty descendants so the style traversal
594        // will visit this node's subtree
595        self.mark_ancestors_dirty();
596    }
597
598    /// Returns whether this node has any descendants that need restyling.
599    pub fn has_dirty_descendants(&self) -> bool {
600        self.dirty_descendants_flag()
601            .is_some_and(|flag| flag.load(Ordering::Relaxed))
602    }
603
604    /// Sets the dirty_descendants flag on this node.
605    pub fn set_dirty_descendants(&self) {
606        if let Some(flag) = self.dirty_descendants_flag() {
607            flag.store(true, Ordering::Relaxed);
608        }
609    }
610
611    /// Clears the dirty_descendants flag on this node.
612    pub fn unset_dirty_descendants(&self) {
613        if let Some(flag) = self.dirty_descendants_flag() {
614            flag.store(false, Ordering::Relaxed);
615        }
616    }
617
618    /// Marks all ancestors of this node as having dirty descendants.
619    /// This propagates the dirty flag up the tree so that the style traversal
620    /// knows to visit the subtree containing this node.
621    pub fn mark_ancestors_dirty(&self) {
622        let mut current_id = self.parent;
623        while let Some(parent_id) = current_id {
624            let parent = &self.tree()[parent_id];
625            // If this ancestor already has dirty_descendants set, we can stop
626            // because all further ancestors must also have it set
627            if let Some(flag) = parent.dirty_descendants_flag() {
628                if flag.swap(true, Ordering::Relaxed) {
629                    break;
630                }
631            }
632            current_id = parent.parent;
633        }
634    }
635
636    /// Returns whether this node or any of its descendants may carry damage.
637    pub fn has_damaged_descendants(&self) -> bool {
638        self.damaged_descendants_flag()
639            .is_some_and(|flag| flag.load(Ordering::Relaxed))
640    }
641
642    /// Clears the damaged_descendants flag on this node.
643    pub fn unset_damaged_descendants(&self) {
644        if let Some(flag) = self.damaged_descendants_flag() {
645            flag.store(false, Ordering::Relaxed);
646        }
647    }
648
649    /// Marks this node and all of its ancestors as (potentially) carrying
650    /// damage, so that the damage propagation pass visits this node's subtree.
651    ///
652    /// The invariant is: if a node carries damage (or needs damage-phase
653    /// processing such as pseudo-element style syncing), then it and all of
654    /// its ancestors have `damaged_descendants` set.
655    pub fn mark_damaged(&self) {
656        if let Some(flag) = self.damaged_descendants_flag() {
657            if flag.swap(true, Ordering::Relaxed) {
658                return;
659            }
660        }
661        let mut current_id = self.parent;
662        while let Some(parent_id) = current_id {
663            let parent = &self.tree()[parent_id];
664            // If this ancestor already has damaged_descendants set, we can stop
665            // because all further ancestors must also have it set
666            if let Some(flag) = parent.damaged_descendants_flag() {
667                if flag.swap(true, Ordering::Relaxed) {
668                    break;
669                }
670            }
671            current_id = parent.parent;
672        }
673    }
674
675    // pub fn damage_mut(&mut self) -> Option<&mut RestyleDamage> {
676    //     self.stylo_element_data
677    //         .get_mut()
678    //         .map(|mut data: ElementDataMut<'a>| &'a mut data.damage)
679    // }
680
681    pub fn damage(&self) -> Option<RestyleDamage> {
682        self.try_stylo_element_data()
683            .and_then(|stylo| stylo.get().map(|data| data.damage))
684    }
685
686    pub fn set_damage(&mut self, damage: RestyleDamage) {
687        if let Some(stylo) = self.try_stylo_element_data_mut() {
688            if let Some(mut data) = stylo.get_mut() {
689                data.damage = damage;
690            }
691        }
692    }
693
694    pub fn insert_damage(&mut self, damage: RestyleDamage) {
695        if let Some(stylo) = self.try_stylo_element_data_mut() {
696            if let Some(mut data) = stylo.get_mut() {
697                data.damage |= damage;
698            }
699        }
700        if !damage.is_empty() {
701            self.mark_damaged();
702        }
703    }
704
705    pub fn remove_damage(&mut self, damage: RestyleDamage) {
706        if let Some(stylo) = self.try_stylo_element_data_mut() {
707            if let Some(mut data) = stylo.get_mut() {
708                data.damage.remove(damage);
709            }
710        }
711    }
712
713    pub fn clear_damage_mut(&mut self) {
714        if let Some(stylo) = self.try_stylo_element_data_mut() {
715            if let Some(mut data) = stylo.get_mut() {
716                data.damage = RestyleDamage::empty();
717            }
718        }
719    }
720
721    // State changes (hover/focus/active/disabled) do not set a restyle hint.
722    // Invalidation is driven by element snapshots: the style traversal diffs the
723    // snapshotted (pre-change) state against the current state and invalidates
724    // only the elements matched by selectors that depend on the changed state
725    // bits. Ancestors are marked dirty so the traversal reaches this node.
726    pub fn hover(&mut self) {
727        if let Some(data) = self.element_data_mut() {
728            data.element_state.insert(ElementState::HOVER);
729        }
730        self.mark_ancestors_dirty();
731    }
732
733    pub fn unhover(&mut self) {
734        if let Some(data) = self.element_data_mut() {
735            data.element_state.remove(ElementState::HOVER);
736        }
737        self.mark_ancestors_dirty();
738    }
739
740    pub fn is_hovered(&self) -> bool {
741        self.element_data()
742            .is_some_and(|data| data.element_state.contains(ElementState::HOVER))
743    }
744
745    /// Mark this node as containing focus (`:focus-within`). Set on the
746    /// focused element and all of its DOM ancestors.
747    pub fn add_focus_within(&mut self) {
748        if let Some(data) = self.element_data_mut() {
749            data.element_state.insert(ElementState::FOCUS_WITHIN);
750        }
751        self.mark_ancestors_dirty();
752    }
753
754    /// Mark this node as no longer containing focus (`:focus-within`).
755    pub fn remove_focus_within(&mut self) {
756        if let Some(data) = self.element_data_mut() {
757            data.element_state.remove(ElementState::FOCUS_WITHIN);
758        }
759        self.mark_ancestors_dirty();
760    }
761
762    /// Whether this node is the focused node or one of its DOM ancestors (`:focus-within`).
763    pub fn contains_focus(&self) -> bool {
764        self.element_data()
765            .is_some_and(|data| data.element_state.contains(ElementState::FOCUS_WITHIN))
766    }
767
768    pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
769        if let Some(data) = self.element_data_mut() {
770            data.element_state
771                .insert(ElementState::FOCUS | ElementState::FOCUSRING);
772        }
773        self.mark_ancestors_dirty();
774
775        // If focussing a text input, enable IME and set IME area
776        if self
777            .element_data()
778            .and_then(|elem| elem.text_input_data())
779            .is_some()
780        {
781            let password = self
782                .element_data()
783                .and_then(|element| element.text_input_data())
784                .is_some_and(|input| input.is_password());
785            shell_provider.set_ime_purpose(if password {
786                apace_traits::shell::ImePurpose::Password
787            } else {
788                apace_traits::shell::ImePurpose::Normal
789            });
790            shell_provider.set_ime_enabled(true);
791            if let Some(input) = self.element_data().and_then(|e| e.text_input_data()) {
792                input.sync_ime_context(shell_provider.as_ref());
793            }
794            if let Some((x, y, width, height)) = self.text_input_ime_cursor_area() {
795                shell_provider.set_ime_cursor_area(x, y, width, height);
796            }
797        }
798    }
799
800    pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
801        if let Some(data) = self.element_data_mut() {
802            data.element_state
803                .remove(ElementState::FOCUS | ElementState::FOCUSRING);
804        }
805        self.mark_ancestors_dirty();
806
807        // If blurring a text input, disable IME
808        if self
809            .element_data()
810            .and_then(|elem| elem.text_input_data())
811            .is_some()
812        {
813            shell_provider.set_ime_enabled(false);
814        }
815    }
816
817    pub fn is_focussed(&self) -> bool {
818        self.element_data()
819            .is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
820    }
821
822    pub fn active(&mut self) {
823        if let Some(data) = self.element_data_mut() {
824            data.element_state.insert(ElementState::ACTIVE);
825        }
826        self.mark_ancestors_dirty();
827    }
828
829    pub fn unactive(&mut self) {
830        if let Some(data) = self.element_data_mut() {
831            data.element_state.remove(ElementState::ACTIVE);
832        }
833        self.mark_ancestors_dirty();
834    }
835
836    pub fn is_active(&self) -> bool {
837        self.element_data()
838            .is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
839    }
840
841    // Marks the node as disabled if it can be.
842    // It does not disable any children which should be disabled as well (relevant for the `select` element).
843    pub fn disable(&mut self) {
844        if let Some(data) = self.element_data_mut() {
845            if data.can_be_disabled() {
846                data.element_state.insert(ElementState::DISABLED);
847                data.element_state.remove(ElementState::ENABLED);
848            }
849        }
850        self.mark_ancestors_dirty();
851    }
852
853    // Marks the node as enabled if it can be.
854    // It does not enable any children which should be enabled as well (relevant for the `select` element).
855    pub fn enable(&mut self) {
856        if let Some(data) = self.element_data_mut() {
857            if data.can_be_disabled() {
858                data.element_state.insert(ElementState::ENABLED);
859                data.element_state.remove(ElementState::DISABLED);
860            }
861        }
862        self.mark_ancestors_dirty();
863    }
864
865    pub fn subdoc(&self) -> Option<&dyn Document> {
866        self.element_data().and_then(|el| el.sub_doc_data())
867    }
868
869    pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
870        self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
871    }
872
873    pub(crate) fn text_input_ime_cursor_area(&self) -> Option<(f32, f32, f32, f32)> {
874        let input = self.element_data()?.text_input_data()?;
875        let scale = f64::from(input.display_editor().try_layout()?.scale());
876        let rect = input.ime_cursor_area();
877        let pos = self.absolute_position(0.0, 0.0);
878        let x = pos.x + self.final_layout().content_box_x() + (rect.x0 / scale) as f32
879            - if input.is_multiline {
880                0.0
881            } else {
882                input.scroll_offset
883            };
884        let y = pos.y
885            + self.final_layout().content_box_y()
886            + self.text_input_v_centering_offset(scale) as f32
887            + (rect.y0 / scale) as f32
888            - if input.is_multiline {
889                input.scroll_offset
890            } else {
891                0.0
892            };
893        Some((
894            x,
895            y,
896            ((rect.x1 - rect.x0) / scale) as f32,
897            ((rect.y1 - rect.y0) / scale) as f32,
898        ))
899    }
900
901    pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
902        // For single-line inputs, add an offset to vertically center the text input layout
903        // within the content box of it's node.
904        if let Some(input_data) = self
905            .data
906            .downcast_element()
907            .and_then(|el| el.text_input_data())
908        {
909            if !input_data.is_multiline {
910                let content_box_height = self.final_layout().content_box_height();
911                let input_height =
912                    input_data.display_editor().try_layout().unwrap().height() / scale as f32;
913                let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
914
915                return y_offset as f64;
916            }
917        }
918
919        0.0
920    }
921}
922
923#[derive(Debug, Clone, Copy, PartialEq)]
924pub enum NodeKind {
925    Document,
926    Element,
927    AnonymousBlock,
928    Text,
929    Comment,
930}
931
932/// The different kinds of nodes in the DOM.
933#[derive(Debug, Clone)]
934pub enum NodeData {
935    /// The `Document` itself - the root node of a HTML document.
936    Document(Box<DocumentData>),
937
938    /// An element with attributes.
939    Element(Box<ElementData>),
940
941    /// An anonymous block box
942    AnonymousBlock(Box<ElementData>),
943
944    /// A text node.
945    Text(TextNodeData),
946
947    /// A comment.
948    Comment {
949        /// The textual content of the comment
950        contents: String,
951    },
952    // /// A `DOCTYPE` with name, public id, and system id. See
953    // /// [document type declaration on wikipedia][https://en.wikipedia.org/wiki/Document_type_declaration]
954    // Doctype { name: String, public_id: String, system_id: String },
955
956    // /// A Processing instruction.
957    // ProcessingInstruction { target: String, contents: String },
958}
959
960impl NodeData {
961    pub fn downcast_element(&self) -> Option<&ElementData> {
962        match self {
963            Self::Element(data) => Some(data),
964            Self::AnonymousBlock(data) => Some(data),
965            _ => None,
966        }
967    }
968
969    pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
970        match self {
971            Self::Element(data) => Some(data),
972            Self::AnonymousBlock(data) => Some(data),
973            _ => None,
974        }
975    }
976
977    pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
978        let Some(elem) = self.downcast_element() else {
979            return false;
980        };
981        *name == elem.name.local
982    }
983
984    pub fn attrs(&self) -> Option<&[Attribute]> {
985        Some(&self.downcast_element()?.attrs)
986    }
987
988    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
989        self.downcast_element()?.attr(name)
990    }
991
992    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
993        self.downcast_element()
994            .is_some_and(|elem| elem.has_attr(name))
995    }
996
997    pub fn kind(&self) -> NodeKind {
998        match self {
999            NodeData::Document(_) => NodeKind::Document,
1000            NodeData::Element(_) => NodeKind::Element,
1001            NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
1002            NodeData::Text(_) => NodeKind::Text,
1003            NodeData::Comment { .. } => NodeKind::Comment,
1004        }
1005    }
1006}
1007
1008#[derive(Debug, Clone)]
1009pub struct TextNodeData {
1010    /// The textual content of the text node
1011    pub content: String,
1012}
1013
1014impl TextNodeData {
1015    pub fn new(content: String) -> Self {
1016        Self { content }
1017    }
1018}
1019
1020/*
1021-> Computed styles
1022-> Layout
1023-----> Needs to happen only when styles are computed
1024*/
1025
1026// type DomRefCell<T> = RefCell<T>;
1027
1028// pub struct DomData {
1029//     // ... we can probs just get away with using the html5ever types directly. basically just using the servo dom, but without the bindings
1030//     local_name: html5ever::LocalName,
1031//     tag_name: html5ever::QualName,
1032//     namespace: html5ever::Namespace,
1033//     prefix: DomRefCell<Option<html5ever::Prefix>>,
1034//     attrs: DomRefCell<Vec<Attr>>,
1035//     // attrs: DomRefCell<Vec<Dom<Attr>>>,
1036//     id_attribute: DomRefCell<Option<Atom>>,
1037//     is: DomRefCell<Option<LocalName>>,
1038//     // style_attribute: DomRefCell<Option<Arc<Locked<PropertyDeclarationBlock>>>>,
1039//     // attr_list: MutNullableDom<NamedNodeMap>,
1040//     // class_list: MutNullableDom<DOMTokenList>,
1041//     state: Cell<ElementState>,
1042// }
1043
1044impl Node {
1045    pub fn tree(&self) -> &crate::NodeTree {
1046        unsafe { &*self.tree }
1047    }
1048
1049    #[track_caller]
1050    pub fn with(&self, id: NodeId) -> &Node {
1051        self.tree().get(id).unwrap()
1052    }
1053
1054    pub fn print_tree(&self, level: usize) {
1055        println!(
1056            "{} {} {:?} {} {:?}",
1057            "  ".repeat(level),
1058            self.id,
1059            self.parent,
1060            self.node_debug_str().replace('\n', ""),
1061            self.children
1062        );
1063        // println!("{} {:?}", "  ".repeat(level), self.children);
1064        for child_id in self.children.iter() {
1065            let child = self.with(*child_id);
1066            child.print_tree(level + 1)
1067        }
1068    }
1069
1070    // Get the index of the current node in the parents child list
1071    pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
1072        self.children.iter().position(|id| *id == child_id)
1073    }
1074
1075    // Get the index of the current node in the parents child list
1076    pub fn child_index(&self) -> Option<usize> {
1077        self.tree()[self.parent?]
1078            .children
1079            .iter()
1080            .position(|id| *id == self.id)
1081    }
1082
1083    // Get the nth node in the parents child list
1084    pub fn forward(&self, n: usize) -> Option<&Node> {
1085        let child_idx = self.child_index().unwrap_or(0);
1086        self.tree()[self.parent?]
1087            .children
1088            .get(child_idx + n)
1089            .map(|id| self.with(*id))
1090    }
1091
1092    pub fn backward(&self, n: usize) -> Option<&Node> {
1093        let child_idx = self.child_index().unwrap_or(0);
1094        if child_idx < n {
1095            return None;
1096        }
1097
1098        self.tree()[self.parent?]
1099            .children
1100            .get(child_idx - n)
1101            .map(|id| self.with(*id))
1102    }
1103
1104    pub fn is_element(&self) -> bool {
1105        matches!(self.data, NodeData::Element { .. })
1106    }
1107
1108    pub fn is_anonymous(&self) -> bool {
1109        matches!(self.data, NodeData::AnonymousBlock { .. })
1110    }
1111
1112    pub fn is_text_node(&self) -> bool {
1113        matches!(self.data, NodeData::Text { .. })
1114    }
1115
1116    pub fn element_data(&self) -> Option<&ElementData> {
1117        match self.data {
1118            NodeData::Element(ref data) => Some(data),
1119            NodeData::AnonymousBlock(ref data) => Some(data),
1120            _ => None,
1121        }
1122    }
1123
1124    pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1125        match self.data {
1126            NodeData::Element(ref mut data) => Some(data),
1127            NodeData::AnonymousBlock(ref mut data) => Some(data),
1128            _ => None,
1129        }
1130    }
1131
1132    pub fn text_data(&self) -> Option<&TextNodeData> {
1133        match self.data {
1134            NodeData::Text(ref data) => Some(data),
1135            _ => None,
1136        }
1137    }
1138
1139    pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1140        match self.data {
1141            NodeData::Text(ref mut data) => Some(data),
1142            _ => None,
1143        }
1144    }
1145
1146    pub fn node_debug_str(&self) -> String {
1147        let mut s = String::new();
1148
1149        match &self.data {
1150            NodeData::Document(_) => write!(s, "DOCUMENT"),
1151            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1152            NodeData::Text(data) => {
1153                let bytes = data.content.as_bytes();
1154                write!(
1155                    s,
1156                    "TEXT {}",
1157                    std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1158                        .unwrap_or("INVALID UTF8")
1159                )
1160            }
1161            NodeData::Comment { .. } => write!(s, "COMMENT"),
1162            NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1163            NodeData::Element(data) => {
1164                let name = &data.name;
1165                let class = self.attr(local_name!("class")).unwrap_or("");
1166                let id = self.attr(local_name!("id")).unwrap_or("");
1167                let display = self.display_constructed_as().to_css_string();
1168                write!(s, "<{}", name.local).unwrap();
1169                if !id.is_empty() {
1170                    write!(s, " #{id}").unwrap();
1171                }
1172                if !class.is_empty() {
1173                    if class.contains(' ') {
1174                        write!(s, " class=\"{class}\"").unwrap()
1175                    } else {
1176                        write!(s, " .{class}").unwrap()
1177                    }
1178                }
1179                write!(s, "> ({display})")
1180            } // NodeData::ProcessingInstruction { .. } => write!(s, "ProcessingInstruction"),
1181        }
1182        .unwrap();
1183        s
1184    }
1185
1186    pub fn attrs(&self) -> Option<&[Attribute]> {
1187        Some(&self.element_data()?.attrs)
1188    }
1189
1190    pub fn attr(&self, name: LocalName) -> Option<&str> {
1191        let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
1192        Some(&attr.value)
1193    }
1194
1195    pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1196        self.try_stylo_element_data()
1197            .and_then(|stylo| stylo.primary_styles())
1198    }
1199
1200    /// A lazy Takia style backed by this node's primary stylo style.
1201    ///
1202    /// Panics if the node has no computed styles: layout always runs after
1203    /// styling, and the only unstyled nodes (text, comments, descendants of
1204    /// `display: none`) are never queried by Takia.
1205    pub fn layout_style(&self) -> stylo_takia::TakiaStyloStyle<ComputedStyleRef<'_>> {
1206        let styles = self
1207            .try_stylo_element_data()
1208            .and_then(|stylo| stylo.computed_styles())
1209            .expect("layout_style() called on a node without computed styles");
1210
1211        let mut flags = stylo_takia::StyleFlags::empty();
1212        if let Some(el) = self.data.downcast_element() {
1213            if crate::layout::replaced::is_replaced_element(&el.name.local) {
1214                flags |= stylo_takia::StyleFlags::IS_REPLACED;
1215            }
1216            if el.name.local == local_name!("button") {
1217                flags |= stylo_takia::StyleFlags::IS_BUTTON;
1218            }
1219            if el.is_button_input() {
1220                flags |= stylo_takia::StyleFlags::IS_BUTTON_INPUT;
1221            }
1222            if let Some(inline_layout) = el.inline_layout_data.as_deref() {
1223                flags |= inline_layout.span_cb_flags;
1224            }
1225        }
1226
1227        #[cfg(feature = "writing-mode")]
1228        {
1229            // Own-layout_wm by default: the node's own algorithm reads its style in its own axes
1230            let layout_wm = styles.writing_mode;
1231            stylo_takia::TakiaStyloStyle::new_in(styles, flags, layout_wm)
1232        }
1233        #[cfg(not(feature = "writing-mode"))]
1234        {
1235            stylo_takia::TakiaStyloStyle::new(styles, flags)
1236        }
1237    }
1238
1239    /// The node's style for Takia, expressed in the axes of the writing mode `layout_wm` (the writing
1240    /// mode of the box laying this node out). Used for the child-style getters.
1241    #[cfg(feature = "writing-mode")]
1242    pub fn layout_style_in(
1243        &self,
1244        layout_wm: stylo_takia::WritingMode,
1245    ) -> stylo_takia::TakiaStyloStyle<ComputedStyleRef<'_>> {
1246        let mut style = self.layout_style();
1247        style.layout_wm = layout_wm;
1248        style
1249    }
1250
1251    /// Whether the node has any `contain` value, which blocks `writing-mode` propagation from
1252    /// `<body>` to the root (css-writing-modes-3 §8).
1253    #[cfg(feature = "writing-mode")]
1254    pub(crate) fn has_containment(&self) -> bool {
1255        self.primary_styles()
1256            .is_some_and(|style| !style.clone_contain().is_empty())
1257    }
1258
1259    /// Whether the node is a flex container with a column `flex-direction`, i.e. aligns its
1260    /// children's `align-self` in its inline axis.
1261    #[cfg(feature = "writing-mode")]
1262    pub(crate) fn is_column_flex_container(&self) -> bool {
1263        self.primary_styles().is_some_and(|s| {
1264            s.clone_display().inside() == DisplayInside::Flex
1265                && !self.element_data().is_some_and(|el| el.is_button_input())
1266                && matches!(
1267                    s.get_position().flex_direction,
1268                    FlexDirection::Column | FlexDirection::ColumnReverse
1269                )
1270        })
1271    }
1272
1273    /// The node's computed `writing-mode` (horizontal-tb for nodes without styles)
1274    #[cfg(feature = "writing-mode")]
1275    pub fn writing_mode(&self) -> stylo_takia::WritingMode {
1276        self.primary_styles()
1277            .map(|s| s.writing_mode)
1278            .unwrap_or(stylo_takia::WritingMode::empty())
1279    }
1280
1281    /// The node's `display` as a [`takia::Display`]. Returns [`takia::Display::Block`]
1282    /// for nodes without computed styles (e.g. text nodes).
1283    pub fn takia_display(&self) -> takia::Display {
1284        self.primary_styles()
1285            .map(|style| {
1286                let display = style.clone_display();
1287                if self.element_data().is_some_and(|el| el.is_button_input())
1288                    && !matches!(
1289                        display.inside(),
1290                        DisplayInside::None | DisplayInside::Contents
1291                    )
1292                {
1293                    takia::Display::FlowRoot
1294                } else {
1295                    stylo_takia::convert::display(display)
1296                }
1297            })
1298            .unwrap_or(takia::Display::Block)
1299    }
1300
1301    /// The node's `position` as a [`takia::Position`]. Returns [`takia::Position::Static`]
1302    /// for nodes without computed styles (e.g. text nodes).
1303    pub fn takia_position(&self) -> takia::Position {
1304        self.primary_styles()
1305            .map(|s| stylo_takia::convert::position(s.get_box().position))
1306            .unwrap_or(takia::Position::Static)
1307    }
1308
1309    /// Whether the node is an out-of-flow box that Takia positions from its containing
1310    /// block's hoisted child list rather than from its parent (`display: none` boxes
1311    /// generate no box and are never hoisted).
1312    pub fn is_out_of_flow(&self) -> bool {
1313        self.primary_styles()
1314            .is_some_and(|s| style_is_out_of_flow(&s))
1315    }
1316
1317    /// Record in [`Node::out_of_flow`] whether `style` (the node's new primary style)
1318    /// makes its box out of flow.
1319    pub(crate) fn record_out_of_flow(&self, style: &ComputedValues) {
1320        self.out_of_flow
1321            .store(style_is_out_of_flow(style), Ordering::Relaxed);
1322    }
1323
1324    pub fn text_content(&self) -> String {
1325        let mut out = String::new();
1326        self.write_text_content(&mut out);
1327        out
1328    }
1329
1330    fn write_text_content(&self, out: &mut String) {
1331        match &self.data {
1332            NodeData::Text(data) => {
1333                out.push_str(&data.content);
1334            }
1335            NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
1336                for child_id in self.children.iter() {
1337                    self.with(*child_id).write_text_content(out);
1338                }
1339            }
1340            _ => {}
1341        }
1342    }
1343
1344    pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1345        if let NodeData::Element(ref mut elem_data) = self.data {
1346            if let Some(guard) = elem_data.guard.clone() {
1347                elem_data.flush_style_attribute(&guard, url_extra_data);
1348            }
1349        }
1350    }
1351
1352    /// The `order` sort key of this node within a flex/grid container's
1353    /// `layout_children`. The list is stable-sorted by this key when the
1354    /// container's children are constructed (`collect_layout_children`) and
1355    /// re-collected when a child's `order` changes (`REORDER_CHILDREN` damage).
1356    ///
1357    /// ::before/::after pseudos are flex/grid items and honor `order`. They
1358    /// sit first/last in the constructed list, and the sort is stable, so
1359    /// ties keep ::before first and ::after last. Out-of-flow children are
1360    /// not flex/grid items, so they key as 0 and keep source order.
1361    pub fn order(&self) -> i32 {
1362        self.primary_styles()
1363            .filter(|s| !matches!(s.get_box().position, Position::Absolute | Position::Fixed))
1364            .map(|s| s.clone_order())
1365            .unwrap_or(0)
1366    }
1367
1368    pub fn z_index(&self) -> i32 {
1369        self.primary_styles()
1370            .map(|s| s.clone_z_index().integer_or(0))
1371            .unwrap_or(0)
1372    }
1373
1374    // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts
1375    pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1376        let Some(style) = self.primary_styles() else {
1377            return false;
1378        };
1379
1380        let position = style.clone_position();
1381        let has_z_index = !style.clone_z_index().is_auto();
1382
1383        if style.clone_opacity() != 1.0 {
1384            return true;
1385        }
1386
1387        let position_based = match position {
1388            Position::Fixed | Position::Sticky => true,
1389            Position::Relative | Position::Absolute => has_z_index,
1390            Position::Static => has_z_index && is_flex_or_grid_item,
1391        };
1392        if position_based {
1393            return true;
1394        }
1395
1396        let box_style = style.get_box();
1397        if !box_style.transform.0.is_empty()
1398            || !matches!(box_style.rotate, Rotate::None)
1399            || !matches!(box_style.scale, Scale::None)
1400            || !matches!(box_style.translate, Translate::None)
1401        {
1402            return true;
1403        }
1404
1405        if self.applies_atomic_paint_effect() {
1406            return true;
1407        }
1408
1409        if style.get_box().isolation == Isolation::Isolate {
1410            return true;
1411        }
1412
1413        if style.get_effects().mix_blend_mode != style::computed_values::mix_blend_mode::T::Normal {
1414            return true;
1415        }
1416        // TODO: contain
1417
1418        false
1419    }
1420
1421    /// Whether this node's styles apply an atomic paint effect (opacity, filter,
1422    /// clip-path, mask) to its subtree.
1423    pub(crate) fn applies_atomic_paint_effect(&self) -> bool {
1424        use style::values::computed::basic_shape::ClipPath;
1425        use style::values::generics::image::GenericImage;
1426
1427        let Some(style) = self.primary_styles() else {
1428            return false;
1429        };
1430
1431        if style.clone_opacity() != 1.0 {
1432            return true;
1433        }
1434
1435        let effects = style.get_effects();
1436        if !effects.filter.0.is_empty() {
1437            return true;
1438        }
1439
1440        if !matches!(style.get_clip_path(), ClipPath::None) {
1441            return true;
1442        }
1443
1444        style
1445            .get_svg()
1446            .mask_image
1447            .0
1448            .iter()
1449            .any(|image| !matches!(image, GenericImage::None))
1450    }
1451
1452    /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns:
1453    ///    - None if the position is outside of this node's bounds
1454    ///    - Some(HitResult) if the position is within the node but doesn't match any children
1455    ///    - The result of recursively calling child.hit() on the the child element that is
1456    ///      positioned at that position if there is one.
1457    ///
1458    /// Hit testing may run between a DOM mutation and the next `resolve`, so the
1459    /// paint lists and inline layouts it walks can still name nodes the mutation
1460    /// dropped; those are skipped.
1461    ///
1462    /// TODO: z-index
1463    /// (If multiple children are positioned at the position then a random one will be recursed into)
1464    pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
1465        self.hit_inner(x, y, scale, &mut None)
1466    }
1467
1468    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
1469    /// thumb under the point into `scrollbar` during the same descent (so
1470    /// thumb hit-testing shares the exact coordinate handling — transforms
1471    /// included — of every other hit test).
1472    pub(crate) fn hit_inner(
1473        &self,
1474        x: f32,
1475        y: f32,
1476        scale: f64,
1477        scrollbar: &mut Option<crate::node::ScrollbarRef>,
1478    ) -> Option<HitResult> {
1479        use style::computed_values::pointer_events::T as PointerEvents;
1480        use style::computed_values::visibility::T as Visibility;
1481
1482        // Don't hit on visbility:hidden elements
1483        if let Some(style) = self.primary_styles() {
1484            if matches!(
1485                style.clone_visibility(),
1486                Visibility::Hidden | Visibility::Collapse
1487            ) {
1488                return None;
1489            }
1490        }
1491
1492        // pointer-events:none makes this element transparent to hits, but its
1493        // descendants are still tested (one may restore pointer-events:auto).
1494        let pointer_events_none = self
1495            .primary_styles()
1496            .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1497
1498        let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
1499        let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
1500
1501        if let Some(t) = self.transform().as_deref() {
1502            let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
1503            x = (p.x / scale) as f32;
1504            y = (p.y / scale) as f32;
1505        }
1506
1507        let size = self.final_layout().size;
1508        let matches_self = !(x < self.scroll_offset().x as f32
1509            || x > size.width + self.scroll_offset().x as f32
1510            || y < self.scroll_offset().y as f32
1511            || y > size.height + self.scroll_offset().y as f32);
1512
1513        let layout = self.final_layout();
1514        let overflow_rect = layout.scrollable_overflow_rect;
1515        // Not `layout_style()`: hit testing also reaches elements that have no computed styles
1516        // yet (a pointer event before the first resolve). Those count as left-to-right.
1517        let is_rtl = self.primary_styles().is_some_and(|style| {
1518            style.clone_direction() == style::computed_values::direction::T::Rtl
1519        });
1520        let (gutter_left, gutter_top) = (is_rtl, false);
1521        #[cfg(feature = "writing-mode")]
1522        let (gutter_left, gutter_top) = {
1523            let wm = self.layout_data().writing_mode;
1524            if wm.swaps_axes() {
1525                (wm.block_left_is_end(), wm.direction().is_rtl())
1526            } else {
1527                (gutter_left, gutter_top)
1528            }
1529        };
1530        let client_x = layout.border.left
1531            + if gutter_left {
1532                layout.scrollbar_size.width
1533            } else {
1534                0.0
1535            };
1536        let client_y = layout.border.top
1537            + if gutter_top {
1538                layout.scrollbar_size.height
1539            } else {
1540                0.0
1541            };
1542        let matches_content = !(x < overflow_rect.left() + client_x
1543            || x > overflow_rect.right() + client_x
1544            || y < overflow_rect.top() + client_y
1545            || y > overflow_rect.bottom() + client_y);
1546
1547        let matches_hoisted_content = match &self.stacking_context {
1548            Some(sc) => {
1549                // Both the point (after the scroll adjustment above) and the
1550                // bbox are in this node's unscrolled content coordinates.
1551                let bbox = sc.hoisted_content_bbox(self.tree(), self.id, scale);
1552                x >= bbox.left && x <= bbox.right && y >= bbox.top && y <= bbox.bottom
1553            }
1554            None => false,
1555        };
1556
1557        // `scrollable_overflow` is stored in device (scaled) pixels, whereas the
1558        // coordinates here are in CSS pixels, so unscale it before comparing.
1559        let overflow = *self.scrollable_overflow();
1560
1561        let matches_overflow = x >= (overflow.x0 / scale) as f32
1562            && x <= (overflow.x1 / scale) as f32
1563            && y >= (overflow.y0 / scale) as f32
1564            && y <= (overflow.y1 / scale) as f32;
1565
1566        if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
1567            return None;
1568        }
1569
1570        // Descendants overwrite, so the innermost scroll container's thumb
1571        // wins. Thumb coords are border-box relative (unscrolled).
1572        if matches_self
1573            && let Some(sb) = self.scrollbar_at_local(
1574                (x - self.scroll_offset().x as f32) as f64,
1575                (y - self.scroll_offset().y as f32) as f64,
1576            )
1577        {
1578            *scrollbar = Some(sb);
1579        }
1580
1581        let content_box_offset = takia::Point {
1582            x: self.final_layout().padding.left + self.final_layout().border.left,
1583            y: self.final_layout().padding.top + self.final_layout().border.top,
1584        };
1585        if self.flags.is_inline_root() {
1586            x -= content_box_offset.x;
1587            y -= content_box_offset.y;
1588        }
1589
1590        // Positive z_index hoisted children
1591        if matches_hoisted_content {
1592            if let Some(hoisted) = &self.stacking_context {
1593                for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
1594                    let Some(child) = self.tree().get(hoisted_child.node_id) else {
1595                        continue;
1596                    };
1597                    let pos = hoisted_child.position(self.tree(), self.id);
1598                    let x = x - pos.x;
1599                    let y = y - pos.y;
1600                    if let Some(hit) = child.hit_inner(x, y, scale, scrollbar) {
1601                        return Some(hit);
1602                    }
1603                }
1604            }
1605        }
1606
1607        // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id).
1608        for child_id in self.paint_children.borrow().iter().flatten().rev() {
1609            let Some(child) = self.tree().get(*child_id) else {
1610                continue;
1611            };
1612            let child_position = child.takia_position();
1613            let mut child_x = x;
1614            let mut child_y = y;
1615            if child_position.is_out_of_flow() {
1616                // Out-of-flow children's layout location is relative to this node's
1617                // border box, so undo the inline-root content-box offset applied above
1618                if self.flags.is_inline_root() {
1619                    child_x += content_box_offset.x;
1620                    child_y += content_box_offset.y;
1621                }
1622                // Fixed-position children of the root element are positioned
1623                // against the viewport and do not scroll with it. (A fixed box
1624                // whose containing block is a transformed ancestor scrolls with
1625                // that ancestor like any other out-of-flow box.)
1626                if child_position == takia::Position::Fixed
1627                    && self.containing_block().is_none()
1628                    && !self.flags.is_top_layer()
1629                {
1630                    child_x -= self.scroll_offset().x as f32;
1631                    child_y -= self.scroll_offset().y as f32;
1632                    let viewport_scroll = self.tree().viewport_scroll();
1633                    child_x -= viewport_scroll.x as f32;
1634                    child_y -= viewport_scroll.y as f32;
1635                }
1636            }
1637            if let Some(hit) = child.hit_inner(child_x, child_y, scale, scrollbar) {
1638                return Some(hit);
1639            }
1640        }
1641
1642        // Negative z_index hoisted children
1643        if matches_hoisted_content {
1644            if let Some(hoisted) = &self.stacking_context {
1645                for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
1646                    let Some(child) = self.tree().get(hoisted_child.node_id) else {
1647                        continue;
1648                    };
1649                    let pos = hoisted_child.position(self.tree(), self.id);
1650                    let x = x - pos.x;
1651                    let y = y - pos.y;
1652                    if let Some(hit) = child.hit_inner(x, y, scale, scrollbar) {
1653                        return Some(hit);
1654                    }
1655                }
1656            }
1657        }
1658
1659        // Inline children
1660        if self.flags.is_inline_root() {
1661            let element_data = &self.element_data().unwrap();
1662            if let Some(ild) = element_data.inline_layout_data.as_ref() {
1663                let layout = &ild.layout;
1664                let scale = layout.scale();
1665                let y = y - ild.block_offset;
1666
1667                if let Some((cluster, _side)) =
1668                    Cluster::from_point_exact(layout, x * scale, y * scale)
1669                {
1670                    let node_id = cluster.style().brush.id;
1671                    if let Some(text_node) = self.tree().get(node_id)
1672                        && !text_node.primary_styles().is_some_and(|style| {
1673                            style.clone_pointer_events() == PointerEvents::None
1674                        })
1675                    {
1676                        return Some(HitResult {
1677                            node_id,
1678                            x,
1679                            y,
1680                            is_text: true,
1681                        });
1682                    }
1683                }
1684            }
1685        }
1686
1687        // Self (this node)
1688        if matches_self && !pointer_events_none {
1689            return Some(HitResult {
1690                node_id: self.id,
1691                x,
1692                y,
1693                is_text: false,
1694            });
1695        }
1696
1697        None
1698    }
1699
1700    /// Find the inline root ancestor of this node (or self if this is an inline root).
1701    /// Returns None if no inline root ancestor exists.
1702    pub fn inline_root_ancestor(&self) -> Option<&Node> {
1703        let mut node = self;
1704        loop {
1705            if node.flags.is_inline_root() {
1706                return Some(node);
1707            }
1708            let id = node.layout_parent.get()?;
1709            node = self.with(id);
1710        }
1711    }
1712
1713    /// Get the text byte offset at a given point, using coordinates already transformed
1714    /// to be relative to this inline root's content box.
1715    /// Returns Some(byte_offset) if the point hits text, None otherwise.
1716    pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
1717        if !self.flags.is_inline_root() {
1718            return None;
1719        }
1720
1721        let element_data = self.element_data()?;
1722        let inline_layout = element_data.inline_layout_data.as_ref()?;
1723        let layout = &inline_layout.layout;
1724        let scale = layout.scale();
1725        let y = y - inline_layout.block_offset;
1726
1727        // Use Parley's cluster hit testing (from_point is more forgiving than from_point_exact)
1728        let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
1729
1730        // Determine byte offset based on which side of the cluster was clicked
1731        // For LTR text: left side = start of cluster, right side = end of cluster
1732        // For RTL text: left side = end of cluster, right side = start of cluster
1733        // Also, explicit line breaks should always use start to avoid cursor appearing on next line
1734        let is_leading = side == ClusterSide::Left;
1735        let offset = if cluster.is_rtl() {
1736            if is_leading {
1737                cluster.text_range().end
1738            } else {
1739                cluster.text_range().start
1740            }
1741        } else {
1742            // LTR text
1743            if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
1744                cluster.text_range().start
1745            } else {
1746                cluster.text_range().end
1747            }
1748        };
1749
1750        Some(offset)
1751    }
1752
1753    /// Whether this node is a non-atomic inline element: one that has no layout box of its
1754    /// own because it is flattened into the containing inline root's text layout as a style
1755    /// span.
1756    pub fn is_non_atomic_inline(&self) -> bool {
1757        let Some(element) = self.element_data() else {
1758            return false;
1759        };
1760        if self.flags.is_inline_root()
1761            || crate::layout::replaced::is_inline_box_element(&element.name.local)
1762        {
1763            return false;
1764        }
1765        self.primary_styles().is_some_and(|styles| {
1766            let display = styles.clone_display();
1767            display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow
1768        })
1769    }
1770
1771    /// The node whose box this node's `Layout.location` is relative to: the
1772    /// `oof_containing_block` for a hoisted out-of-flow box, otherwise the
1773    /// `layout_parent`.
1774    pub fn containing_block(&self) -> Option<NodeId> {
1775        if self.flags.is_top_layer() {
1776            return None;
1777        }
1778        self.oof_containing_block
1779            .get()
1780            .or_else(|| self.layout_parent.get())
1781    }
1782
1783    /// Computes the Document-relative coordinates of the `Node`
1784    pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
1785        let x = x + self.final_layout().location.x - self.scroll_offset().x as f32;
1786        let y = y + self.final_layout().location.y - self.scroll_offset().y as f32;
1787        if self.flags.is_top_layer() {
1788            let scroll = self.tree().viewport_scroll();
1789            return crate::util::Point {
1790                x: x + scroll.x as f32,
1791                y: y + scroll.y as f32,
1792            };
1793        }
1794
1795        // Recurse up the positioning hierarchy
1796        self.containing_block()
1797            .map(|i| self.with(i).absolute_position(x, y))
1798            .unwrap_or(crate::util::Point { x, y })
1799    }
1800
1801    /// Whether this node can act as an [`offset_parent`](Self::offset_parent): a positioned
1802    /// element, or one of the elements that always qualify (`body`, `td`, `th`).
1803    pub(crate) fn is_offset_parent(&self) -> bool {
1804        let Some(styles) = self.primary_styles() else {
1805            return false;
1806        };
1807        if styles.get_box().position != Position::Static {
1808            return true;
1809        }
1810        self.data.is_element_with_tag_name(&local_name!("body"))
1811            || self.data.is_element_with_tag_name(&local_name!("td"))
1812            || self.data.is_element_with_tag_name(&local_name!("th"))
1813    }
1814
1815    /// Whether this node is a non-positioned `body` element. When such an element is the
1816    /// `offsetParent`, `offsetLeft`/`offsetTop` are measured from the initial containing
1817    /// block origin rather than from the `body`'s padding edge.
1818    pub(crate) fn is_static_body(&self) -> bool {
1819        self.data.is_element_with_tag_name(&local_name!("body"))
1820            && self
1821                .primary_styles()
1822                .is_some_and(|styles| styles.get_box().position == Position::Static)
1823    }
1824
1825    /// The nearest layout ancestor that [is an offset parent](Self::is_offset_parent), as in
1826    /// CSSOM View's `offsetParent`.
1827    pub fn offset_parent(&self) -> Option<&Node> {
1828        let mut node = self;
1829        loop {
1830            node = self.with(node.containing_block()?);
1831            if node.is_offset_parent() {
1832                return Some(node);
1833            }
1834        }
1835    }
1836
1837    /// The per-line-box fragment boxes of a non-atomic inline element, in CSS pixels relative
1838    /// to the border box of its inline root.
1839    ///
1840    /// Returns `None` for nodes that have their own layout box.
1841    pub fn inline_fragment_boxes(&self) -> Option<impl Iterator<Item = takia::Rect<f32>> + '_> {
1842        use parley::PositionedLayoutItem;
1843
1844        if !self.is_non_atomic_inline() {
1845            return None;
1846        }
1847
1848        let inline_root = self.inline_root_ancestor()?;
1849        let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
1850        let layout = &inline_layout.layout;
1851        let scale = layout.scale();
1852
1853        // Walk up the DOM parent chain from `id` to check whether it is (or is
1854        // inside) the target node, stopping at the inline root.
1855        let inline_root_id = inline_root.id;
1856        let is_in_target = move |mut id: NodeId| -> bool {
1857            loop {
1858                if id == self.id {
1859                    return true;
1860                }
1861                if id == inline_root_id {
1862                    return false;
1863                }
1864                match self.with(id).parent {
1865                    Some(parent) => id = parent,
1866                    None => return false,
1867                }
1868            }
1869        };
1870
1871        let root_layout = inline_root.unrounded_layout();
1872        let content_box_inset = root_layout.padding + root_layout.border;
1873        let origin_x = content_box_inset.left;
1874        let origin_y = content_box_inset.top + inline_layout.block_offset;
1875
1876        fn union(acc: &mut Option<takia::Rect<f32>>, left: f32, top: f32, right: f32, bottom: f32) {
1877            *acc = Some(match *acc {
1878                Some(rect) => takia::Rect {
1879                    left: rect.left.min(left),
1880                    top: rect.top.min(top),
1881                    right: rect.right.max(right),
1882                    bottom: rect.bottom.max(bottom),
1883                },
1884                None => takia::Rect {
1885                    left,
1886                    top,
1887                    right,
1888                    bottom,
1889                },
1890            });
1891        }
1892
1893        // One rect per line box: the union of all of the target's fragments on that line
1894        Some(layout.lines().filter_map(move |line| {
1895            let line_metrics = line.metrics();
1896            let mut line_rect: Option<takia::Rect<f32>> = None;
1897
1898            for item in line.items() {
1899                match item {
1900                    PositionedLayoutItem::GlyphRun(glyph_run) => {
1901                        if !is_in_target(glyph_run.style().brush.id) {
1902                            continue;
1903                        }
1904                        let x0 = glyph_run.offset();
1905                        let x1 = x0 + glyph_run.advance();
1906                        // Use the line box's block extent rather than the
1907                        // run's font ascent/descent: fonts with small
1908                        // typographic metrics would otherwise produce rects
1909                        // that clip the rendered glyphs. This matches the
1910                        // geometry used for text selection highlights.
1911                        let y0 = line_metrics.block_min_coord;
1912                        let y1 = line_metrics.block_max_coord;
1913                        union(&mut line_rect, x0, y0, x1, y1);
1914                    }
1915                    PositionedLayoutItem::InlineBox(inline_box) => {
1916                        if !is_in_target(NodeId::from_u64(inline_box.id)) {
1917                            continue;
1918                        }
1919                        let x0 = inline_box.x;
1920                        let y0 = inline_box.y;
1921                        union(
1922                            &mut line_rect,
1923                            x0,
1924                            y0,
1925                            x0 + inline_box.width,
1926                            y0 + inline_box.height,
1927                        );
1928                    }
1929                }
1930            }
1931
1932            line_rect.map(|rect| takia::Rect {
1933                left: origin_x + rect.left / scale,
1934                top: origin_y + rect.top / scale,
1935                right: origin_x + rect.right / scale,
1936                bottom: origin_y + rect.bottom / scale,
1937            })
1938        }))
1939    }
1940
1941    /// CSSOM View's `offsetLeft`/`offsetTop`: the offset of this node's border box from the
1942    /// padding edge of its [`offset_parent`](Self::offset_parent).
1943    pub fn offset_top_left(&self) -> crate::util::Point<f32> {
1944        let mut x = 0.0;
1945        let mut y = 0.0;
1946        let mut current = self;
1947
1948        // Non-atomic inlines have no layout box of their own: start from the first
1949        // fragment box (relative to the inline root's border box) and continue the
1950        // walk from the inline root.
1951        if let Some(mut fragments) = self.inline_fragment_boxes() {
1952            if let Some(first) = fragments.next() {
1953                x += first.left;
1954                y += first.top;
1955            }
1956            let Some(inline_root) = self.inline_root_ancestor() else {
1957                return crate::util::Point { x, y };
1958            };
1959            if inline_root.is_offset_parent() && !inline_root.is_static_body() {
1960                let border = inline_root.final_layout().border;
1961                return crate::util::Point {
1962                    x: x - border.left,
1963                    y: y - border.top,
1964                };
1965            }
1966            current = inline_root;
1967        }
1968
1969        loop {
1970            let layout = current.final_layout();
1971            x += layout.location.x;
1972            y += layout.location.y;
1973
1974            let Some(parent_id) = current.containing_block() else {
1975                break;
1976            };
1977            let parent = self.with(parent_id);
1978            if parent.is_offset_parent() && !parent.is_static_body() {
1979                let border = parent.final_layout().border;
1980                x -= border.left;
1981                y -= border.top;
1982                break;
1983            }
1984            current = parent;
1985        }
1986        crate::util::Point { x, y }
1987    }
1988
1989    /// CSSOM View's `clientWidth`: the width of the padding box (border box minus
1990    /// borders and scrollbar)
1991    pub fn client_width(&self) -> f32 {
1992        let layout = self.final_layout();
1993        layout.size.width - layout.border.left - layout.border.right - layout.scrollbar_size.width
1994    }
1995
1996    /// CSSOM View's `clientHeight`: the height of the padding box (border box minus
1997    /// borders and scrollbar)
1998    pub fn client_height(&self) -> f32 {
1999        let layout = self.final_layout();
2000        layout.size.height - layout.border.top - layout.border.bottom - layout.scrollbar_size.height
2001    }
2002
2003    /// CSSOM View's `scrollWidth`: the width of the node's content, including
2004    /// content not visible due to overflow
2005    pub fn scroll_width(&self) -> f32 {
2006        self.cssom_scroll_size().width
2007    }
2008
2009    /// CSSOM View's `scrollHeight`: the height of the node's content, including
2010    /// content not visible due to overflow
2011    pub fn scroll_height(&self) -> f32 {
2012        self.cssom_scroll_size().height
2013    }
2014
2015    fn cssom_scroll_size(&self) -> takia::Size<f32> {
2016        let layout = self.final_layout();
2017        if self.primary_styles().is_none() {
2018            return takia::Size {
2019                width: self.client_width() + layout.scroll_width(),
2020                height: self.client_height() + layout.scroll_height(),
2021            };
2022        }
2023        let overflow = takia::CoreStyle::overflow(&self.layout_style());
2024        if overflow.x.is_scroll_container() || overflow.y.is_scroll_container() {
2025            return takia::Size {
2026                width: self.client_width() + layout.scroll_width(),
2027                height: self.client_height() + layout.scroll_height(),
2028            };
2029        }
2030
2031        // Non-scroll containers retain both sides for propagation and hit testing, but do not
2032        // carry scroll-origin flags. CSSOM still measures their scrolling area from its origin.
2033        let mut inline = false;
2034        let mut block = false;
2035        let mut vertical = false;
2036        if let Some(style) = self.primary_styles() {
2037            let wm = style.writing_mode;
2038            inline = wm.direction().is_rtl();
2039            block = wm.block_left_is_end();
2040            vertical = wm.swaps_axes();
2041            if self.takia_display() == takia::Display::Flex {
2042                let position = style.get_position();
2043                let row = matches!(
2044                    position.flex_direction,
2045                    FlexDirection::Row | FlexDirection::RowReverse
2046                );
2047                let reverse = matches!(
2048                    position.flex_direction,
2049                    FlexDirection::RowReverse | FlexDirection::ColumnReverse
2050                );
2051                let wrap_reverse = position
2052                    .flex_wrap
2053                    .contains(style::values::computed::FlexWrap::WRAP_REVERSE);
2054                inline ^= if row { reverse } else { wrap_reverse };
2055                block ^= if row { wrap_reverse } else { reverse };
2056            }
2057        }
2058        let (reverse_x, reverse_y) = if vertical {
2059            (block, inline)
2060        } else {
2061            (inline, block)
2062        };
2063        let overflow = layout.scrollable_overflow_rect;
2064        takia::Size {
2065            width: if reverse_x {
2066                self.client_width() - overflow.left()
2067            } else {
2068                self.client_width().max(overflow.right())
2069            },
2070            height: if reverse_y {
2071                self.client_height() - overflow.top()
2072            } else {
2073                self.client_height().max(overflow.bottom())
2074            },
2075        }
2076    }
2077
2078    /// Signed CSSOM scroll offsets, with zero at the initial scroll position.
2079    pub(crate) fn scroll_range(&self) -> (crate::Point<f64>, crate::Point<f64>) {
2080        let layout = self.final_layout();
2081        let reversed = layout.scrollable_overflow_rect.is_reversed();
2082        let width = layout.scroll_width() as f64;
2083        let height = layout.scroll_height() as f64;
2084        (
2085            crate::Point {
2086                x: if reversed.x { -width } else { 0.0 },
2087                y: if reversed.y { -height } else { 0.0 },
2088            },
2089            crate::Point {
2090                x: if reversed.x { 0.0 } else { width },
2091                y: if reversed.y { 0.0 } else { height },
2092            },
2093        )
2094    }
2095
2096    /// Does the node generate any boxes? (e.g. `getClientRects()` returns an empty
2097    /// list for boxless nodes, such as `display: none`, `display: contents`, or
2098    /// detached elements)
2099    pub fn has_boxes(&self) -> bool {
2100        self.flags.is_in_document()
2101            && !self.display_style().is_some_and(|display| {
2102                matches!(
2103                    display.inside(),
2104                    style::values::specified::box_::DisplayInside::None
2105                        | style::values::specified::box_::DisplayInside::Contents
2106                )
2107            })
2108    }
2109
2110    /// Creates a synthetic click event
2111    pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
2112        DomEventData::Click(self.synthetic_click_event_data(mods))
2113    }
2114
2115    pub fn synthetic_click_event_data(&self, mods: Modifiers) -> ApacePointerEvent {
2116        let absolute_position = self.absolute_position(0.0, 0.0);
2117        let x = absolute_position.x + (self.final_layout().size.width / 2.0);
2118        let y = absolute_position.y + (self.final_layout().size.height / 2.0);
2119
2120        ApacePointerEvent {
2121            id: ApacePointerId::Mouse,
2122            is_primary: true,
2123            coords: PointerCoords {
2124                page_x: x,
2125                page_y: y,
2126
2127                // TODO: should these be different?
2128                screen_x: x,
2129                screen_y: y,
2130                client_x: x,
2131                client_y: y,
2132            },
2133            mods,
2134            button: Default::default(),
2135            buttons: Default::default(),
2136            details: Default::default(),
2137            element: Default::default(),
2138            active_pointers: Default::default(),
2139        }
2140    }
2141}
2142
2143/// It might be wrong to expose this since what does *equality* mean outside the dom?
2144impl PartialEq for Node {
2145    fn eq(&self, other: &Self) -> bool {
2146        self.id == other.id
2147    }
2148}
2149
2150impl Eq for Node {}
2151
2152impl std::fmt::Debug for Node {
2153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2154        // FIXME: update to reflect changes to fields
2155        f.debug_struct("NodeData")
2156            .field("parent", &self.parent)
2157            .field("id", &self.id)
2158            .field("is_inline_root", &self.flags.is_inline_root())
2159            .field("children", &self.children)
2160            .field("layout_children", &self.layout_children.borrow())
2161            // .field("style", &self.style)
2162            .field("node", &self.data)
2163            .field("stylo_element_data", &self.try_stylo_element_data())
2164            // .field("unrounded_layout", &self.unrounded_layout)
2165            // .field("final_layout", &self.final_layout)
2166            .finish()
2167    }
2168}
2169
2170/// Whether a box with `style` is out of flow: Takia positions it from its containing
2171/// block's hoisted child list rather than from its parent (`display: none` boxes generate
2172/// no box and are never hoisted).
2173pub(crate) fn style_is_out_of_flow(style: &ComputedValues) -> bool {
2174    let box_style = style.get_box();
2175    matches!(box_style.position, Position::Absolute | Position::Fixed)
2176        && !box_style.display.is_none()
2177}
2178
2179#[cfg(test)]
2180mod test {
2181    use style_dom::ElementState;
2182
2183    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
2184
2185    #[test]
2186    fn create_node_with_disabled_attr() {
2187        let mut document = BaseDocument::new(DocumentConfig::default());
2188        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2189            qual_name!("button"),
2190            vec![Attribute {
2191                name: qual_name!("disabled"),
2192                value: "".into(),
2193            }],
2194        ))));
2195        let node = document.get_node(node).unwrap();
2196
2197        assert!(
2198            node.element_state().contains(ElementState::DISABLED),
2199            "form node is disabled"
2200        );
2201        assert!(
2202            !node.element_state().contains(ElementState::ENABLED),
2203            "form node is not enabled"
2204        );
2205    }
2206
2207    #[test]
2208    fn ignore_disabled_attr_content() {
2209        let mut document = BaseDocument::new(DocumentConfig::default());
2210        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2211            qual_name!("button"),
2212            vec![Attribute {
2213                name: qual_name!("disabled"),
2214                value: "false".into(),
2215            }],
2216        ))));
2217        let node = document.get_node(node).unwrap();
2218
2219        assert!(
2220            node.element_state().contains(ElementState::DISABLED),
2221            "form node is disabled"
2222        );
2223        assert!(
2224            !node.element_state().contains(ElementState::ENABLED),
2225            "form node is not enabled"
2226        );
2227    }
2228
2229    #[test]
2230    fn create_node_with_ignored_disable() {
2231        let mut document = BaseDocument::new(DocumentConfig::default());
2232        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2233            qual_name!("a"),
2234            vec![Attribute {
2235                name: qual_name!("disabled"),
2236                value: "".into(),
2237            }],
2238        ))));
2239        let node = document.get_node(node).unwrap();
2240
2241        assert!(
2242            !node.element_state().contains(ElementState::DISABLED),
2243            "Non form node cannot be disabled"
2244        );
2245        assert!(
2246            !node.element_state().contains(ElementState::ENABLED),
2247            "Non form node cannot be enabled"
2248        );
2249    }
2250
2251    #[test]
2252    fn create_empty_enabled_node() {
2253        let mut document = BaseDocument::new(DocumentConfig::default());
2254        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2255            qual_name!("button"),
2256            vec![],
2257        ))));
2258        let node = document.get_node(node).unwrap();
2259
2260        assert!(
2261            node.element_state().contains(ElementState::ENABLED),
2262            "Button should be enabled by default"
2263        );
2264    }
2265}