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