1use atomic_refcell::{AtomicRef, AtomicRefCell};
2use bitflags::bitflags;
3use blitz_traits::events::{BlitzMouseButtonEvent, DomEventData, HitResult};
4use keyboard_types::Modifiers;
5use markup5ever::{LocalName, local_name};
6use parley::Cluster;
7use peniko::kurbo;
8use selectors::matching::ElementSelectorFlags;
9use slab::Slab;
10use std::cell::{Cell, RefCell};
11use std::fmt::Write;
12use std::sync::atomic::AtomicBool;
13use style::Atom;
14use style::invalidation::element::restyle_hints::RestyleHint;
15use style::properties::ComputedValues;
16use style::properties::generated::longhands::position::computed_value::T as Position;
17use style::selector_parser::PseudoElement;
18use style::stylesheets::UrlExtraData;
19use style::values::computed::Display as StyloDisplay;
20use style::values::specified::box_::{DisplayInside, DisplayOutside};
21use style::{data::ElementData as StyloElementData, shared_lock::SharedRwLock};
22use style_dom::ElementState;
23use style_traits::values::ToCss;
24use taffy::{
25 Cache,
26 prelude::{Layout, Style},
27};
28
29use super::{Attribute, ElementData};
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum DisplayOuter {
33 Block,
34 Inline,
35 None,
36}
37
38bitflags! {
39 #[derive(Clone, Copy, PartialEq)]
40 pub struct NodeFlags: u32 {
41 const IS_INLINE_ROOT = 0b00000001;
43 const IS_TABLE_ROOT = 0b00000010;
45 const IS_IN_DOCUMENT = 0b00000100;
47 }
48}
49
50impl NodeFlags {
51 #[inline(always)]
52 pub fn is_inline_root(&self) -> bool {
53 self.contains(Self::IS_INLINE_ROOT)
54 }
55
56 #[inline(always)]
57 pub fn is_table_root(&self) -> bool {
58 self.contains(Self::IS_TABLE_ROOT)
59 }
60
61 #[inline(always)]
62 pub fn is_in_document(&self) -> bool {
63 self.contains(Self::IS_IN_DOCUMENT)
64 }
65
66 #[inline(always)]
67 pub fn reset_construction_flags(&mut self) {
68 self.remove(Self::IS_INLINE_ROOT);
69 self.remove(Self::IS_TABLE_ROOT);
70 }
71}
72
73pub struct Node {
74 tree: *mut Slab<Node>,
76
77 pub id: usize,
79 pub parent: Option<usize>,
81 pub children: Vec<usize>,
83 pub layout_parent: Cell<Option<usize>>,
85 pub layout_children: RefCell<Option<Vec<usize>>>,
87 pub paint_children: RefCell<Option<Vec<usize>>>,
89
90 pub flags: NodeFlags,
92
93 pub data: NodeData,
95
96 pub stylo_element_data: AtomicRefCell<Option<StyloElementData>>,
99 pub selector_flags: AtomicRefCell<ElementSelectorFlags>,
100 pub guard: SharedRwLock,
101 pub element_state: ElementState,
102
103 pub before: Option<usize>,
105 pub after: Option<usize>,
106
107 pub style: Style<Atom>,
109 pub has_snapshot: bool,
110 pub snapshot_handled: AtomicBool,
111 pub display_constructed_as: StyloDisplay,
112 pub cache: Cache,
113 pub unrounded_layout: Layout,
114 pub final_layout: Layout,
115 pub scroll_offset: kurbo::Point,
116}
117
118impl Node {
119 pub(crate) fn new(
120 tree: *mut Slab<Node>,
121 id: usize,
122 guard: SharedRwLock,
123 data: NodeData,
124 ) -> Self {
125 Self {
126 tree,
127
128 id,
129 parent: None,
130 children: vec![],
131 layout_parent: Cell::new(None),
132 layout_children: RefCell::new(None),
133 paint_children: RefCell::new(None),
134
135 flags: NodeFlags::empty(),
136 data,
137
138 stylo_element_data: Default::default(),
139 selector_flags: AtomicRefCell::new(ElementSelectorFlags::empty()),
140 guard,
141 element_state: ElementState::empty(),
142
143 before: None,
144 after: None,
145
146 style: Default::default(),
147 has_snapshot: false,
148 snapshot_handled: AtomicBool::new(false),
149 display_constructed_as: StyloDisplay::Block,
150 cache: Cache::new(),
151 unrounded_layout: Layout::new(),
152 final_layout: Layout::new(),
153 scroll_offset: kurbo::Point::ZERO,
154 }
155 }
156
157 pub fn pe_by_index(&self, index: usize) -> Option<usize> {
158 match index {
159 0 => self.after,
160 1 => self.before,
161 _ => panic!("Invalid pseudo element index"),
162 }
163 }
164
165 pub fn set_pe_by_index(&mut self, index: usize, value: Option<usize>) {
166 match index {
167 0 => self.after = value,
168 1 => self.before = value,
169 _ => panic!("Invalid pseudo element index"),
170 }
171 }
172
173 pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
174 Some(self.primary_styles().as_ref()?.clone_display())
175 }
176
177 pub fn is_or_contains_block(&self) -> bool {
178 let style = self.primary_styles();
179 let style = style.as_ref();
180
181 let position = style
183 .map(|s| s.clone_position())
184 .unwrap_or(Position::Relative);
185 let is_in_flow = matches!(
186 position,
187 Position::Static | Position::Relative | Position::Sticky
188 );
189 if !is_in_flow {
190 return false;
191 }
192 let display = style
193 .map(|s| s.clone_display())
194 .unwrap_or(StyloDisplay::inline());
195 match display.outside() {
196 DisplayOutside::None => false,
197 DisplayOutside::Block => true,
198 _ => {
199 if display.inside() == DisplayInside::Flow {
200 self.children
201 .iter()
202 .copied()
203 .any(|child_id| self.tree()[child_id].is_or_contains_block())
204 } else {
205 false
206 }
207 }
208 }
209 }
210
211 pub fn is_focussable(&self) -> bool {
212 self.data
213 .downcast_element()
214 .map(|el| el.is_focussable)
215 .unwrap_or(false)
216 }
217
218 pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
219 if let Some(element_data) = self.stylo_element_data.borrow_mut().as_mut() {
220 element_data.hint.insert(hint);
221 }
222 }
223
224 pub fn hover(&mut self) {
225 self.element_state.insert(ElementState::HOVER);
226 self.set_restyle_hint(RestyleHint::restyle_subtree());
227 }
228
229 pub fn unhover(&mut self) {
230 self.element_state.remove(ElementState::HOVER);
231 self.set_restyle_hint(RestyleHint::restyle_subtree());
232 }
233
234 pub fn is_hovered(&self) -> bool {
235 self.element_state.contains(ElementState::HOVER)
236 }
237
238 pub fn focus(&mut self) {
239 self.element_state
240 .insert(ElementState::FOCUS | ElementState::FOCUSRING);
241 self.set_restyle_hint(RestyleHint::restyle_subtree());
242 }
243
244 pub fn blur(&mut self) {
245 self.element_state
246 .remove(ElementState::FOCUS | ElementState::FOCUSRING);
247 self.set_restyle_hint(RestyleHint::restyle_subtree());
248 }
249
250 pub fn is_focussed(&self) -> bool {
251 self.element_state.contains(ElementState::FOCUS)
252 }
253
254 pub fn active(&mut self) {
255 self.element_state.insert(ElementState::ACTIVE);
256 self.set_restyle_hint(RestyleHint::restyle_subtree());
257 }
258
259 pub fn unactive(&mut self) {
260 self.element_state.remove(ElementState::ACTIVE);
261 self.set_restyle_hint(RestyleHint::restyle_subtree());
262 }
263
264 pub fn is_active(&self) -> bool {
265 self.element_state.contains(ElementState::ACTIVE)
266 }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq)]
270pub enum NodeKind {
271 Document,
272 Element,
273 AnonymousBlock,
274 Text,
275 Comment,
276}
277
278#[derive(Debug, Clone)]
280pub enum NodeData {
281 Document,
283
284 Element(ElementData),
286
287 AnonymousBlock(ElementData),
289
290 Text(TextNodeData),
292
293 Comment,
295 }
304
305impl NodeData {
306 pub fn downcast_element(&self) -> Option<&ElementData> {
307 match self {
308 Self::Element(data) => Some(data),
309 Self::AnonymousBlock(data) => Some(data),
310 _ => None,
311 }
312 }
313
314 pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
315 match self {
316 Self::Element(data) => Some(data),
317 Self::AnonymousBlock(data) => Some(data),
318 _ => None,
319 }
320 }
321
322 pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
323 let Some(elem) = self.downcast_element() else {
324 return false;
325 };
326 *name == elem.name.local
327 }
328
329 pub fn attrs(&self) -> Option<&[Attribute]> {
330 Some(&self.downcast_element()?.attrs)
331 }
332
333 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
334 self.downcast_element()?.attr(name)
335 }
336
337 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
338 self.downcast_element()
339 .is_some_and(|elem| elem.has_attr(name))
340 }
341
342 pub fn kind(&self) -> NodeKind {
343 match self {
344 NodeData::Document => NodeKind::Document,
345 NodeData::Element(_) => NodeKind::Element,
346 NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
347 NodeData::Text(_) => NodeKind::Text,
348 NodeData::Comment => NodeKind::Comment,
349 }
350 }
351}
352
353#[derive(Debug, Clone)]
354pub struct TextNodeData {
355 pub content: String,
357}
358
359impl TextNodeData {
360 pub fn new(content: String) -> Self {
361 Self { content }
362 }
363}
364
365impl Node {
390 pub fn tree(&self) -> &Slab<Node> {
391 unsafe { &*self.tree }
392 }
393
394 #[track_caller]
395 pub fn with(&self, id: usize) -> &Node {
396 self.tree().get(id).unwrap()
397 }
398
399 pub fn print_tree(&self, level: usize) {
400 println!(
401 "{} {} {:?} {} {:?}",
402 " ".repeat(level),
403 self.id,
404 self.parent,
405 self.node_debug_str().replace('\n', ""),
406 self.children
407 );
408 for child_id in self.children.iter() {
410 let child = self.with(*child_id);
411 child.print_tree(level + 1)
412 }
413 }
414
415 pub fn index_of_child(&self, child_id: usize) -> Option<usize> {
417 self.children.iter().position(|id| *id == child_id)
418 }
419
420 pub fn child_index(&self) -> Option<usize> {
422 self.tree()[self.parent?]
423 .children
424 .iter()
425 .position(|id| *id == self.id)
426 }
427
428 pub fn forward(&self, n: usize) -> Option<&Node> {
430 let child_idx = self.child_index().unwrap_or(0);
431 self.tree()[self.parent?]
432 .children
433 .get(child_idx + n)
434 .map(|id| self.with(*id))
435 }
436
437 pub fn backward(&self, n: usize) -> Option<&Node> {
438 let child_idx = self.child_index().unwrap_or(0);
439 if child_idx < n {
440 return None;
441 }
442
443 self.tree()[self.parent?]
444 .children
445 .get(child_idx - n)
446 .map(|id| self.with(*id))
447 }
448
449 pub fn is_element(&self) -> bool {
450 matches!(self.data, NodeData::Element { .. })
451 }
452
453 pub fn is_anonymous(&self) -> bool {
454 matches!(self.data, NodeData::AnonymousBlock { .. })
455 }
456
457 pub fn is_text_node(&self) -> bool {
458 matches!(self.data, NodeData::Text { .. })
459 }
460
461 pub fn element_data(&self) -> Option<&ElementData> {
462 match self.data {
463 NodeData::Element(ref data) => Some(data),
464 NodeData::AnonymousBlock(ref data) => Some(data),
465 _ => None,
466 }
467 }
468
469 pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
470 match self.data {
471 NodeData::Element(ref mut data) => Some(data),
472 NodeData::AnonymousBlock(ref mut data) => Some(data),
473 _ => None,
474 }
475 }
476
477 pub fn text_data(&self) -> Option<&TextNodeData> {
478 match self.data {
479 NodeData::Text(ref data) => Some(data),
480 _ => None,
481 }
482 }
483
484 pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
485 match self.data {
486 NodeData::Text(ref mut data) => Some(data),
487 _ => None,
488 }
489 }
490
491 pub fn node_debug_str(&self) -> String {
492 let mut s = String::new();
493
494 match &self.data {
495 NodeData::Document => write!(s, "DOCUMENT"),
496 NodeData::Text(data) => {
498 let bytes = data.content.as_bytes();
499 write!(
500 s,
501 "TEXT {}",
502 &std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
503 .unwrap_or("INVALID UTF8")
504 )
505 }
506 NodeData::Comment => write!(
507 s,
508 "COMMENT",
509 ),
511 NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
512 NodeData::Element(data) => {
513 let name = &data.name;
514 let class = self.attr(local_name!("class")).unwrap_or("");
515 let display = self.display_constructed_as.to_css_string();
516 if !class.is_empty() {
517 write!(s, "<{} class=\"{}\"> ({})", name.local, class, display)
518 } else {
519 write!(s, "<{}> ({})", name.local, display)
520 }
521 } }
523 .unwrap();
524 s
525 }
526
527 pub fn outer_html(&self) -> String {
528 let mut output = String::new();
529 self.write_outer_html(&mut output);
530 output
531 }
532
533 pub fn write_outer_html(&self, writer: &mut String) {
534 let has_children = !self.children.is_empty();
535 let current_color = self
536 .primary_styles()
537 .map(|style| style.clone_color())
538 .map(|color| color.to_css_string());
539
540 match &self.data {
541 NodeData::Document => {}
542 NodeData::Comment => {}
543 NodeData::AnonymousBlock(_) => {}
544 NodeData::Text(data) => {
546 writer.push_str(data.content.as_str());
547 }
548 NodeData::Element(data) => {
549 writer.push('<');
550 writer.push_str(&data.name.local);
551
552 for attr in data.attrs() {
553 writer.push(' ');
554 writer.push_str(&attr.name.local);
555 writer.push_str("=\"");
556 #[allow(clippy::unnecessary_unwrap)] if current_color.is_some() && attr.value.contains("currentColor") {
558 writer.push_str(
559 &attr
560 .value
561 .replace("currentColor", current_color.as_ref().unwrap()),
562 );
563 } else {
564 writer.push_str(&attr.value);
565 }
566 writer.push('"');
567 }
568 if !has_children {
569 writer.push_str(" /");
570 }
571 writer.push('>');
572
573 if has_children {
574 for &child_id in &self.children {
575 self.tree()[child_id].write_outer_html(writer);
576 }
577
578 writer.push_str("</");
579 writer.push_str(&data.name.local);
580 writer.push('>');
581 }
582 }
583 }
584 }
585
586 pub fn attrs(&self) -> Option<&[Attribute]> {
587 Some(&self.element_data()?.attrs)
588 }
589
590 pub fn attr(&self, name: LocalName) -> Option<&str> {
591 let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
592 Some(&attr.value)
593 }
594
595 pub fn primary_styles(&self) -> Option<AtomicRef<'_, ComputedValues>> {
596 let stylo_element_data = self.stylo_element_data.borrow();
597 if stylo_element_data
598 .as_ref()
599 .and_then(|d| d.styles.get_primary())
600 .is_some()
601 {
602 Some(AtomicRef::map(
603 stylo_element_data,
604 |data: &Option<StyloElementData>| -> &ComputedValues {
605 data.as_ref().unwrap().styles.get_primary().unwrap()
606 },
607 ))
608 } else {
609 None
610 }
611 }
612
613 pub fn text_content(&self) -> String {
614 let mut out = String::new();
615 self.write_text_content(&mut out);
616 out
617 }
618
619 fn write_text_content(&self, out: &mut String) {
620 match &self.data {
621 NodeData::Text(data) => {
622 out.push_str(&data.content);
623 }
624 NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
625 for child_id in self.children.iter() {
626 self.with(*child_id).write_text_content(out);
627 }
628 }
629 _ => {}
630 }
631 }
632
633 pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
634 if let NodeData::Element(ref mut elem_data) = self.data {
635 elem_data.flush_style_attribute(&self.guard, url_extra_data);
636 }
637 }
638
639 pub fn order(&self) -> i32 {
640 self.primary_styles()
641 .map(|s| match s.pseudo() {
642 Some(PseudoElement::Before) => i32::MIN,
643 Some(PseudoElement::After) => i32::MAX,
644 _ => s.clone_order(),
645 })
646 .unwrap_or(0)
647 }
648
649 pub fn z_index(&self) -> i32 {
650 self.primary_styles()
651 .map(|s| s.clone_z_index().integer_or(0))
652 .unwrap_or(0)
653 }
654
655 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
664 let mut x = x - self.final_layout.location.x + self.scroll_offset.x as f32;
665 let mut y = y - self.final_layout.location.y + self.scroll_offset.y as f32;
666
667 let size = self.final_layout.size;
668 let matches_self = !(x < 0.0
669 || x > size.width + self.scroll_offset.x as f32
670 || y < 0.0
671 || y > size.height + self.scroll_offset.y as f32);
672
673 let content_size = self.final_layout.content_size;
674 let matches_content = !(x < 0.0
675 || x > content_size.width + self.scroll_offset.x as f32
676 || y < 0.0
677 || y > content_size.height + self.scroll_offset.y as f32);
678
679 if !matches_self && !matches_content {
680 return None;
681 }
682
683 if self.flags.is_inline_root() {
684 let content_box_offset = taffy::Point {
685 x: self.final_layout.padding.left + self.final_layout.border.left,
686 y: self.final_layout.padding.top + self.final_layout.border.top,
687 };
688 x -= content_box_offset.x;
689 y -= content_box_offset.y;
690 }
691
692 self.paint_children
694 .borrow()
695 .iter()
696 .flatten()
697 .rev()
698 .find_map(|&i| self.with(i).hit(x, y))
699 .or_else(|| {
700 if self.flags.is_inline_root() {
701 let element_data = &self.element_data().unwrap();
702 let layout = &element_data.inline_layout_data.as_ref().unwrap().layout;
703 let scale = layout.scale();
704
705 Cluster::from_point(layout, x * scale, y * scale).and_then(|(cluster, _)| {
706 let style_index = cluster.glyphs().next()?.style_index();
707 let node_id = layout.styles()[style_index].brush.id;
708 Some(HitResult { node_id, x, y })
709 })
710 } else {
711 None
712 }
713 })
714 .or(Some(HitResult {
715 node_id: self.id,
716 x,
717 y,
718 })
719 .filter(|_| matches_self))
720 }
721
722 pub fn absolute_position(&self, x: f32, y: f32) -> taffy::Point<f32> {
724 let x = x + self.final_layout.location.x - self.scroll_offset.x as f32;
725 let y = y + self.final_layout.location.y - self.scroll_offset.y as f32;
726
727 self.layout_parent
729 .get()
730 .map(|i| self.with(i).absolute_position(x, y))
731 .unwrap_or(taffy::Point { x, y })
732 }
733
734 pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
736 DomEventData::Click(self.synthetic_click_event_data(mods))
737 }
738
739 pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzMouseButtonEvent {
740 let absolute_position = self.absolute_position(0.0, 0.0);
741 let x = absolute_position.x + (self.final_layout.size.width / 2.0);
742 let y = absolute_position.y + (self.final_layout.size.height / 2.0);
743
744 BlitzMouseButtonEvent {
745 x,
746 y,
747 mods,
748 button: Default::default(),
749 buttons: Default::default(),
750 }
751 }
752}
753
754impl PartialEq for Node {
756 fn eq(&self, other: &Self) -> bool {
757 self.id == other.id
758 }
759}
760
761impl Eq for Node {}
762
763impl std::fmt::Debug for Node {
764 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765 f.debug_struct("NodeData")
767 .field("parent", &self.parent)
768 .field("id", &self.id)
769 .field("is_inline_root", &self.flags.is_inline_root())
770 .field("children", &self.children)
771 .field("layout_children", &self.layout_children.borrow())
772 .field("node", &self.data)
774 .field("stylo_element_data", &self.stylo_element_data)
775 .finish()
778 }
779}