diff --git a/Cargo.lock b/Cargo.lock index c5ecce9..2472a3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1880,6 +1880,7 @@ version = "0.1.0" dependencies = [ "ab_glyph", "base64", + "cssparser", "image", "obscura-dom", "taffy", diff --git a/crates/obscura-render/Cargo.toml b/crates/obscura-render/Cargo.toml index 937c02f..9ddbe9f 100644 --- a/crates/obscura-render/Cargo.toml +++ b/crates/obscura-render/Cargo.toml @@ -20,5 +20,6 @@ ab_glyph = { workspace = true, optional = true } image = { version = "0.24", default-features = false, features = ["jpeg", "png", "webp"], optional = true } ureq = { version = "2.9", default-features = false, features = ["tls"], optional = true } base64 = { workspace = true, optional = true } +cssparser = "0.34" [lib] path = "src/lib.rs" diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index c51ba75..eaf28fb 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -11,15 +11,57 @@ use std::collections::HashMap; use obscura_dom::tree::{DomTree, NodeId}; use taffy::prelude::*; -use crate::{compute_style, Rect, to_taffy_style}; +use crate::{Rect, to_taffy_style}; /// Per-element border boxes after layout, in viewport coordinates. pub struct DomLayout { pub rects: HashMap, + pub styles: 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 css_rules = Vec::new(); + for nid in tree.descendants(tree.document()) { + if let Some(node) = tree.get_node(nid) { + if let Some(elem) = node.as_element() { + if elem.local.as_ref() == "style" { + let css = tree.text_content(nid); + css_rules.extend(parse_simple_css(&css)); + } + } + } + } + + let mut styles: HashMap = HashMap::new(); + for nid in tree.descendants(tree.document()) { + if let Some(node) = tree.get_node(nid) { + if let Some(elem) = node.as_element() { + styles.insert(nid, crate::style::ua_style(elem.local.as_ref())); + } + } + } + + for (selector, decls) in &css_rules { + if let Ok(matched) = tree.query_selector_all(selector) { + for nid in matched { + if let Some(style) = styles.get_mut(&nid) { + crate::style::apply_inline(style, decls); + } + } + } + } + + for nid in tree.descendants(tree.document()) { + if let Some(node) = tree.get_node(nid) { + if let Some(inline) = node.get_attribute("style") { + if let Some(style) = styles.get_mut(&nid) { + crate::style::apply_inline(style, inline); + } + } + } + } + let mut taffy_tree: TaffyTree = TaffyTree::new(); let mut id_map: HashMap = HashMap::new(); @@ -32,7 +74,7 @@ pub fn layout_dom(tree: &DomTree, viewport: (f32, f32)) -> DomLayout { 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) { + if let Some(taffy_root) = build(tree, root_id, &mut taffy_tree, &mut id_map, &styles) { let _ = taffy_tree.compute_layout( taffy_root, taffy::Size { @@ -55,7 +97,39 @@ pub fn layout_dom(tree: &DomTree, viewport: (f32, f32)) -> DomLayout { } } } - DomLayout { rects } + DomLayout { rects, styles } +} + +fn parse_simple_css(css: &str) -> Vec<(String, String)> { + let mut rules = Vec::new(); + let mut current_selector = String::new(); + let mut current_decls = String::new(); + let mut in_block = false; + + for c in css.chars() { + if c == '{' && !in_block { + in_block = true; + } else if c == '}' && in_block { + in_block = false; + let sel = current_selector.trim(); + let decls = current_decls.trim(); + for s in sel.split(',') { + let s = s.trim(); + if !s.is_empty() { + rules.push((s.to_string(), decls.to_string())); + } + } + current_selector.clear(); + current_decls.clear(); + } else { + if in_block { + current_decls.push(c); + } else { + current_selector.push(c); + } + } + } + rules } fn build( @@ -63,17 +137,17 @@ fn build( id: NodeId, taffy_tree: &mut TaffyTree, id_map: &mut HashMap, + styles: &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 _name = node.as_element()?; + let style = styles.get(&id)?; + 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)) + .filter_map(|cid| build(tree, cid, taffy_tree, id_map, styles)) .collect(); let taffy_id = if child_ids.is_empty() { diff --git a/crates/obscura-render/src/paint.rs b/crates/obscura-render/src/paint.rs index 7d098c0..bbc14f8 100644 --- a/crates/obscura-render/src/paint.rs +++ b/crates/obscura-render/src/paint.rs @@ -46,7 +46,10 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32)) -> Option { None => continue, }; - let style = crate::compute_style(name.local.as_ref(), node.get_attribute("style")); + let style = match laid.styles.get(&nid) { + Some(s) => s, + None => continue, + }; if let Some(bg) = style.background_color { let mut path = PathBuilder::new(); @@ -119,12 +122,9 @@ fn paint_text_node( }; // Find parent element to get geometry and color - let parent_id = node.parent?; - let parent = tree.get_node(parent_id)?; - let parent_name = parent.as_element()?; - - let rect = laid.rects.get(&parent_id)?; - let style = crate::compute_style(parent_name.local.as_ref(), parent.get_attribute("style")); + let parent = node.parent?; + let rect = laid.rects.get(&parent)?; + let style = laid.styles.get(&parent)?; // Default color to black if not specified. let color = style.color.unwrap_or([0, 0, 0, 255]); diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index d134ab9..3fd2f98 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -22,12 +22,12 @@ pub fn compute_style(tag: &str, inline_css: Option<&str>) -> LayoutStyle { /// Built-in UA defaults. Inline elements currently map to block layout; real /// inline/text layout arrives with the text/paint phase. -fn ua_style(tag: &str) -> LayoutStyle { +pub fn ua_style(tag: &str) -> LayoutStyle { let _ = tag; LayoutStyle { display: Display::Block, ..Default::default() } } -fn apply_inline(style: &mut LayoutStyle, css: &str) { +pub fn apply_inline(style: &mut LayoutStyle, css: &str) { for raw in css.split(';') { let decl = raw.trim(); if decl.is_empty() {