render: percentage padding, box-shadow, external-sprite svg, gradient text

percentage padding/margin: top/bottom percentages resolve against the
containing-block width (the responsive aspect-ratio-box trick, e.g.
padding-top:56.25%), resolved to px in the top-down layout pass so card
media boxes reserve space instead of collapsing.
box-shadow: parse and paint an outset shadow (rounded, offset, spread,
soft-edge approximation) so cards/buttons/menus have depth.
external-sprite svg: resolve <use href="url#id"> to an external sprite
file, inject the referenced symbol so inline-svg icon systems rasterize.
background-clip:text: paint transparent-with-gradient text (the common
gradient hero/button label) instead of leaving it invisible.
This commit is contained in:
SGavrl committed 2026-07-11 21:02:57 +00:00
1 parent 58dee6330e
commit 23594b1f96
6 files changed
+954 -24

No files matched your search

+245 -17
View File
@@ -253,17 +253,17 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
"margin-block" => { let (s, e) = two(value); set_margin_side(style, 0, s); set_margin_side(style, 2, e); }
"margin-block-start" => set_margin_side(style, 0, value),
"margin-block-end" => set_margin_side(style, 2, 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)),
"padding-inline" => { let (s, e) = two(value); set_edge(&mut style.padding, Side::Left, px(s)); set_edge(&mut style.padding, Side::Right, px(e)); }
"padding-inline-start" => set_edge(&mut style.padding, Side::Left, px(value)),
"padding-inline-end" => set_edge(&mut style.padding, Side::Right, px(value)),
"padding-block" => { let (s, e) = two(value); set_edge(&mut style.padding, Side::Top, px(s)); set_edge(&mut style.padding, Side::Bottom, px(e)); }
"padding-block-start" => set_edge(&mut style.padding, Side::Top, px(value)),
"padding-block-end" => set_edge(&mut style.padding, Side::Bottom, px(value)),
"padding" => apply_padding_shorthand(style, value),
"padding-top" => set_padding_side(style, 0, value),
"padding-right" => set_padding_side(style, 1, value),
"padding-bottom" => set_padding_side(style, 2, value),
"padding-left" => set_padding_side(style, 3, value),
"padding-inline" => { let (s, e) = two(value); set_padding_side(style, 3, s); set_padding_side(style, 1, e); }
"padding-inline-start" => set_padding_side(style, 3, value),
"padding-inline-end" => set_padding_side(style, 1, value),
"padding-block" => { let (s, e) = two(value); set_padding_side(style, 0, s); set_padding_side(style, 2, e); }
"padding-block-start" => set_padding_side(style, 0, value),
"padding-block-end" => set_padding_side(style, 2, value),
"border-radius" => {
// Uniform radius from the first value (ignore per-corner / the
// `/` vertical-radius form; the common case is one length).
@@ -300,6 +300,12 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
"background-size" => style.background_size = parse_background_size(value),
"background-position" => style.background_position = parse_background_position(value),
"mask-image" | "-webkit-mask-image" => style.mask_image = parse_url(value),
"background-clip" | "-webkit-background-clip" => {
// `text` clips the background to the element's glyphs (the gradient/
// solid-color text technique). Any box value (border-box/padding-box/
// content-box) is an ordinary background, so clear the flag.
style.background_clip_text = value.trim().eq_ignore_ascii_case("text");
}
"color" => style.color = parse_color(value),
"border-color" => style.border_color = parse_color(value),
"font-size" => {
@@ -510,6 +516,9 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
"grid-column" => set_grid_placement(style, value, true),
"grid-row" => set_grid_placement(style, value, false),
"transform" => parse_transform(style, value),
"box-shadow" | "-webkit-box-shadow" => {
style.box_shadow = parse_box_shadow(value, style.color);
}
_ => {}
}
}
@@ -1509,19 +1518,86 @@ fn two(value: &str) -> (&str, &str) {
(a, b)
}
/// Set one margin side (0=top,1=right,2=bottom,3=left), tracking `auto`.
/// Set one margin side (0=top,1=right,2=bottom,3=left), tracking `auto` and, for
/// a percentage, deferring resolution to the containing-block width in the
/// top-down pass (recorded in `margin_percent`).
fn set_margin_side(style: &mut LayoutStyle, idx: usize, value: &str) {
let v = value.trim();
let is_auto = v.eq_ignore_ascii_case("auto");
if let Some(frac) = percent_fraction(v) {
style.margin_percent[idx] = Some(frac);
set_margin_px(&mut style.margin, idx, 0.0);
style.margin_auto[idx] = false;
return;
}
let px = if is_auto { 0.0 } else { px(value).unwrap_or(0.0) };
set_margin_px(&mut style.margin, idx, px);
style.margin_auto[idx] = is_auto;
style.margin_percent[idx] = None;
}
fn set_margin_px(margin: &mut Edges, idx: usize, px: f32) {
match idx {
0 => style.margin.top = px,
1 => style.margin.right = px,
2 => style.margin.bottom = px,
3 => style.margin.left = px,
0 => margin.top = px,
1 => margin.right = px,
2 => margin.bottom = px,
3 => margin.left = px,
_ => {}
}
style.margin_auto[idx] = is_auto;
}
/// Set one padding side (0=top,1=right,2=bottom,3=left). A percentage is
/// recorded in `padding_percent` and resolved against the containing-block
/// width during the top-down pass; a length is stored directly.
fn set_padding_side(style: &mut LayoutStyle, idx: usize, value: &str) {
if let Some(frac) = percent_fraction(value.trim()) {
style.padding_percent[idx] = Some(frac);
set_padding_px(&mut style.padding, idx, 0.0);
return;
}
if let Some(px) = px(value) {
set_padding_px(&mut style.padding, idx, px);
style.padding_percent[idx] = None;
}
}
fn set_padding_px(padding: &mut Edges, idx: usize, px: f32) {
match idx {
0 => padding.top = px,
1 => padding.right = px,
2 => padding.bottom = px,
3 => padding.left = px,
_ => {}
}
}
/// `padding: <t> <r>? <b>? <l>?`, percentage-aware per side.
fn apply_padding_shorthand(style: &mut LayoutStyle, value: &str) {
let toks: Vec<&str> = value.split_whitespace().collect();
let (t, r, b, l) = match toks.as_slice() {
[a] => (*a, *a, *a, *a),
[v, h] => (*v, *h, *v, *h),
[t, h, b] => (*t, *h, *b, *h),
[t, r, b, l, ..] => (*t, *r, *b, *l),
[] => return,
};
set_padding_side(style, 0, t);
set_padding_side(style, 1, r);
set_padding_side(style, 2, b);
set_padding_side(style, 3, l);
}
/// A bare `<number>%` token as a 0..1 fraction (`56.25%` -> `0.5625`). Returns
/// `None` for anything that is not a plain percentage (lengths, `calc(...%)`,
/// keywords), so those keep their existing length handling.
fn percent_fraction(tok: &str) -> Option<f32> {
let num = tok.trim().strip_suffix('%')?;
let v: f32 = num.trim().parse().ok()?;
if v.is_finite() {
Some(v / 100.0)
} else {
None
}
}
/// `margin: <t> <r>? <b>? <l>?` with per-side `auto` (so `margin: 0 auto`
@@ -1818,6 +1894,86 @@ fn parse_background_position(value: &str) -> (f32, f32) {
(x.unwrap_or(0.5), y.unwrap_or(0.5))
}
/// Parse a `box-shadow` value into its first layer:
/// `[inset]? <offset-x> <offset-y> <blur>? <spread>? <color>?`. The `inset`
/// keyword and the color may each lead or trail the lengths; comma-separated
/// multiples are accepted but only the first layer is stored. `current_color`
/// supplies the default when the color is omitted (CSS `currentColor`).
fn parse_box_shadow(value: &str, current_color: Option<[u8; 4]>) -> Option<crate::BoxShadow> {
let v = value.trim();
if v.is_empty() || v.eq_ignore_ascii_case("none") {
return None;
}
// Only the first comma-separated layer is modeled; split at paren depth 0 so
// the commas inside an rgba()/hsl() color are not treated as separators.
let layer = split_top_level(v, ',').into_iter().next()?;
let mut inset = false;
let mut color: Option<[u8; 4]> = None;
let mut lengths: Vec<f32> = Vec::new();
for tok in split_ws_paren(layer.trim()) {
let t = tok.trim();
if t.is_empty() {
continue;
}
if t.eq_ignore_ascii_case("inset") {
inset = true;
continue;
}
// Try a length first so a bare `0` is an offset, not a failed color;
// px_value rejects color tokens (`#ccc`, `red`, `rgba(...)`) so they
// fall through to parse_color.
if lengths.len() < 4 {
if let Some(px) = px_value(t) {
lengths.push(px);
continue;
}
}
if let Some(c) = parse_color(t) {
color = Some(c);
}
}
if lengths.len() < 2 {
return None;
}
Some(crate::BoxShadow {
offset_x: lengths[0],
offset_y: lengths[1],
blur: lengths.get(2).copied().unwrap_or(0.0),
spread: lengths.get(3).copied().unwrap_or(0.0),
color: color.or(current_color).unwrap_or([0, 0, 0, 255]),
inset,
})
}
/// Split on ASCII whitespace at paren depth 0, so a functional color like
/// `rgba(0, 0, 0, .15)` (whose internal spaces are not token separators) stays
/// one token.
fn split_ws_paren(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut depth = 0i32;
let mut start: Option<usize> = None;
for (i, c) in s.char_indices() {
if c.is_whitespace() && depth == 0 {
if let Some(st) = start.take() {
out.push(&s[st..i]);
}
continue;
}
match c {
'(' => depth += 1,
')' => depth = (depth - 1).max(0),
_ => {}
}
if start.is_none() {
start = Some(i);
}
}
if let Some(st) = start {
out.push(&s[st..]);
}
out
}
/// 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> {
@@ -1855,12 +2011,46 @@ mod tests {
assert_eq!(s.padding.left, 30.0);
}
#[test]
fn percentage_padding_recorded_not_pixelized() {
// A percentage padding must be deferred (recorded as a 0..1 fraction),
// not eagerly converted to a bogus px value: it resolves against the
// containing block width later, in the top-down pass.
let s = compute_style("div", Some("padding-top: 56.25%"));
assert_eq!(s.padding_percent[0], Some(0.5625));
assert_eq!(s.padding.top, 0.0);
// The shorthand splits per side, so a mix of length and percent lands
// in the right buckets.
let s = compute_style("div", Some("padding: 10px 25%"));
assert_eq!(s.padding.top, 10.0);
assert_eq!(s.padding_percent[0], None);
assert_eq!(s.padding_percent[1], Some(0.25));
}
#[test]
fn percentage_margin_recorded() {
let s = compute_style("div", Some("margin-left: 10%"));
assert_eq!(s.margin_percent[3], Some(0.1));
assert!(!s.margin_auto[3]);
}
#[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 background_clip_text_flag() {
let s = compute_style("h1", Some("color: transparent; -webkit-background-clip: text"));
assert!(s.background_clip_text);
let n = compute_style("h1", Some("background-clip: border-box"));
assert!(!n.background_clip_text);
let l = compute_style("h1", Some("background-clip: text"));
assert!(l.background_clip_text);
}
#[test]
fn important_and_auto() {
let s = compute_style("div", Some("width: 100px !important; height: auto"));
@@ -1938,4 +2128,42 @@ mod tests {
assert_eq!(s.flex_grow, Some(2.0));
assert_eq!(s.flex_shrink, Some(1.0));
}
#[test]
fn box_shadow_outset_parses() {
let s = compute_style("div", Some("box-shadow: 0 2px 8px rgba(0,0,0,.15)"));
let sh = s.box_shadow.expect("box-shadow parsed");
assert!(!sh.inset);
assert_eq!(sh.offset_x, 0.0);
assert_eq!(sh.offset_y, 2.0);
assert_eq!(sh.blur, 8.0);
assert_eq!(sh.spread, 0.0);
assert_eq!(sh.color, [0, 0, 0, 38]);
}
#[test]
fn box_shadow_inset_parses() {
let s = compute_style("div", Some("box-shadow: inset 0 0 0 1px #ccc"));
let sh = s.box_shadow.expect("box-shadow parsed");
assert!(sh.inset);
assert_eq!(sh.offset_x, 0.0);
assert_eq!(sh.offset_y, 0.0);
assert_eq!(sh.blur, 0.0);
assert_eq!(sh.spread, 1.0);
assert_eq!(sh.color, [204, 204, 204, 255]);
}
#[test]
fn box_shadow_color_defaults_to_current_color() {
// No explicit color: falls back to the element's text color.
let s = compute_style("div", Some("color: red; box-shadow: 1px 1px 2px"));
let sh = s.box_shadow.expect("box-shadow parsed");
assert_eq!(sh.color, [255, 0, 0, 255]);
}
#[test]
fn box_shadow_none_clears() {
let s = compute_style("div", Some("box-shadow: none"));
assert!(s.box_shadow.is_none());
}
}