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 computed_style_properties(&self) -> Option<Vec<(&'static str, String)>> {
421 let style = self.primary_styles()?;
422 let layout = *self.final_layout();
423 Some(vec![
424 ("display", style.clone_display().to_css_string()),
425 ("position", style.clone_position().to_css_string()),
426 ("visibility", style.clone_visibility().to_css_string()),
427 ("opacity", style.clone_opacity().to_css_string()),
428 ("color", style.clone_color().to_css_string()),
429 (
430 "background-color",
431 style.clone_background_color().to_css_string(),
432 ),
433 (
434 "font-size",
435 format!("{}px", style.clone_font_size().computed_size().px()),
436 ),
437 ("font-weight", style.clone_font_weight().to_css_string()),
438 ("font-style", style.clone_font_style().to_css_string()),
439 ("font-family", style.clone_font_family().to_css_string()),
440 ("text-align", style.clone_text_align().to_css_string()),
441 ("overflow-x", style.clone_overflow_x().to_css_string()),
442 ("overflow-y", style.clone_overflow_y().to_css_string()),
443 ("z-index", style.clone_z_index().to_css_string()),
444 ("box-sizing", style.clone_box_sizing().to_css_string()),
445 (
446 "flex-direction",
447 style.clone_flex_direction().to_css_string(),
448 ),
449 (
450 "justify-content",
451 style.clone_justify_content().to_css_string(),
452 ),
453 ("align-items", style.clone_align_items().to_css_string()),
454 ("width", format!("{}px", layout.size.width)),
458 ("height", format!("{}px", layout.size.height)),
459 ])
460 }
461
462 pub fn diagnostic_computed_style(&self) -> Option<Vec<(&'static str, String)>> {
463 let style = self.primary_styles()?;
464 Some(vec![
465 ("display", style.clone_display().to_css_string()),
466 ("color", style.clone_color().to_css_string()),
467 (
468 "background-color",
469 style.clone_background_color().to_css_string(),
470 ),
471 (
472 "font-size",
473 format!("{}px", style.clone_font_size().computed_size().px()),
474 ),
475 ("width", style.clone_width().to_css_string()),
476 ])
477 }
478
479 pub fn is_display_none(&self) -> bool {
481 self.display_style()
482 .is_some_and(|display| display.is_none())
483 }
484
485 pub fn is_or_contains_block(&self) -> bool {
486 let style = self.primary_styles();
487 let style = style.as_ref();
488
489 let position = style
491 .map(|s| s.clone_position())
492 .unwrap_or(Position::Relative);
493 let is_in_flow = matches!(
494 position,
495 Position::Static | Position::Relative | Position::Sticky
496 );
497 if !is_in_flow {
498 return false;
499 }
500 let is_floating = style
503 .map(|s| s.clone_float().is_floating())
504 .unwrap_or(false);
505 if is_floating {
506 return false;
507 }
508 let display = style
509 .map(|s| s.clone_display())
510 .unwrap_or(StyloDisplay::inline());
511 match display.outside() {
512 DisplayOutside::None => false,
513 DisplayOutside::Block => true,
514 _ => {
515 if display.inside() == DisplayInside::Flow {
516 self.children
517 .iter()
518 .copied()
519 .any(|child_id| self.tree()[child_id].is_or_contains_block())
520 } else {
521 false
522 }
523 }
524 }
525 }
526
527 pub fn is_whitespace_node(&self) -> bool {
528 match &self.data {
529 NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
530 _ => false,
531 }
532 }
533
534 pub fn is_focussable(&self) -> bool {
535 self.data
536 .downcast_element()
537 .map(|el| el.is_focussable)
538 .unwrap_or(false)
539 }
540
541 pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
542 if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
543 if let Some(mut element_data) = stylo_element_data.get_mut() {
544 element_data.hint.insert(hint);
545 }
546 }
547 self.mark_ancestors_dirty();
550 }
551
552 pub fn has_dirty_descendants(&self) -> bool {
554 self.dirty_descendants_flag()
555 .is_some_and(|flag| flag.load(Ordering::Relaxed))
556 }
557
558 pub fn set_dirty_descendants(&self) {
560 if let Some(flag) = self.dirty_descendants_flag() {
561 flag.store(true, Ordering::Relaxed);
562 }
563 }
564
565 pub fn unset_dirty_descendants(&self) {
567 if let Some(flag) = self.dirty_descendants_flag() {
568 flag.store(false, Ordering::Relaxed);
569 }
570 }
571
572 pub(crate) fn mark_style_attr_updated(&mut self) {
574 if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
575 if let Some(mut data) = stylo_element_data.get_mut() {
576 data.hint |= RestyleHint::RESTYLE_STYLE_ATTRIBUTE;
577 }
578 }
579 self.set_dirty_descendants();
580 self.mark_ancestors_dirty();
581 }
582
583 pub fn mark_ancestors_dirty(&self) {
587 let mut current_id = self.parent;
588 while let Some(parent_id) = current_id {
589 let parent = &self.tree()[parent_id];
590 if let Some(flag) = parent.dirty_descendants_flag() {
593 if flag.swap(true, Ordering::Relaxed) {
594 break;
595 }
596 }
597 current_id = parent.parent;
598 }
599 }
600
601 pub fn damage(&self) -> Option<RestyleDamage> {
608 self.stylo_element_data_opt()
609 .and_then(|stylo| stylo.get().map(|data| data.damage))
610 }
611
612 pub fn set_damage(&mut self, damage: RestyleDamage) {
613 if let Some(stylo) = self.stylo_element_data_opt_mut() {
614 if let Some(mut data) = stylo.get_mut() {
615 data.damage = damage;
616 }
617 }
618 }
619
620 pub fn insert_damage(&mut self, damage: RestyleDamage) {
621 if let Some(stylo) = self.stylo_element_data_opt_mut() {
622 if let Some(mut data) = stylo.get_mut() {
623 data.damage |= damage;
624 }
625 }
626 }
627
628 pub fn remove_damage(&mut self, damage: RestyleDamage) {
629 if let Some(stylo) = self.stylo_element_data_opt_mut() {
630 if let Some(mut data) = stylo.get_mut() {
631 data.damage.remove(damage);
632 }
633 }
634 }
635
636 pub fn clear_damage_mut(&mut self) {
637 if let Some(stylo) = self.stylo_element_data_opt_mut() {
638 if let Some(mut data) = stylo.get_mut() {
639 data.damage = RestyleDamage::empty();
640 }
641 }
642 }
643
644 pub fn hover(&mut self) {
645 if let Some(data) = self.element_data_mut() {
646 data.element_state.insert(ElementState::HOVER);
647 }
648 self.set_restyle_hint(RestyleHint::restyle_subtree());
649 }
650
651 pub fn unhover(&mut self) {
652 if let Some(data) = self.element_data_mut() {
653 data.element_state.remove(ElementState::HOVER);
654 }
655 self.set_restyle_hint(RestyleHint::restyle_subtree());
656 }
657
658 pub fn is_hovered(&self) -> bool {
659 self.element_data()
660 .is_some_and(|data| data.element_state.contains(ElementState::HOVER))
661 }
662
663 pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
664 if let Some(data) = self.element_data_mut() {
665 data.element_state
666 .insert(ElementState::FOCUS | ElementState::FOCUSRING);
667 }
668 self.set_restyle_hint(RestyleHint::restyle_subtree());
669
670 if self
672 .element_data()
673 .and_then(|elem| elem.text_input_data())
674 .is_some()
675 {
676 shell_provider.set_ime_enabled(true);
677 let mut pos = self.absolute_position(0.0, 0.0);
678 pos.x += self.final_layout().content_box_x();
679 pos.y += self.final_layout().content_box_y();
680 let width = self.final_layout().content_box_width();
681 let height = self.final_layout().content_box_height();
682 shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
683 }
684 }
685
686 pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
687 if let Some(data) = self.element_data_mut() {
688 data.element_state
689 .remove(ElementState::FOCUS | ElementState::FOCUSRING);
690 }
691 self.set_restyle_hint(RestyleHint::restyle_subtree());
692
693 if self
695 .element_data()
696 .and_then(|elem| elem.text_input_data())
697 .is_some()
698 {
699 shell_provider.set_ime_enabled(false);
700 }
701 }
702
703 pub fn is_focussed(&self) -> bool {
704 self.element_data()
705 .is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
706 }
707
708 pub fn active(&mut self) {
709 if let Some(data) = self.element_data_mut() {
710 data.element_state.insert(ElementState::ACTIVE);
711 }
712 self.set_restyle_hint(RestyleHint::restyle_subtree());
713 }
714
715 pub fn unactive(&mut self) {
716 if let Some(data) = self.element_data_mut() {
717 data.element_state.remove(ElementState::ACTIVE);
718 }
719 self.set_restyle_hint(RestyleHint::restyle_subtree());
720 }
721
722 pub fn is_active(&self) -> bool {
723 self.element_data()
724 .is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
725 }
726
727 pub fn disable(&mut self) {
730 if let Some(data) = self.element_data_mut() {
731 if data.can_be_disabled() {
732 data.element_state.insert(ElementState::DISABLED);
733 data.element_state.remove(ElementState::ENABLED);
734 }
735 }
736 self.set_restyle_hint(RestyleHint::restyle_subtree());
737 }
738
739 pub fn enable(&mut self) {
742 if let Some(data) = self.element_data_mut() {
743 if data.can_be_disabled() {
744 data.element_state.insert(ElementState::ENABLED);
745 data.element_state.remove(ElementState::DISABLED);
746 }
747 }
748 self.set_restyle_hint(RestyleHint::restyle_subtree());
749 }
750
751 pub fn subdoc(&self) -> Option<&dyn Document> {
752 self.element_data().and_then(|el| el.sub_doc_data())
753 }
754
755 pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
756 self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
757 }
758
759 pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
760 if let Some(input_data) = self
763 .data
764 .downcast_element()
765 .and_then(|el| el.text_input_data())
766 {
767 if !input_data.is_multiline {
768 let content_box_height = self.final_layout().content_box_height();
769 let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
770 let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
771
772 return y_offset as f64;
773 }
774 }
775
776 0.0
777 }
778}
779
780#[derive(Debug, Clone, Copy, PartialEq)]
781pub enum NodeKind {
782 Document,
783 Element,
784 AnonymousBlock,
785 Text,
786 Comment,
787 DocumentFragment,
788 ShadowRoot,
789}
790
791#[derive(Debug, Clone, Copy, PartialEq, Eq)]
797pub enum TextGranularity {
798 Word,
800 Line,
802}
803
804impl TextGranularity {
805 pub fn from_click_count(count: u16) -> Option<Self> {
808 match count {
809 0 | 1 => None,
810 2 => Some(Self::Word),
811 _ => Some(Self::Line),
812 }
813 }
814}
815
816#[derive(Debug, Clone, Copy, PartialEq, Eq)]
820pub enum ShadowRootMode {
821 Open,
824 Closed,
827}
828
829#[derive(Debug, Clone)]
837pub struct ShadowRootData {
838 pub host: NodeId,
840 pub mode: ShadowRootMode,
842 pub stylesheet_nodes: Vec<NodeId>,
845}
846
847impl ShadowRootData {
848 pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
849 Self {
850 host,
851 mode,
852 stylesheet_nodes: Vec::new(),
853 }
854 }
855}
856
857#[derive(Debug, Clone)]
859pub enum NodeData {
860 Document(Box<DocumentData>),
862
863 Element(Box<ElementData>),
865
866 AnonymousBlock(Box<ElementData>),
868
869 Text(TextNodeData),
871
872 Comment {
874 contents: String,
876 },
877
878 DocumentFragment,
881
882 ShadowRoot(ShadowRootData),
884 }
891
892impl NodeData {
893 pub fn downcast_element(&self) -> Option<&ElementData> {
894 match self {
895 Self::Element(data) => Some(data),
896 Self::AnonymousBlock(data) => Some(data),
897 _ => None,
898 }
899 }
900
901 pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
902 match self {
903 Self::Element(data) => Some(data),
904 Self::AnonymousBlock(data) => Some(data),
905 _ => None,
906 }
907 }
908
909 pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
910 let Some(elem) = self.downcast_element() else {
911 return false;
912 };
913 *name == elem.name.local
914 }
915
916 pub fn attrs(&self) -> Option<&[Attribute]> {
917 Some(&self.downcast_element()?.attrs)
918 }
919
920 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
921 self.downcast_element()?.attr(name)
922 }
923
924 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
925 self.downcast_element()
926 .is_some_and(|elem| elem.has_attr(name))
927 }
928
929 pub fn kind(&self) -> NodeKind {
930 match self {
931 NodeData::Document(_) => NodeKind::Document,
932 NodeData::Element(_) => NodeKind::Element,
933 NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
934 NodeData::Text(_) => NodeKind::Text,
935 NodeData::Comment { .. } => NodeKind::Comment,
936 NodeData::DocumentFragment => NodeKind::DocumentFragment,
937 NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
938 }
939 }
940
941 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
942 match self {
943 Self::ShadowRoot(data) => Some(data),
944 _ => None,
945 }
946 }
947
948 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
949 match self {
950 Self::ShadowRoot(data) => Some(data),
951 _ => None,
952 }
953 }
954}
955
956#[derive(Debug, Clone)]
957pub struct TextNodeData {
958 pub content: String,
960}
961
962impl TextNodeData {
963 pub fn new(content: String) -> Self {
964 Self { content }
965 }
966}
967
968impl Node {
993 pub fn tree(&self) -> &crate::NodeTree {
994 unsafe { &*self.tree }
995 }
996
997 #[track_caller]
998 pub fn with(&self, id: NodeId) -> &Node {
999 self.tree().get(id).unwrap()
1000 }
1001
1002 pub fn print_tree(&self, level: usize) {
1003 println!(
1004 "{} {} {:?} {} {:?}",
1005 " ".repeat(level),
1006 self.id,
1007 self.parent,
1008 self.node_debug_str().replace('\n', ""),
1009 self.children
1010 );
1011 for child_id in self.children.iter() {
1013 let child = self.with(*child_id);
1014 child.print_tree(level + 1)
1015 }
1016 }
1017
1018 pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
1020 self.children.iter().position(|id| *id == child_id)
1021 }
1022
1023 pub fn child_index(&self) -> Option<usize> {
1025 self.tree()[self.parent?]
1026 .children
1027 .iter()
1028 .position(|id| *id == self.id)
1029 }
1030
1031 pub fn forward(&self, n: usize) -> Option<&Node> {
1033 let child_idx = self.child_index().unwrap_or(0);
1034 self.tree()[self.parent?]
1035 .children
1036 .get(child_idx + n)
1037 .map(|id| self.with(*id))
1038 }
1039
1040 pub fn backward(&self, n: usize) -> Option<&Node> {
1041 let child_idx = self.child_index().unwrap_or(0);
1042 if child_idx < n {
1043 return None;
1044 }
1045
1046 self.tree()[self.parent?]
1047 .children
1048 .get(child_idx - n)
1049 .map(|id| self.with(*id))
1050 }
1051
1052 pub fn is_element(&self) -> bool {
1053 matches!(self.data, NodeData::Element { .. })
1054 }
1055
1056 pub fn is_anonymous(&self) -> bool {
1057 matches!(self.data, NodeData::AnonymousBlock { .. })
1058 }
1059
1060 pub fn is_shadow_root(&self) -> bool {
1061 matches!(self.data, NodeData::ShadowRoot { .. })
1062 }
1063
1064 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1065 self.data.shadow_root_data()
1066 }
1067
1068 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1069 self.data.shadow_root_data_mut()
1070 }
1071
1072 pub fn shadow_root_id(&self) -> Option<NodeId> {
1075 self.element_data().and_then(|el| el.shadow_root)
1076 }
1077
1078 #[cfg(feature = "shadow-dom")]
1082 pub fn layout_dom_children(&self) -> &[NodeId] {
1083 match &self.flattened_children {
1084 Some(children) => children,
1085 None => &self.children,
1086 }
1087 }
1088
1089 #[cfg(not(feature = "shadow-dom"))]
1091 #[inline(always)]
1092 pub fn layout_dom_children(&self) -> &[NodeId] {
1093 &self.children
1094 }
1095
1096 pub fn is_text_node(&self) -> bool {
1097 matches!(self.data, NodeData::Text { .. })
1098 }
1099
1100 pub fn element_data(&self) -> Option<&ElementData> {
1101 match self.data {
1102 NodeData::Element(ref data) => Some(data),
1103 NodeData::AnonymousBlock(ref data) => Some(data),
1104 _ => None,
1105 }
1106 }
1107
1108 pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1109 match self.data {
1110 NodeData::Element(ref mut data) => Some(data),
1111 NodeData::AnonymousBlock(ref mut data) => Some(data),
1112 _ => None,
1113 }
1114 }
1115
1116 pub fn text_data(&self) -> Option<&TextNodeData> {
1117 match self.data {
1118 NodeData::Text(ref data) => Some(data),
1119 _ => None,
1120 }
1121 }
1122
1123 pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1124 match self.data {
1125 NodeData::Text(ref mut data) => Some(data),
1126 _ => None,
1127 }
1128 }
1129
1130 pub fn node_debug_str(&self) -> String {
1131 let mut s = String::new();
1132
1133 match &self.data {
1134 NodeData::Document(_) => write!(s, "DOCUMENT"),
1135 NodeData::DocumentFragment => write!(s, "FRAGMENT"),
1136 NodeData::Text(data) => {
1138 let bytes = data.content.as_bytes();
1139 write!(
1140 s,
1141 "TEXT {}",
1142 std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1143 .unwrap_or("INVALID UTF8")
1144 )
1145 }
1146 NodeData::Comment { .. } => write!(s, "COMMENT"),
1147 NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1148 NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1149 NodeData::Element(data) => {
1150 let name = &data.name;
1151 let class = self.attr(local_name!("class")).unwrap_or("");
1152 let id = self.attr(local_name!("id")).unwrap_or("");
1153 let display = self.display_constructed_as().to_css_string();
1154 write!(s, "<{}", name.local).unwrap();
1155 if !id.is_empty() {
1156 write!(s, " #{id}").unwrap();
1157 }
1158 if !class.is_empty() {
1159 if class.contains(' ') {
1160 write!(s, " class=\"{class}\"").unwrap()
1161 } else {
1162 write!(s, " .{class}").unwrap()
1163 }
1164 }
1165 write!(s, "> ({display})")
1166 } }
1168 .unwrap();
1169 s
1170 }
1171
1172 pub fn outer_html(&self) -> String {
1180 let mut output = String::new();
1181 self.write_outer_html(&mut output);
1182 output
1183 }
1184
1185 pub fn outer_html_pretty(&self) -> String {
1201 let mut output = String::new();
1202 self.write_outer_html_pretty(&mut output);
1203 output
1204 }
1205
1206 pub fn write_outer_html(&self, writer: &mut String) {
1207 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1208 }
1209
1210 #[cfg(feature = "svg")]
1211 pub(crate) fn write_outer_html_with_current_color(
1212 &self,
1213 writer: &mut String,
1214 current_color: &str,
1215 ) {
1216 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1217 }
1218
1219 pub fn write_outer_html_pretty(&self, writer: &mut String) {
1220 self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1221 }
1222
1223 fn write_outer_html_in_style(
1224 &self,
1225 writer: &mut String,
1226 style: OutputStyle,
1227 nesting: usize,
1228 current_color_override: Option<&str>,
1229 ) {
1230 const INDENT: &str = " ";
1231 let has_children = !self.children.is_empty();
1232 let computed_current_color = || {
1233 self.primary_styles()
1234 .map(|style| style.clone_color())
1235 .map(|color| crate::util::absolute_color_to_svg_css(&color))
1236 };
1237 let current_color = current_color_override
1238 .map(ToOwned::to_owned)
1239 .or_else(computed_current_color);
1240
1241 match &self.data {
1242 NodeData::Document(_) => {}
1243 NodeData::Comment { .. } => {}
1244 NodeData::AnonymousBlock(_) => {}
1245 NodeData::ShadowRoot(_) => {}
1246 NodeData::DocumentFragment => {}
1250 NodeData::Text(data) => {
1252 if matches!(style, OutputStyle::Pretty) {
1253 for _ in 0..nesting {
1254 writer.push_str(INDENT);
1255 }
1256 }
1257 writer.push_str(data.content.as_str());
1258 if matches!(style, OutputStyle::Pretty) {
1259 writer.push('\n');
1260 }
1261 }
1262 NodeData::Element(data) => {
1263 if matches!(style, OutputStyle::Pretty) {
1264 for _ in 0..nesting {
1265 writer.push_str(INDENT);
1266 }
1267 }
1268 writer.push('<');
1269 writer.push_str(&data.name.local);
1270
1271 for attr in data.attrs() {
1272 writer.push(' ');
1273 writer.push_str(&attr.name.local);
1274 writer.push_str("=\"");
1275 #[allow(clippy::unnecessary_unwrap)] if current_color.is_some() && attr.value.contains("currentColor") {
1277 let value = attr
1278 .value
1279 .replace("currentColor", current_color.as_ref().unwrap());
1280 encode_quoted_attribute_to_string(&value, writer);
1281 } else {
1282 encode_quoted_attribute_to_string(&attr.value, writer);
1283 }
1284 writer.push('"');
1285 }
1286 if !has_children {
1287 writer.push_str(" /");
1288 }
1289 writer.push('>');
1290 if matches!(style, OutputStyle::Pretty) {
1291 writer.push('\n');
1292 }
1293
1294 if has_children {
1295 for &child_id in &self.children {
1296 self.tree()[child_id].write_outer_html_in_style(
1297 writer,
1298 style,
1299 nesting + 1,
1300 current_color_override,
1301 );
1302 }
1303
1304 if matches!(style, OutputStyle::Pretty) {
1305 for _ in 0..nesting {
1306 writer.push_str(INDENT);
1307 }
1308 }
1309 writer.push_str("</");
1310 writer.push_str(&data.name.local);
1311 writer.push('>');
1312 if matches!(style, OutputStyle::Pretty) {
1313 writer.push('\n');
1314 }
1315 }
1316 }
1317 }
1318 }
1319
1320 pub fn attrs(&self) -> Option<&[Attribute]> {
1321 Some(&self.element_data()?.attrs)
1322 }
1323
1324 pub fn attr(&self, name: LocalName) -> Option<&str> {
1325 let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
1326 Some(&attr.value)
1327 }
1328
1329 pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1330 self.stylo_element_data_opt()
1331 .and_then(|stylo| stylo.primary_styles())
1332 }
1333
1334 pub fn text_content(&self) -> String {
1335 let mut out = String::new();
1336 self.write_text_content(&mut out);
1337 out
1338 }
1339
1340 pub fn write_text_content<W: Write>(&self, out: &mut W) {
1353 match &self.data {
1354 NodeData::Text(data) => {
1355 let _ = out.write_str(&data.content);
1356 }
1357 NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
1358 for child_id in self.children.iter() {
1359 self.with(*child_id).write_text_content(out);
1360 }
1361 }
1362 _ => {}
1363 }
1364 }
1365
1366 pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1367 if let NodeData::Element(ref mut elem_data) = self.data {
1368 if let Some(guard) = elem_data.guard.clone() {
1369 elem_data.flush_style_attribute(&guard, url_extra_data);
1370 }
1371 }
1372 }
1373
1374 pub fn order(&self) -> i32 {
1375 self.primary_styles()
1376 .map(|s| match s.pseudo() {
1377 Some(PseudoElement::Before) => i32::MIN,
1378 Some(PseudoElement::After) => i32::MAX,
1379 _ => s.clone_order(),
1380 })
1381 .unwrap_or(0)
1382 }
1383
1384 pub fn z_index(&self) -> i32 {
1385 self.primary_styles()
1386 .map(|s| s.clone_z_index().integer_or(0))
1387 .unwrap_or(0)
1388 }
1389
1390 pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1392 let Some(style) = self.primary_styles() else {
1393 return false;
1394 };
1395
1396 let position = style.clone_position();
1397 let has_z_index = !style.clone_z_index().is_auto();
1398
1399 if style.clone_opacity() != 1.0 {
1400 return true;
1401 }
1402
1403 let position_based = match position {
1404 Position::Fixed | Position::Sticky => true,
1405 Position::Relative | Position::Absolute => has_z_index,
1406 Position::Static => has_z_index && is_flex_or_grid_item,
1407 };
1408 if position_based {
1409 return true;
1410 }
1411
1412 let box_styles = style.get_box();
1420 if !box_styles.transform.0.is_empty()
1421 || !matches!(box_styles.translate, Translate::None)
1422 || !matches!(box_styles.rotate, Rotate::None)
1423 || !matches!(box_styles.scale, Scale::None)
1424 {
1425 return true;
1426 }
1427
1428 if box_styles.isolation == Isolation::Isolate {
1433 return true;
1434 }
1435
1436 false
1443 }
1444
1445 pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
1454 self.hit_inner(x, y, scale, &mut None)
1455 }
1456
1457 pub(crate) fn hit_inner(
1462 &self,
1463 x: f32,
1464 y: f32,
1465 scale: f64,
1466 scrollbar: &mut Option<crate::node::ScrollbarRef>,
1467 ) -> Option<HitResult> {
1468 use style::computed_values::pointer_events::T as PointerEvents;
1469 use style::computed_values::visibility::T as Visibility;
1470
1471 if matches!(self.style().display, taffy::Display::None) {
1480 return None;
1481 }
1482
1483 if let Some(style) = self.primary_styles() {
1485 if matches!(
1486 style.clone_visibility(),
1487 Visibility::Hidden | Visibility::Collapse
1488 ) {
1489 return None;
1490 }
1491 }
1492
1493 let pointer_events_none = self
1496 .primary_styles()
1497 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1498
1499 let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
1500 let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
1501
1502 if let Some(t) = *self.transform() {
1503 let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
1504 x = (p.x / scale) as f32;
1505 y = (p.y / scale) as f32;
1506 }
1507
1508 let size = self.final_layout().size;
1509 let matches_self = !(x < 0.0
1510 || x > size.width + self.scroll_offset().x as f32
1511 || y < 0.0
1512 || y > size.height + self.scroll_offset().y as f32);
1513
1514 let content_size = self.final_layout().content_size;
1515 let matches_content = !(x < 0.0
1516 || x > content_size.width + self.scroll_offset().x as f32
1517 || y < 0.0
1518 || y > content_size.height + self.scroll_offset().y as f32);
1519
1520 let matches_hoisted_content = match &self.stacking_context {
1521 Some(sc) => {
1522 let content_area = sc.content_area;
1523 x >= content_area.left + self.scroll_offset().x as f32
1524 && x <= content_area.right + self.scroll_offset().x as f32
1525 && y >= content_area.top + self.scroll_offset().y as f32
1526 && y <= content_area.bottom + self.scroll_offset().y as f32
1527 }
1528 None => false,
1529 };
1530
1531 let overflow = *self.scrollable_overflow();
1534
1535 let matches_overflow = x >= (overflow.x0 / scale) as f32
1536 && x <= (overflow.x1 / scale) as f32
1537 && y >= (overflow.y0 / scale) as f32
1538 && y <= (overflow.y1 / scale) as f32;
1539
1540 if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
1541 return None;
1542 }
1543
1544 if matches_self
1547 && let Some(sb) = self.scrollbar_at_local(
1548 (x - self.scroll_offset().x as f32) as f64,
1549 (y - self.scroll_offset().y as f32) as f64,
1550 )
1551 {
1552 *scrollbar = Some(sb);
1553 }
1554
1555 if self.flags.is_inline_root() {
1556 let content_box_offset = taffy::Point {
1557 x: self.final_layout().padding.left + self.final_layout().border.left,
1558 y: self.final_layout().padding.top + self.final_layout().border.top,
1559 };
1560 x -= content_box_offset.x;
1561 y -= content_box_offset.y;
1562 }
1563
1564 if matches_hoisted_content {
1566 if let Some(hoisted) = &self.stacking_context {
1567 for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
1568 let x = x - hoisted_child.position.x;
1569 let y = y - hoisted_child.position.y;
1570 if let Some(hit) = self
1571 .with(hoisted_child.node_id)
1572 .hit_inner(x, y, scale, scrollbar)
1573 {
1574 return Some(hit);
1575 }
1576 }
1577 }
1578 }
1579
1580 for child_id in self.paint_children.borrow().iter().flatten().rev() {
1582 if let Some(hit) = self.with(*child_id).hit_inner(x, y, scale, scrollbar) {
1583 return Some(hit);
1584 }
1585 }
1586
1587 if matches_hoisted_content {
1589 if let Some(hoisted) = &self.stacking_context {
1590 for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
1591 let x = x - hoisted_child.position.x;
1592 let y = y - hoisted_child.position.y;
1593 if let Some(hit) = self
1594 .with(hoisted_child.node_id)
1595 .hit_inner(x, y, scale, scrollbar)
1596 {
1597 return Some(hit);
1598 }
1599 }
1600 }
1601 }
1602
1603 if self.flags.is_inline_root() {
1605 let element_data = &self.element_data().unwrap();
1606 if let Some(ild) = element_data.inline_layout_data.as_ref() {
1607 let layout = &ild.layout;
1608 let scale = layout.scale();
1609
1610 if let Some((cluster, _side)) =
1611 Cluster::from_point_exact(layout, x * scale, y * scale)
1612 {
1613 let style_index = cluster.glyphs().next()?.style_index();
1614 let node_id = layout.styles()[style_index].brush.id;
1615 let text_pointer_events_none = self
1616 .with(node_id)
1617 .primary_styles()
1618 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1619 if !text_pointer_events_none {
1620 return Some(HitResult {
1621 node_id,
1622 x,
1623 y,
1624 is_text: true,
1625 });
1626 }
1627 }
1628 }
1629 }
1630
1631 if matches_self && !pointer_events_none {
1633 return Some(HitResult {
1634 node_id: self.id,
1635 x,
1636 y,
1637 is_text: false,
1638 });
1639 }
1640
1641 None
1642 }
1643
1644 pub fn inline_root_ancestor(&self) -> Option<&Node> {
1647 let mut node = self;
1648 loop {
1649 if node.flags.is_inline_root() {
1650 return Some(node);
1651 }
1652 let id = node.layout_parent.get()?;
1653 node = self.with(id);
1654 }
1655 }
1656
1657 pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
1661 if !self.flags.is_inline_root() {
1662 return None;
1663 }
1664
1665 let element_data = self.element_data()?;
1666 let inline_layout = element_data.inline_layout_data.as_ref()?;
1667 let layout = &inline_layout.layout;
1668 let scale = layout.scale();
1669
1670 let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
1672
1673 let is_leading = side == ClusterSide::Left;
1678 let offset = if cluster.is_rtl() {
1679 if is_leading {
1680 cluster.text_range().end
1681 } else {
1682 cluster.text_range().start
1683 }
1684 } else {
1685 if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
1687 cluster.text_range().start
1688 } else {
1689 cluster.text_range().end
1690 }
1691 };
1692
1693 Some(offset)
1694 }
1695
1696 pub fn text_range_at_point(
1706 &self,
1707 x: f32,
1708 y: f32,
1709 granularity: TextGranularity,
1710 ) -> Option<Range<usize>> {
1711 if !self.flags.is_inline_root() {
1712 return None;
1713 }
1714
1715 let element_data = self.element_data()?;
1716 let inline_layout = element_data.inline_layout_data.as_ref()?;
1717 let layout = &inline_layout.layout;
1718 let scale = layout.scale();
1719 let (x, y) = (x * scale, y * scale);
1720
1721 Cluster::from_point(layout, x, y)?;
1726
1727 let selection = match granularity {
1728 TextGranularity::Word => Selection::word_from_point(layout, x, y),
1729 TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
1733 };
1734
1735 let range = selection.text_range();
1736 if range.is_empty() { None } else { Some(range) }
1737 }
1738
1739 pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
1741 let x = x + self.final_layout().location.x;
1744 let y = y + self.final_layout().location.y;
1745
1746 self.layout_parent
1748 .get()
1749 .and_then(|id| self.tree().get(id))
1750 .map(|parent| {
1751 parent.absolute_position(
1752 x - parent.scroll_offset().x as f32,
1753 y - parent.scroll_offset().y as f32,
1754 )
1755 })
1756 .unwrap_or(crate::util::Point { x, y })
1757 }
1758
1759 fn is_offset_parent(&self) -> bool {
1762 let Some(styles) = self.primary_styles() else {
1763 return false;
1764 };
1765 if styles.get_box().position != Position::Static {
1766 return true;
1767 }
1768 self.data.is_element_with_tag_name(&local_name!("body"))
1769 || self.data.is_element_with_tag_name(&local_name!("td"))
1770 || self.data.is_element_with_tag_name(&local_name!("th"))
1771 }
1772
1773 pub fn offset_parent(&self) -> Option<&Node> {
1776 let mut node = self;
1777 loop {
1778 node = self.with(node.layout_parent.get()?);
1779 if node.is_offset_parent() {
1780 return Some(node);
1781 }
1782 }
1783 }
1784
1785 pub fn offset_top_left(&self) -> crate::util::Point<f32> {
1788 let mut x = 0.0;
1789 let mut y = 0.0;
1790 let mut current = self;
1791 loop {
1792 let layout = current.final_layout();
1793 x += layout.location.x;
1794 y += layout.location.y;
1795
1796 let Some(parent_id) = current.layout_parent.get() else {
1797 break;
1798 };
1799 let parent = self.with(parent_id);
1800 if parent.is_offset_parent() {
1801 let border = parent.final_layout().border;
1802 x -= border.left;
1803 y -= border.top;
1804 break;
1805 }
1806 current = parent;
1807 }
1808 crate::util::Point { x, y }
1809 }
1810
1811 pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
1813 DomEventData::Click(self.synthetic_click_event_data(mods))
1814 }
1815
1816 pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
1817 let absolute_position = self.absolute_position(0.0, 0.0);
1818 let x = absolute_position.x + (self.final_layout().size.width / 2.0);
1819 let y = absolute_position.y + (self.final_layout().size.height / 2.0);
1820
1821 BlitzPointerEvent {
1822 id: BlitzPointerId::Mouse,
1823 is_primary: true,
1824 coords: PointerCoords {
1825 page_x: x,
1826 page_y: y,
1827
1828 screen_x: x,
1830 screen_y: y,
1831 client_x: x,
1832 client_y: y,
1833 },
1834 mods,
1835 button: Default::default(),
1836 buttons: Default::default(),
1837 details: Default::default(),
1838 element: Default::default(),
1839 active_pointers: Default::default(),
1840 }
1841 }
1842}
1843
1844impl PartialEq for Node {
1846 fn eq(&self, other: &Self) -> bool {
1847 self.id == other.id
1848 }
1849}
1850
1851impl Eq for Node {}
1852
1853impl std::fmt::Debug for Node {
1854 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1855 f.debug_struct("NodeData")
1857 .field("parent", &self.parent)
1858 .field("id", &self.id)
1859 .field("is_inline_root", &self.flags.is_inline_root())
1860 .field("children", &self.children)
1861 .field("layout_children", &self.layout_children.borrow())
1862 .field("node", &self.data)
1864 .field("stylo_element_data", &self.stylo_element_data_opt())
1865 .finish()
1868 }
1869}
1870
1871#[cfg(test)]
1872mod test {
1873 use style_dom::ElementState;
1874
1875 use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
1876
1877 #[test]
1878 fn absolute_position_tolerates_a_detached_layout_parent() {
1879 let mut document = BaseDocument::new(DocumentConfig::default());
1880 let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
1881 qual_name!("section"),
1882 vec![],
1883 ))));
1884 let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
1885 qual_name!("button"),
1886 vec![],
1887 ))));
1888 document
1889 .get_node(child)
1890 .unwrap()
1891 .layout_parent
1892 .set(Some(parent));
1893
1894 document.remove_node_from_tree(parent);
1899
1900 assert_eq!(
1901 document
1902 .get_node(child)
1903 .unwrap()
1904 .absolute_position(4.0, 7.0),
1905 crate::util::Point { x: 4.0, y: 7.0 },
1906 );
1907 }
1908
1909 #[test]
1910 fn create_node_with_disabled_attr() {
1911 let mut document = BaseDocument::new(DocumentConfig::default());
1912 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1913 qual_name!("button"),
1914 vec![Attribute {
1915 name: qual_name!("disabled"),
1916 value: "".into(),
1917 }],
1918 ))));
1919 let node = document.get_node(node).unwrap();
1920
1921 assert!(
1922 node.element_state().contains(ElementState::DISABLED),
1923 "form node is disabled"
1924 );
1925 assert!(
1926 !node.element_state().contains(ElementState::ENABLED),
1927 "form node is not enabled"
1928 );
1929 }
1930
1931 #[test]
1932 fn ignore_disabled_attr_content() {
1933 let mut document = BaseDocument::new(DocumentConfig::default());
1934 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1935 qual_name!("button"),
1936 vec![Attribute {
1937 name: qual_name!("disabled"),
1938 value: "false".into(),
1939 }],
1940 ))));
1941 let node = document.get_node(node).unwrap();
1942
1943 assert!(
1944 node.element_state().contains(ElementState::DISABLED),
1945 "form node is disabled"
1946 );
1947 assert!(
1948 !node.element_state().contains(ElementState::ENABLED),
1949 "form node is not enabled"
1950 );
1951 }
1952
1953 #[test]
1954 fn create_node_with_ignored_disable() {
1955 let mut document = BaseDocument::new(DocumentConfig::default());
1956 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1957 qual_name!("a"),
1958 vec![Attribute {
1959 name: qual_name!("disabled"),
1960 value: "".into(),
1961 }],
1962 ))));
1963 let node = document.get_node(node).unwrap();
1964
1965 assert!(
1966 !node.element_state().contains(ElementState::DISABLED),
1967 "Non form node cannot be disabled"
1968 );
1969 assert!(
1970 !node.element_state().contains(ElementState::ENABLED),
1971 "Non form node cannot be enabled"
1972 );
1973 }
1974
1975 #[test]
1976 fn create_empty_enabled_node() {
1977 let mut document = BaseDocument::new(DocumentConfig::default());
1978 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1979 qual_name!("button"),
1980 vec![],
1981 ))));
1982 let node = document.get_node(node).unwrap();
1983
1984 assert!(
1985 node.element_state().contains(ElementState::ENABLED),
1986 "Button should be enabled by default"
1987 );
1988 }
1989}