render: computed-style lite (inline + UA defaults)

Parse inline style attributes into the layout-relevant subset of
LayoutStyle: display, width/height, margin, padding, and border width,
including box shorthands (1-4 values) and per-side longhands. !important
is stripped; auto and percentages leave the size unset for phase 1.

Layered on a small built-in UA default sheet. A compact hand-rolled
tokenizer handles inline attributes without a cssparser dependency; a
real cascade can harden it later.
This commit is contained in:
SGavrl
2026-06-30 16:26:42 +00:00
parent 1971380da8
commit 5ed34ea8d4
2 changed files with 167 additions and 0 deletions

View File

@@ -13,6 +13,9 @@
use taffy::prelude::*;
mod style;
pub use style::compute_style;
/// An axis-aligned rectangle in CSS pixels, relative to the containing block.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Rect {

View File

@@ -0,0 +1,164 @@
//! Computed-style lite: parse inline CSS declarations plus a small UA default
//! sheet into the layout-relevant subset of [`crate::LayoutStyle`].
//!
//! This is deliberately not a full CSS cascade. It handles the properties that
//! influence phase-1 box layout (display, width/height, margin, padding, border
//! width) from inline `style="..."` attributes, layered on a tiny built-in UA
//! sheet. A real cascade with selector matching and `cssparser` can harden this
//! later; for inline-style attributes a compact tokenizer is enough and keeps
//! the crate dependency-light.
use crate::{Display, Edges, LayoutStyle};
/// Compute the layout-relevant style for an element: UA defaults for its tag,
/// overridden by its inline `style="..."` declarations.
pub fn compute_style(tag: &str, inline_css: Option<&str>) -> LayoutStyle {
let mut style = ua_style(tag);
if let Some(css) = inline_css {
apply_inline(&mut style, css);
}
style
}
/// 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 {
let _ = tag;
LayoutStyle { display: Display::Block, ..Default::default() }
}
fn apply_inline(style: &mut LayoutStyle, css: &str) {
for raw in css.split(';') {
let decl = raw.trim();
if decl.is_empty() {
continue;
}
let Some((name, value)) = decl.split_once(':') else { continue };
let name = name.trim().to_ascii_lowercase();
// Drop `!important`: it does not change the computed value here.
let value: String = value
.trim()
.split_whitespace()
.take_while(|t| !t.eq_ignore_ascii_case("!important") && *t != "!")
.collect::<Vec<_>>()
.join(" ");
apply_value(style, &name, &value);
}
}
fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
match name {
"display" => match value {
"block" => style.display = Display::Block,
"flex" => style.display = Display::Flex,
"none" => style.display = Display::None,
_ => {}
},
"width" => style.width = px(value),
"height" => style.height = px(value),
"margin" => { if let Some(e) = edges(value) { style.margin = e; } }
"margin-top" => set_edge(&mut style.margin, Side::Top, px(value)),
"margin-right" => set_edge(&mut style.margin, Side::Right, px(value)),
"margin-bottom" => set_edge(&mut style.margin, Side::Bottom, px(value)),
"margin-left" => set_edge(&mut style.margin, Side::Left, px(value)),
"padding" => { if let Some(e) = edges(value) { style.padding = e; } }
"padding-top" => set_edge(&mut style.padding, Side::Top, px(value)),
"padding-right" => set_edge(&mut style.padding, Side::Right, px(value)),
"padding-bottom" => set_edge(&mut style.padding, Side::Bottom, px(value)),
"padding-left" => set_edge(&mut style.padding, Side::Left, px(value)),
"border-width" | "border" => { if let Some(e) = edges(value) { style.border = e; } }
"border-top-width" | "border-top" => set_edge(&mut style.border, Side::Top, px(value)),
"border-right-width" | "border-right" => set_edge(&mut style.border, Side::Right, px(value)),
"border-bottom-width" | "border-bottom" => set_edge(&mut style.border, Side::Bottom, px(value)),
"border-left-width" | "border-left" => set_edge(&mut style.border, Side::Left, px(value)),
_ => {}
}
}
enum Side { Top, Right, Bottom, Left }
fn set_edge(edges: &mut Edges, side: Side, v: Option<f32>) {
let v = match v { Some(v) => v, None => return };
match side {
Side::Top => edges.top = v,
Side::Right => edges.right = v,
Side::Bottom => edges.bottom = v,
Side::Left => edges.left = v,
}
}
/// Parse the first length in a value as CSS pixels. `auto`, percentages, and
/// non-numeric values return None (treated as "no explicit size" in phase 1).
fn px(value: &str) -> Option<f32> {
token(value).and_then(px_value)
}
/// Parse every length token in a value as px (for box shorthands).
fn edges(value: &str) -> Option<Edges> {
let dims: Vec<f32> = value.split_whitespace().filter_map(px_value).collect();
edges_from(dims)
}
fn px_value(tok: &str) -> Option<f32> {
let n = tok.strip_suffix("px").unwrap_or(tok);
// Reject percentages and other units for phase 1.
if n.chars().any(|c| !(c.is_ascii_digit() || c == '.' || c == '-')) {
return None;
}
n.parse::<f32>().ok()
}
fn token(value: &str) -> Option<&str> {
value.split_whitespace().next()
}
/// CSS box shorthand: 1 value applies to all sides; 2 values are (v, h);
/// 3 values are (top, h, bottom); 4 values are (top, right, bottom, left).
fn edges_from(dims: Vec<f32>) -> Option<Edges> {
match dims.len() {
0 => None,
1 => Some(Edges { top: dims[0], right: dims[0], bottom: dims[0], left: dims[0] }),
2 => Some(Edges { top: dims[0], right: dims[1], bottom: dims[0], left: dims[1] }),
3 => Some(Edges { top: dims[0], right: dims[1], bottom: dims[2], left: dims[1] }),
_ => Some(Edges { top: dims[0], right: dims[1], bottom: dims[2], left: dims[3] }),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_display_and_size() {
let s = compute_style("div", Some("display: flex; width: 200px; height: 50px"));
assert_eq!(s.display, Display::Flex);
assert_eq!(s.width, Some(200.0));
assert_eq!(s.height, Some(50.0));
}
#[test]
fn margin_shorthand_expands() {
let s = compute_style("div", Some("margin: 10px 20px"));
assert_eq!(s.margin, Edges { top: 10.0, right: 20.0, bottom: 10.0, left: 20.0 });
}
#[test]
fn longhand_overrides_shorthand() {
let s = compute_style("div", Some("padding: 5px; padding-left: 30px"));
assert_eq!(s.padding.top, 5.0);
assert_eq!(s.padding.left, 30.0);
}
#[test]
fn ua_defaults_and_ignore_unknown() {
let s = compute_style("span", Some("color: red; ; bogus: ; display: none"));
assert_eq!(s.display, Display::None);
}
#[test]
fn important_and_auto() {
let s = compute_style("div", Some("width: 100px !important; height: auto"));
assert_eq!(s.width, Some(100.0));
assert_eq!(s.height, None);
}
}