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