use crate::Document;
use crate::layout::paint_tree::{HoistedPaintChild, StackingContext};
use bitflags::bitflags;
use blitz_traits::events::{
BlitzPointerEvent, BlitzPointerId, DomEventData, HitResult, PointerCoords,
};
use blitz_traits::node_id::NodeId;
use blitz_traits::shell::ShellProvider;
use euclid::{Point2D, Rect, Size2D};
use keyboard_types::Modifiers;
use kurbo::{Affine, Rect as KurboRect};
use markup5ever::{LocalName, local_name};
use parley::{BreakReason, Cluster, ClusterSide};
use selectors::matching::ElementSelectorFlags;
use std::cell::{Cell, RefCell};
use std::fmt::Write;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "writing-mode")]
use style::computed_values::flex_direction::T as FlexDirection;
use style::computed_values::isolation::T as Isolation;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::invalidation::element::restyle_hints::RestyleHint;
use style::properties::ComputedValues;
use style::properties::generated::longhands::position::computed_value::T as Position;
use style::selector_parser::RestyleDamage;
use style::servo_arc::Arc as ServoArc;
use style::shared_lock::SharedRwLock;
use style::stylesheets::UrlExtraData;
use style::values::computed::CSSPixelLength;
use style::values::computed::Display as StyloDisplay;
use style::values::computed::Rotate;
use style::values::generics::transform::{Scale, Translate};
use style::values::specified::box_::{DisplayInside, DisplayOutside};
use style_dom::ElementState;
use style_traits::values::ToCss;
use taffy::{Cache, prelude::Layout};
use thin_vec::ThinVec;
use super::stylo_data::{ComputedStyleRef, StyloData};
use super::{Attribute, DocumentData, ElementData, LayoutData};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DisplayOuter {
Block,
Inline,
None,
}
bitflags! {
#[derive(Clone, Copy, PartialEq)]
pub struct NodeFlags: u32 {
const IS_INLINE_ROOT = 0b00000001;
const IS_TABLE_ROOT = 0b00000010;
const IS_IN_DOCUMENT = 0b00000100;
}
}
impl NodeFlags {
#[inline(always)]
pub fn is_inline_root(&self) -> bool {
self.contains(Self::IS_INLINE_ROOT)
}
#[inline(always)]
pub fn is_table_root(&self) -> bool {
self.contains(Self::IS_TABLE_ROOT)
}
#[inline(always)]
pub fn is_in_document(&self) -> bool {
self.contains(Self::IS_IN_DOCUMENT)
}
#[inline(always)]
pub fn reset_construction_flags(&mut self) {
self.remove(Self::IS_INLINE_ROOT);
self.remove(Self::IS_TABLE_ROOT);
}
}
pub struct Node {
tree: *mut crate::NodeTree,
pub id: NodeId,
pub parent: Option<NodeId>,
pub children: ThinVec<NodeId>,
pub layout_parent: Cell<Option<NodeId>>,
pub layout_children: RefCell<Option<ThinVec<NodeId>>>,
pub hoisted_children: RefCell<ThinVec<NodeId>>,
pub oof_containing_block: Cell<Option<NodeId>>,
pub anonymous_blocks: ThinVec<NodeId>,
pub paint_children: RefCell<Option<ThinVec<NodeId>>>,
pub stacking_context: Option<Box<StackingContext>>,
pub sc_contribution_cache: RefCell<ThinVec<HoistedPaintChild>>,
pub flags: NodeFlags,
pub data: NodeData,
}
unsafe impl Send for Node {}
unsafe impl Sync for Node {}
macro_rules! universal_accessors {
($($(#[$meta:meta])* $field:ident / $field_mut:ident : $ty:ty),* $(,)?) => {
impl Node {
$(
$(#[$meta])*
#[inline]
pub fn $field(&self) -> &$ty {
match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.$field,
NodeData::Document(data) => &data.$field,
_ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
}
}
$(#[$meta])*
#[inline]
pub fn $field_mut(&mut self) -> &mut $ty {
match &mut self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.$field,
NodeData::Document(data) => &mut data.$field,
_ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
}
}
)*
}
};
}
universal_accessors! {
stylo_element_data / stylo_element_data_mut: StyloData,
transform / transform_mut: Option<Box<Affine>>,
display_constructed_as / display_constructed_as_mut: StyloDisplay,
element_state / element_state_mut: ElementState,
snapshot_handled / snapshot_handled_mut: AtomicBool,
selector_flags / selector_flags_mut: Cell<ElementSelectorFlags>,
}
impl Node {
#[inline]
pub fn layout_data(&self) -> &LayoutData {
match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.layout_data(),
NodeData::Document(data) => data.layout_data(),
_ => panic!("`layout_data` is not available on this node kind"),
}
}
#[inline]
pub fn layout_data_mut(&mut self) -> &mut LayoutData {
match &mut self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.layout_data_mut(),
NodeData::Document(data) => data.layout_data_mut(),
_ => panic!("`layout_data` is not available on this node kind"),
}
}
#[inline]
pub fn try_layout_data_mut(&mut self) -> Option<&mut LayoutData> {
match &mut self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
data.layout_data.as_deref_mut()
}
NodeData::Document(data) => data.layout_data.as_deref_mut(),
_ => None,
}
}
#[inline]
pub fn clear_layout_cache(&mut self) {
if let Some(layout_data) = self.try_layout_data_mut() {
layout_data.cache.clear();
}
}
pub fn invalidate_layout_cache(&mut self) {
self.clear_layout_cache();
if let Some(inline_layout) = self
.data
.downcast_element_mut()
.and_then(|el| el.inline_layout_data.as_mut())
{
inline_layout.content_widths = None;
}
}
#[inline]
pub fn cache(&self) -> &Cache {
&self.layout_data().cache
}
#[inline]
pub fn cache_mut(&mut self) -> &mut Cache {
&mut self.layout_data_mut().cache
}
#[inline]
pub fn unrounded_layout(&self) -> &Layout {
&self.layout_data().unrounded_layout
}
#[inline]
pub fn unrounded_layout_mut(&mut self) -> &mut Layout {
&mut self.layout_data_mut().unrounded_layout
}
#[inline]
pub fn final_layout(&self) -> &Layout {
&self.layout_data().final_layout
}
#[inline]
pub fn final_layout_mut(&mut self) -> &mut Layout {
&mut self.layout_data_mut().final_layout
}
#[inline]
pub fn scroll_offset(&self) -> &crate::Point<f64> {
&self.layout_data().scroll_offset
}
#[inline]
pub fn scroll_offset_mut(&mut self) -> &mut crate::Point<f64> {
&mut self.layout_data_mut().scroll_offset
}
#[inline]
pub fn scrollable_overflow(&self) -> &KurboRect {
&self.layout_data().scrollable_overflow
}
#[inline]
pub fn scrollable_overflow_mut(&mut self) -> &mut KurboRect {
&mut self.layout_data_mut().scrollable_overflow
}
#[inline]
pub fn try_stylo_element_data(&self) -> Option<&StyloData> {
match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
Some(&data.stylo_element_data)
}
NodeData::Document(data) => Some(&data.stylo_element_data),
_ => None,
}
}
#[inline]
pub fn try_stylo_element_data_mut(&mut self) -> Option<&mut StyloData> {
match &mut self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
Some(&mut data.stylo_element_data)
}
NodeData::Document(data) => Some(&mut data.stylo_element_data),
_ => None,
}
}
#[inline]
fn dirty_descendants_flag(&self) -> Option<&AtomicBool> {
match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
Some(&data.dirty_descendants)
}
NodeData::Document(data) => Some(&data.dirty_descendants),
_ => None,
}
}
#[inline]
fn damaged_descendants_flag(&self) -> Option<&AtomicBool> {
match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
Some(&data.damaged_descendants)
}
NodeData::Document(data) => Some(&data.damaged_descendants),
_ => None,
}
}
#[inline]
pub fn guard(&self) -> &SharedRwLock {
let guard = match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.guard.as_ref(),
NodeData::Document(data) => data.guard.as_ref(),
_ => None,
};
guard.expect("`guard` is not available on this node kind")
}
#[inline]
pub fn has_snapshot(&self) -> bool {
match &self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot,
NodeData::Document(data) => data.has_snapshot,
_ => false,
}
}
#[inline]
pub fn set_has_snapshot(&mut self, value: bool) {
match &mut self.data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot = value,
NodeData::Document(data) => data.has_snapshot = value,
_ => {}
}
}
#[inline]
pub fn before(&self) -> Option<NodeId> {
self.element_data().and_then(|data| data.before)
}
#[inline]
pub fn after(&self) -> Option<NodeId> {
self.element_data().and_then(|data| data.after)
}
}
impl Node {
pub(crate) fn new(
tree: *mut crate::NodeTree,
id: NodeId,
guard: SharedRwLock,
mut data: NodeData,
) -> Self {
match &mut data {
NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
data.guard = Some(guard);
}
NodeData::Document(data) => data.guard = Some(guard),
_ => {}
}
Self {
tree,
id,
parent: None,
children: ThinVec::new(),
layout_parent: Cell::new(None),
oof_containing_block: Cell::new(None),
layout_children: RefCell::new(None),
hoisted_children: RefCell::new(ThinVec::new()),
anonymous_blocks: ThinVec::new(),
paint_children: RefCell::new(None),
stacking_context: None,
sc_contribution_cache: RefCell::new(ThinVec::new()),
flags: NodeFlags::empty(),
data,
}
}
pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
let transform = self.primary_styles().and_then(|s| {
let size = self.final_layout().size;
let reference_box = Rect::new(
Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
Size2D::new(
CSSPixelLength::new(size.width),
CSSPixelLength::new(size.height),
),
);
crate::resolve_2d_transform(s.get_box(), reference_box).map(|t| {
let scale = scale as f64;
let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
})
});
let slot = self.transform_mut();
match (slot.as_deref_mut(), transform) {
(Some(existing), Some(new)) => *existing = new,
(None, Some(new)) => *slot = Some(Box::new(new)),
(_, None) => *slot = None,
}
transform
}
pub fn pe_by_index(&self, index: usize) -> Option<NodeId> {
match index {
0 => self.after(),
1 => self.before(),
_ => panic!("Invalid pseudo element index"),
}
}
pub fn set_pe_by_index(&mut self, index: usize, value: Option<NodeId>) {
let Some(data) = self.element_data_mut() else {
return;
};
match index {
0 => data.after = value,
1 => data.before = value,
_ => panic!("Invalid pseudo element index"),
}
}
pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
let display = self.primary_styles().as_ref()?.clone_display();
Some(
if self.element_data().is_some_and(|el| el.is_button_input()) {
stylo_taffy::convert::button_input_display(display)
} else {
display
},
)
}
pub fn is_or_contains_block(&self) -> bool {
let style = self.primary_styles();
let style = style.as_ref();
let position = style
.map(|s| s.clone_position())
.unwrap_or(Position::Relative);
let is_in_flow = matches!(
position,
Position::Static | Position::Relative | Position::Sticky
);
if !is_in_flow {
return false;
}
let is_floating = style
.map(|s| s.clone_float().is_floating())
.unwrap_or(false);
if is_floating {
return false;
}
let display = self.display_style().unwrap_or(StyloDisplay::inline());
match display.outside() {
DisplayOutside::None => false,
DisplayOutside::Block => true,
_ => {
if display.inside() == DisplayInside::Flow {
self.children
.iter()
.copied()
.any(|child_id| self.tree()[child_id].is_or_contains_block())
} else {
false
}
}
}
}
pub fn is_whitespace_node(&self) -> bool {
match &self.data {
NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
_ => false,
}
}
pub fn is_collapsible_whitespace_node(&self) -> bool {
let NodeData::Text(data) = &self.data else {
return false;
};
if data.content.is_empty() {
return true;
}
if !data.content.chars().all(|c| c.is_ascii_whitespace()) {
return false;
}
let white_space_collapse = self
.parent
.and_then(|parent_id| self.with(parent_id).primary_styles())
.map(|style| style.clone_white_space_collapse());
match white_space_collapse {
Some(WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces) => false,
Some(WhiteSpaceCollapse::PreserveBreaks) => !data.content.contains('\n'),
Some(WhiteSpaceCollapse::Collapse) | None => true,
}
}
pub fn is_focussable(&self) -> bool {
self.data
.downcast_element()
.map(|el| el.is_focussable)
.unwrap_or(false)
}
pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
if let Some(stylo_element_data) = self.try_stylo_element_data_mut() {
if let Some(mut element_data) = stylo_element_data.get_mut() {
element_data.hint.insert(hint);
}
}
self.mark_ancestors_dirty();
}
pub fn has_dirty_descendants(&self) -> bool {
self.dirty_descendants_flag()
.is_some_and(|flag| flag.load(Ordering::Relaxed))
}
pub fn set_dirty_descendants(&self) {
if let Some(flag) = self.dirty_descendants_flag() {
flag.store(true, Ordering::Relaxed);
}
}
pub fn unset_dirty_descendants(&self) {
if let Some(flag) = self.dirty_descendants_flag() {
flag.store(false, Ordering::Relaxed);
}
}
pub fn mark_ancestors_dirty(&self) {
let mut current_id = self.parent;
while let Some(parent_id) = current_id {
let parent = &self.tree()[parent_id];
if let Some(flag) = parent.dirty_descendants_flag() {
if flag.swap(true, Ordering::Relaxed) {
break;
}
}
current_id = parent.parent;
}
}
pub fn has_damaged_descendants(&self) -> bool {
self.damaged_descendants_flag()
.is_some_and(|flag| flag.load(Ordering::Relaxed))
}
pub fn unset_damaged_descendants(&self) {
if let Some(flag) = self.damaged_descendants_flag() {
flag.store(false, Ordering::Relaxed);
}
}
pub fn mark_damaged(&self) {
if let Some(flag) = self.damaged_descendants_flag() {
if flag.swap(true, Ordering::Relaxed) {
return;
}
}
let mut current_id = self.parent;
while let Some(parent_id) = current_id {
let parent = &self.tree()[parent_id];
if let Some(flag) = parent.damaged_descendants_flag() {
if flag.swap(true, Ordering::Relaxed) {
break;
}
}
current_id = parent.parent;
}
}
pub fn damage(&self) -> Option<RestyleDamage> {
self.try_stylo_element_data()
.and_then(|stylo| stylo.get().map(|data| data.damage))
}
pub fn set_damage(&mut self, damage: RestyleDamage) {
if let Some(stylo) = self.try_stylo_element_data_mut() {
if let Some(mut data) = stylo.get_mut() {
data.damage = damage;
}
}
}
pub fn insert_damage(&mut self, damage: RestyleDamage) {
if let Some(stylo) = self.try_stylo_element_data_mut() {
if let Some(mut data) = stylo.get_mut() {
data.damage |= damage;
}
}
if !damage.is_empty() {
self.mark_damaged();
}
}
pub fn remove_damage(&mut self, damage: RestyleDamage) {
if let Some(stylo) = self.try_stylo_element_data_mut() {
if let Some(mut data) = stylo.get_mut() {
data.damage.remove(damage);
}
}
}
pub fn clear_damage_mut(&mut self) {
if let Some(stylo) = self.try_stylo_element_data_mut() {
if let Some(mut data) = stylo.get_mut() {
data.damage = RestyleDamage::empty();
}
}
}
pub fn hover(&mut self) {
if let Some(data) = self.element_data_mut() {
data.element_state.insert(ElementState::HOVER);
}
self.mark_ancestors_dirty();
}
pub fn unhover(&mut self) {
if let Some(data) = self.element_data_mut() {
data.element_state.remove(ElementState::HOVER);
}
self.mark_ancestors_dirty();
}
pub fn is_hovered(&self) -> bool {
self.element_data()
.is_some_and(|data| data.element_state.contains(ElementState::HOVER))
}
pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
if let Some(data) = self.element_data_mut() {
data.element_state
.insert(ElementState::FOCUS | ElementState::FOCUSRING);
}
self.mark_ancestors_dirty();
if self
.element_data()
.and_then(|elem| elem.text_input_data())
.is_some()
{
shell_provider.set_ime_enabled(true);
let mut pos = self.absolute_position(0.0, 0.0);
pos.x += self.final_layout().content_box_x();
pos.y += self.final_layout().content_box_y();
let width = self.final_layout().content_box_width();
let height = self.final_layout().content_box_height();
shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
}
}
pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
if let Some(data) = self.element_data_mut() {
data.element_state
.remove(ElementState::FOCUS | ElementState::FOCUSRING);
}
self.mark_ancestors_dirty();
if self
.element_data()
.and_then(|elem| elem.text_input_data())
.is_some()
{
shell_provider.set_ime_enabled(false);
}
}
pub fn is_focussed(&self) -> bool {
self.element_data()
.is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
}
pub fn active(&mut self) {
if let Some(data) = self.element_data_mut() {
data.element_state.insert(ElementState::ACTIVE);
}
self.mark_ancestors_dirty();
}
pub fn unactive(&mut self) {
if let Some(data) = self.element_data_mut() {
data.element_state.remove(ElementState::ACTIVE);
}
self.mark_ancestors_dirty();
}
pub fn is_active(&self) -> bool {
self.element_data()
.is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
}
pub fn disable(&mut self) {
if let Some(data) = self.element_data_mut() {
if data.can_be_disabled() {
data.element_state.insert(ElementState::DISABLED);
data.element_state.remove(ElementState::ENABLED);
}
}
self.mark_ancestors_dirty();
}
pub fn enable(&mut self) {
if let Some(data) = self.element_data_mut() {
if data.can_be_disabled() {
data.element_state.insert(ElementState::ENABLED);
data.element_state.remove(ElementState::DISABLED);
}
}
self.mark_ancestors_dirty();
}
pub fn subdoc(&self) -> Option<&dyn Document> {
self.element_data().and_then(|el| el.sub_doc_data())
}
pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
}
pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
if let Some(input_data) = self
.data
.downcast_element()
.and_then(|el| el.text_input_data())
{
if !input_data.is_multiline {
let content_box_height = self.final_layout().content_box_height();
let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
return y_offset as f64;
}
}
0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NodeKind {
Document,
Element,
AnonymousBlock,
Text,
Comment,
}
#[derive(Debug, Clone)]
pub enum NodeData {
Document(Box<DocumentData>),
Element(Box<ElementData>),
AnonymousBlock(Box<ElementData>),
Text(TextNodeData),
Comment {
contents: String,
},
}
impl NodeData {
pub fn downcast_element(&self) -> Option<&ElementData> {
match self {
Self::Element(data) => Some(data),
Self::AnonymousBlock(data) => Some(data),
_ => None,
}
}
pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
match self {
Self::Element(data) => Some(data),
Self::AnonymousBlock(data) => Some(data),
_ => None,
}
}
pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
let Some(elem) = self.downcast_element() else {
return false;
};
*name == elem.name.local
}
pub fn attrs(&self) -> Option<&[Attribute]> {
Some(&self.downcast_element()?.attrs)
}
pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
self.downcast_element()?.attr(name)
}
pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
self.downcast_element()
.is_some_and(|elem| elem.has_attr(name))
}
pub fn kind(&self) -> NodeKind {
match self {
NodeData::Document(_) => NodeKind::Document,
NodeData::Element(_) => NodeKind::Element,
NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
NodeData::Text(_) => NodeKind::Text,
NodeData::Comment { .. } => NodeKind::Comment,
}
}
}
#[derive(Debug, Clone)]
pub struct TextNodeData {
pub content: String,
}
impl TextNodeData {
pub fn new(content: String) -> Self {
Self { content }
}
}
impl Node {
pub fn tree(&self) -> &crate::NodeTree {
unsafe { &*self.tree }
}
#[track_caller]
pub fn with(&self, id: NodeId) -> &Node {
self.tree().get(id).unwrap()
}
pub fn print_tree(&self, level: usize) {
println!(
"{} {} {:?} {} {:?}",
" ".repeat(level),
self.id,
self.parent,
self.node_debug_str().replace('\n', ""),
self.children
);
for child_id in self.children.iter() {
let child = self.with(*child_id);
child.print_tree(level + 1)
}
}
pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
self.children.iter().position(|id| *id == child_id)
}
pub fn child_index(&self) -> Option<usize> {
self.tree()[self.parent?]
.children
.iter()
.position(|id| *id == self.id)
}
pub fn forward(&self, n: usize) -> Option<&Node> {
let child_idx = self.child_index().unwrap_or(0);
self.tree()[self.parent?]
.children
.get(child_idx + n)
.map(|id| self.with(*id))
}
pub fn backward(&self, n: usize) -> Option<&Node> {
let child_idx = self.child_index().unwrap_or(0);
if child_idx < n {
return None;
}
self.tree()[self.parent?]
.children
.get(child_idx - n)
.map(|id| self.with(*id))
}
pub fn is_element(&self) -> bool {
matches!(self.data, NodeData::Element { .. })
}
pub fn is_anonymous(&self) -> bool {
matches!(self.data, NodeData::AnonymousBlock { .. })
}
pub fn is_text_node(&self) -> bool {
matches!(self.data, NodeData::Text { .. })
}
pub fn element_data(&self) -> Option<&ElementData> {
match self.data {
NodeData::Element(ref data) => Some(data),
NodeData::AnonymousBlock(ref data) => Some(data),
_ => None,
}
}
pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
match self.data {
NodeData::Element(ref mut data) => Some(data),
NodeData::AnonymousBlock(ref mut data) => Some(data),
_ => None,
}
}
pub fn text_data(&self) -> Option<&TextNodeData> {
match self.data {
NodeData::Text(ref data) => Some(data),
_ => None,
}
}
pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
match self.data {
NodeData::Text(ref mut data) => Some(data),
_ => None,
}
}
pub fn node_debug_str(&self) -> String {
let mut s = String::new();
match &self.data {
NodeData::Document(_) => write!(s, "DOCUMENT"),
NodeData::Text(data) => {
let bytes = data.content.as_bytes();
write!(
s,
"TEXT {}",
std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
.unwrap_or("INVALID UTF8")
)
}
NodeData::Comment { .. } => write!(s, "COMMENT"),
NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
NodeData::Element(data) => {
let name = &data.name;
let class = self.attr(local_name!("class")).unwrap_or("");
let id = self.attr(local_name!("id")).unwrap_or("");
let display = self.display_constructed_as().to_css_string();
write!(s, "<{}", name.local).unwrap();
if !id.is_empty() {
write!(s, " #{id}").unwrap();
}
if !class.is_empty() {
if class.contains(' ') {
write!(s, " class=\"{class}\"").unwrap()
} else {
write!(s, " .{class}").unwrap()
}
}
write!(s, "> ({display})")
} }
.unwrap();
s
}
pub fn attrs(&self) -> Option<&[Attribute]> {
Some(&self.element_data()?.attrs)
}
pub fn attr(&self, name: LocalName) -> Option<&str> {
let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
Some(&attr.value)
}
pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
self.try_stylo_element_data()
.and_then(|stylo| stylo.primary_styles())
}
pub fn layout_style(&self) -> stylo_taffy::TaffyStyloStyle<ComputedStyleRef<'_>> {
let styles = self
.try_stylo_element_data()
.and_then(|stylo| stylo.computed_styles())
.expect("layout_style() called on a node without computed styles");
let mut flags = stylo_taffy::StyleFlags::empty();
if let Some(el) = self.data.downcast_element() {
if crate::layout::replaced::is_replaced_element(&el.name.local) {
flags |= stylo_taffy::StyleFlags::IS_REPLACED;
}
if el.name.local == local_name!("button") {
flags |= stylo_taffy::StyleFlags::IS_BUTTON;
}
if el.is_button_input() {
flags |= stylo_taffy::StyleFlags::IS_BUTTON_INPUT;
}
if let Some(inline_layout) = el.inline_layout_data.as_deref() {
flags |= inline_layout.span_cb_flags;
}
}
#[cfg(feature = "writing-mode")]
{
let layout_wm = styles.writing_mode;
stylo_taffy::TaffyStyloStyle::new_in(styles, flags, layout_wm)
}
#[cfg(not(feature = "writing-mode"))]
{
stylo_taffy::TaffyStyloStyle::new(styles, flags)
}
}
#[cfg(feature = "writing-mode")]
pub fn layout_style_in(
&self,
layout_wm: stylo_taffy::WritingMode,
) -> stylo_taffy::TaffyStyloStyle<ComputedStyleRef<'_>> {
let mut style = self.layout_style();
style.layout_wm = layout_wm;
style
}
#[cfg(feature = "writing-mode")]
pub(crate) fn has_containment(&self) -> bool {
self.primary_styles()
.is_some_and(|style| !style.clone_contain().is_empty())
}
#[cfg(feature = "writing-mode")]
pub(crate) fn is_column_flex_container(&self) -> bool {
self.primary_styles().is_some_and(|s| {
s.clone_display().inside() == DisplayInside::Flex
&& !self.element_data().is_some_and(|el| el.is_button_input())
&& matches!(
s.get_position().flex_direction,
FlexDirection::Column | FlexDirection::ColumnReverse
)
})
}
#[cfg(feature = "writing-mode")]
pub fn writing_mode(&self) -> stylo_taffy::WritingMode {
self.primary_styles()
.map(|s| s.writing_mode)
.unwrap_or(stylo_taffy::WritingMode::empty())
}
pub fn taffy_display(&self) -> taffy::Display {
self.primary_styles()
.map(|style| {
let display = style.clone_display();
if self.element_data().is_some_and(|el| el.is_button_input())
&& !matches!(
display.inside(),
DisplayInside::None | DisplayInside::Contents
)
{
taffy::Display::FlowRoot
} else {
stylo_taffy::convert::display(display)
}
})
.unwrap_or(taffy::Display::Block)
}
pub fn taffy_position(&self) -> taffy::Position {
self.primary_styles()
.map(|s| stylo_taffy::convert::position(s.get_box().position))
.unwrap_or(taffy::Position::Static)
}
pub fn is_out_of_flow(&self) -> bool {
self.primary_styles().is_some_and(|s| {
let box_style = s.get_box();
stylo_taffy::convert::position(box_style.position).is_out_of_flow()
&& stylo_taffy::convert::display(box_style.display) != taffy::Display::None
})
}
pub fn text_content(&self) -> String {
let mut out = String::new();
self.write_text_content(&mut out);
out
}
fn write_text_content(&self, out: &mut String) {
match &self.data {
NodeData::Text(data) => {
out.push_str(&data.content);
}
NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
for child_id in self.children.iter() {
self.with(*child_id).write_text_content(out);
}
}
_ => {}
}
}
pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
if let NodeData::Element(ref mut elem_data) = self.data {
if let Some(guard) = elem_data.guard.clone() {
elem_data.flush_style_attribute(&guard, url_extra_data);
}
}
}
pub fn order(&self) -> i32 {
self.primary_styles()
.filter(|s| !matches!(s.get_box().position, Position::Absolute | Position::Fixed))
.map(|s| s.clone_order())
.unwrap_or(0)
}
pub fn z_index(&self) -> i32 {
self.primary_styles()
.map(|s| s.clone_z_index().integer_or(0))
.unwrap_or(0)
}
pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
let Some(style) = self.primary_styles() else {
return false;
};
let position = style.clone_position();
let has_z_index = !style.clone_z_index().is_auto();
if style.clone_opacity() != 1.0 {
return true;
}
let position_based = match position {
Position::Fixed | Position::Sticky => true,
Position::Relative | Position::Absolute => has_z_index,
Position::Static => has_z_index && is_flex_or_grid_item,
};
if position_based {
return true;
}
let box_style = style.get_box();
if !box_style.transform.0.is_empty()
|| !matches!(box_style.rotate, Rotate::None)
|| !matches!(box_style.scale, Scale::None)
|| !matches!(box_style.translate, Translate::None)
{
return true;
}
if self.applies_atomic_paint_effect() {
return true;
}
if style.get_box().isolation == Isolation::Isolate {
return true;
}
false
}
pub(crate) fn applies_atomic_paint_effect(&self) -> bool {
use style::values::computed::basic_shape::ClipPath;
use style::values::generics::image::GenericImage;
let Some(style) = self.primary_styles() else {
return false;
};
if style.clone_opacity() != 1.0 {
return true;
}
let effects = style.get_effects();
if !effects.filter.0.is_empty() {
return true;
}
if !matches!(style.clone_clip_path(), ClipPath::None) {
return true;
}
style
.get_svg()
.mask_image
.0
.iter()
.any(|image| !matches!(image, GenericImage::None))
}
pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
self.hit_inner(x, y, scale, &mut None)
}
pub(crate) fn hit_inner(
&self,
x: f32,
y: f32,
scale: f64,
scrollbar: &mut Option<crate::node::ScrollbarRef>,
) -> Option<HitResult> {
use style::computed_values::pointer_events::T as PointerEvents;
use style::computed_values::visibility::T as Visibility;
if let Some(style) = self.primary_styles() {
if matches!(
style.clone_visibility(),
Visibility::Hidden | Visibility::Collapse
) {
return None;
}
}
let pointer_events_none = self
.primary_styles()
.is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
if let Some(t) = self.transform().as_deref() {
let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
x = (p.x / scale) as f32;
y = (p.y / scale) as f32;
}
let size = self.final_layout().size;
let matches_self = !(x < 0.0
|| x > size.width + self.scroll_offset().x as f32
|| y < 0.0
|| y > size.height + self.scroll_offset().y as f32);
let overflow_rect = self.final_layout().scrollable_overflow_rect;
let matches_content = !(x < 0.0
|| x > overflow_rect.right + self.scroll_offset().x as f32
|| y < 0.0
|| y > overflow_rect.bottom + self.scroll_offset().y as f32);
let matches_hoisted_content = match &self.stacking_context {
Some(sc) => {
let bbox = sc.hoisted_content_bbox(self.tree(), self.id, scale);
x >= bbox.left && x <= bbox.right && y >= bbox.top && y <= bbox.bottom
}
None => false,
};
let overflow = *self.scrollable_overflow();
let matches_overflow = x >= (overflow.x0 / scale) as f32
&& x <= (overflow.x1 / scale) as f32
&& y >= (overflow.y0 / scale) as f32
&& y <= (overflow.y1 / scale) as f32;
if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
return None;
}
if matches_self
&& let Some(sb) = self.scrollbar_at_local(
(x - self.scroll_offset().x as f32) as f64,
(y - self.scroll_offset().y as f32) as f64,
)
{
*scrollbar = Some(sb);
}
let content_box_offset = taffy::Point {
x: self.final_layout().padding.left + self.final_layout().border.left,
y: self.final_layout().padding.top + self.final_layout().border.top,
};
if self.flags.is_inline_root() {
x -= content_box_offset.x;
y -= content_box_offset.y;
}
if matches_hoisted_content {
if let Some(hoisted) = &self.stacking_context {
for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
let pos = hoisted_child.position(self.tree(), self.id);
let x = x - pos.x;
let y = y - pos.y;
if let Some(hit) = self
.with(hoisted_child.node_id)
.hit_inner(x, y, scale, scrollbar)
{
return Some(hit);
}
}
}
}
for child_id in self.paint_children.borrow().iter().flatten().rev() {
let child = self.with(*child_id);
let child_position = child.taffy_position();
let mut child_x = x;
let mut child_y = y;
if child_position.is_out_of_flow() {
if self.flags.is_inline_root() {
child_x += content_box_offset.x;
child_y += content_box_offset.y;
}
if child_position == taffy::Position::Fixed && self.containing_block().is_none() {
child_x -= self.scroll_offset().x as f32;
child_y -= self.scroll_offset().y as f32;
let viewport_scroll = self.tree().viewport_scroll();
child_x -= viewport_scroll.x as f32;
child_y -= viewport_scroll.y as f32;
}
}
if let Some(hit) = child.hit_inner(child_x, child_y, scale, scrollbar) {
return Some(hit);
}
}
if matches_hoisted_content {
if let Some(hoisted) = &self.stacking_context {
for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
let pos = hoisted_child.position(self.tree(), self.id);
let x = x - pos.x;
let y = y - pos.y;
if let Some(hit) = self
.with(hoisted_child.node_id)
.hit_inner(x, y, scale, scrollbar)
{
return Some(hit);
}
}
}
}
if self.flags.is_inline_root() {
let element_data = &self.element_data().unwrap();
if let Some(ild) = element_data.inline_layout_data.as_ref() {
let layout = &ild.layout;
let scale = layout.scale();
let y = y - ild.block_offset;
if let Some((cluster, _side)) =
Cluster::from_point_exact(layout, x * scale, y * scale)
{
let node_id = cluster.style().brush.id;
let text_pointer_events_none = self
.with(node_id)
.primary_styles()
.is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
if !text_pointer_events_none {
return Some(HitResult {
node_id,
x,
y,
is_text: true,
});
}
}
}
}
if matches_self && !pointer_events_none {
return Some(HitResult {
node_id: self.id,
x,
y,
is_text: false,
});
}
None
}
pub fn inline_root_ancestor(&self) -> Option<&Node> {
let mut node = self;
loop {
if node.flags.is_inline_root() {
return Some(node);
}
let id = node.layout_parent.get()?;
node = self.with(id);
}
}
pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
if !self.flags.is_inline_root() {
return None;
}
let element_data = self.element_data()?;
let inline_layout = element_data.inline_layout_data.as_ref()?;
let layout = &inline_layout.layout;
let scale = layout.scale();
let y = y - inline_layout.block_offset;
let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
let is_leading = side == ClusterSide::Left;
let offset = if cluster.is_rtl() {
if is_leading {
cluster.text_range().end
} else {
cluster.text_range().start
}
} else {
if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
cluster.text_range().start
} else {
cluster.text_range().end
}
};
Some(offset)
}
pub fn is_non_atomic_inline(&self) -> bool {
let Some(element) = self.element_data() else {
return false;
};
if self.flags.is_inline_root()
|| crate::layout::replaced::is_inline_box_element(&element.name.local)
{
return false;
}
self.primary_styles().is_some_and(|styles| {
let display = styles.clone_display();
display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow
})
}
pub fn containing_block(&self) -> Option<NodeId> {
self.oof_containing_block
.get()
.or_else(|| self.layout_parent.get())
}
pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
let x = x + self.final_layout().location.x - self.scroll_offset().x as f32;
let y = y + self.final_layout().location.y - self.scroll_offset().y as f32;
self.containing_block()
.map(|i| self.with(i).absolute_position(x, y))
.unwrap_or(crate::util::Point { x, y })
}
pub(crate) fn is_offset_parent(&self) -> bool {
let Some(styles) = self.primary_styles() else {
return false;
};
if styles.get_box().position != Position::Static {
return true;
}
self.data.is_element_with_tag_name(&local_name!("body"))
|| self.data.is_element_with_tag_name(&local_name!("td"))
|| self.data.is_element_with_tag_name(&local_name!("th"))
}
pub(crate) fn is_static_body(&self) -> bool {
self.data.is_element_with_tag_name(&local_name!("body"))
&& self
.primary_styles()
.is_some_and(|styles| styles.get_box().position == Position::Static)
}
pub fn offset_parent(&self) -> Option<&Node> {
let mut node = self;
loop {
node = self.with(node.containing_block()?);
if node.is_offset_parent() {
return Some(node);
}
}
}
pub fn inline_fragment_boxes(&self) -> Option<impl Iterator<Item = taffy::Rect<f32>> + '_> {
use parley::PositionedLayoutItem;
if !self.is_non_atomic_inline() {
return None;
}
let inline_root = self.inline_root_ancestor()?;
let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
let layout = &inline_layout.layout;
let scale = layout.scale();
let inline_root_id = inline_root.id;
let is_in_target = move |mut id: NodeId| -> bool {
loop {
if id == self.id {
return true;
}
if id == inline_root_id {
return false;
}
match self.with(id).parent {
Some(parent) => id = parent,
None => return false,
}
}
};
let root_layout = inline_root.unrounded_layout();
let content_box_inset = root_layout.padding + root_layout.border;
let origin_x = content_box_inset.left;
let origin_y = content_box_inset.top + inline_layout.block_offset;
fn union(acc: &mut Option<taffy::Rect<f32>>, left: f32, top: f32, right: f32, bottom: f32) {
*acc = Some(match *acc {
Some(rect) => taffy::Rect {
left: rect.left.min(left),
top: rect.top.min(top),
right: rect.right.max(right),
bottom: rect.bottom.max(bottom),
},
None => taffy::Rect {
left,
top,
right,
bottom,
},
});
}
Some(layout.lines().filter_map(move |line| {
let line_metrics = line.metrics();
let mut line_rect: Option<taffy::Rect<f32>> = None;
for item in line.items() {
match item {
PositionedLayoutItem::GlyphRun(glyph_run) => {
if !is_in_target(glyph_run.style().brush.id) {
continue;
}
let x0 = glyph_run.offset();
let x1 = x0 + glyph_run.advance();
let y0 = line_metrics.block_min_coord;
let y1 = line_metrics.block_max_coord;
union(&mut line_rect, x0, y0, x1, y1);
}
PositionedLayoutItem::InlineBox(inline_box) => {
if !is_in_target(NodeId::from_u64(inline_box.id)) {
continue;
}
let x0 = inline_box.x;
let y0 = inline_box.y;
union(
&mut line_rect,
x0,
y0,
x0 + inline_box.width,
y0 + inline_box.height,
);
}
}
}
line_rect.map(|rect| taffy::Rect {
left: origin_x + rect.left / scale,
top: origin_y + rect.top / scale,
right: origin_x + rect.right / scale,
bottom: origin_y + rect.bottom / scale,
})
}))
}
pub fn offset_top_left(&self) -> crate::util::Point<f32> {
let mut x = 0.0;
let mut y = 0.0;
let mut current = self;
if let Some(mut fragments) = self.inline_fragment_boxes() {
if let Some(first) = fragments.next() {
x += first.left;
y += first.top;
}
let Some(inline_root) = self.inline_root_ancestor() else {
return crate::util::Point { x, y };
};
if inline_root.is_offset_parent() && !inline_root.is_static_body() {
let border = inline_root.final_layout().border;
return crate::util::Point {
x: x - border.left,
y: y - border.top,
};
}
current = inline_root;
}
loop {
let layout = current.final_layout();
x += layout.location.x;
y += layout.location.y;
let Some(parent_id) = current.containing_block() else {
break;
};
let parent = self.with(parent_id);
if parent.is_offset_parent() && !parent.is_static_body() {
let border = parent.final_layout().border;
x -= border.left;
y -= border.top;
break;
}
current = parent;
}
crate::util::Point { x, y }
}
pub fn client_width(&self) -> f32 {
let layout = self.final_layout();
layout.size.width - layout.border.left - layout.border.right - layout.scrollbar_size.width
}
pub fn client_height(&self) -> f32 {
let layout = self.final_layout();
layout.size.height - layout.border.top - layout.border.bottom - layout.scrollbar_size.height
}
pub fn scroll_width(&self) -> f32 {
self.client_width()
.max(self.final_layout().scrollable_overflow_rect.right)
}
pub fn scroll_height(&self) -> f32 {
self.client_height()
.max(self.final_layout().scrollable_overflow_rect.bottom)
}
pub fn has_boxes(&self) -> bool {
self.flags.is_in_document()
&& !self.display_style().is_some_and(|display| {
matches!(
display.inside(),
style::values::specified::box_::DisplayInside::None
| style::values::specified::box_::DisplayInside::Contents
)
})
}
pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
DomEventData::Click(self.synthetic_click_event_data(mods))
}
pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
let absolute_position = self.absolute_position(0.0, 0.0);
let x = absolute_position.x + (self.final_layout().size.width / 2.0);
let y = absolute_position.y + (self.final_layout().size.height / 2.0);
BlitzPointerEvent {
id: BlitzPointerId::Mouse,
is_primary: true,
coords: PointerCoords {
page_x: x,
page_y: y,
screen_x: x,
screen_y: y,
client_x: x,
client_y: y,
},
mods,
button: Default::default(),
buttons: Default::default(),
details: Default::default(),
element: Default::default(),
active_pointers: Default::default(),
}
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Node {}
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeData")
.field("parent", &self.parent)
.field("id", &self.id)
.field("is_inline_root", &self.flags.is_inline_root())
.field("children", &self.children)
.field("layout_children", &self.layout_children.borrow())
.field("node", &self.data)
.field("stylo_element_data", &self.try_stylo_element_data())
.finish()
}
}
#[cfg(test)]
mod test {
use style_dom::ElementState;
use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
#[test]
fn create_node_with_disabled_attr() {
let mut document = BaseDocument::new(DocumentConfig::default());
let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("button"),
vec![Attribute {
name: qual_name!("disabled"),
value: "".into(),
}],
))));
let node = document.get_node(node).unwrap();
assert!(
node.element_state().contains(ElementState::DISABLED),
"form node is disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"form node is not enabled"
);
}
#[test]
fn ignore_disabled_attr_content() {
let mut document = BaseDocument::new(DocumentConfig::default());
let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("button"),
vec![Attribute {
name: qual_name!("disabled"),
value: "false".into(),
}],
))));
let node = document.get_node(node).unwrap();
assert!(
node.element_state().contains(ElementState::DISABLED),
"form node is disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"form node is not enabled"
);
}
#[test]
fn create_node_with_ignored_disable() {
let mut document = BaseDocument::new(DocumentConfig::default());
let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("a"),
vec![Attribute {
name: qual_name!("disabled"),
value: "".into(),
}],
))));
let node = document.get_node(node).unwrap();
assert!(
!node.element_state().contains(ElementState::DISABLED),
"Non form node cannot be disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"Non form node cannot be enabled"
);
}
#[test]
fn create_empty_enabled_node() {
let mut document = BaseDocument::new(DocumentConfig::default());
let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("button"),
vec![],
))));
let node = document.get_node(node).unwrap();
assert!(
node.element_state().contains(ElementState::ENABLED),
"Button should be enabled by default"
);
}
}