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:
1 parent
5ed34ea8d4
commit
f0f9b048d0
3 files changed
+132
-1
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user