diff --git a/crates/obscura-render/Cargo.toml b/crates/obscura-render/Cargo.toml index 7423add..98f5e53 100644 --- a/crates/obscura-render/Cargo.toml +++ b/crates/obscura-render/Cargo.toml @@ -8,6 +8,7 @@ description = "Optional scoped render layer for Obscura: layout (taffy) and, wit [dependencies] taffy = { workspace = true } +obscura-dom = { workspace = true } [lib] path = "src/lib.rs" diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs new file mode 100644 index 0000000..c51ba75 --- /dev/null +++ b/crates/obscura-render/src/dom.rs @@ -0,0 +1,127 @@ +//! DOM integration: build a taffy layout tree from a live [`DomTree`], run +//! layout, and return border-box geometry keyed by [`NodeId`]. +//! +//! Phase 3. Text nodes do not yet contribute to size (no inline/text layout +//! until the text/paint phase), so a leaf element with only text may have zero +//! height. Block and flex structure, plus explicit sizes and box model, are +//! correct. + +use std::collections::HashMap; + +use obscura_dom::tree::{DomTree, NodeId}; +use taffy::prelude::*; + +use crate::{compute_style, Rect, to_taffy_style}; + +/// Per-element border boxes after layout, in viewport coordinates. +pub struct DomLayout { + pub rects: HashMap, +} + +/// Lay out a DOM tree within `viewport` (width, height) in CSS pixels. +pub fn layout_dom(tree: &DomTree, viewport: (f32, f32)) -> DomLayout { + let mut taffy_tree: TaffyTree = TaffyTree::new(); + let mut id_map: HashMap = HashMap::new(); + + // The document node itself is not an element; lay out from the first + // element descendant (the root). + let root = tree + .descendants(tree.document()) + .into_iter() + .find(|id| tree.get_node(*id).map(|n| n.is_element()).unwrap_or(false)); + + let mut rects = HashMap::new(); + if let Some(root_id) = root { + if let Some(taffy_root) = build(tree, root_id, &mut taffy_tree, &mut id_map) { + let _ = taffy_tree.compute_layout( + taffy_root, + taffy::Size { + width: taffy::AvailableSpace::Definite(viewport.0), + height: taffy::AvailableSpace::Definite(viewport.1), + }, + ); + for (taffy_id, dom_id) in &id_map { + if let Ok(layout) = taffy_tree.layout(*taffy_id) { + rects.insert( + *dom_id, + Rect { + x: layout.location.x, + y: layout.location.y, + width: layout.size.width, + height: layout.size.height, + }, + ); + } + } + } + } + DomLayout { rects } +} + +fn build( + tree: &DomTree, + id: NodeId, + taffy_tree: &mut TaffyTree, + id_map: &mut HashMap, +) -> Option { + let node = tree.get_node(id)?; + let name = node.as_element()?; + let tag = name.local.as_ref(); + let style = compute_style(tag, node.get_attribute("style")); + let taffy_style = to_taffy_style(&style); + + let child_ids: Vec = tree + .children(id) + .into_iter() + .filter_map(|cid| build(tree, cid, taffy_tree, id_map)) + .collect(); + + let taffy_id = if child_ids.is_empty() { + taffy_tree.new_leaf(taffy_style).ok()? + } else { + taffy_tree.new_with_children(taffy_style, &child_ids).ok()? + }; + id_map.insert(taffy_id, id); + Some(taffy_id) +} + +#[cfg(test)] +mod tests { + use super::*; + use obscura_dom::tree_sink::parse_html; + + #[test] + fn lays_out_real_dom() { + let tree = parse_html( + "
", + ); + let laid = layout_dom(&tree, (1280.0, 720.0)); + // Every element gets a rect. + assert!(laid.rects.len() >= 4, "expected >=4 element rects, got {}", laid.rects.len()); + + // The two inner divs stack vertically inside the 300px container. + let stacks = laid + .rects + .values() + .filter(|r| (r.width - 100.0).abs() < 0.1) + .map(|r| (r.y, r.height)) + .collect::>(); + assert_eq!(stacks.len(), 2, "expected two 100px-wide children"); + let mut sorted = stacks.clone(); + sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + assert!( + (sorted[0].1 - 40.0).abs() < 0.1 && (sorted[1].1 - 60.0).abs() < 0.1, + "children heights should be 40 and 60, got {:?}", + sorted + ); + } + + #[test] + fn empty_document_is_safe() { + // html5ever always synthesizes html/head/body, so an empty document + // still has a few element rects. The point is that it does not panic. + let tree = parse_html(""); + let laid = layout_dom(&tree, (1280.0, 720.0)); + assert!(laid.rects.len() <= 4, "got {}", laid.rects.len()); + } +} diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index ed42e45..978f987 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -16,6 +16,9 @@ use taffy::prelude::*; mod style; pub use style::compute_style; +mod dom; +pub use dom::{layout_dom, DomLayout}; + /// An axis-aligned rectangle in CSS pixels, relative to the containing block. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct Rect { @@ -125,7 +128,7 @@ fn read_node(tree: &TaffyTree, id: NodeId) -> NodeRect { } } -fn to_taffy_style(style: &LayoutStyle) -> Style { +pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style { let mut s = Style::DEFAULT; s.display = match style.display { Display::Block => taffy::style::Display::Block,