1use crate::Document;
2use crate::layout::damage::HoistedPaintChildren;
3use bitflags::bitflags;
4use blitz_traits::events::{
5 BlitzPointerEvent, BlitzPointerId, DomEventData, HitResult, PointerCoords,
6};
7use blitz_traits::node_id::NodeId;
8use blitz_traits::shell::ShellProvider;
9use euclid::{Point2D, Rect, Size2D};
10use html_escape::encode_quoted_attribute_to_string;
11use keyboard_types::Modifiers;
12use kurbo::{Affine, Rect as KurboRect};
13use markup5ever::{LocalName, local_name};
14use parley::{BreakReason, Cluster, ClusterSide, Selection};
15use selectors::matching::ElementSelectorFlags;
16use std::cell::{Cell, RefCell};
17use std::fmt::Write;
18use std::ops::{Deref, Range};
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, Ordering};
21use style::Atom;
22use style::computed_values::isolation::T as Isolation;
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::{PseudoElement, 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 taffy::{
38 Cache,
39 prelude::{Layout, Style},
40};
41use thin_vec::ThinVec;
42
43use super::stylo_data::StyloData;
44use super::{Attribute, DocumentData, ElementData};
45
46#[derive(Clone, Copy)]
47enum OutputStyle {
48 Normal,
49 Pretty,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum DisplayOuter {
54 Block,
55 Inline,
56 None,
57}
58
59bitflags! {
60 #[derive(Clone, Copy, PartialEq)]
61 pub struct NodeFlags: u32 {
62 const IS_INLINE_ROOT = 0b00000001;
64 const IS_TABLE_ROOT = 0b00000010;
66 const IS_IN_DOCUMENT = 0b00000100;
68 }
69}
70
71impl NodeFlags {
72 #[inline(always)]
73 pub fn is_inline_root(&self) -> bool {
74 self.contains(Self::IS_INLINE_ROOT)
75 }
76
77 #[inline(always)]
78 pub fn is_table_root(&self) -> bool {
79 self.contains(Self::IS_TABLE_ROOT)
80 }
81
82 #[inline(always)]
83 pub fn is_in_document(&self) -> bool {
84 self.contains(Self::IS_IN_DOCUMENT)
85 }
86
87 #[inline(always)]
88 pub fn reset_construction_flags(&mut self) {
89 self.remove(Self::IS_INLINE_ROOT);
90 self.remove(Self::IS_TABLE_ROOT);
91 }
92}
93
94pub struct Node {
95 tree: *mut crate::NodeTree,
97
98 pub id: NodeId,
100 pub parent: Option<NodeId>,
102 pub children: ThinVec<NodeId>,
104 pub layout_parent: Cell<Option<NodeId>>,
106 pub layout_children: RefCell<Option<ThinVec<NodeId>>>,
108 pub anonymous_blocks: ThinVec<NodeId>,
114 pub paint_children: RefCell<Option<ThinVec<NodeId>>>,
116 pub stacking_context: Option<Box<HoistedPaintChildren>>,
117
118 #[cfg(feature = "shadow-dom")]
124 pub flattened_children: Option<Vec<NodeId>>,
125
126 pub flags: NodeFlags,
128
129 pub data: NodeData,
136}
137
138unsafe impl Send for Node {}
139unsafe impl Sync for Node {}
140
141macro_rules! universal_accessors {
144 ($($(#[$meta:meta])* $field:ident / $field_mut:ident : $ty:ty),* $(,)?) => {
145 impl Node {
146 $(
147 $(#[$meta])*
148 #[inline]
149 pub fn $field(&self) -> &$ty {
150 match &self.data {
151 NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.$field,
152 NodeData::Document(data) => &data.$field,
153 _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
154 }
155 }
156
157 $(#[$meta])*
158 #[inline]
159 pub fn $field_mut(&mut self) -> &mut $ty {
160 match &mut self.data {
161 NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.$field,
162 NodeData::Document(data) => &mut data.$field,
163 _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
164 }
165 }
166 )*
167 }
168 };
169}
170
171universal_accessors! {
172 stylo_element_data / stylo_element_data_mut: StyloData,
173 style / style_mut: Style<Atom>,
174 style_source / style_source_mut: Option<ServoArc<ComputedValues>>,
175 subtree_hoists / subtree_hoists_mut: bool,
176 unrounded_layout / unrounded_layout_mut: Layout,
180 final_layout / final_layout_mut: Layout,
181 scroll_offset / scroll_offset_mut: crate::Point<f64>,
182 scrollable_overflow / scrollable_overflow_mut: KurboRect,
183 transform / transform_mut: Option<Affine>,
184 display_constructed_as / display_constructed_as_mut: StyloDisplay,
185 element_state / element_state_mut: ElementState,
188 snapshot_handled / snapshot_handled_mut: AtomicBool,
189 selector_flags / selector_flags_mut: Cell<ElementSelectorFlags>,
193}
194
195impl Node {
196 #[inline]
204 pub fn cache(&self) -> &Cache {
205 match &self.data {
206 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache(),
207 NodeData::Document(data) => data.cache(),
208 _ => panic!("`cache` is not available on this node kind"),
209 }
210 }
211
212 #[inline]
214 pub fn cache_mut(&mut self) -> &mut Cache {
215 match &mut self.data {
216 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_mut(),
217 NodeData::Document(data) => data.cache_mut(),
218 _ => panic!("`cache_mut` is not available on this node kind"),
219 }
220 }
221
222 #[inline]
224 pub fn cache_release(&mut self) {
225 match &mut self.data {
226 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_release(),
227 NodeData::Document(data) => data.cache_release(),
228 _ => {}
229 }
230 }
231
232 #[inline]
238 pub fn style_source_opt(&self) -> Option<&ServoArc<ComputedValues>> {
239 match &self.data {
240 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.style_source.as_ref(),
241 NodeData::Document(data) => data.style_source.as_ref(),
242 _ => None,
243 }
244 }
245
246 #[inline]
247 pub fn stylo_element_data_opt(&self) -> Option<&StyloData> {
248 match &self.data {
249 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
250 Some(&data.stylo_element_data)
251 }
252 NodeData::Document(data) => Some(&data.stylo_element_data),
253 _ => None,
254 }
255 }
256
257 #[inline]
258 pub fn stylo_element_data_opt_mut(&mut self) -> Option<&mut StyloData> {
259 match &mut self.data {
260 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
261 Some(&mut data.stylo_element_data)
262 }
263 NodeData::Document(data) => Some(&mut data.stylo_element_data),
264 _ => None,
265 }
266 }
267
268 #[inline]
271 fn dirty_descendants_flag(&self) -> Option<&AtomicBool> {
272 match &self.data {
273 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
274 Some(&data.dirty_descendants)
275 }
276 NodeData::Document(data) => Some(&data.dirty_descendants),
277 _ => None,
278 }
279 }
280
281 #[inline]
284 pub fn guard(&self) -> &SharedRwLock {
285 let guard = match &self.data {
286 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.guard.as_ref(),
287 NodeData::Document(data) => data.guard.as_ref(),
288 _ => None,
289 };
290 guard.expect("`guard` is not available on this node kind")
291 }
292
293 #[inline]
294 pub fn has_snapshot(&self) -> bool {
295 match &self.data {
296 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot,
297 NodeData::Document(data) => data.has_snapshot,
298 _ => false,
299 }
300 }
301
302 #[inline]
303 pub fn set_has_snapshot(&mut self, value: bool) {
304 match &mut self.data {
305 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot = value,
306 NodeData::Document(data) => data.has_snapshot = value,
307 _ => {}
308 }
309 }
310
311 #[inline]
312 pub fn before(&self) -> Option<NodeId> {
313 self.element_data().and_then(|data| data.before)
314 }
315
316 #[inline]
317 pub fn after(&self) -> Option<NodeId> {
318 self.element_data().and_then(|data| data.after)
319 }
320}
321
322impl Node {
323 pub(crate) fn new(
324 tree: *mut crate::NodeTree,
325 id: NodeId,
326 guard: SharedRwLock,
327 mut data: NodeData,
328 ) -> Self {
329 match &mut data {
332 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
333 data.guard = Some(guard);
334 }
335 NodeData::Document(data) => data.guard = Some(guard),
336 _ => {}
337 }
338
339 Self {
340 tree,
341
342 id,
343 parent: None,
344 children: ThinVec::new(),
345 layout_parent: Cell::new(None),
346 layout_children: RefCell::new(None),
347 anonymous_blocks: ThinVec::new(),
348 paint_children: RefCell::new(None),
349 stacking_context: None,
350 #[cfg(feature = "shadow-dom")]
351 flattened_children: None,
352
353 flags: NodeFlags::empty(),
354 data,
355 }
356 }
357
358 pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
359 let transform = self.primary_styles().and_then(|s| {
360 let size = self.final_layout().size;
361 let reference_box = Rect::new(
362 Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
363 Size2D::new(
364 CSSPixelLength::new(size.width),
365 CSSPixelLength::new(size.height),
366 ),
367 );
368 crate::resolve_2d_transform(s.get_box(), reference_box).map(|t| {
371 let scale = scale as f64;
372 let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
373 Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
374 })
375 });
376
377 *self.transform_mut() = transform;
378 transform
379 }
380
381 pub fn pe_by_index(&self, index: usize) -> Option<NodeId> {
382 match index {
383 0 => self.after(),
384 1 => self.before(),
385 _ => panic!("Invalid pseudo element index"),
386 }
387 }
388
389 pub fn set_pe_by_index(&mut self, index: usize, value: Option<NodeId>) {
390 let Some(data) = self.element_data_mut() else {
391 return;
392 };
393 match index {
394 0 => data.after = value,
395 1 => data.before = value,
396 _ => panic!("Invalid pseudo element index"),
397 }
398 }
399
400 pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
401 Some(self.primary_styles().as_ref()?.clone_display())
402 }
403
404 pub fn diagnostic_computed_style(&self) -> Option<Vec<(&'static str, String)>> {
406 let style = self.primary_styles()?;
407 Some(vec![
408 ("display", style.clone_display().to_css_string()),
409 ("color", style.clone_color().to_css_string()),
410 (
411 "background-color",
412 style.clone_background_color().to_css_string(),
413 ),
414 (
415 "font-size",
416 format!("{}px", style.clone_font_size().computed_size().px()),
417 ),
418 ("width", style.clone_width().to_css_string()),
419 ])
420 }
421
422 pub fn is_display_none(&self) -> bool {
424 self.display_style()
425 .is_some_and(|display| display.is_none())
426 }
427
428 pub fn is_or_contains_block(&self) -> bool {
429 let style = self.primary_styles();
430 let style = style.as_ref();
431
432 let position = style
434 .map(|s| s.clone_position())
435 .unwrap_or(Position::Relative);
436 let is_in_flow = matches!(
437 position,
438 Position::Static | Position::Relative | Position::Sticky
439 );
440 if !is_in_flow {
441 return false;
442 }
443 let is_floating = style
446 .map(|s| s.clone_float().is_floating())
447 .unwrap_or(false);
448 if is_floating {
449 return false;
450 }
451 let display = style
452 .map(|s| s.clone_display())
453 .unwrap_or(StyloDisplay::inline());
454 match display.outside() {
455 DisplayOutside::None => false,
456 DisplayOutside::Block => true,
457 _ => {
458 if display.inside() == DisplayInside::Flow {
459 self.children
460 .iter()
461 .copied()
462 .any(|child_id| self.tree()[child_id].is_or_contains_block())
463 } else {
464 false
465 }
466 }
467 }
468 }
469
470 pub fn is_whitespace_node(&self) -> bool {
471 match &self.data {
472 NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
473 _ => false,
474 }
475 }
476
477 pub fn is_focussable(&self) -> bool {
478 self.data
479 .downcast_element()
480 .map(|el| el.is_focussable)
481 .unwrap_or(false)
482 }
483
484 pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
485 if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
486 if let Some(mut element_data) = stylo_element_data.get_mut() {
487 element_data.hint.insert(hint);
488 }
489 }
490 self.mark_ancestors_dirty();
493 }
494
495 pub fn has_dirty_descendants(&self) -> bool {
497 self.dirty_descendants_flag()
498 .is_some_and(|flag| flag.load(Ordering::Relaxed))
499 }
500
501 pub fn set_dirty_descendants(&self) {
503 if let Some(flag) = self.dirty_descendants_flag() {
504 flag.store(true, Ordering::Relaxed);
505 }
506 }
507
508 pub fn unset_dirty_descendants(&self) {
510 if let Some(flag) = self.dirty_descendants_flag() {
511 flag.store(false, Ordering::Relaxed);
512 }
513 }
514
515 pub(crate) fn mark_style_attr_updated(&mut self) {
517 if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
518 if let Some(mut data) = stylo_element_data.get_mut() {
519 data.hint |= RestyleHint::RESTYLE_STYLE_ATTRIBUTE;
520 }
521 }
522 self.set_dirty_descendants();
523 self.mark_ancestors_dirty();
524 }
525
526 pub fn mark_ancestors_dirty(&self) {
530 let mut current_id = self.parent;
531 while let Some(parent_id) = current_id {
532 let parent = &self.tree()[parent_id];
533 if let Some(flag) = parent.dirty_descendants_flag() {
536 if flag.swap(true, Ordering::Relaxed) {
537 break;
538 }
539 }
540 current_id = parent.parent;
541 }
542 }
543
544 pub fn damage(&self) -> Option<RestyleDamage> {
551 self.stylo_element_data_opt()
552 .and_then(|stylo| stylo.get().map(|data| data.damage))
553 }
554
555 pub fn set_damage(&mut self, damage: RestyleDamage) {
556 if let Some(stylo) = self.stylo_element_data_opt_mut() {
557 if let Some(mut data) = stylo.get_mut() {
558 data.damage = damage;
559 }
560 }
561 }
562
563 pub fn insert_damage(&mut self, damage: RestyleDamage) {
564 if let Some(stylo) = self.stylo_element_data_opt_mut() {
565 if let Some(mut data) = stylo.get_mut() {
566 data.damage |= damage;
567 }
568 }
569 }
570
571 pub fn remove_damage(&mut self, damage: RestyleDamage) {
572 if let Some(stylo) = self.stylo_element_data_opt_mut() {
573 if let Some(mut data) = stylo.get_mut() {
574 data.damage.remove(damage);
575 }
576 }
577 }
578
579 pub fn clear_damage_mut(&mut self) {
580 if let Some(stylo) = self.stylo_element_data_opt_mut() {
581 if let Some(mut data) = stylo.get_mut() {
582 data.damage = RestyleDamage::empty();
583 }
584 }
585 }
586
587 pub fn hover(&mut self) {
588 if let Some(data) = self.element_data_mut() {
589 data.element_state.insert(ElementState::HOVER);
590 }
591 self.set_restyle_hint(RestyleHint::restyle_subtree());
592 }
593
594 pub fn unhover(&mut self) {
595 if let Some(data) = self.element_data_mut() {
596 data.element_state.remove(ElementState::HOVER);
597 }
598 self.set_restyle_hint(RestyleHint::restyle_subtree());
599 }
600
601 pub fn is_hovered(&self) -> bool {
602 self.element_data()
603 .is_some_and(|data| data.element_state.contains(ElementState::HOVER))
604 }
605
606 pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
607 if let Some(data) = self.element_data_mut() {
608 data.element_state
609 .insert(ElementState::FOCUS | ElementState::FOCUSRING);
610 }
611 self.set_restyle_hint(RestyleHint::restyle_subtree());
612
613 if self
615 .element_data()
616 .and_then(|elem| elem.text_input_data())
617 .is_some()
618 {
619 shell_provider.set_ime_enabled(true);
620 let mut pos = self.absolute_position(0.0, 0.0);
621 pos.x += self.final_layout().content_box_x();
622 pos.y += self.final_layout().content_box_y();
623 let width = self.final_layout().content_box_width();
624 let height = self.final_layout().content_box_height();
625 shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
626 }
627 }
628
629 pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
630 if let Some(data) = self.element_data_mut() {
631 data.element_state
632 .remove(ElementState::FOCUS | ElementState::FOCUSRING);
633 }
634 self.set_restyle_hint(RestyleHint::restyle_subtree());
635
636 if self
638 .element_data()
639 .and_then(|elem| elem.text_input_data())
640 .is_some()
641 {
642 shell_provider.set_ime_enabled(false);
643 }
644 }
645
646 pub fn is_focussed(&self) -> bool {
647 self.element_data()
648 .is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
649 }
650
651 pub fn active(&mut self) {
652 if let Some(data) = self.element_data_mut() {
653 data.element_state.insert(ElementState::ACTIVE);
654 }
655 self.set_restyle_hint(RestyleHint::restyle_subtree());
656 }
657
658 pub fn unactive(&mut self) {
659 if let Some(data) = self.element_data_mut() {
660 data.element_state.remove(ElementState::ACTIVE);
661 }
662 self.set_restyle_hint(RestyleHint::restyle_subtree());
663 }
664
665 pub fn is_active(&self) -> bool {
666 self.element_data()
667 .is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
668 }
669
670 pub fn disable(&mut self) {
673 if let Some(data) = self.element_data_mut() {
674 if data.can_be_disabled() {
675 data.element_state.insert(ElementState::DISABLED);
676 data.element_state.remove(ElementState::ENABLED);
677 }
678 }
679 self.set_restyle_hint(RestyleHint::restyle_subtree());
680 }
681
682 pub fn enable(&mut self) {
685 if let Some(data) = self.element_data_mut() {
686 if data.can_be_disabled() {
687 data.element_state.insert(ElementState::ENABLED);
688 data.element_state.remove(ElementState::DISABLED);
689 }
690 }
691 self.set_restyle_hint(RestyleHint::restyle_subtree());
692 }
693
694 pub fn subdoc(&self) -> Option<&dyn Document> {
695 self.element_data().and_then(|el| el.sub_doc_data())
696 }
697
698 pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
699 self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
700 }
701
702 pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
703 if let Some(input_data) = self
706 .data
707 .downcast_element()
708 .and_then(|el| el.text_input_data())
709 {
710 if !input_data.is_multiline {
711 let content_box_height = self.final_layout().content_box_height();
712 let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
713 let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
714
715 return y_offset as f64;
716 }
717 }
718
719 0.0
720 }
721}
722
723#[derive(Debug, Clone, Copy, PartialEq)]
724pub enum NodeKind {
725 Document,
726 Element,
727 AnonymousBlock,
728 Text,
729 Comment,
730 ShadowRoot,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
739pub enum TextGranularity {
740 Word,
742 Line,
744}
745
746impl TextGranularity {
747 pub fn from_click_count(count: u16) -> Option<Self> {
750 match count {
751 0 | 1 => None,
752 2 => Some(Self::Word),
753 _ => Some(Self::Line),
754 }
755 }
756}
757
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
762pub enum ShadowRootMode {
763 Open,
766 Closed,
769}
770
771#[derive(Debug, Clone)]
779pub struct ShadowRootData {
780 pub host: NodeId,
782 pub mode: ShadowRootMode,
784 pub stylesheet_nodes: Vec<NodeId>,
787}
788
789impl ShadowRootData {
790 pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
791 Self {
792 host,
793 mode,
794 stylesheet_nodes: Vec::new(),
795 }
796 }
797}
798
799#[derive(Debug, Clone)]
801pub enum NodeData {
802 Document(Box<DocumentData>),
804
805 Element(Box<ElementData>),
807
808 AnonymousBlock(Box<ElementData>),
810
811 Text(TextNodeData),
813
814 Comment {
816 contents: String,
818 },
819
820 ShadowRoot(ShadowRootData),
822 }
829
830impl NodeData {
831 pub fn downcast_element(&self) -> Option<&ElementData> {
832 match self {
833 Self::Element(data) => Some(data),
834 Self::AnonymousBlock(data) => Some(data),
835 _ => None,
836 }
837 }
838
839 pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
840 match self {
841 Self::Element(data) => Some(data),
842 Self::AnonymousBlock(data) => Some(data),
843 _ => None,
844 }
845 }
846
847 pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
848 let Some(elem) = self.downcast_element() else {
849 return false;
850 };
851 *name == elem.name.local
852 }
853
854 pub fn attrs(&self) -> Option<&[Attribute]> {
855 Some(&self.downcast_element()?.attrs)
856 }
857
858 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
859 self.downcast_element()?.attr(name)
860 }
861
862 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
863 self.downcast_element()
864 .is_some_and(|elem| elem.has_attr(name))
865 }
866
867 pub fn kind(&self) -> NodeKind {
868 match self {
869 NodeData::Document(_) => NodeKind::Document,
870 NodeData::Element(_) => NodeKind::Element,
871 NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
872 NodeData::Text(_) => NodeKind::Text,
873 NodeData::Comment { .. } => NodeKind::Comment,
874 NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
875 }
876 }
877
878 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
879 match self {
880 Self::ShadowRoot(data) => Some(data),
881 _ => None,
882 }
883 }
884
885 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
886 match self {
887 Self::ShadowRoot(data) => Some(data),
888 _ => None,
889 }
890 }
891}
892
893#[derive(Debug, Clone)]
894pub struct TextNodeData {
895 pub content: String,
897}
898
899impl TextNodeData {
900 pub fn new(content: String) -> Self {
901 Self { content }
902 }
903}
904
905impl Node {
930 pub fn tree(&self) -> &crate::NodeTree {
931 unsafe { &*self.tree }
932 }
933
934 #[track_caller]
935 pub fn with(&self, id: NodeId) -> &Node {
936 self.tree().get(id).unwrap()
937 }
938
939 pub fn print_tree(&self, level: usize) {
940 println!(
941 "{} {} {:?} {} {:?}",
942 " ".repeat(level),
943 self.id,
944 self.parent,
945 self.node_debug_str().replace('\n', ""),
946 self.children
947 );
948 for child_id in self.children.iter() {
950 let child = self.with(*child_id);
951 child.print_tree(level + 1)
952 }
953 }
954
955 pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
957 self.children.iter().position(|id| *id == child_id)
958 }
959
960 pub fn child_index(&self) -> Option<usize> {
962 self.tree()[self.parent?]
963 .children
964 .iter()
965 .position(|id| *id == self.id)
966 }
967
968 pub fn forward(&self, n: usize) -> Option<&Node> {
970 let child_idx = self.child_index().unwrap_or(0);
971 self.tree()[self.parent?]
972 .children
973 .get(child_idx + n)
974 .map(|id| self.with(*id))
975 }
976
977 pub fn backward(&self, n: usize) -> Option<&Node> {
978 let child_idx = self.child_index().unwrap_or(0);
979 if child_idx < n {
980 return None;
981 }
982
983 self.tree()[self.parent?]
984 .children
985 .get(child_idx - n)
986 .map(|id| self.with(*id))
987 }
988
989 pub fn is_element(&self) -> bool {
990 matches!(self.data, NodeData::Element { .. })
991 }
992
993 pub fn is_anonymous(&self) -> bool {
994 matches!(self.data, NodeData::AnonymousBlock { .. })
995 }
996
997 pub fn is_shadow_root(&self) -> bool {
998 matches!(self.data, NodeData::ShadowRoot { .. })
999 }
1000
1001 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1002 self.data.shadow_root_data()
1003 }
1004
1005 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1006 self.data.shadow_root_data_mut()
1007 }
1008
1009 pub fn shadow_root_id(&self) -> Option<NodeId> {
1012 self.element_data().and_then(|el| el.shadow_root)
1013 }
1014
1015 #[cfg(feature = "shadow-dom")]
1019 pub fn layout_dom_children(&self) -> &[NodeId] {
1020 match &self.flattened_children {
1021 Some(children) => children,
1022 None => &self.children,
1023 }
1024 }
1025
1026 #[cfg(not(feature = "shadow-dom"))]
1028 #[inline(always)]
1029 pub fn layout_dom_children(&self) -> &[NodeId] {
1030 &self.children
1031 }
1032
1033 pub fn is_text_node(&self) -> bool {
1034 matches!(self.data, NodeData::Text { .. })
1035 }
1036
1037 pub fn element_data(&self) -> Option<&ElementData> {
1038 match self.data {
1039 NodeData::Element(ref data) => Some(data),
1040 NodeData::AnonymousBlock(ref data) => Some(data),
1041 _ => None,
1042 }
1043 }
1044
1045 pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1046 match self.data {
1047 NodeData::Element(ref mut data) => Some(data),
1048 NodeData::AnonymousBlock(ref mut data) => Some(data),
1049 _ => None,
1050 }
1051 }
1052
1053 pub fn text_data(&self) -> Option<&TextNodeData> {
1054 match self.data {
1055 NodeData::Text(ref data) => Some(data),
1056 _ => None,
1057 }
1058 }
1059
1060 pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1061 match self.data {
1062 NodeData::Text(ref mut data) => Some(data),
1063 _ => None,
1064 }
1065 }
1066
1067 pub fn node_debug_str(&self) -> String {
1068 let mut s = String::new();
1069
1070 match &self.data {
1071 NodeData::Document(_) => write!(s, "DOCUMENT"),
1072 NodeData::Text(data) => {
1074 let bytes = data.content.as_bytes();
1075 write!(
1076 s,
1077 "TEXT {}",
1078 std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1079 .unwrap_or("INVALID UTF8")
1080 )
1081 }
1082 NodeData::Comment { .. } => write!(s, "COMMENT"),
1083 NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1084 NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1085 NodeData::Element(data) => {
1086 let name = &data.name;
1087 let class = self.attr(local_name!("class")).unwrap_or("");
1088 let id = self.attr(local_name!("id")).unwrap_or("");
1089 let display = self.display_constructed_as().to_css_string();
1090 write!(s, "<{}", name.local).unwrap();
1091 if !id.is_empty() {
1092 write!(s, " #{id}").unwrap();
1093 }
1094 if !class.is_empty() {
1095 if class.contains(' ') {
1096 write!(s, " class=\"{class}\"").unwrap()
1097 } else {
1098 write!(s, " .{class}").unwrap()
1099 }
1100 }
1101 write!(s, "> ({display})")
1102 } }
1104 .unwrap();
1105 s
1106 }
1107
1108 pub fn outer_html(&self) -> String {
1116 let mut output = String::new();
1117 self.write_outer_html(&mut output);
1118 output
1119 }
1120
1121 pub fn outer_html_pretty(&self) -> String {
1137 let mut output = String::new();
1138 self.write_outer_html_pretty(&mut output);
1139 output
1140 }
1141
1142 pub fn write_outer_html(&self, writer: &mut String) {
1143 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1144 }
1145
1146 #[cfg(feature = "svg")]
1147 pub(crate) fn write_outer_html_with_current_color(
1148 &self,
1149 writer: &mut String,
1150 current_color: &str,
1151 ) {
1152 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1153 }
1154
1155 pub fn write_outer_html_pretty(&self, writer: &mut String) {
1156 self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1157 }
1158
1159 fn write_outer_html_in_style(
1160 &self,
1161 writer: &mut String,
1162 style: OutputStyle,
1163 nesting: usize,
1164 current_color_override: Option<&str>,
1165 ) {
1166 const INDENT: &str = " ";
1167 let has_children = !self.children.is_empty();
1168 let computed_current_color = || {
1169 self.primary_styles()
1170 .map(|style| style.clone_color())
1171 .map(|color| crate::util::absolute_color_to_svg_css(&color))
1172 };
1173 let current_color = current_color_override
1174 .map(ToOwned::to_owned)
1175 .or_else(computed_current_color);
1176
1177 match &self.data {
1178 NodeData::Document(_) => {}
1179 NodeData::Comment { .. } => {}
1180 NodeData::AnonymousBlock(_) => {}
1181 NodeData::ShadowRoot(_) => {}
1182 NodeData::Text(data) => {
1184 if matches!(style, OutputStyle::Pretty) {
1185 for _ in 0..nesting {
1186 writer.push_str(INDENT);
1187 }
1188 }
1189 writer.push_str(data.content.as_str());
1190 if matches!(style, OutputStyle::Pretty) {
1191 writer.push('\n');
1192 }
1193 }
1194 NodeData::Element(data) => {
1195 if matches!(style, OutputStyle::Pretty) {
1196 for _ in 0..nesting {
1197 writer.push_str(INDENT);
1198 }
1199 }
1200 writer.push('<');
1201 writer.push_str(&data.name.local);
1202
1203 for attr in data.attrs() {
1204 writer.push(' ');
1205 writer.push_str(&attr.name.local);
1206 writer.push_str("=\"");
1207 #[allow(clippy::unnecessary_unwrap)] if current_color.is_some() && attr.value.contains("currentColor") {
1209 let value = attr
1210 .value
1211 .replace("currentColor", current_color.as_ref().unwrap());
1212 encode_quoted_attribute_to_string(&value, writer);
1213 } else {
1214 encode_quoted_attribute_to_string(&attr.value, writer);
1215 }
1216 writer.push('"');
1217 }
1218 if !has_children {
1219 writer.push_str(" /");
1220 }
1221 writer.push('>');
1222 if matches!(style, OutputStyle::Pretty) {
1223 writer.push('\n');
1224 }
1225
1226 if has_children {
1227 for &child_id in &self.children {
1228 self.tree()[child_id].write_outer_html_in_style(
1229 writer,
1230 style,
1231 nesting + 1,
1232 current_color_override,
1233 );
1234 }
1235
1236 if matches!(style, OutputStyle::Pretty) {
1237 for _ in 0..nesting {
1238 writer.push_str(INDENT);
1239 }
1240 }
1241 writer.push_str("</");
1242 writer.push_str(&data.name.local);
1243 writer.push('>');
1244 if matches!(style, OutputStyle::Pretty) {
1245 writer.push('\n');
1246 }
1247 }
1248 }
1249 }
1250 }
1251
1252 pub fn attrs(&self) -> Option<&[Attribute]> {
1253 Some(&self.element_data()?.attrs)
1254 }
1255
1256 pub fn attr(&self, name: LocalName) -> Option<&str> {
1257 let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
1258 Some(&attr.value)
1259 }
1260
1261 pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1262 self.stylo_element_data_opt()
1263 .and_then(|stylo| stylo.primary_styles())
1264 }
1265
1266 pub fn text_content(&self) -> String {
1267 let mut out = String::new();
1268 self.write_text_content(&mut out);
1269 out
1270 }
1271
1272 pub fn write_text_content<W: Write>(&self, out: &mut W) {
1285 match &self.data {
1286 NodeData::Text(data) => {
1287 let _ = out.write_str(&data.content);
1288 }
1289 NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
1290 for child_id in self.children.iter() {
1291 self.with(*child_id).write_text_content(out);
1292 }
1293 }
1294 _ => {}
1295 }
1296 }
1297
1298 pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1299 if let NodeData::Element(ref mut elem_data) = self.data {
1300 if let Some(guard) = elem_data.guard.clone() {
1301 elem_data.flush_style_attribute(&guard, url_extra_data);
1302 }
1303 }
1304 }
1305
1306 pub fn order(&self) -> i32 {
1307 self.primary_styles()
1308 .map(|s| match s.pseudo() {
1309 Some(PseudoElement::Before) => i32::MIN,
1310 Some(PseudoElement::After) => i32::MAX,
1311 _ => s.clone_order(),
1312 })
1313 .unwrap_or(0)
1314 }
1315
1316 pub fn z_index(&self) -> i32 {
1317 self.primary_styles()
1318 .map(|s| s.clone_z_index().integer_or(0))
1319 .unwrap_or(0)
1320 }
1321
1322 pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1324 let Some(style) = self.primary_styles() else {
1325 return false;
1326 };
1327
1328 let position = style.clone_position();
1329 let has_z_index = !style.clone_z_index().is_auto();
1330
1331 if style.clone_opacity() != 1.0 {
1332 return true;
1333 }
1334
1335 let position_based = match position {
1336 Position::Fixed | Position::Sticky => true,
1337 Position::Relative | Position::Absolute => has_z_index,
1338 Position::Static => has_z_index && is_flex_or_grid_item,
1339 };
1340 if position_based {
1341 return true;
1342 }
1343
1344 let box_styles = style.get_box();
1352 if !box_styles.transform.0.is_empty()
1353 || !matches!(box_styles.translate, Translate::None)
1354 || !matches!(box_styles.rotate, Rotate::None)
1355 || !matches!(box_styles.scale, Scale::None)
1356 {
1357 return true;
1358 }
1359
1360 if box_styles.isolation == Isolation::Isolate {
1365 return true;
1366 }
1367
1368 false
1375 }
1376
1377 pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
1386 self.hit_inner(x, y, scale, &mut None)
1387 }
1388
1389 pub(crate) fn hit_inner(
1394 &self,
1395 x: f32,
1396 y: f32,
1397 scale: f64,
1398 scrollbar: &mut Option<crate::node::ScrollbarRef>,
1399 ) -> Option<HitResult> {
1400 use style::computed_values::pointer_events::T as PointerEvents;
1401 use style::computed_values::visibility::T as Visibility;
1402
1403 if matches!(self.style().display, taffy::Display::None) {
1412 return None;
1413 }
1414
1415 if let Some(style) = self.primary_styles() {
1417 if matches!(
1418 style.clone_visibility(),
1419 Visibility::Hidden | Visibility::Collapse
1420 ) {
1421 return None;
1422 }
1423 }
1424
1425 let pointer_events_none = self
1428 .primary_styles()
1429 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1430
1431 let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
1432 let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
1433
1434 if let Some(t) = *self.transform() {
1435 let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
1436 x = (p.x / scale) as f32;
1437 y = (p.y / scale) as f32;
1438 }
1439
1440 let size = self.final_layout().size;
1441 let matches_self = !(x < 0.0
1442 || x > size.width + self.scroll_offset().x as f32
1443 || y < 0.0
1444 || y > size.height + self.scroll_offset().y as f32);
1445
1446 let content_size = self.final_layout().content_size;
1447 let matches_content = !(x < 0.0
1448 || x > content_size.width + self.scroll_offset().x as f32
1449 || y < 0.0
1450 || y > content_size.height + self.scroll_offset().y as f32);
1451
1452 let matches_hoisted_content = match &self.stacking_context {
1453 Some(sc) => {
1454 let content_area = sc.content_area;
1455 x >= content_area.left + self.scroll_offset().x as f32
1456 && x <= content_area.right + self.scroll_offset().x as f32
1457 && y >= content_area.top + self.scroll_offset().y as f32
1458 && y <= content_area.bottom + self.scroll_offset().y as f32
1459 }
1460 None => false,
1461 };
1462
1463 let overflow = *self.scrollable_overflow();
1466
1467 let matches_overflow = x >= (overflow.x0 / scale) as f32
1468 && x <= (overflow.x1 / scale) as f32
1469 && y >= (overflow.y0 / scale) as f32
1470 && y <= (overflow.y1 / scale) as f32;
1471
1472 if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
1473 return None;
1474 }
1475
1476 if matches_self
1479 && let Some(sb) = self.scrollbar_at_local(
1480 (x - self.scroll_offset().x as f32) as f64,
1481 (y - self.scroll_offset().y as f32) as f64,
1482 )
1483 {
1484 *scrollbar = Some(sb);
1485 }
1486
1487 if self.flags.is_inline_root() {
1488 let content_box_offset = taffy::Point {
1489 x: self.final_layout().padding.left + self.final_layout().border.left,
1490 y: self.final_layout().padding.top + self.final_layout().border.top,
1491 };
1492 x -= content_box_offset.x;
1493 y -= content_box_offset.y;
1494 }
1495
1496 if matches_hoisted_content {
1498 if let Some(hoisted) = &self.stacking_context {
1499 for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
1500 let x = x - hoisted_child.position.x;
1501 let y = y - hoisted_child.position.y;
1502 if let Some(hit) = self
1503 .with(hoisted_child.node_id)
1504 .hit_inner(x, y, scale, scrollbar)
1505 {
1506 return Some(hit);
1507 }
1508 }
1509 }
1510 }
1511
1512 for child_id in self.paint_children.borrow().iter().flatten().rev() {
1514 if let Some(hit) = self.with(*child_id).hit_inner(x, y, scale, scrollbar) {
1515 return Some(hit);
1516 }
1517 }
1518
1519 if matches_hoisted_content {
1521 if let Some(hoisted) = &self.stacking_context {
1522 for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
1523 let x = x - hoisted_child.position.x;
1524 let y = y - hoisted_child.position.y;
1525 if let Some(hit) = self
1526 .with(hoisted_child.node_id)
1527 .hit_inner(x, y, scale, scrollbar)
1528 {
1529 return Some(hit);
1530 }
1531 }
1532 }
1533 }
1534
1535 if self.flags.is_inline_root() {
1537 let element_data = &self.element_data().unwrap();
1538 if let Some(ild) = element_data.inline_layout_data.as_ref() {
1539 let layout = &ild.layout;
1540 let scale = layout.scale();
1541
1542 if let Some((cluster, _side)) =
1543 Cluster::from_point_exact(layout, x * scale, y * scale)
1544 {
1545 let style_index = cluster.glyphs().next()?.style_index();
1546 let node_id = layout.styles()[style_index].brush.id;
1547 let text_pointer_events_none = self
1548 .with(node_id)
1549 .primary_styles()
1550 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1551 if !text_pointer_events_none {
1552 return Some(HitResult {
1553 node_id,
1554 x,
1555 y,
1556 is_text: true,
1557 });
1558 }
1559 }
1560 }
1561 }
1562
1563 if matches_self && !pointer_events_none {
1565 return Some(HitResult {
1566 node_id: self.id,
1567 x,
1568 y,
1569 is_text: false,
1570 });
1571 }
1572
1573 None
1574 }
1575
1576 pub fn inline_root_ancestor(&self) -> Option<&Node> {
1579 let mut node = self;
1580 loop {
1581 if node.flags.is_inline_root() {
1582 return Some(node);
1583 }
1584 let id = node.layout_parent.get()?;
1585 node = self.with(id);
1586 }
1587 }
1588
1589 pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
1593 if !self.flags.is_inline_root() {
1594 return None;
1595 }
1596
1597 let element_data = self.element_data()?;
1598 let inline_layout = element_data.inline_layout_data.as_ref()?;
1599 let layout = &inline_layout.layout;
1600 let scale = layout.scale();
1601
1602 let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
1604
1605 let is_leading = side == ClusterSide::Left;
1610 let offset = if cluster.is_rtl() {
1611 if is_leading {
1612 cluster.text_range().end
1613 } else {
1614 cluster.text_range().start
1615 }
1616 } else {
1617 if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
1619 cluster.text_range().start
1620 } else {
1621 cluster.text_range().end
1622 }
1623 };
1624
1625 Some(offset)
1626 }
1627
1628 pub fn text_range_at_point(
1638 &self,
1639 x: f32,
1640 y: f32,
1641 granularity: TextGranularity,
1642 ) -> Option<Range<usize>> {
1643 if !self.flags.is_inline_root() {
1644 return None;
1645 }
1646
1647 let element_data = self.element_data()?;
1648 let inline_layout = element_data.inline_layout_data.as_ref()?;
1649 let layout = &inline_layout.layout;
1650 let scale = layout.scale();
1651 let (x, y) = (x * scale, y * scale);
1652
1653 Cluster::from_point(layout, x, y)?;
1658
1659 let selection = match granularity {
1660 TextGranularity::Word => Selection::word_from_point(layout, x, y),
1661 TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
1665 };
1666
1667 let range = selection.text_range();
1668 if range.is_empty() { None } else { Some(range) }
1669 }
1670
1671 pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
1673 let x = x + self.final_layout().location.x;
1676 let y = y + self.final_layout().location.y;
1677
1678 self.layout_parent
1680 .get()
1681 .and_then(|id| self.tree().get(id))
1682 .map(|parent| {
1683 parent.absolute_position(
1684 x - parent.scroll_offset().x as f32,
1685 y - parent.scroll_offset().y as f32,
1686 )
1687 })
1688 .unwrap_or(crate::util::Point { x, y })
1689 }
1690
1691 fn is_offset_parent(&self) -> bool {
1694 let Some(styles) = self.primary_styles() else {
1695 return false;
1696 };
1697 if styles.get_box().position != Position::Static {
1698 return true;
1699 }
1700 self.data.is_element_with_tag_name(&local_name!("body"))
1701 || self.data.is_element_with_tag_name(&local_name!("td"))
1702 || self.data.is_element_with_tag_name(&local_name!("th"))
1703 }
1704
1705 pub fn offset_parent(&self) -> Option<&Node> {
1708 let mut node = self;
1709 loop {
1710 node = self.with(node.layout_parent.get()?);
1711 if node.is_offset_parent() {
1712 return Some(node);
1713 }
1714 }
1715 }
1716
1717 pub fn offset_top_left(&self) -> crate::util::Point<f32> {
1720 let mut x = 0.0;
1721 let mut y = 0.0;
1722 let mut current = self;
1723 loop {
1724 let layout = current.final_layout();
1725 x += layout.location.x;
1726 y += layout.location.y;
1727
1728 let Some(parent_id) = current.layout_parent.get() else {
1729 break;
1730 };
1731 let parent = self.with(parent_id);
1732 if parent.is_offset_parent() {
1733 let border = parent.final_layout().border;
1734 x -= border.left;
1735 y -= border.top;
1736 break;
1737 }
1738 current = parent;
1739 }
1740 crate::util::Point { x, y }
1741 }
1742
1743 pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
1745 DomEventData::Click(self.synthetic_click_event_data(mods))
1746 }
1747
1748 pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
1749 let absolute_position = self.absolute_position(0.0, 0.0);
1750 let x = absolute_position.x + (self.final_layout().size.width / 2.0);
1751 let y = absolute_position.y + (self.final_layout().size.height / 2.0);
1752
1753 BlitzPointerEvent {
1754 id: BlitzPointerId::Mouse,
1755 is_primary: true,
1756 coords: PointerCoords {
1757 page_x: x,
1758 page_y: y,
1759
1760 screen_x: x,
1762 screen_y: y,
1763 client_x: x,
1764 client_y: y,
1765 },
1766 mods,
1767 button: Default::default(),
1768 buttons: Default::default(),
1769 details: Default::default(),
1770 element: Default::default(),
1771 active_pointers: Default::default(),
1772 }
1773 }
1774}
1775
1776impl PartialEq for Node {
1778 fn eq(&self, other: &Self) -> bool {
1779 self.id == other.id
1780 }
1781}
1782
1783impl Eq for Node {}
1784
1785impl std::fmt::Debug for Node {
1786 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1787 f.debug_struct("NodeData")
1789 .field("parent", &self.parent)
1790 .field("id", &self.id)
1791 .field("is_inline_root", &self.flags.is_inline_root())
1792 .field("children", &self.children)
1793 .field("layout_children", &self.layout_children.borrow())
1794 .field("node", &self.data)
1796 .field("stylo_element_data", &self.stylo_element_data_opt())
1797 .finish()
1800 }
1801}
1802
1803#[cfg(test)]
1804mod test {
1805 use style_dom::ElementState;
1806
1807 use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
1808
1809 #[test]
1810 fn absolute_position_tolerates_a_detached_layout_parent() {
1811 let mut document = BaseDocument::new(DocumentConfig::default());
1812 let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
1813 qual_name!("section"),
1814 vec![],
1815 ))));
1816 let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
1817 qual_name!("button"),
1818 vec![],
1819 ))));
1820 document
1821 .get_node(child)
1822 .unwrap()
1823 .layout_parent
1824 .set(Some(parent));
1825
1826 document.remove_node_from_tree(parent);
1831
1832 assert_eq!(
1833 document
1834 .get_node(child)
1835 .unwrap()
1836 .absolute_position(4.0, 7.0),
1837 crate::util::Point { x: 4.0, y: 7.0 },
1838 );
1839 }
1840
1841 #[test]
1842 fn create_node_with_disabled_attr() {
1843 let mut document = BaseDocument::new(DocumentConfig::default());
1844 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1845 qual_name!("button"),
1846 vec![Attribute {
1847 name: qual_name!("disabled"),
1848 value: "".into(),
1849 }],
1850 ))));
1851 let node = document.get_node(node).unwrap();
1852
1853 assert!(
1854 node.element_state().contains(ElementState::DISABLED),
1855 "form node is disabled"
1856 );
1857 assert!(
1858 !node.element_state().contains(ElementState::ENABLED),
1859 "form node is not enabled"
1860 );
1861 }
1862
1863 #[test]
1864 fn ignore_disabled_attr_content() {
1865 let mut document = BaseDocument::new(DocumentConfig::default());
1866 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1867 qual_name!("button"),
1868 vec![Attribute {
1869 name: qual_name!("disabled"),
1870 value: "false".into(),
1871 }],
1872 ))));
1873 let node = document.get_node(node).unwrap();
1874
1875 assert!(
1876 node.element_state().contains(ElementState::DISABLED),
1877 "form node is disabled"
1878 );
1879 assert!(
1880 !node.element_state().contains(ElementState::ENABLED),
1881 "form node is not enabled"
1882 );
1883 }
1884
1885 #[test]
1886 fn create_node_with_ignored_disable() {
1887 let mut document = BaseDocument::new(DocumentConfig::default());
1888 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1889 qual_name!("a"),
1890 vec![Attribute {
1891 name: qual_name!("disabled"),
1892 value: "".into(),
1893 }],
1894 ))));
1895 let node = document.get_node(node).unwrap();
1896
1897 assert!(
1898 !node.element_state().contains(ElementState::DISABLED),
1899 "Non form node cannot be disabled"
1900 );
1901 assert!(
1902 !node.element_state().contains(ElementState::ENABLED),
1903 "Non form node cannot be enabled"
1904 );
1905 }
1906
1907 #[test]
1908 fn create_empty_enabled_node() {
1909 let mut document = BaseDocument::new(DocumentConfig::default());
1910 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1911 qual_name!("button"),
1912 vec![],
1913 ))));
1914 let node = document.get_node(node).unwrap();
1915
1916 assert!(
1917 node.element_state().contains(ElementState::ENABLED),
1918 "Button should be enabled by default"
1919 );
1920 }
1921}