From 42874989eb1a664681511886c345e7968c3e9b02 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:20:26 +0000 Subject: [PATCH] feat(render): parse presentational attributes and font sizes for beautiful layout --- crates/obscura-render/src/dom.rs | 84 +++++++++++++++++++++++------- crates/obscura-render/src/lib.rs | 2 +- crates/obscura-render/src/paint.rs | 7 +-- crates/obscura-render/src/style.rs | 23 ++++++-- 4 files changed, 90 insertions(+), 26 deletions(-) diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 5d48c0e..3bc71c2 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -54,9 +54,28 @@ pub fn layout_dom(tree: &DomTree, viewport: (f32, f32)) -> DomLayout { for nid in tree.descendants(tree.document()) { if let Some(node) = tree.get_node(nid) { - if let Some(inline) = node.get_attribute("style") { + if node.is_element() { if let Some(style) = styles.get_mut(&nid) { - crate::style::apply_inline(style, inline); + if let Some(inline) = node.get_attribute("style") { + crate::style::apply_inline(style, inline); + } + if let Some(bgcolor) = node.get_attribute("bgcolor") { + crate::style::apply_inline(style, &format!("background-color: {}", bgcolor)); + } + if let Some(width) = node.get_attribute("width") { + if width.chars().all(|c| c.is_ascii_digit()) { + crate::style::apply_inline(style, &format!("width: {}px", width)); + } else { + crate::style::apply_inline(style, &format!("width: {}", width)); + } + } + if let Some(height) = node.get_attribute("height") { + if height.chars().all(|c| c.is_ascii_digit()) { + crate::style::apply_inline(style, &format!("height: {}px", height)); + } else { + crate::style::apply_inline(style, &format!("height: {}", height)); + } + } } } } @@ -82,24 +101,44 @@ pub fn layout_dom(tree: &DomTree, viewport: (f32, f32)) -> DomLayout { 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, - }, - ); - } - } + compute_absolute_rects(&taffy_tree, taffy_root, 0.0, 0.0, &id_map, &mut rects); } } DomLayout { rects, styles } } +fn compute_absolute_rects( + taffy_tree: &TaffyTree, + taffy_id: taffy::NodeId, + abs_x: f32, + abs_y: f32, + id_map: &HashMap, + rects: &mut HashMap, +) { + if let Ok(layout) = taffy_tree.layout(taffy_id) { + let x = abs_x + layout.location.x; + let y = abs_y + layout.location.y; + + if let Some(dom_id) = id_map.get(&taffy_id) { + rects.insert( + *dom_id, + Rect { + x, + y, + width: layout.size.width, + height: layout.size.height, + }, + ); + } + + if let Ok(children) = taffy_tree.children(taffy_id) { + for child_id in children { + compute_absolute_rects(taffy_tree, child_id, x, y, id_map, rects); + } + } + } +} + fn parse_simple_css(css: &str) -> Vec<(String, String)> { let mut rules = Vec::new(); let mut current_selector = String::new(); @@ -146,15 +185,22 @@ fn build( if text.is_empty() { return None; } - let width = text.chars().count() as f32 * 9.0; - let style = taffy::Style { + + let parent = node.parent?; + let style = styles.get(&parent)?; + let fsize = style.font_size.unwrap_or(16.0); + + let width = text.chars().count() as f32 * (fsize * 0.55); // rough width estimate + let height = fsize * 1.2; + + let taffy_style = taffy::Style { size: taffy::Size { width: taffy::Dimension::Length(width), - height: taffy::Dimension::Length(19.0), + height: taffy::Dimension::Length(height), }, ..Default::default() }; - let taffy_id = taffy_tree.new_leaf(style).ok()?; + let taffy_id = taffy_tree.new_leaf(taffy_style).ok()?; id_map.insert(taffy_id, id); return Some(taffy_id); } diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index 29ad94e..6268ed0 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -68,8 +68,8 @@ pub struct LayoutStyle { pub background_color: Option<[u8; 4]>, /// Foreground (text) color for the paint step. pub color: Option<[u8; 4]>, - /// Border color for the paint step. pub border_color: Option<[u8; 4]>, + pub font_size: Option, } /// A node in the input layout tree. `text` is carried for the paint phase; it diff --git a/crates/obscura-render/src/paint.rs b/crates/obscura-render/src/paint.rs index ec1fe30..c2302da 100644 --- a/crates/obscura-render/src/paint.rs +++ b/crates/obscura-render/src/paint.rs @@ -127,6 +127,7 @@ fn paint_text_node( let parent = node.parent?; let style = laid.styles.get(&parent)?; let color = style.color.unwrap_or([0, 0, 0, 255]); + let fsize = style.font_size.unwrap_or(16.0); // Get geometry of the text node itself! let rect = match laid.rects.get(&nid) { @@ -135,13 +136,13 @@ fn paint_text_node( }; // Paint text at rect.x, rect.y - draw_text(pixmap, text, rect.x, rect.y, color); + draw_text(pixmap, text, rect.x, rect.y, color, fsize); Some(()) } -fn draw_text(pixmap: &mut Pixmap, text: &str, x: f32, y: f32, color: [u8; 4]) { +fn draw_text(pixmap: &mut Pixmap, text: &str, x: f32, y: f32, color: [u8; 4], size: f32) { let font = FontRef::try_from_slice(FONT_BYTES).unwrap(); - let scale = PxScale::from(16.0); // Default font size + let scale = PxScale::from(size); let scaled_font = font.as_scaled(scale); let mut caret = ab_glyph::point(x, y + scaled_font.ascent()); diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index ace332c..2fca229 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -78,6 +78,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { "background-color" | "background" => style.background_color = parse_color(value), "color" => style.color = parse_color(value), "border-color" => style.border_color = parse_color(value), + "font-size" => style.font_size = px(value), _ => {} } } @@ -156,12 +157,28 @@ fn edges(value: &str) -> Option { } fn px_value(tok: &str) -> Option { - let n = tok.strip_suffix("px").unwrap_or(tok); - // Reject percentages and other units for phase 1. + let mut n = tok; + let mut scale = 1.0; + + if n.ends_with("px") { + n = &n[..n.len() - 2]; + } else if n.ends_with("pt") { + n = &n[..n.len() - 2]; + scale = 1.333; // 1pt ≈ 1.333px + } else if n.ends_with("em") || n.ends_with("rem") { + n = n.trim_end_matches(|c: char| c.is_ascii_alphabetic()); + scale = 16.0; // 1em = 16px + } else if n.ends_with('%') { + n = &n[..n.len() - 1]; + scale = 16.0 / 100.0; + } else { + n = n.trim_end_matches(|c: char| c.is_ascii_alphabetic()); + } + if n.chars().any(|c| !(c.is_ascii_digit() || c == '.' || c == '-')) { return None; } - n.parse::().ok() + n.parse::().ok().map(|v| v * scale) } fn token(value: &str) -> Option<&str> {