render: DOM to layout tree and per-node geometry

Walk a live DomTree, compute each element's style (tag UA defaults plus
inline style), build a taffy layout tree, and return border-box geometry
keyed by NodeId. Block/flex structure, explicit sizes, and the box model
are correct; text nodes do not yet contribute to size (inline/text
layout lands with the paint phase).

This completes the layout core: a real DOM now produces real geometry,
pure Rust, behind the opt-in render crate. JS exposure is next.
This commit is contained in:
SGavrl committed 2026-06-30 16:30:18 +00:00
1 parent 5ed34ea8d4
commit f0f9b048d0
3 files changed
+132 -1

No files matched your search

+127
View File
@@ -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<NodeId, Rect>,
}
/// 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<taffy::NodeId, NodeId> = HashMap::new();
// The document node itself is not an element; lay out from the first
// element descendant (the <html> 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<taffy::NodeId, NodeId>,
) -> Option<taffy::NodeId> {
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<taffy::NodeId> = 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(
"<html><body><div style=\"width: 300px\"><div style=\"width: 100px; height: 40px\"></div><div style=\"width: 100px; height: 60px\"></div></div></body></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::<Vec<_>>();
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());
}
}
+4 -1
View File
@@ -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,