diff --git a/packages/blitz-dom/src/document.rs b/packages/blitz-dom/src/document.rs index f299aa11d..1a4af0b8b 100644 --- a/packages/blitz-dom/src/document.rs +++ b/packages/blitz-dom/src/document.rs @@ -1,6 +1,6 @@ use crate::events::{EventData, HitResult, RendererEvent}; use crate::node::{ImageData, NodeSpecificData, TextBrush}; -use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; +use crate::{ElementNodeData, Handle, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; use html5ever::local_name; use parley::PlainEditorOp; @@ -430,6 +430,13 @@ impl Document { ctx } + pub fn get(&self, id: usize) -> Option { + self.nodes.get(id).map(|node| Handle { + node, + tree: &self.nodes, + }) + } + /// Set base url for resolving linked resources (stylesheets, images, fonts, etc) pub fn set_base_url(&mut self, url: &str) { self.base_url = Url::parse(url).ok(); @@ -505,12 +512,14 @@ impl Document { *is_checked = !*is_checked; } - pub fn root_node(&self) -> &Node { - &self.nodes[0] + pub fn root_node(&self) -> Handle { + self.get(0).unwrap() } pub fn try_root_element(&self) -> Option<&Node> { - TDocument::as_node(&self.root_node()).first_element_child() + TDocument::as_node(&self.root_node()) + .first_element_child() + .map(|blitz_node| blitz_node.node) } pub fn root_element(&self) -> &Node { @@ -519,15 +528,15 @@ impl Document { .unwrap() .as_element() .unwrap() + .node } pub fn create_node(&mut self, node_data: NodeData) -> usize { - let slab_ptr = self.nodes.as_mut() as *mut Slab; let entry = self.nodes.vacant_entry(); let id = entry.key(); let guard = self.guard.clone(); - entry.insert(Node::new(slab_ptr, id, guard, node_data)); + entry.insert(Node::new(id, guard, node_data)); // self.quadtree.insert( // AreaBuilder::default() @@ -666,11 +675,11 @@ impl Document { } pub fn print_subtree(&self, node_id: usize) { - crate::util::walk_tree(0, &self.nodes[node_id]); + crate::util::walk_tree(0, self.get(node_id).unwrap()); } pub fn process_style_element(&mut self, target_id: usize) { - let css = self.nodes[target_id].text_content(); + let css = self.get(target_id).unwrap().text_content(); let css = html_escape::decode_html_entities(&css); let sheet = self.make_stylesheet(&css, Origin::Author); self.add_stylesheet_for_node(sheet, target_id); @@ -709,7 +718,7 @@ impl Document { } pub fn upsert_stylesheet_for_node(&mut self, node_id: usize) { - let raw_styles = self.nodes[node_id].text_content(); + let raw_styles = self.get(node_id).unwrap().text_content(); let sheet = self.make_stylesheet(raw_styles, Origin::Author); self.add_stylesheet_for_node(sheet, node_id); } @@ -761,8 +770,13 @@ impl Document { } pub fn snapshot_node(&mut self, node_id: usize) { + let node = &self.nodes[node_id]; + let opaque_node_id = TNode::opaque(&Handle { + node, + tree: &self.nodes, + }); + let node = &mut self.nodes[node_id]; - let opaque_node_id = TNode::opaque(&&*node); node.has_snapshot = true; node.snapshot_handled .store(false, std::sync::atomic::Ordering::SeqCst); @@ -827,9 +841,12 @@ impl Document { /// Restyle the tree and then relayout it pub fn resolve(&mut self) { - if TDocument::as_node(&&self.nodes[0]) - .first_element_child() - .is_none() + if TDocument::as_node(&Handle { + node: &self.nodes[0], + tree: self.tree(), + }) + .first_element_child() + .is_none() { println!("No DOM - not resolving"); return; @@ -850,15 +867,22 @@ impl Document { // Takes (x, y) co-ordinates (relative to the ) pub fn hit(&self, x: f32, y: f32) -> Option { - if TDocument::as_node(&&self.nodes[0]) - .first_element_child() - .is_none() + if TDocument::as_node(&Handle { + node: &self.nodes[0], + tree: self.tree(), + }) + .first_element_child() + .is_none() { println!("No DOM - not resolving"); return None; } - self.root_element().hit(x, y) + Handle { + node: self.root_element(), + tree: self.tree(), + } + .hit(x, y) } pub fn next_node(&self, start: &Node, mut filter: impl FnMut(&Node) -> bool) -> Option { @@ -872,28 +896,34 @@ impl Document { &self.nodes[node_id] } // Next is next sibling or parent - else if let Some(parent) = node.parent_node() { + else if let Some(parent) = (Handle { + node, + tree: self.tree(), + }) + .parent_node() + { let self_idx = parent + .node .children .iter() .position(|id| *id == node.id) .unwrap(); // Next is next sibling - if let Some(sibling_id) = parent.children.get(self_idx + 1) { + if let Some(sibling_id) = parent.node.children.get(self_idx + 1) { look_in_children = true; &self.nodes[*sibling_id] } // Next is parent else { look_in_children = false; - node = parent; + node = parent.node; continue; } } // Continue search from the root else { look_in_children = true; - self.root_node() + self.root_node().node }; if filter(next) { @@ -995,7 +1025,7 @@ impl Document { /// Ensure that the layout_children field is populated for all nodes pub fn resolve_layout_children(&mut self) { - let root_node_id = self.root_node().id; + let root_node_id = self.root_node().node.id; resolve_layout_children_recursive(self, root_node_id); pub fn resolve_layout_children_recursive(doc: &mut Document, node_id: usize) { diff --git a/packages/blitz-dom/src/handle.rs b/packages/blitz-dom/src/handle.rs new file mode 100644 index 000000000..05119c530 --- /dev/null +++ b/packages/blitz-dom/src/handle.rs @@ -0,0 +1,1141 @@ +//! Enable the dom to participate in styling by servo +//! + +use crate::events::EventData; +use crate::events::HitResult; +use crate::node::Node; +use crate::node::NodeData; +use atomic_refcell::{AtomicRef, AtomicRefMut}; +use html5ever::LocalNameStaticSet; +use html5ever::NamespaceStaticSet; +use html5ever::{local_name, LocalName, Namespace}; +use selectors::{ + attr::{AttrSelectorOperation, AttrSelectorOperator, NamespaceConstraint}, + matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode}, + sink::Push, + Element, OpaqueElement, +}; +use slab::Slab; +use std::sync::atomic::Ordering; +use style::applicable_declarations::ApplicableDeclarationBlock; +use style::color::AbsoluteColor; +use style::properties::{Importance, PropertyDeclaration}; +use style::rule_tree::CascadeLevel; +use style::selector_parser::PseudoElement; +use style::stylesheets::layer_rule::LayerOrder; +use style::values::computed::text::TextAlign as StyloTextAlign; +use style::values::computed::Display; +use style::values::computed::Percentage; +use style::values::specified::box_::DisplayInside; +use style::values::specified::box_::DisplayOutside; +use style::values::AtomString; +use style::CaseSensitivityExt; +use style::{ + context::{ + QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters, + SharedStyleContext, StyleContext, + }, + dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot}, + properties::PropertyDeclarationBlock, + selector_parser::{NonTSPseudoClass, SelectorImpl}, + servo_arc::{Arc, ArcBorrow}, + shared_lock::{Locked, SharedRwLock}, + traversal::{DomTraversal, PerLevelTraversalData}, + values::{AtomIdent, GenericAtomIdent}, + Atom, +}; +use style_dom::ElementState; +use winit::event::Modifiers; + +/// A handle to a node that Servo's style traits are implemented against +/// +/// Since BlitzNodes are not persistent (IE we don't keep the pointers around between frames), we choose to just implement +/// the tree structure in the nodes themselves, and temporarily give out pointers during the layout phase. +#[derive(Clone, Copy, Debug)] +pub struct Handle<'a> { + pub node: &'a Node, + pub tree: &'a Slab, +} + +impl Handle<'_> { + pub fn get(&self, id: usize) -> Self { + Self { + node: &self.tree[id], + tree: self.tree, + } + } + + // Get the index of the current node in the parents child list + pub fn child_index(&self) -> Option { + self.node.parent.and_then(|parent_id| { + self.tree[parent_id] + .children + .iter() + .position(|id| *id == self.node.id) + }) + } + + pub fn forward(&self, n: usize) -> Option { + let child_idx = self.child_index().unwrap_or(0); + self.tree[self.node.parent?] + .children + .get(child_idx + n) + .map(|id| Self { + node: &self.tree[*id], + tree: self.tree, + }) + } + + pub fn backward(&self, n: usize) -> Option { + let child_idx = self.child_index().unwrap_or(0); + self.tree[self.node.parent?] + .children + .get(child_idx - n) + .map(|id| Self { + node: &self.tree[*id], + tree: self.tree, + }) + } + + /// Computes the Document-relative coordinates of the Node + pub fn absolute_position(&self, x: f32, y: f32) -> taffy::Point { + let x = x + self.node.final_layout.location.x - self.node.scroll_offset.x as f32; + let y = y + self.node.final_layout.location.y - self.node.scroll_offset.y as f32; + + self.node + .layout_parent + .get() + .map(|i| { + Self { + node: &self.tree[i], + tree: self.tree, + } + .absolute_position(x, y) + }) + .unwrap_or(taffy::Point { x, y }) + } + + /// Creates a synteh + pub fn synthetic_click_event(&self, mods: Modifiers) -> EventData { + let absolute_position = self.absolute_position(0.0, 0.0); + let x = absolute_position.x + (self.node.final_layout.size.width / 2.0); + let y = absolute_position.y + (self.node.final_layout.size.height / 2.0); + + EventData::Click { x, y, mods } + } + + /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns: + /// - None if the position is outside of this node's bounds + /// - Some(HitResult) if the position is within the node but doesn't match any children + /// - The result of recursively calling child.hit() on the the child element that is + /// positioned at that position if there is one. + /// + /// TODO: z-index + /// (If multiple children are positioned at the position then a random one will be recursed into) + pub fn hit(&self, x: f32, y: f32) -> Option { + let x = x - self.node.final_layout.location.x + self.node.scroll_offset.x as f32; + let y = y - self.node.final_layout.location.y + self.node.scroll_offset.y as f32; + + let size = self.node.final_layout.size; + + if x < 0.0 + || x > size.width + self.node.scroll_offset.x as f32 + || y < 0.0 + || y > size.height + self.node.scroll_offset.y as f32 + { + return None; + } + + // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id). + self.node + .layout_children + .borrow() + .iter() + .flatten() + .find_map(|&id| self.get(id).hit(x, y)) + .or(Some(HitResult { + node_id: self.node.id, + x, + y, + })) + } + + 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.node.raw_dom_data { + NodeData::Text(data) => { + out.push_str(&data.content); + } + NodeData::Element(..) | NodeData::AnonymousBlock(..) => { + for child_id in &self.node.children { + self.get(*child_id).write_text_content(out); + } + } + _ => {} + } + } + + /// Returns true if this node, or any of its children, is a block. + pub fn is_or_contains_block(&self) -> bool { + let display = self.node.display_style().unwrap_or(Display::inline()); + + match display.outside() { + DisplayOutside::None => false, + DisplayOutside::Block => true, + _ => { + if display.inside() == DisplayInside::Flow { + self.node + .children + .iter() + .copied() + .any(|child_id| self.get(child_id).is_or_contains_block()) + } else { + false + } + } + } + } + + pub fn print_tree(&self, level: usize) { + println!( + "{} {} {:?} {} {:?}", + " ".repeat(level), + self.node.id, + self.node.parent, + self.node.node_debug_str().replace('\n', ""), + self.node.children + ); + + for child_id in &self.node.children { + let child = self.get(*child_id); + child.print_tree(level + 1) + } + } +} + +impl PartialEq for Handle<'_> { + fn eq(&self, other: &Self) -> bool { + self.node == other.node + } +} + +// TODO (Matt) +impl Eq for Handle<'_> {} + +impl TDocument for Handle<'_> { + type ConcreteNode = Self; + + fn as_node(&self) -> Self::ConcreteNode { + *self + } + + fn is_html_document(&self) -> bool { + true + } + + fn quirks_mode(&self) -> QuirksMode { + QuirksMode::NoQuirks + } + + fn shared_lock(&self) -> &SharedRwLock { + &self.node.guard + } +} + +impl NodeInfo for Handle<'_> { + fn is_element(&self) -> bool { + self.node.is_element() + } + + fn is_text_node(&self) -> bool { + self.node.is_text_node() + } +} + +impl TShadowRoot for Handle<'_> { + type ConcreteNode = Self; + + fn as_node(&self) -> Self::ConcreteNode { + *self + } + + fn host(&self) -> ::ConcreteElement { + todo!("Shadow roots not implemented") + } + + fn style_data<'b>(&self) -> Option<&'b style::stylist::CascadeData> + where + Self: 'b, + { + todo!("Shadow roots not implemented") + } +} + +// components/styleaapper.rs: +impl TNode for Handle<'_> { + type ConcreteElement = Self; + type ConcreteDocument = Self; + type ConcreteShadowRoot = Self; + + fn parent_node(&self) -> Option { + self.node.parent.map(|id| self.get(id)) + } + + fn first_child(&self) -> Option { + self.node.children.first().map(|id| self.get(*id)) + } + + fn last_child(&self) -> Option { + self.node.children.last().map(|id| self.get(*id)) + } + + fn prev_sibling(&self) -> Option { + self.backward(1) + } + + fn next_sibling(&self) -> Option { + self.forward(1) + } + + fn owner_doc(&self) -> Self::ConcreteDocument { + self.get(1) + } + + fn is_in_document(&self) -> bool { + true + } + + // I think this is the same as parent_node only in the cases when the direct parent is not a real element, forcing us + // to travel upwards + // + // For the sake of this demo, we're just going to return the parent node ann + fn traversal_parent(&self) -> Option { + self.parent_node().and_then(|node| node.as_element()) + } + + fn opaque(&self) -> OpaqueNode { + OpaqueNode(self.node.id) + } + + fn debug_id(self) -> usize { + self.node.id + } + + fn as_element(&self) -> Option { + match self.node.raw_dom_data { + NodeData::Element { .. } => Some(*self), + _ => None, + } + } + + fn as_document(&self) -> Option { + match self.node.raw_dom_data { + NodeData::Document { .. } => Some(*self), + _ => None, + } + } + + fn as_shadow_root(&self) -> Option { + todo!("Shadow roots aren't real, yet") + } +} + +impl selectors::Element for Handle<'_> { + type Impl = SelectorImpl; + + fn opaque(&self) -> selectors::OpaqueElement { + // FIXME: this is wrong in the case where pushing new elements casuses reallocations. + // We should see if selectors will accept a PR that allows creation from a usize + OpaqueElement::new(self) + } + + fn parent_element(&self) -> Option { + TElement::traversal_parent(self) + } + + fn parent_node_is_shadow_root(&self) -> bool { + false + } + + fn containing_shadow_host(&self) -> Option { + None + } + + fn is_pseudo_element(&self) -> bool { + matches!(self.node.raw_dom_data, NodeData::AnonymousBlock(_)) + } + + // These methods are implemented naively since we only threaded real nodes and not fake nodes + // we should try and use `find` instead of this foward/backward stuff since its ugly and slow + fn prev_sibling_element(&self) -> Option { + let mut n = 1; + while let Some(node) = self.backward(n) { + if node.is_element() { + return Some(node); + } + n += 1; + } + + None + } + + fn next_sibling_element(&self) -> Option { + let mut n = 1; + while let Some(node) = self.forward(n) { + if node.is_element() { + return Some(node); + } + n += 1; + } + + None + } + + fn first_element_child(&self) -> Option { + let mut children = self.dom_children(); + children.find(|child| child.is_element()) + } + + fn is_html_element_in_html_document(&self) -> bool { + true // self.has_namespace(ns!(html)) + } + + fn has_local_name(&self, local_name: &LocalName) -> bool { + self.node.raw_dom_data.is_element_with_tag_name(local_name) + } + + fn has_namespace(&self, ns: &Namespace) -> bool { + self.node.element_data().expect("Not an element").name.ns == *ns + } + + fn is_same_type(&self, _other: &Self) -> bool { + // FIXME: implementing this correctly currently triggers a debug_assert ("Invalid cache") in selectors + //self.local_name() == other.local_name() && self.namespace() == other.namespace() + false + } + + fn attr_matches( + &self, + _ns: &NamespaceConstraint<&GenericAtomIdent>, + local_name: &GenericAtomIdent, + operation: &AttrSelectorOperation<&AtomString>, + ) -> bool { + let Some(attr_value) = self.node.raw_dom_data.attr(local_name.0.clone()) else { + return false; + }; + + match operation { + AttrSelectorOperation::Exists => true, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity: _, + value, + } => { + let value = value.as_ref(); + + // TODO: case sensitivity + match operator { + AttrSelectorOperator::Equal => attr_value == value, + AttrSelectorOperator::Includes => attr_value + .split_ascii_whitespace() + .any(|word| word == value), + AttrSelectorOperator::DashMatch => { + // Represents elements with an attribute name of attr whose value can be exactly value + // or can begin with value immediately followed by a hyphen, - (U+002D) + attr_value.starts_with(value) + && (attr_value.len() == value.len() + || attr_value.chars().nth(value.len()) == Some('-')) + } + AttrSelectorOperator::Prefix => attr_value.starts_with(value), + AttrSelectorOperator::Substring => attr_value.contains(value), + AttrSelectorOperator::Suffix => attr_value.ends_with(value), + } + } + } + } + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &::NonTSPseudoClass, + _context: &mut MatchingContext, + ) -> bool { + match *pseudo_class { + NonTSPseudoClass::Active => false, + NonTSPseudoClass::AnyLink => self + .node + .raw_dom_data + .downcast_element() + .map(|elem| { + (elem.name.local == local_name!("a") || elem.name.local == local_name!("area")) + && elem.attr(local_name!("href")).is_some() + }) + .unwrap_or(false), + NonTSPseudoClass::Checked => self + .node + .raw_dom_data + .downcast_element() + .and_then(|elem| elem.checkbox_input_checked()) + .unwrap_or(false), + NonTSPseudoClass::Valid => false, + NonTSPseudoClass::Invalid => false, + NonTSPseudoClass::Defined => false, + NonTSPseudoClass::Disabled => false, + NonTSPseudoClass::Enabled => false, + NonTSPseudoClass::Focus => self.node.element_state.contains(ElementState::FOCUS), + NonTSPseudoClass::FocusWithin => false, + NonTSPseudoClass::FocusVisible => false, + NonTSPseudoClass::Fullscreen => false, + NonTSPseudoClass::Hover => self.node.element_state.contains(ElementState::HOVER), + NonTSPseudoClass::Indeterminate => false, + NonTSPseudoClass::Lang(_) => false, + NonTSPseudoClass::CustomState(_) => false, + NonTSPseudoClass::Link => self + .node + .raw_dom_data + .downcast_element() + .map(|elem| { + (elem.name.local == local_name!("a") || elem.name.local == local_name!("area")) + && elem.attr(local_name!("href")).is_some() + }) + .unwrap_or(false), + NonTSPseudoClass::PlaceholderShown => false, + NonTSPseudoClass::ReadWrite => false, + NonTSPseudoClass::ReadOnly => false, + NonTSPseudoClass::ServoNonZeroBorder => false, + NonTSPseudoClass::Target => false, + NonTSPseudoClass::Visited => false, + NonTSPseudoClass::Autofill => false, + NonTSPseudoClass::Default => false, + + NonTSPseudoClass::InRange => false, + NonTSPseudoClass::Modal => false, + NonTSPseudoClass::Optional => false, + NonTSPseudoClass::OutOfRange => false, + NonTSPseudoClass::PopoverOpen => false, + NonTSPseudoClass::Required => false, + NonTSPseudoClass::UserInvalid => false, + NonTSPseudoClass::UserValid => false, + } + } + + fn match_pseudo_element( + &self, + pe: &PseudoElement, + _context: &mut MatchingContext, + ) -> bool { + match self.node.raw_dom_data { + NodeData::AnonymousBlock(_) => *pe == PseudoElement::ServoAnonymousBox, + _ => false, + } + } + + fn apply_selector_flags(&self, flags: ElementSelectorFlags) { + // Handle flags that apply to the element. + let self_flags = flags.for_self(); + if !self_flags.is_empty() { + *self.node.selector_flags.borrow_mut() |= self_flags; + } + + // Handle flags that apply to the parent. + let parent_flags = flags.for_parent(); + if !parent_flags.is_empty() { + if let Some(parent) = self.parent_node() { + *parent.node.selector_flags.borrow_mut() |= self_flags; + } + } + } + + fn is_link(&self) -> bool { + self.node + .raw_dom_data + .is_element_with_tag_name(&local_name!("a")) + } + + fn is_html_slot_element(&self) -> bool { + false + } + + fn has_id( + &self, + id: &::Identifier, + case_sensitivity: selectors::attr::CaseSensitivity, + ) -> bool { + self.node + .element_data() + .and_then(|data| data.id.as_ref()) + .map(|id_attr| case_sensitivity.eq_atom(id_attr, id)) + .unwrap_or(false) + } + + fn has_class( + &self, + search_name: &::Identifier, + case_sensitivity: selectors::attr::CaseSensitivity, + ) -> bool { + let class_attr = self.node.raw_dom_data.attr(local_name!("class")); + if let Some(class_attr) = class_attr { + // split the class attribute + for pheme in class_attr.split_ascii_whitespace() { + let atom = Atom::from(pheme); + if case_sensitivity.eq_atom(&atom, search_name) { + return true; + } + } + } + + false + } + + fn imported_part( + &self, + _name: &::Identifier, + ) -> Option<::Identifier> { + None + } + + fn is_part(&self, _name: &::Identifier) -> bool { + false + } + + fn is_empty(&self) -> bool { + self.dom_children().next().is_none() + } + + fn is_root(&self) -> bool { + self.parent_node() + .and_then(|parent| parent.parent_node()) + .is_none() + } + + fn has_custom_state( + &self, + _name: &::Identifier, + ) -> bool { + false + } + + fn add_element_unique_hashes(&self, _filter: &mut selectors::bloom::BloomFilter) -> bool { + false + } +} + +impl<'a> TElement for Handle<'a> { + type ConcreteNode = Handle<'a>; + + type TraversalChildrenIterator = Traverser<'a>; + + fn as_node(&self) -> Self::ConcreteNode { + *self + } + + fn unopaque(opaque: OpaqueElement) -> Self { + // FIXME: this is wrong in the case where pushing new elements casuses reallocations. + // We should see if selectors will accept a PR that allows creation from a usize + unsafe { *opaque.as_const_ptr() } + } + + fn traversal_children(&self) -> style::dom::LayoutIterator { + LayoutIterator(Traverser { + // dom: self.tree(), + parent: *self, + child_index: 0, + }) + } + + fn is_html_element(&self) -> bool { + self.is_element() + } + + // not implemented..... + fn is_mathml_element(&self) -> bool { + false + } + + // need to check the namespace + fn is_svg_element(&self) -> bool { + false + } + + fn style_attribute(&self) -> Option>> { + self.node + .element_data() + .expect("Not an element") + .style_attribute + .as_ref() + .map(|f| f.borrow_arc()) + } + + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option>> { + None + } + + fn transition_rule( + &self, + _context: &SharedStyleContext, + ) -> Option>> { + None + } + + fn state(&self) -> ElementState { + self.node.element_state + } + + fn has_part_attr(&self) -> bool { + false + } + + fn exports_any_part(&self) -> bool { + false + } + + fn id(&self) -> Option<&style::Atom> { + self.node.element_data().and_then(|data| data.id.as_ref()) + } + + fn each_class(&self, mut callback: F) + where + F: FnMut(&style::values::AtomIdent), + { + let class_attr = self.node.raw_dom_data.attr(local_name!("class")); + if let Some(class_attr) = class_attr { + // split the class attribute + for pheme in class_attr.split_ascii_whitespace() { + let atom = Atom::from(pheme); // interns the string + callback(AtomIdent::cast(&atom)); + } + } + } + + fn each_attr_name(&self, mut callback: F) + where + F: FnMut(&style::LocalName), + { + if let Some(attrs) = self.node.raw_dom_data.attrs() { + for attr in attrs.iter() { + callback(&GenericAtomIdent(attr.name.local.clone())); + } + } + } + + fn has_dirty_descendants(&self) -> bool { + true + } + + fn has_snapshot(&self) -> bool { + self.node.has_snapshot + } + + fn handled_snapshot(&self) -> bool { + self.node.snapshot_handled.load(Ordering::SeqCst) + } + + unsafe fn set_handled_snapshot(&self) { + self.node.snapshot_handled.store(true, Ordering::SeqCst); + } + + unsafe fn set_dirty_descendants(&self) {} + + unsafe fn unset_dirty_descendants(&self) {} + + fn store_children_to_process(&self, _n: isize) { + unimplemented!() + } + + fn did_process_child(&self) -> isize { + unimplemented!() + } + + unsafe fn ensure_data(&self) -> AtomicRefMut { + let mut stylo_data = self.node.stylo_element_data.borrow_mut(); + if stylo_data.is_none() { + *stylo_data = Some(Default::default()); + } + AtomicRefMut::map(stylo_data, |sd| sd.as_mut().unwrap()) + } + + unsafe fn clear_data(&self) { + *self.node.stylo_element_data.borrow_mut() = None; + } + + fn has_data(&self) -> bool { + self.node.stylo_element_data.borrow().is_some() + } + + fn borrow_data(&self) -> Option> { + let stylo_data = self.node.stylo_element_data.borrow(); + if stylo_data.is_some() { + Some(AtomicRef::map(stylo_data, |sd| sd.as_ref().unwrap())) + } else { + None + } + } + + fn mutate_data(&self) -> Option> { + let stylo_data = self.node.stylo_element_data.borrow_mut(); + if stylo_data.is_some() { + Some(AtomicRefMut::map(stylo_data, |sd| sd.as_mut().unwrap())) + } else { + None + } + } + + fn skip_item_display_fixup(&self) -> bool { + false + } + + fn may_have_animations(&self) -> bool { + false + } + + fn has_animations(&self, _context: &SharedStyleContext) -> bool { + false + } + + fn has_css_animations( + &self, + _context: &SharedStyleContext, + _pseudo_element: Option, + ) -> bool { + false + } + + fn has_css_transitions( + &self, + _context: &SharedStyleContext, + _pseudo_element: Option, + ) -> bool { + false + } + + fn shadow_root(&self) -> Option<::ConcreteShadowRoot> { + None + } + + fn containing_shadow(&self) -> Option<::ConcreteShadowRoot> { + None + } + + fn lang_attr(&self) -> Option { + None + } + + fn match_element_lang( + &self, + _override_lang: Option>, + _value: &style::selector_parser::Lang, + ) -> bool { + false + } + + fn is_html_document_body_element(&self) -> bool { + // Check node is a element + let is_body_element = self + .node + .raw_dom_data + .is_element_with_tag_name(&local_name!("body")); + + // If it isn't then return early + if !is_body_element { + return false; + } + + // If it is then check if it is a child of the root () element + let root_node = &self.get(0); + let root_element = TDocument::as_node(root_node).first_element_child().unwrap(); + root_element.node.children.contains(&self.node.id) + } + + fn synthesize_presentational_hints_for_legacy_attributes( + &self, + _visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push, + { + let Some(elem) = self.node.raw_dom_data.downcast_element() else { + return; + }; + + let mut push_style = |decl: PropertyDeclaration| { + hints.push(ApplicableDeclarationBlock::from_declarations( + Arc::new( + self.node + .guard + .wrap(PropertyDeclarationBlock::with_one(decl, Importance::Normal)), + ), + CascadeLevel::PresHints, + LayerOrder::root(), + )); + }; + + fn parse_color_attr(value: &str) -> Option<(u8, u8, u8, f32)> { + if !value.starts_with('#') { + return None; + } + + let value = &value[1..]; + if value.len() == 3 { + let r = u8::from_str_radix(&value[0..1], 16).ok()?; + let g = u8::from_str_radix(&value[1..2], 16).ok()?; + let b = u8::from_str_radix(&value[2..3], 16).ok()?; + return Some((r, g, b, 1.0)); + } + + if value.len() == 6 { + let r = u8::from_str_radix(&value[0..2], 16).ok()?; + let g = u8::from_str_radix(&value[2..4], 16).ok()?; + let b = u8::from_str_radix(&value[4..6], 16).ok()?; + return Some((r, g, b, 1.0)); + } + + None + } + + fn parse_size_attr(value: &str) -> Option { + use style::values::specified::{AbsoluteLength, LengthPercentage, NoCalcLength}; + if let Some(value) = value.strip_suffix("px") { + let val: f32 = value.parse().ok()?; + return Some(LengthPercentage::Length(NoCalcLength::Absolute( + AbsoluteLength::Px(val), + ))); + } + + if let Some(value) = value.strip_suffix("%") { + let val: f32 = value.parse().ok()?; + return Some(LengthPercentage::Percentage(Percentage(val / 100.0))); + } + + let val: f32 = value.parse().ok()?; + Some(LengthPercentage::Length(NoCalcLength::Absolute( + AbsoluteLength::Px(val), + ))) + } + + for attr in elem.attrs() { + let name = &attr.name.local; + let value = attr.value.as_str(); + + if *name == local_name!("align") { + use style::values::specified::TextAlign; + let keyword = match value { + "left" => Some(StyloTextAlign::MozLeft), + "right" => Some(StyloTextAlign::MozRight), + "center" => Some(StyloTextAlign::MozCenter), + _ => None, + }; + + if let Some(keyword) = keyword { + push_style(PropertyDeclaration::TextAlign(TextAlign::Keyword(keyword))); + } + } + + if *name == local_name!("width") { + if let Some(width) = parse_size_attr(value) { + use style::values::generics::{length::Size, NonNegative}; + push_style(PropertyDeclaration::Width(Size::LengthPercentage( + NonNegative(width), + ))); + } + } + + if *name == local_name!("height") { + if let Some(height) = parse_size_attr(value) { + use style::values::generics::{length::Size, NonNegative}; + push_style(PropertyDeclaration::Height(Size::LengthPercentage( + NonNegative(height), + ))); + } + } + + if *name == local_name!("bgcolor") { + use style::values::specified::Color; + if let Some((r, g, b, a)) = parse_color_attr(value) { + push_style(PropertyDeclaration::BackgroundColor( + Color::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a)), + )); + } + } + } + } + + fn local_name(&self) -> &LocalName { + &self.node.element_data().expect("Not an element").name.local + } + + fn namespace(&self) -> &Namespace { + &self.node.element_data().expect("Not an element").name.ns + } + + fn query_container_size( + &self, + _display: &style::values::specified::Display, + ) -> euclid::default::Size2D> { + // FIXME: Implement container queries. For now this effectively disables them without panicking. + Default::default() + } + + fn each_custom_state(&self, _callback: F) + where + F: FnMut(&AtomIdent), + { + todo!() + } + + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool { + self.node.selector_flags.borrow().contains(flags) + } + + fn relative_selector_search_direction(&self) -> ElementSelectorFlags { + self.node + .selector_flags + .borrow() + .intersection(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING) + } + + // fn update_animations( + // &self, + // before_change_style: Option>, + // tasks: style::context::UpdateAnimationsTasks, + // ) { + // todo!() + // } + + // fn process_post_animation(&self, tasks: style::context::PostAnimationTasks) { + // todo!() + // } + + // fn needs_transitions_update( + // &self, + // before_change_style: &ComputedValues, + // after_change_style: &ComputedValues, + // ) -> bool { + // todo!() + // } +} + +pub struct Traverser<'a> { + // dom: &'a Slab, + parent: Handle<'a>, + child_index: usize, +} + +impl<'a> Iterator for Traverser<'a> { + type Item = Handle<'a>; + + fn next(&mut self) -> Option { + let node_id = self.parent.node.children.get(self.child_index)?; + let node = self.parent.tree.get(*node_id)?; + + self.child_index += 1; + + Some(Handle { + node, + tree: self.parent.tree, + }) + } +} + +impl std::hash::Hash for Handle<'_> { + fn hash(&self, state: &mut H) { + state.write_usize(self.node.id) + } +} + +/// Handle custom painters like images for layouting +/// +/// todo: actually implement this +pub struct RegisteredPaintersImpl; +impl RegisteredSpeculativePainters for RegisteredPaintersImpl { + fn get(&self, _name: &Atom) -> Option<&dyn RegisteredSpeculativePainter> { + None + } +} + +use style::traversal::recalc_style_at; + +pub struct RecalcStyle<'a> { + context: SharedStyleContext<'a>, +} + +impl<'a> RecalcStyle<'a> { + pub fn new(context: SharedStyleContext<'a>) -> Self { + RecalcStyle { context } + } +} + +#[allow(unsafe_code)] +impl DomTraversal for RecalcStyle<'_> +where + E: TElement, +{ + fn process_preorder( + &self, + traversal_data: &PerLevelTraversalData, + context: &mut StyleContext, + node: E::ConcreteNode, + note_child: F, + ) { + // Don't process textnodees in this traversal + if node.is_text_node() { + return; + } + + let el = node.as_element().unwrap(); + // let mut data = el.mutate_data().unwrap(); + let mut data = unsafe { el.ensure_data() }; + recalc_style_at(self, traversal_data, context, el, &mut data, note_child); + + // Gets set later on + unsafe { el.unset_dirty_descendants() } + } + + #[inline] + fn needs_postorder_traversal() -> bool { + false + } + + fn process_postorder(&self, _style_context: &mut StyleContext, _node: E::ConcreteNode) { + panic!("this should never be called") + } + + #[inline] + fn shared_context(&self) -> &SharedStyleContext { + &self.context + } +} + +#[test] +fn assert_size_of_equals() { + // use std::mem; + + // fn assert_layout() { + // assert_eq!( + // mem::size_of::>(), + // mem::size_of::() + // ); + // assert_eq!( + // mem::align_of::>(), + // mem::align_of::() + // ); + // } + + // let size = mem::size_of::>(); + // dbg!(size); +} + +#[test] +fn parse_inline() { + // let attrs = style::attr::AttrValue::from_serialized_tokenlist( + // r#"visibility: hidden; left: 1306.5px; top: 50px; display: none;"#.to_string(), + // ); + + // let val = CSSInlineStyleDeclaration(); +} diff --git a/packages/blitz-dom/src/image.rs b/packages/blitz-dom/src/image.rs index 577492f7e..09c84bad4 100644 --- a/packages/blitz-dom/src/image.rs +++ b/packages/blitz-dom/src/image.rs @@ -1,12 +1,17 @@ use taffy::{MaybeMath, MaybeResolve}; +/// Layout context for an image. #[derive(Debug, Clone, Copy)] pub struct ImageContext { + /// Inherent size of the image. pub inherent_size: taffy::Size, + + /// Specified size of the image. pub attr_size: taffy::Size>, } -pub fn image_measure_function( +/// Measure an image. +pub fn measure_image( known_dimensions: taffy::Size>, parent_size: taffy::Size>, image_context: &ImageContext, diff --git a/packages/blitz-dom/src/layout/construct.rs b/packages/blitz-dom/src/layout/construct.rs index 48edbad4c..c16b2cdbe 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -1,9 +1,16 @@ +use super::table::build_table_context; +use crate::{ + node::{ + ListItemLayout, ListItemLayoutPosition, Marker, NodeKind, NodeSpecificData, TextBrush, + TextInputData, TextLayout, + }, + stylo_to_parley, Document, ElementNodeData, Handle, Node, NodeData, +}; use core::str; -use std::sync::Arc; - use html5ever::{local_name, namespace_url, ns, QualName}; use parley::{FontStack, InlineBox, PlainEditorOp, StyleProperty, TreeBuilder, WhiteSpaceCollapse}; use slab::Slab; +use std::sync::Arc; use style::{ data::ElementData, properties::longhands::{ @@ -17,16 +24,6 @@ use style::{ }, }; -use crate::{ - node::{ - ListItemLayout, ListItemLayoutPosition, Marker, NodeKind, NodeSpecificData, TextBrush, - TextInputData, TextLayout, - }, - stylo_to_parley, Document, ElementNodeData, Node, NodeData, -}; - -use super::table::build_table_context; - pub(crate) fn collect_layout_children( doc: &mut Document, container_node_id: usize, @@ -119,7 +116,12 @@ pub(crate) fn collect_layout_children( all_block = false; // We need the "complex" tree fixing when an inline contains a block - if child.is_or_contains_block() { + if (Handle { + node: child, + tree: doc.tree(), + }) + .is_or_contains_block() + { all_inline = false; } } @@ -407,7 +409,7 @@ fn collect_complex_layout_children( let child_node_kind = doc.nodes[child_id].raw_dom_data.kind(); // Get Display style. Default to inline because nodes without styles are probably text nodes - let contains_block = doc.nodes[child_id].is_or_contains_block(); + let contains_block = doc.get(child_id).unwrap().is_or_contains_block(); let child_display = &doc.nodes[child_id] .display_style() .unwrap_or(Display::inline()); @@ -454,7 +456,7 @@ fn collect_complex_layout_children( let parent_style = doc.nodes[container_node_id].primary_styles().unwrap(); let read_guard = doc.guard.read(); let guards = StylesheetGuards::same(&read_guard); - let style = doc.stylist.style_for_anonymous::<&Node>( + let style = doc.stylist.style_for_anonymous::( &guards, &PseudoElement::ServoAnonymousBox, &parent_style, diff --git a/packages/blitz-dom/src/layout/mod.rs b/packages/blitz-dom/src/layout/mod.rs index 52a32f66e..d770eb2ed 100644 --- a/packages/blitz-dom/src/layout/mod.rs +++ b/packages/blitz-dom/src/layout/mod.rs @@ -7,7 +7,7 @@ use crate::node::{NodeData, NodeKind, NodeSpecificData}; use crate::{ document::Document, - image::{image_measure_function, ImageContext}, + image::{measure_image, ImageContext}, node::Node, }; use html5ever::local_name; @@ -245,7 +245,7 @@ impl LayoutPartialTree for Document { inputs, &node.style, |known_dimensions, _available_space| { - image_measure_function( + measure_image( known_dimensions, inputs.parent_size, &image_context, diff --git a/packages/blitz-dom/src/lib.rs b/packages/blitz-dom/src/lib.rs index 2934ab538..a88679113 100644 --- a/packages/blitz-dom/src/lib.rs +++ b/packages/blitz-dom/src/lib.rs @@ -15,6 +15,11 @@ /// This is the primary entry point for this crate. pub mod document; +/// Node handle. +pub mod handle; +pub use self::handle::Handle; + +/// HTML document data structure. pub mod html_document; /// An implementation for Html5ever's sink trait, allowing us to parse HTML into a DOM. pub mod htmlsink; @@ -37,14 +42,19 @@ pub mod stylo_to_parley; /// Conversions from Stylo types to Taffy and Parley types pub mod stylo_to_taffy; +/// Image layout. pub mod image; +/// Utility functions. pub mod util; +/// Debugging. pub mod debug; +/// Events. pub mod events; +/// Window viewport. pub mod viewport; pub use document::{Document, DocumentLike}; diff --git a/packages/blitz-dom/src/node.rs b/packages/blitz-dom/src/node.rs index b684509cd..38522e6aa 100644 --- a/packages/blitz-dom/src/node.rs +++ b/packages/blitz-dom/src/node.rs @@ -4,7 +4,6 @@ use image::DynamicImage; use parley::{FontContext, LayoutContext, PlainEditorOp}; use peniko::kurbo; use selectors::matching::{ElementSelectorFlags, QuirksMode}; -use slab::Slab; use std::cell::{Cell, RefCell}; use std::fmt::Write; use std::str::FromStr; @@ -14,7 +13,6 @@ use style::invalidation::element::restyle_hints::RestyleHint; use style::properties::ComputedValues; use style::stylesheets::UrlExtraData; use style::values::computed::Display; -use style::values::specified::box_::{DisplayInside, DisplayOutside}; use style::Atom; use style::{ data::ElementData, @@ -29,9 +27,8 @@ use taffy::{ Cache, }; use url::Url; -use winit::event::Modifiers; -use crate::events::{EventData, EventListener, HitResult}; +use crate::events::EventListener; use crate::layout::table::TableContext; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -41,55 +38,87 @@ pub enum DisplayOuter { None, } -// todo: might be faster to migrate this to ecs and split apart at a different boundary +/// A node in a [`Document`](crate::Document). pub struct Node { - // The actual tree we belong to. This is unsafe!! - pub tree: *mut Slab, - - /// Our Id + /// This node's ID. pub id: usize, - /// Our parent's ID + + /// This node's parent ID. pub parent: Option, - // What are our children? + + /// This node's child IDs. pub children: Vec, - /// Our parent in the layout hierachy: a separate list that includes anonymous collections of inline elements + + /// This node's parent ID in the layout hierachy: a separate list that includes anonymous collections of inline elements. pub layout_parent: Cell>, - /// A separate child list that includes anonymous collections of inline elements + + /// This node's child IDs in the layout hierachy: a separate list that includes anonymous collections of inline elements. pub layout_children: RefCell>>, - /// Node type (Element, TextNode, etc) specific data + /// Data specific to this node's type. pub raw_dom_data: NodeData, // This little bundle of joy is our style data from stylo and a lock guard that allows access to it // TODO: See if guard can be hoisted to a higher level + /// The (optional) stylo data for this node. pub stylo_element_data: AtomicRefCell>, + + /// The selector flags for this node. pub selector_flags: AtomicRefCell, + + /// Lock guard for this node's stylo data. pub guard: SharedRwLock, + + /// The state of the element, represented as a bitfield. pub element_state: ElementState, // Taffy layout data: pub style: Style, + + /// Flag indicating whether this node is hidden. pub hidden: bool, + + /// Flag indicating whether this node is hovered. pub is_hovered: bool, + + /// Flag indicating whether this node has a stylo snapshot. pub has_snapshot: bool, + + /// Flag indicating whether the current snapshot has been handled. pub snapshot_handled: AtomicBool, + + /// Outer display type of this node. pub display_outer: DisplayOuter, + + /// Cached layout for this node. pub cache: Cache, + + /// Cached layout before rounding for this node. pub unrounded_layout: Layout, + + /// Cached final layout for this node. pub final_layout: Layout, + + /// Registered event listeners for this node. pub listeners: Vec, + + /// Current scroll offset for this node. pub scroll_offset: kurbo::Point, - // Flags + /// Flag indicating whether this node is the root element of an inline layout. pub is_inline_root: bool, + + /// Flag indicating whether this node is the root element of a table. pub is_table_root: bool, } impl Node { - pub fn new(tree: *mut Slab, id: usize, guard: SharedRwLock, data: NodeData) -> Self { + /// Create a new node in a tree. + /// + /// # Safety + /// `tree` must outlive this `Node`. + pub fn new(id: usize, guard: SharedRwLock, data: NodeData) -> Self { Self { - tree, - id, parent: None, children: vec![], @@ -135,25 +164,7 @@ impl Node { ) } - pub fn is_or_contains_block(&self) -> bool { - let display = self.display_style().unwrap_or(Display::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 - } - } - } - } - + /// Returns true if this node is able to focused. pub fn is_focussable(&self) -> bool { self.raw_dom_data .downcast_element() @@ -167,24 +178,28 @@ impl Node { } } + /// Flag this node as hovered. pub fn hover(&mut self) { self.is_hovered = true; self.element_state.insert(ElementState::HOVER); self.set_restyle_hint(RestyleHint::RESTYLE_SELF); } + /// Un-flag this node as hovered. pub fn unhover(&mut self) { self.is_hovered = false; self.element_state.remove(ElementState::HOVER); self.set_restyle_hint(RestyleHint::RESTYLE_SELF); } + /// Flag this node as focused. pub fn focus(&mut self) { self.element_state .insert(ElementState::FOCUS | ElementState::FOCUSRING); self.set_restyle_hint(RestyleHint::RESTYLE_SELF); } + /// Un-flag this node as focused. pub fn blur(&mut self) { self.element_state .remove(ElementState::FOCUS | ElementState::FOCUSRING); @@ -192,6 +207,7 @@ impl Node { } } +/// The kind of a [`Node`]. #[derive(Debug, Clone, Copy, PartialEq)] pub enum NodeKind { Document, @@ -416,7 +432,7 @@ impl ElementNodeData { } } - pub fn flush_is_focussable(&mut self) { + pub(crate) fn flush_is_focussable(&mut self) { let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false); let tabindex: Option = self.attr_parsed(local_name!("tabindex")); @@ -465,6 +481,7 @@ impl ElementNodeData { }); } + /// Take the current cached inline text layout. pub fn take_inline_layout(&mut self) -> Option> { std::mem::take(&mut self.inline_layout_data) } @@ -638,60 +655,6 @@ impl TextNodeData { // } impl Node { - pub fn tree(&self) -> &Slab { - unsafe { &*self.tree } - } - - #[track_caller] - pub fn with(&self, id: usize) -> &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 - ); - // println!("{} {:?}", " ".repeat(level), self.children); - for child_id in self.children.iter() { - let child = self.with(*child_id); - child.print_tree(level + 1) - } - } - - // Get the index of the current node in the parents child list - pub fn child_index(&self) -> Option { - self.tree()[self.parent?] - .children - .iter() - .position(|id| *id == self.id) - } - - // Get the nth node in the parents child list - 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.raw_dom_data, NodeData::Element { .. }) } @@ -796,26 +759,6 @@ impl Node { } } - 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.raw_dom_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) { if let NodeData::Element(ref mut elem_data) = self.raw_dom_data { elem_data.flush_style_attribute(&self.guard); @@ -830,61 +773,6 @@ impl Node { .map(|s| s.clone_order()) .unwrap_or(0) } - - /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns: - /// - None if the position is outside of this node's bounds - /// - Some(HitResult) if the position is within the node but doesn't match any children - /// - The result of recursively calling child.hit() on the the child element that is - /// positioned at that position if there is one. - /// - /// TODO: z-index - /// (If multiple children are positioned at the position then a random one will be recursed into) - pub fn hit(&self, x: f32, y: f32) -> Option { - 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; - - let size = self.final_layout.size; - if x < 0.0 - || x > size.width + self.scroll_offset.x as f32 - || y < 0.0 - || y > size.height + self.scroll_offset.y as f32 - { - return None; - } - - // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id). - self.layout_children - .borrow() - .iter() - .flatten() - .find_map(|&i| self.with(i).hit(x, y)) - .or(Some(HitResult { - node_id: self.id, - x, - y, - })) - } - - /// Computes the Document-relative coordinates of the Node - pub fn absolute_position(&self, x: f32, y: f32) -> taffy::Point { - 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; - - // Recurse up the layout hierarchy - self.layout_parent - .get() - .map(|i| self.with(i).absolute_position(x, y)) - .unwrap_or(taffy::Point { x, y }) - } - - /// Creates a synteh - pub fn synthetic_click_event(&self, mods: Modifiers) -> EventData { - 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); - - EventData::Click { x, y, mods } - } } /// It might be wrong to expose this since what does *equality* mean outside the dom? diff --git a/packages/blitz-dom/src/stylo.rs b/packages/blitz-dom/src/stylo.rs index 01c510053..ac51f334a 100644 --- a/packages/blitz-dom/src/stylo.rs +++ b/packages/blitz-dom/src/stylo.rs @@ -1,59 +1,27 @@ //! Enable the dom to participate in styling by servo //! -use std::sync::atomic::Ordering; - -use crate::node::Node; - -use crate::node::NodeData; -use atomic_refcell::{AtomicRef, AtomicRefMut}; -use html5ever::LocalNameStaticSet; -use html5ever::NamespaceStaticSet; -use html5ever::{local_name, LocalName, Namespace}; -use selectors::{ - attr::{AttrSelectorOperation, AttrSelectorOperator, NamespaceConstraint}, - matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode}, - sink::Push, - Element, OpaqueElement, -}; -use style::applicable_declarations::ApplicableDeclarationBlock; -use style::color::AbsoluteColor; +use super::stylo_to_taffy; +use crate::handle::RecalcStyle; +use crate::handle::RegisteredPaintersImpl; +use crate::Handle; +use selectors::Element; use style::invalidation::element::restyle_hints::RestyleHint; -use style::properties::{Importance, PropertyDeclaration}; -use style::rule_tree::CascadeLevel; -use style::selector_parser::PseudoElement; -use style::stylesheets::layer_rule::LayerOrder; -use style::values::computed::Percentage; use style::values::specified::box_::DisplayOutside; -use style::values::AtomString; -use style::CaseSensitivityExt; use style::{ animation::DocumentAnimationSet, - context::{ - QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters, - SharedStyleContext, StyleContext, - }, - dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot}, + context::SharedStyleContext, + dom::{TDocument, TNode}, global_style_data::GLOBAL_STYLE_DATA, - properties::PropertyDeclarationBlock, - selector_parser::{NonTSPseudoClass, SelectorImpl}, - servo_arc::{Arc, ArcBorrow}, - shared_lock::{Locked, SharedRwLock, StylesheetGuards}, + shared_lock::StylesheetGuards, thread_state::ThreadState, - traversal::{DomTraversal, PerLevelTraversalData}, + traversal::DomTraversal, traversal_flags::TraversalFlags, - values::{AtomIdent, GenericAtomIdent}, - Atom, }; -use style_dom::ElementState; - -use super::stylo_to_taffy; -use style::values::computed::text::TextAlign as StyloTextAlign; impl crate::document::Document { /// Walk the whole tree, converting styles to layout pub fn flush_styles_to_layout(&mut self, node_id: usize) { - let display = { let node = self.nodes.get_mut(node_id).unwrap(); let stylo_element_data = node.stylo_element_data.borrow(); @@ -85,7 +53,6 @@ impl crate::document::Document { // If the node has children, then take those children and... let children = self.nodes[node_id].layout_children.borrow_mut().take(); if let Some(mut children) = children { - // Recursively call flush_styles_to_layout on each child for child in children.iter() { self.flush_styles_to_layout(*child); @@ -114,15 +81,18 @@ impl crate::document::Document { ua_or_user: &guard.read(), }; - let root = TDocument::as_node(&&self.nodes[0]) - .first_element_child() - .unwrap() - .as_element() - .unwrap(); + let root = TDocument::as_node(&Handle { + node: &self.nodes[0], + tree: &self.nodes, + }) + .first_element_child() + .unwrap() + .as_element() + .unwrap(); // Force restyle all nodes // TODO: finer grained style invalidation - let mut stylo_element_data = root.stylo_element_data.borrow_mut(); + let mut stylo_element_data = root.node.stylo_element_data.borrow_mut(); if let Some(data) = &mut *stylo_element_data { data.hint |= RestyleHint::restyle_subtree(); data.hint |= RestyleHint::recascade_subtree(); @@ -148,7 +118,13 @@ impl crate::document::Document { // components/layout_2020/lib.rs:983 let root = self.root_element(); // dbg!(root); - let token = RecalcStyle::pre_traverse(root, &context); + let token = RecalcStyle::pre_traverse( + Handle { + node: root, + tree: self.tree(), + }, + &context, + ); if token.should_traverse() { // Style the elements, resolving their data @@ -159,912 +135,3 @@ impl crate::document::Document { style::thread_state::exit(ThreadState::LAYOUT); } } - -/// A handle to a node that Servo's style traits are implemented against -/// -/// Since BlitzNodes are not persistent (IE we don't keep the pointers around between frames), we choose to just implement -/// the tree structure in the nodes themselves, and temporarily give out pointers during the layout phase. -type BlitzNode<'a> = &'a Node; - -impl<'a> TDocument for BlitzNode<'a> { - type ConcreteNode = BlitzNode<'a>; - - fn as_node(&self) -> Self::ConcreteNode { - self - } - - fn is_html_document(&self) -> bool { - true - } - - fn quirks_mode(&self) -> QuirksMode { - QuirksMode::NoQuirks - } - - fn shared_lock(&self) -> &SharedRwLock { - &self.guard - } -} - -impl NodeInfo for BlitzNode<'_> { - fn is_element(&self) -> bool { - Node::is_element(self) - } - - fn is_text_node(&self) -> bool { - Node::is_text_node(self) - } -} - -impl<'a> TShadowRoot for BlitzNode<'a> { - type ConcreteNode = BlitzNode<'a>; - - fn as_node(&self) -> Self::ConcreteNode { - self - } - - fn host(&self) -> ::ConcreteElement { - todo!("Shadow roots not implemented") - } - - fn style_data<'b>(&self) -> Option<&'b style::stylist::CascadeData> - where - Self: 'b, - { - todo!("Shadow roots not implemented") - } -} - -// components/styleaapper.rs: -impl<'a> TNode for BlitzNode<'a> { - type ConcreteElement = BlitzNode<'a>; - type ConcreteDocument = BlitzNode<'a>; - type ConcreteShadowRoot = BlitzNode<'a>; - - fn parent_node(&self) -> Option { - self.parent.map(|id| self.with(id)) - } - - fn first_child(&self) -> Option { - self.children.first().map(|id| self.with(*id)) - } - - fn last_child(&self) -> Option { - self.children.last().map(|id| self.with(*id)) - } - - fn prev_sibling(&self) -> Option { - self.backward(1) - } - - fn next_sibling(&self) -> Option { - self.forward(1) - } - - fn owner_doc(&self) -> Self::ConcreteDocument { - self.with(1) - } - - fn is_in_document(&self) -> bool { - true - } - - // I think this is the same as parent_node only in the cases when the direct parent is not a real element, forcing us - // to travel upwards - // - // For the sake of this demo, we're just going to return the parent node ann - fn traversal_parent(&self) -> Option { - self.parent_node().and_then(|node| node.as_element()) - } - - fn opaque(&self) -> OpaqueNode { - OpaqueNode(self.id) - } - - fn debug_id(self) -> usize { - self.id - } - - fn as_element(&self) -> Option { - match self.raw_dom_data { - NodeData::Element { .. } => Some(self), - _ => None, - } - } - - fn as_document(&self) -> Option { - match self.raw_dom_data { - NodeData::Document { .. } => Some(self), - _ => None, - } - } - - fn as_shadow_root(&self) -> Option { - todo!("Shadow roots aren't real, yet") - } -} - -impl selectors::Element for BlitzNode<'_> { - type Impl = SelectorImpl; - - fn opaque(&self) -> selectors::OpaqueElement { - // FIXME: this is wrong in the case where pushing new elements casuses reallocations. - // We should see if selectors will accept a PR that allows creation from a usize - OpaqueElement::new(self) - } - - fn parent_element(&self) -> Option { - TElement::traversal_parent(self) - } - - fn parent_node_is_shadow_root(&self) -> bool { - false - } - - fn containing_shadow_host(&self) -> Option { - None - } - - fn is_pseudo_element(&self) -> bool { - matches!(self.raw_dom_data, NodeData::AnonymousBlock(_)) - } - - // These methods are implemented naively since we only threaded real nodes and not fake nodes - // we should try and use `find` instead of this foward/backward stuff since its ugly and slow - fn prev_sibling_element(&self) -> Option { - let mut n = 1; - while let Some(node) = self.backward(n) { - if node.is_element() { - return Some(node); - } - n += 1; - } - - None - } - - fn next_sibling_element(&self) -> Option { - let mut n = 1; - while let Some(node) = self.forward(n) { - if node.is_element() { - return Some(node); - } - n += 1; - } - - None - } - - fn first_element_child(&self) -> Option { - let mut children = self.dom_children(); - children.find(|child| child.is_element()) - } - - fn is_html_element_in_html_document(&self) -> bool { - true // self.has_namespace(ns!(html)) - } - - fn has_local_name(&self, local_name: &LocalName) -> bool { - self.raw_dom_data.is_element_with_tag_name(local_name) - } - - fn has_namespace(&self, ns: &Namespace) -> bool { - self.element_data().expect("Not an element").name.ns == *ns - } - - fn is_same_type(&self, _other: &Self) -> bool { - // FIXME: implementing this correctly currently triggers a debug_assert ("Invalid cache") in selectors - //self.local_name() == other.local_name() && self.namespace() == other.namespace() - false - } - - fn attr_matches( - &self, - _ns: &NamespaceConstraint<&GenericAtomIdent>, - local_name: &GenericAtomIdent, - operation: &AttrSelectorOperation<&AtomString>, - ) -> bool { - let Some(attr_value) = self.raw_dom_data.attr(local_name.0.clone()) else { - return false; - }; - - match operation { - AttrSelectorOperation::Exists => true, - AttrSelectorOperation::WithValue { - operator, - case_sensitivity: _, - value, - } => { - let value = value.as_ref(); - - // TODO: case sensitivity - match operator { - AttrSelectorOperator::Equal => attr_value == value, - AttrSelectorOperator::Includes => attr_value - .split_ascii_whitespace() - .any(|word| word == value), - AttrSelectorOperator::DashMatch => { - // Represents elements with an attribute name of attr whose value can be exactly value - // or can begin with value immediately followed by a hyphen, - (U+002D) - attr_value.starts_with(value) - && (attr_value.len() == value.len() - || attr_value.chars().nth(value.len()) == Some('-')) - } - AttrSelectorOperator::Prefix => attr_value.starts_with(value), - AttrSelectorOperator::Substring => attr_value.contains(value), - AttrSelectorOperator::Suffix => attr_value.ends_with(value), - } - } - } - } - - fn match_non_ts_pseudo_class( - &self, - pseudo_class: &::NonTSPseudoClass, - _context: &mut MatchingContext, - ) -> bool { - match *pseudo_class { - NonTSPseudoClass::Active => false, - NonTSPseudoClass::AnyLink => self - .raw_dom_data - .downcast_element() - .map(|elem| { - (elem.name.local == local_name!("a") || elem.name.local == local_name!("area")) - && elem.attr(local_name!("href")).is_some() - }) - .unwrap_or(false), - NonTSPseudoClass::Checked => self - .raw_dom_data - .downcast_element() - .and_then(|elem| elem.checkbox_input_checked()) - .unwrap_or(false), - NonTSPseudoClass::Valid => false, - NonTSPseudoClass::Invalid => false, - NonTSPseudoClass::Defined => false, - NonTSPseudoClass::Disabled => false, - NonTSPseudoClass::Enabled => false, - NonTSPseudoClass::Focus => self.element_state.contains(ElementState::FOCUS), - NonTSPseudoClass::FocusWithin => false, - NonTSPseudoClass::FocusVisible => false, - NonTSPseudoClass::Fullscreen => false, - NonTSPseudoClass::Hover => self.element_state.contains(ElementState::HOVER), - NonTSPseudoClass::Indeterminate => false, - NonTSPseudoClass::Lang(_) => false, - NonTSPseudoClass::CustomState(_) => false, - NonTSPseudoClass::Link => self - .raw_dom_data - .downcast_element() - .map(|elem| { - (elem.name.local == local_name!("a") || elem.name.local == local_name!("area")) - && elem.attr(local_name!("href")).is_some() - }) - .unwrap_or(false), - NonTSPseudoClass::PlaceholderShown => false, - NonTSPseudoClass::ReadWrite => false, - NonTSPseudoClass::ReadOnly => false, - NonTSPseudoClass::ServoNonZeroBorder => false, - NonTSPseudoClass::Target => false, - NonTSPseudoClass::Visited => false, - NonTSPseudoClass::Autofill => false, - NonTSPseudoClass::Default => false, - - NonTSPseudoClass::InRange => false, - NonTSPseudoClass::Modal => false, - NonTSPseudoClass::Optional => false, - NonTSPseudoClass::OutOfRange => false, - NonTSPseudoClass::PopoverOpen => false, - NonTSPseudoClass::Required => false, - NonTSPseudoClass::UserInvalid => false, - NonTSPseudoClass::UserValid => false, - } - } - - fn match_pseudo_element( - &self, - pe: &PseudoElement, - _context: &mut MatchingContext, - ) -> bool { - match self.raw_dom_data { - NodeData::AnonymousBlock(_) => *pe == PseudoElement::ServoAnonymousBox, - _ => false, - } - } - - fn apply_selector_flags(&self, flags: ElementSelectorFlags) { - // Handle flags that apply to the element. - let self_flags = flags.for_self(); - if !self_flags.is_empty() { - *self.selector_flags.borrow_mut() |= self_flags; - } - - // Handle flags that apply to the parent. - let parent_flags = flags.for_parent(); - if !parent_flags.is_empty() { - if let Some(parent) = self.parent_node() { - *parent.selector_flags.borrow_mut() |= self_flags; - } - } - } - - fn is_link(&self) -> bool { - self.raw_dom_data - .is_element_with_tag_name(&local_name!("a")) - } - - fn is_html_slot_element(&self) -> bool { - false - } - - fn has_id( - &self, - id: &::Identifier, - case_sensitivity: selectors::attr::CaseSensitivity, - ) -> bool { - self.element_data() - .and_then(|data| data.id.as_ref()) - .map(|id_attr| case_sensitivity.eq_atom(id_attr, id)) - .unwrap_or(false) - } - - fn has_class( - &self, - search_name: &::Identifier, - case_sensitivity: selectors::attr::CaseSensitivity, - ) -> bool { - let class_attr = self.raw_dom_data.attr(local_name!("class")); - if let Some(class_attr) = class_attr { - // split the class attribute - for pheme in class_attr.split_ascii_whitespace() { - let atom = Atom::from(pheme); - if case_sensitivity.eq_atom(&atom, search_name) { - return true; - } - } - } - - false - } - - fn imported_part( - &self, - _name: &::Identifier, - ) -> Option<::Identifier> { - None - } - - fn is_part(&self, _name: &::Identifier) -> bool { - false - } - - fn is_empty(&self) -> bool { - self.dom_children().next().is_none() - } - - fn is_root(&self) -> bool { - self.parent_node() - .and_then(|parent| parent.parent_node()) - .is_none() - } - - fn has_custom_state( - &self, - _name: &::Identifier, - ) -> bool { - false - } - - fn add_element_unique_hashes(&self, _filter: &mut selectors::bloom::BloomFilter) -> bool { - false - } -} - -impl<'a> TElement for BlitzNode<'a> { - type ConcreteNode = BlitzNode<'a>; - - type TraversalChildrenIterator = Traverser<'a>; - - fn as_node(&self) -> Self::ConcreteNode { - self - } - - fn unopaque(opaque: OpaqueElement) -> Self { - // FIXME: this is wrong in the case where pushing new elements casuses reallocations. - // We should see if selectors will accept a PR that allows creation from a usize - unsafe { &*opaque.as_const_ptr() } - } - - fn traversal_children(&self) -> style::dom::LayoutIterator { - LayoutIterator(Traverser { - // dom: self.tree(), - parent: self, - child_index: 0, - }) - } - - fn is_html_element(&self) -> bool { - self.is_element() - } - - // not implemented..... - fn is_mathml_element(&self) -> bool { - false - } - - // need to check the namespace - fn is_svg_element(&self) -> bool { - false - } - - fn style_attribute(&self) -> Option>> { - self.element_data() - .expect("Not an element") - .style_attribute - .as_ref() - .map(|f| f.borrow_arc()) - } - - fn animation_rule( - &self, - _: &SharedStyleContext, - ) -> Option>> { - None - } - - fn transition_rule( - &self, - _context: &SharedStyleContext, - ) -> Option>> { - None - } - - fn state(&self) -> ElementState { - self.element_state - } - - fn has_part_attr(&self) -> bool { - false - } - - fn exports_any_part(&self) -> bool { - false - } - - fn id(&self) -> Option<&style::Atom> { - self.element_data().and_then(|data| data.id.as_ref()) - } - - fn each_class(&self, mut callback: F) - where - F: FnMut(&style::values::AtomIdent), - { - let class_attr = self.raw_dom_data.attr(local_name!("class")); - if let Some(class_attr) = class_attr { - // split the class attribute - for pheme in class_attr.split_ascii_whitespace() { - let atom = Atom::from(pheme); // interns the string - callback(AtomIdent::cast(&atom)); - } - } - } - - fn each_attr_name(&self, mut callback: F) - where - F: FnMut(&style::LocalName), - { - if let Some(attrs) = self.raw_dom_data.attrs() { - for attr in attrs.iter() { - callback(&GenericAtomIdent(attr.name.local.clone())); - } - } - } - - fn has_dirty_descendants(&self) -> bool { - true - } - - fn has_snapshot(&self) -> bool { - self.has_snapshot - } - - fn handled_snapshot(&self) -> bool { - self.snapshot_handled.load(Ordering::SeqCst) - } - - unsafe fn set_handled_snapshot(&self) { - self.snapshot_handled.store(true, Ordering::SeqCst); - } - - unsafe fn set_dirty_descendants(&self) {} - - unsafe fn unset_dirty_descendants(&self) {} - - fn store_children_to_process(&self, _n: isize) { - unimplemented!() - } - - fn did_process_child(&self) -> isize { - unimplemented!() - } - - unsafe fn ensure_data(&self) -> AtomicRefMut { - let mut stylo_data = self.stylo_element_data.borrow_mut(); - if stylo_data.is_none() { - *stylo_data = Some(Default::default()); - } - AtomicRefMut::map(stylo_data, |sd| sd.as_mut().unwrap()) - } - - unsafe fn clear_data(&self) { - *self.stylo_element_data.borrow_mut() = None; - } - - fn has_data(&self) -> bool { - self.stylo_element_data.borrow().is_some() - } - - fn borrow_data(&self) -> Option> { - let stylo_data = self.stylo_element_data.borrow(); - if stylo_data.is_some() { - Some(AtomicRef::map(stylo_data, |sd| sd.as_ref().unwrap())) - } else { - None - } - } - - fn mutate_data(&self) -> Option> { - let stylo_data = self.stylo_element_data.borrow_mut(); - if stylo_data.is_some() { - Some(AtomicRefMut::map(stylo_data, |sd| sd.as_mut().unwrap())) - } else { - None - } - } - - fn skip_item_display_fixup(&self) -> bool { - false - } - - fn may_have_animations(&self) -> bool { - false - } - - fn has_animations(&self, _context: &SharedStyleContext) -> bool { - false - } - - fn has_css_animations( - &self, - _context: &SharedStyleContext, - _pseudo_element: Option, - ) -> bool { - false - } - - fn has_css_transitions( - &self, - _context: &SharedStyleContext, - _pseudo_element: Option, - ) -> bool { - false - } - - fn shadow_root(&self) -> Option<::ConcreteShadowRoot> { - None - } - - fn containing_shadow(&self) -> Option<::ConcreteShadowRoot> { - None - } - - fn lang_attr(&self) -> Option { - None - } - - fn match_element_lang( - &self, - _override_lang: Option>, - _value: &style::selector_parser::Lang, - ) -> bool { - false - } - - fn is_html_document_body_element(&self) -> bool { - // Check node is a element - let is_body_element = self - .raw_dom_data - .is_element_with_tag_name(&local_name!("body")); - - // If it isn't then return early - if !is_body_element { - return false; - } - - // If it is then check if it is a child of the root () element - let root_node = &self.tree()[0]; - let root_element = TDocument::as_node(&root_node) - .first_element_child() - .unwrap(); - root_element.children.contains(&self.id) - } - - fn synthesize_presentational_hints_for_legacy_attributes( - &self, - _visited_handling: VisitedHandlingMode, - hints: &mut V, - ) where - V: Push, - { - let Some(elem) = self.raw_dom_data.downcast_element() else { - return; - }; - - let mut push_style = |decl: PropertyDeclaration| { - hints.push(ApplicableDeclarationBlock::from_declarations( - Arc::new( - self.guard - .wrap(PropertyDeclarationBlock::with_one(decl, Importance::Normal)), - ), - CascadeLevel::PresHints, - LayerOrder::root(), - )); - }; - - fn parse_color_attr(value: &str) -> Option<(u8, u8, u8, f32)> { - if !value.starts_with('#') { - return None; - } - - let value = &value[1..]; - if value.len() == 3 { - let r = u8::from_str_radix(&value[0..1], 16).ok()?; - let g = u8::from_str_radix(&value[1..2], 16).ok()?; - let b = u8::from_str_radix(&value[2..3], 16).ok()?; - return Some((r, g, b, 1.0)); - } - - if value.len() == 6 { - let r = u8::from_str_radix(&value[0..2], 16).ok()?; - let g = u8::from_str_radix(&value[2..4], 16).ok()?; - let b = u8::from_str_radix(&value[4..6], 16).ok()?; - return Some((r, g, b, 1.0)); - } - - None - } - - fn parse_size_attr(value: &str) -> Option { - use style::values::specified::{AbsoluteLength, LengthPercentage, NoCalcLength}; - if let Some(value) = value.strip_suffix("px") { - let val: f32 = value.parse().ok()?; - return Some(LengthPercentage::Length(NoCalcLength::Absolute( - AbsoluteLength::Px(val), - ))); - } - - if let Some(value) = value.strip_suffix("%") { - let val: f32 = value.parse().ok()?; - return Some(LengthPercentage::Percentage(Percentage(val / 100.0))); - } - - let val: f32 = value.parse().ok()?; - Some(LengthPercentage::Length(NoCalcLength::Absolute( - AbsoluteLength::Px(val), - ))) - } - - for attr in elem.attrs() { - let name = &attr.name.local; - let value = attr.value.as_str(); - - if *name == local_name!("align") { - use style::values::specified::TextAlign; - let keyword = match value { - "left" => Some(StyloTextAlign::MozLeft), - "right" => Some(StyloTextAlign::MozRight), - "center" => Some(StyloTextAlign::MozCenter), - _ => None, - }; - - if let Some(keyword) = keyword { - push_style(PropertyDeclaration::TextAlign(TextAlign::Keyword(keyword))); - } - } - - if *name == local_name!("width") { - if let Some(width) = parse_size_attr(value) { - use style::values::generics::{length::Size, NonNegative}; - push_style(PropertyDeclaration::Width(Size::LengthPercentage( - NonNegative(width), - ))); - } - } - - if *name == local_name!("height") { - if let Some(height) = parse_size_attr(value) { - use style::values::generics::{length::Size, NonNegative}; - push_style(PropertyDeclaration::Height(Size::LengthPercentage( - NonNegative(height), - ))); - } - } - - if *name == local_name!("bgcolor") { - use style::values::specified::Color; - if let Some((r, g, b, a)) = parse_color_attr(value) { - push_style(PropertyDeclaration::BackgroundColor( - Color::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a)), - )); - } - } - } - } - - fn local_name(&self) -> &LocalName { - &self.element_data().expect("Not an element").name.local - } - - fn namespace(&self) -> &Namespace { - &self.element_data().expect("Not an element").name.ns - } - - fn query_container_size( - &self, - _display: &style::values::specified::Display, - ) -> euclid::default::Size2D> { - // FIXME: Implement container queries. For now this effectively disables them without panicking. - Default::default() - } - - fn each_custom_state(&self, _callback: F) - where - F: FnMut(&AtomIdent), - { - todo!() - } - - fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool { - self.selector_flags.borrow().contains(flags) - } - - fn relative_selector_search_direction(&self) -> ElementSelectorFlags { - self.selector_flags - .borrow() - .intersection(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING) - } - - // fn update_animations( - // &self, - // before_change_style: Option>, - // tasks: style::context::UpdateAnimationsTasks, - // ) { - // todo!() - // } - - // fn process_post_animation(&self, tasks: style::context::PostAnimationTasks) { - // todo!() - // } - - // fn needs_transitions_update( - // &self, - // before_change_style: &ComputedValues, - // after_change_style: &ComputedValues, - // ) -> bool { - // todo!() - // } -} - -pub struct Traverser<'a> { - // dom: &'a Slab, - parent: BlitzNode<'a>, - child_index: usize, -} - -impl<'a> Iterator for Traverser<'a> { - type Item = BlitzNode<'a>; - - fn next(&mut self) -> Option { - let node_id = self.parent.children.get(self.child_index)?; - let node = self.parent.with(*node_id); - - self.child_index += 1; - - Some(node) - } -} - -impl std::hash::Hash for BlitzNode<'_> { - fn hash(&self, state: &mut H) { - state.write_usize(self.id) - } -} - -/// Handle custom painters like images for layouting -/// -/// todo: actually implement this -pub struct RegisteredPaintersImpl; -impl RegisteredSpeculativePainters for RegisteredPaintersImpl { - fn get(&self, _name: &Atom) -> Option<&dyn RegisteredSpeculativePainter> { - None - } -} - -use style::traversal::recalc_style_at; - -pub struct RecalcStyle<'a> { - context: SharedStyleContext<'a>, -} - -impl<'a> RecalcStyle<'a> { - pub fn new(context: SharedStyleContext<'a>) -> Self { - RecalcStyle { context } - } -} - -#[allow(unsafe_code)] -impl DomTraversal for RecalcStyle<'_> -where - E: TElement, -{ - fn process_preorder( - &self, - traversal_data: &PerLevelTraversalData, - context: &mut StyleContext, - node: E::ConcreteNode, - note_child: F, - ) { - // Don't process textnodees in this traversal - if node.is_text_node() { - return; - } - - let el = node.as_element().unwrap(); - // let mut data = el.mutate_data().unwrap(); - let mut data = unsafe { el.ensure_data() }; - recalc_style_at(self, traversal_data, context, el, &mut data, note_child); - - // Gets set later on - unsafe { el.unset_dirty_descendants() } - } - - #[inline] - fn needs_postorder_traversal() -> bool { - false - } - - fn process_postorder(&self, _style_context: &mut StyleContext, _node: E::ConcreteNode) { - panic!("this should never be called") - } - - #[inline] - fn shared_context(&self) -> &SharedStyleContext { - &self.context - } -} - -#[test] -fn assert_size_of_equals() { - // use std::mem; - - // fn assert_layout() { - // assert_eq!( - // mem::size_of::>(), - // mem::size_of::() - // ); - // assert_eq!( - // mem::align_of::>(), - // mem::align_of::() - // ); - // } - - // let size = mem::size_of::>(); - // dbg!(size); -} - -#[test] -fn parse_inline() { - // let attrs = style::attr::AttrValue::from_serialized_tokenlist( - // r#"visibility: hidden; left: 1306.5px; top: 50px; display: none;"#.to_string(), - // ); - - // let val = CSSInlineStyleDeclaration(); -} diff --git a/packages/blitz-dom/src/util.rs b/packages/blitz-dom/src/util.rs index 3e082257b..633adac2c 100644 --- a/packages/blitz-dom/src/util.rs +++ b/packages/blitz-dom/src/util.rs @@ -1,4 +1,5 @@ -use crate::node::{Node, NodeData}; +use crate::node::NodeData; +use crate::Handle; use image::DynamicImage; use selectors::context::QuirksMode; use std::str::FromStr; @@ -182,17 +183,17 @@ impl RequestHandler for ImageHandler { } // Debug print an RcDom -pub fn walk_tree(indent: usize, node: &Node) { +pub fn walk_tree(indent: usize, handle: Handle) { // Skip all-whitespace text nodes entirely - if let NodeData::Text(data) = &node.raw_dom_data { + if let NodeData::Text(data) = &handle.node.raw_dom_data { if data.content.chars().all(|c| c.is_ascii_whitespace()) { return; } } print!("{}", " ".repeat(indent)); - let id = node.id; - match &node.raw_dom_data { + let id = handle.node.id; + match &handle.node.raw_dom_data { NodeData::Document => println!("#Document {id}"), NodeData::Text(data) => { @@ -223,7 +224,7 @@ pub fn walk_tree(indent: usize, node: &Node) { for attr in data.attrs.iter() { print!(" {}=\"{}\"", attr.name.local, attr.value); } - if !node.children.is_empty() { + if !handle.node.children.is_empty() { println!(">"); } else { println!("/>"); @@ -236,12 +237,12 @@ pub fn walk_tree(indent: usize, node: &Node) { // NodeData::ProcessingInstruction { .. } => unreachable!(), } - if !node.children.is_empty() { - for child_id in node.children.iter() { - walk_tree(indent + 2, node.with(*child_id)); + if !handle.node.children.is_empty() { + for child_id in handle.node.children.iter() { + walk_tree(indent + 2, handle.get(*child_id)); } - if let NodeData::Element(data) = &node.raw_dom_data { + if let NodeData::Element(data) = &handle.node.raw_dom_data { println!("{}", " ".repeat(indent), data.name.local); } } diff --git a/packages/blitz-dom/src/viewport.rs b/packages/blitz-dom/src/viewport.rs index 2774716f5..06ee9fbae 100644 --- a/packages/blitz-dom/src/viewport.rs +++ b/packages/blitz-dom/src/viewport.rs @@ -4,18 +4,23 @@ use style::{ properties::{style_structs::Font, ComputedValues}, }; +/// Window viewport. #[derive(Default, Debug, Clone)] pub struct Viewport { + /// Size of the window. pub window_size: (u32, u32), - hidpi_scale: f32, + /// Font size. + pub font_size: f32, - zoom: f32, + /// Zoom level. + pub zoom: f32, - pub font_size: f32, + hidpi_scale: f32, } impl Viewport { + /// Create a new viewport from a window's physical size and scale factor. pub fn new(physical_width: u32, physical_height: u32, scale_factor: f32) -> Self { Self { window_size: (physical_width, physical_height), @@ -25,31 +30,21 @@ impl Viewport { } } - // Total scaling, the product of the zoom and hdpi scale + /// Total scaling, the product of the zoom and hdpi scale. pub fn scale(&self) -> f32 { self.hidpi_scale * self.zoom } - // Total scaling, the product of the zoom and hdpi scale + + /// Total scaling, the product of the zoom and hdpi scale (as an `f64`). pub fn scale_f64(&self) -> f64 { self.scale() as f64 } + /// Set the hidi scale. pub fn set_hidpi_scale(&mut self, scale: f32) { self.hidpi_scale = scale; } - pub fn zoom(&self) -> f32 { - self.zoom - } - - pub fn set_zoom(&mut self, zoom: f32) { - self.zoom = zoom; - } - - pub fn zoom_mut(&mut self) -> &mut f32 { - &mut self.zoom - } - pub(crate) fn make_device(&self) -> Device { let width = self.window_size.0 as f32 / self.scale(); let height = self.window_size.1 as f32 / self.scale(); diff --git a/packages/blitz-renderer-vello/src/renderer/render.rs b/packages/blitz-renderer-vello/src/renderer/render.rs index 4b7c8fdda..10227e29b 100644 --- a/packages/blitz-renderer-vello/src/renderer/render.rs +++ b/packages/blitz-renderer-vello/src/renderer/render.rs @@ -10,7 +10,7 @@ use blitz_dom::node::{ ListItemLayout, ListItemLayoutPosition, Marker, NodeData, TextBrush, TextInputData, TextNodeData, }; -use blitz_dom::{local_name, Document, Node}; +use blitz_dom::{local_name, Document, Handle}; use parley::Line; use style::{ @@ -338,31 +338,37 @@ impl VelloSceneGenerator<'_> { // - position, table, text, ui, // - custom_properties, writing_mode, rules, visited_style, flags, box_, column, counters, effects, // - inherited_box, inherited_table, inherited_text, inherited_ui, - let element = &self.dom.as_ref().tree()[node_id]; + let element = Handle { + node: &self.dom.as_ref().tree()[node_id], + tree: self.dom.as_ref().tree(), + }; // Early return if the element is hidden - if matches!(element.style.display, taffy::Display::None) { + if matches!(element.node.style.display, taffy::Display::None) { return; } // Only draw elements with a style - if element.primary_styles().is_none() { + if element.node.primary_styles().is_none() { return; } // Hide elements with "hidden" attribute - if let Some("true" | "") = element.attr(local_name!("hidden")) { + if let Some("true" | "") = element.node.attr(local_name!("hidden")) { return; } // Hide inputs with type=hidden // Implemented here rather than using the style engine for performance reasons - if element.local_name() == "input" && element.attr(local_name!("type")) == Some("hidden") { + if element.local_name() == "input" + && element.node.attr(local_name!("type")) == Some("hidden") + { return; } // Hide elements with a visibility style other than visible if element + .node .primary_styles() .unwrap() .get_inherited_box() @@ -373,12 +379,12 @@ impl VelloSceneGenerator<'_> { } // We can't fully support opacity yet, but we can hide elements with opacity 0 - if element.primary_styles().unwrap().get_effects().opacity == 0.0 { + if element.node.primary_styles().unwrap().get_effects().opacity == 0.0 { return; } // TODO: account for overflow_x vs overflow_y - let styles = &element.primary_styles().unwrap(); + let styles = &element.node.primary_styles().unwrap(); let overflow_x = styles.get_box().overflow_x; let overflow_y = styles.get_box().overflow_y; let should_clip = @@ -392,7 +398,7 @@ impl VelloSceneGenerator<'_> { border, padding, .. - } = element.final_layout; + } = element.node.final_layout; let scaled_pb = (padding + border).map(f64::from); let pos = vello::kurbo::Point { x: pos.x + scaled_pb.left, @@ -432,16 +438,16 @@ impl VelloSceneGenerator<'_> { // Now that background has been drawn, offset pos and cx in order to draw our contents scrolled let pos = Point { - x: pos.x - element.scroll_offset.x, - y: pos.y - element.scroll_offset.y, + x: pos.x - element.node.scroll_offset.x, + y: pos.y - element.node.scroll_offset.y, }; cx.pos = Point { - x: cx.pos.x - element.scroll_offset.x, - y: cx.pos.y - element.scroll_offset.y, + x: cx.pos.x - element.node.scroll_offset.x, + y: cx.pos.y - element.node.scroll_offset.y, }; cx.transform = cx.transform.then_translate(Vec2 { - x: -element.scroll_offset.x, - y: -element.scroll_offset.y, + x: -element.node.scroll_offset.x, + y: -element.node.scroll_offset.y, }); cx.draw_image(scene); cx.draw_svg(scene); @@ -477,6 +483,7 @@ impl VelloSceneGenerator<'_> { let x_offset = -(layout.full_width() / layout.scale() + x_padding); //Align the marker with the baseline of the first line of text in the list item let y_offset = if let Some(text_layout) = &element + .node .raw_dom_data .downcast_element() .and_then(|el| el.inline_layout_data.as_ref()) @@ -498,9 +505,9 @@ impl VelloSceneGenerator<'_> { cx.stroke_text(scene, layout.lines(), pos); } - if element.is_inline_root { + if element.node.is_inline_root { let text_layout = &element - .raw_dom_data + .node .raw_dom_data .downcast_element() .unwrap() .inline_layout_data @@ -523,6 +530,7 @@ impl VelloSceneGenerator<'_> { } else { for child_id in cx .element + .node .layout_children .borrow() .as_ref() @@ -558,8 +566,9 @@ impl VelloSceneGenerator<'_> { } } - fn element_cx<'w>(&'w self, element: &'w Node, location: Point) -> ElementCx<'w> { + fn element_cx<'w>(&'w self, element: Handle<'w>, location: Point) -> ElementCx<'w> { let style = element + .node .stylo_element_data .borrow() .as_ref() @@ -568,7 +577,7 @@ impl VelloSceneGenerator<'_> { ComputedValues::initial_values_with_font_override(Font::initial_values()).to_arc(), ); - let (layout, pos) = self.node_position(element.id, location); + let (layout, pos) = self.node_position(element.node.id, location); let scale = self.scale; // the bezpaths for every element are (potentially) cached (not yet, tbd) @@ -588,13 +597,19 @@ impl VelloSceneGenerator<'_> { element, transform, image: element + .node .element_data() .unwrap() .image_data() .map(|data| &*data.image), - svg: element.element_data().unwrap().svg_data(), - text_input: element.element_data().unwrap().text_input_data(), - list_item: element.element_data().unwrap().list_item_data.as_deref(), + svg: element.node.element_data().unwrap().svg_data(), + text_input: element.node.element_data().unwrap().text_input_data(), + list_item: element + .node + .element_data() + .unwrap() + .list_item_data + .as_deref(), devtools: &self.devtools, } } @@ -606,7 +621,7 @@ struct ElementCx<'a> { style: style::servo_arc::Arc, pos: Point, scale: f64, - element: &'a Node, + element: Handle<'a>, transform: Affine, image: Option<&'a DynamicImage>, svg: Option<&'a usvg::Tree>, @@ -722,6 +737,7 @@ impl ElementCx<'_> { if let Some(image) = self.image { let mut resized_image = self .element + .node .element_data() .unwrap() .image_data() @@ -761,7 +777,7 @@ impl ElementCx<'_> { let shape = &self.frame.outer_rect; let stroke = Stroke::new(self.scale); - let stroke_color = match self.element.style.display { + let stroke_color = match self.element.node.style.display { taffy::Display::Block => Color::rgb(1.0, 0.0, 0.0), taffy::Display::Flex => Color::rgb(0.0, 1.0, 0.0), taffy::Display::Grid => Color::rgb(0.0, 0.0, 1.0), @@ -1547,16 +1563,20 @@ impl ElementCx<'_> { fn draw_input(&self, scene: &mut Scene) { if self.element.local_name() == "input" - && matches!(self.element.attr(local_name!("type")), Some("checkbox")) + && matches!( + self.element.node.attr(local_name!("type")), + Some("checkbox") + ) { let Some(checked) = self .element + .node .element_data() .and_then(|data| data.checkbox_input_checked()) else { return; }; - let disabled = self.element.attr(local_name!("disabled")).is_some(); + let disabled = self.element.node.attr(local_name!("disabled")).is_some(); // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it let accent_color = if disabled { diff --git a/packages/dioxus-native/src/accessibility.rs b/packages/dioxus-native/src/accessibility.rs index 8787a2b59..0eb64a522 100644 --- a/packages/dioxus-native/src/accessibility.rs +++ b/packages/dioxus-native/src/accessibility.rs @@ -1,6 +1,6 @@ use crate::waker::BlitzEvent; use accesskit::{NodeBuilder, NodeId, Role, Tree, TreeUpdate}; -use blitz_dom::{local_name, Document, Node}; +use blitz_dom::{local_name, Document, Handle}; use winit::{event_loop::EventLoopProxy, window::Window}; /// State of the accessibility node tree and platform adapter. @@ -29,7 +29,13 @@ impl AccessibilityState { .and_then(|parent_id| nodes.get_mut(&parent_id)) .map(|(_, parent)| parent) .unwrap_or(&mut window); - let (id, node_builder) = self.build_node(node, parent); + let (id, node_builder) = self.build_node( + Handle { + node, + tree: doc.tree(), + }, + parent, + ); nodes.insert(node_id, (id, node_builder)); }); @@ -50,13 +56,13 @@ impl AccessibilityState { self.adapter.update_if_active(|| tree_update) } - fn build_node(&mut self, node: &Node, parent: &mut NodeBuilder) -> (NodeId, NodeBuilder) { + fn build_node(&mut self, handle: Handle, parent: &mut NodeBuilder) -> (NodeId, NodeBuilder) { let mut node_builder = NodeBuilder::default(); let id = NodeId(self.next_id); self.next_id += 1; - if let Some(element_data) = node.element_data() { + if let Some(element_data) = handle.node.element_data() { let name = element_data.name.local.to_string(); // TODO match more roles @@ -80,9 +86,9 @@ impl AccessibilityState { node_builder.set_role(role); node_builder.set_html_tag(name); - } else if node.is_text_node() { + } else if handle.node.is_text_node() { node_builder.set_role(Role::StaticText); - node_builder.set_name(node.text_content()); + node_builder.set_name(handle.text_content()); parent.push_labelled_by(id) } diff --git a/packages/dioxus-native/src/documents/dioxus_document.rs b/packages/dioxus-native/src/documents/dioxus_document.rs index d2373eb6d..0d14062e7 100644 --- a/packages/dioxus-native/src/documents/dioxus_document.rs +++ b/packages/dioxus-native/src/documents/dioxus_document.rs @@ -10,7 +10,7 @@ use blitz_dom::{ events::{EventData, RendererEvent}, local_name, namespace_url, node::{Attribute, NodeSpecificData}, - ns, Atom, Document, DocumentLike, ElementNodeData, Node, NodeData, QualName, Viewport, + ns, Atom, Document, DocumentLike, ElementNodeData, Handle, Node, NodeData, QualName, Viewport, DEFAULT_CSS, }; @@ -178,11 +178,11 @@ impl DocumentLike for DioxusDocument { let &EventData::Click { mods, .. } = &renderer_event.data else { unreachable!(); }; - let input_click_data = self - .inner - .get_node(node_id) - .unwrap() - .synthetic_click_event(mods); + let input_click_data = Handle { + node: self.inner.get_node(node_id).unwrap(), + tree: self.inner.tree(), + } + .synthetic_click_event(mods); let default_event = RendererEvent { target: node_id, data: input_click_data, @@ -391,7 +391,7 @@ impl DioxusDocument { qual_name("html", None), Vec::new(), ))); - let root_node_id = doc.root_node().id; + let root_node_id = doc.root_node().node.id; let html_element = doc.get_node_mut(html_element_id).unwrap(); html_element.parent = Some(root_node_id); let root_node = doc.get_node_mut(root_node_id).unwrap(); @@ -687,12 +687,12 @@ impl WriteMutations for MutationWriter<'_> { let new_nodes = self.state.stack.split_off(self.state.stack.len() - m); let anchor_node_id = self.state.element_to_node_id(id); - let next_sibling_id = self - .doc - .get_node(anchor_node_id) - .unwrap() - .forward(1) - .map(|node| node.id); + let next_sibling_id = Handle { + node: self.doc.get_node(anchor_node_id).unwrap(), + tree: self.doc.as_ref().tree(), + } + .forward(1) + .map(|blitz_node| blitz_node.node.id); match next_sibling_id { Some(anchor_node_id) => { diff --git a/packages/dioxus-native/src/window.rs b/packages/dioxus-native/src/window.rs index 37725efbf..582dacc6f 100644 --- a/packages/dioxus-native/src/window.rs +++ b/packages/dioxus-native/src/window.rs @@ -213,8 +213,8 @@ impl View { pub fn mouse_move(&mut self, x: f32, y: f32) -> bool { let viewport_scroll = self.dom.as_ref().viewport_scroll(); - let dom_x = x + viewport_scroll.x as f32 / self.viewport.zoom(); - let dom_y = y + viewport_scroll.y as f32 / self.viewport.zoom(); + let dom_x = x + viewport_scroll.x as f32 / self.viewport.zoom; + let dom_y = y + viewport_scroll.y as f32 / self.viewport.zoom; // println!("Mouse move: ({}, {})", x, y); // println!("Unscaled: ({}, {})",); @@ -338,15 +338,15 @@ impl View { if ctrl | meta { match key_code { KeyCode::Equal => { - *self.viewport.zoom_mut() += 0.1; + self.viewport.zoom += 0.1; self.kick_dom_viewport(); } KeyCode::Minus => { - *self.viewport.zoom_mut() -= 0.1; + self.viewport.zoom -= 0.1; self.kick_dom_viewport(); } KeyCode::Digit0 => { - *self.viewport.zoom_mut() = 1.0; + self.viewport.zoom = 1.0; self.kick_dom_viewport(); } _ => {}