render: resolve contextual CSS math
This commit is contained in:
@@ -422,6 +422,36 @@ pub fn layout_dom_with_images(
|
||||
// em in non-font-size properties is relative to this element's
|
||||
// OWN computed font-size; resolve every relative length now.
|
||||
let em_px = style.font_size.unwrap_or(parent_fs);
|
||||
let cb_w = inh.cb_width;
|
||||
for index in 0..6 {
|
||||
let Some(expression) = style.size_expressions[index].as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let percent_base = if matches!(index, 1 | 3 | 5) {
|
||||
viewport.1
|
||||
} else {
|
||||
cb_w
|
||||
};
|
||||
let Some(px) = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
em_px,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
percent_base,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let resolved = crate::Dimension::Px(px);
|
||||
match index {
|
||||
0 => style.width = resolved,
|
||||
1 => style.height = resolved,
|
||||
2 => style.min_width = resolved,
|
||||
3 => style.min_height = resolved,
|
||||
4 => style.max_width = resolved,
|
||||
_ => style.max_height = resolved,
|
||||
}
|
||||
}
|
||||
style.width = style.width.resolve(em_px, root_fs, vw, vh);
|
||||
style.height = style.height.resolve(em_px, root_fs, vw, vh);
|
||||
style.min_width = style.min_width.resolve(em_px, root_fs, vw, vh);
|
||||
@@ -454,8 +484,41 @@ pub fn layout_dom_with_images(
|
||||
// (per CSS: `padding-top:56.25%` in a 1000px block is 562.5px,
|
||||
// not a fraction of the height). Bake the resolved px back into
|
||||
// `padding`/`margin`, which then feed taffy.
|
||||
let cb_w = inh.cb_width;
|
||||
for i in 0..4 {
|
||||
if let Some(expression) = style.padding_expressions[i].as_deref() {
|
||||
if let Some(px) = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
em_px,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
cb_w,
|
||||
) {
|
||||
match i {
|
||||
0 => style.padding.top = px.max(0.0),
|
||||
1 => style.padding.right = px.max(0.0),
|
||||
2 => style.padding.bottom = px.max(0.0),
|
||||
_ => style.padding.left = px.max(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(expression) = style.margin_expressions[i].as_deref() {
|
||||
if let Some(px) = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
em_px,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
cb_w,
|
||||
) {
|
||||
match i {
|
||||
0 => style.margin.top = px,
|
||||
1 => style.margin.right = px,
|
||||
2 => style.margin.bottom = px,
|
||||
_ => style.margin.left = px,
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(relative) = style.padding_relative[i] {
|
||||
if let crate::Dimension::Px(px) =
|
||||
relative.resolve(em_px, root_fs, vw, vh)
|
||||
|
||||
@@ -181,6 +181,11 @@ pub struct LayoutStyle {
|
||||
pub min_height: Dimension,
|
||||
pub max_width: Dimension,
|
||||
pub max_height: Dimension,
|
||||
/// Deferred CSS math expressions for width, height, min-width, min-height,
|
||||
/// max-width, and max-height. Functional lengths can depend on the actual
|
||||
/// viewport, containing block, or computed font size and cannot be safely
|
||||
/// collapsed to pixels during stylesheet parsing.
|
||||
pub size_expressions: [Option<String>; 6],
|
||||
/// `aspect-ratio` as width/height, or an image's intrinsic ratio resolved
|
||||
/// at layout. Lets a replaced element (or a padding-box card) derive the
|
||||
/// missing dimension from the given one, so a `width:100%` image gets a
|
||||
@@ -201,6 +206,8 @@ pub struct LayoutStyle {
|
||||
/// These retain their unit until the top-down pass knows the element font
|
||||
/// size, root font size, and viewport dimensions.
|
||||
pub margin_relative: [Option<Dimension>; 4],
|
||||
/// Deferred `calc()`/`min()`/`max()`/`clamp()` margin expressions.
|
||||
pub margin_expressions: [Option<String>; 4],
|
||||
pub padding: Edges,
|
||||
/// Percentage padding per side (top, right, bottom, left) as a 0..1
|
||||
/// fraction, `None` when the side is a fixed length. All four sides resolve
|
||||
@@ -213,6 +220,8 @@ pub struct LayoutStyle {
|
||||
/// Font- and viewport-relative padding lengths (top, right, bottom, left),
|
||||
/// resolved alongside `margin_relative` during the top-down pass.
|
||||
pub padding_relative: [Option<Dimension>; 4],
|
||||
/// Deferred `calc()`/`min()`/`max()`/`clamp()` padding expressions.
|
||||
pub padding_expressions: [Option<String>; 4],
|
||||
pub border: Edges,
|
||||
/// `border-radius` (uniform; the first value of the shorthand). Rounds the
|
||||
/// background fill and border. In px after resolution.
|
||||
|
||||
@@ -259,10 +259,12 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
}
|
||||
"width" => {
|
||||
style.width = dimension_value(value);
|
||||
style.size_expressions[0] = deferred_length_expression(value);
|
||||
style.width_set = true;
|
||||
}
|
||||
"height" => {
|
||||
style.height = dimension_value(value);
|
||||
style.size_expressions[1] = deferred_length_expression(value);
|
||||
style.height_set = true;
|
||||
}
|
||||
"box-sizing" => {
|
||||
@@ -276,10 +278,22 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
style.box_sizing
|
||||
};
|
||||
}
|
||||
"min-width" => style.min_width = dimension_value(value),
|
||||
"min-height" => style.min_height = dimension_value(value),
|
||||
"max-width" => style.max_width = dimension_value(value),
|
||||
"max-height" => style.max_height = dimension_value(value),
|
||||
"min-width" => {
|
||||
style.min_width = dimension_value(value);
|
||||
style.size_expressions[2] = deferred_length_expression(value);
|
||||
}
|
||||
"min-height" => {
|
||||
style.min_height = dimension_value(value);
|
||||
style.size_expressions[3] = deferred_length_expression(value);
|
||||
}
|
||||
"max-width" => {
|
||||
style.max_width = dimension_value(value);
|
||||
style.size_expressions[4] = deferred_length_expression(value);
|
||||
}
|
||||
"max-height" => {
|
||||
style.max_height = dimension_value(value);
|
||||
style.size_expressions[5] = deferred_length_expression(value);
|
||||
}
|
||||
"aspect-ratio" => style.aspect_ratio = parse_aspect_ratio(value),
|
||||
"margin" => apply_margin_shorthand(style, value),
|
||||
"margin-top" => set_margin_side(style, 0, value),
|
||||
@@ -1494,6 +1508,196 @@ fn px(value: &str) -> Option<f32> {
|
||||
token(value).and_then(px_value)
|
||||
}
|
||||
|
||||
fn deferred_length_expression(value: &str) -> Option<String> {
|
||||
value.trim().contains('(').then(|| value.trim().to_string())
|
||||
}
|
||||
|
||||
/// Resolve functional CSS lengths once the actual layout context is known.
|
||||
/// `vw`/`vh` are one-percent viewport units and `percent_base` is the relevant
|
||||
/// containing-block dimension. Custom properties have already been substituted
|
||||
/// by the cascade before this function sees the expression.
|
||||
pub(crate) fn resolve_contextual_length(
|
||||
value: &str,
|
||||
em_px: f32,
|
||||
rem_px: f32,
|
||||
vw: f32,
|
||||
vh: f32,
|
||||
percent_base: f32,
|
||||
) -> Option<f32> {
|
||||
let context = LengthContext {
|
||||
em_px,
|
||||
rem_px,
|
||||
vw,
|
||||
vh,
|
||||
percent_base,
|
||||
};
|
||||
resolve_contextual(value, &context)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct LengthContext {
|
||||
em_px: f32,
|
||||
rem_px: f32,
|
||||
vw: f32,
|
||||
vh: f32,
|
||||
percent_base: f32,
|
||||
}
|
||||
|
||||
fn resolve_contextual(value: &str, context: &LengthContext) -> Option<f32> {
|
||||
let value = value.trim();
|
||||
if let Some(rest) = value.strip_prefix("var(") {
|
||||
let end = find_matching_paren(rest)?;
|
||||
let inner = &rest[..end];
|
||||
let (_, fallback) = inner.split_once(',')?;
|
||||
return resolve_contextual(fallback.trim(), context);
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix("calc(") {
|
||||
let end = find_matching_paren(rest)?;
|
||||
return eval_contextual_calc(&rest[..end], context);
|
||||
}
|
||||
if let Some(rest) = value
|
||||
.strip_prefix("max(")
|
||||
.or_else(|| value.strip_prefix("min("))
|
||||
{
|
||||
let is_max = value.starts_with("max(");
|
||||
let end = find_matching_paren(rest)?;
|
||||
let args = split_top_level(&rest[..end], ',');
|
||||
let mut values = args
|
||||
.iter()
|
||||
.filter_map(|arg| resolve_contextual(arg, context));
|
||||
let mut best = values.next()?;
|
||||
for candidate in values {
|
||||
if (is_max && candidate > best) || (!is_max && candidate < best) {
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
return Some(best);
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix("clamp(") {
|
||||
let end = find_matching_paren(rest)?;
|
||||
let args = split_top_level(&rest[..end], ',');
|
||||
if args.len() == 3 {
|
||||
let low = resolve_contextual(args[0], context)?;
|
||||
let preferred = resolve_contextual(args[1], context)?;
|
||||
let high = resolve_contextual(args[2], context)?;
|
||||
return Some(preferred.min(high).max(low));
|
||||
}
|
||||
}
|
||||
contextual_atom(value, context)
|
||||
}
|
||||
|
||||
fn contextual_atom(value: &str, context: &LengthContext) -> Option<f32> {
|
||||
let lower = value.trim().to_ascii_lowercase();
|
||||
let parse = |number: &str| number.trim().parse::<f32>().ok();
|
||||
if let Some(value) = lower.strip_suffix("rem").and_then(parse) {
|
||||
return Some(value * context.rem_px);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("em").and_then(parse) {
|
||||
return Some(value * context.em_px);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("vmin").and_then(parse) {
|
||||
return Some(value * context.vw.min(context.vh));
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("vmax").and_then(parse) {
|
||||
return Some(value * context.vw.max(context.vh));
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("vw").and_then(parse) {
|
||||
return Some(value * context.vw);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("vh").and_then(parse) {
|
||||
return Some(value * context.vh);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix('%').and_then(parse) {
|
||||
return Some(value * context.percent_base / 100.0);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("px").and_then(parse) {
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("pt").and_then(parse) {
|
||||
return Some(value * 1.333);
|
||||
}
|
||||
parse(&lower)
|
||||
}
|
||||
|
||||
fn eval_contextual_calc(expr: &str, context: &LengthContext) -> Option<f32> {
|
||||
let mut terms: Vec<(f32, String)> = Vec::new();
|
||||
let mut sign = 1.0;
|
||||
let mut current = String::new();
|
||||
let mut depth = 0i32;
|
||||
for character in expr.chars() {
|
||||
match character {
|
||||
'(' => {
|
||||
depth += 1;
|
||||
current.push(character);
|
||||
}
|
||||
')' => {
|
||||
depth -= 1;
|
||||
current.push(character);
|
||||
}
|
||||
'+' | '-' if depth == 0 => {
|
||||
if !current.trim().is_empty() {
|
||||
terms.push((sign, std::mem::take(&mut current)));
|
||||
}
|
||||
sign = if character == '-' { -1.0 } else { 1.0 };
|
||||
}
|
||||
_ => current.push(character),
|
||||
}
|
||||
}
|
||||
if !current.trim().is_empty() {
|
||||
terms.push((sign, current));
|
||||
}
|
||||
if terms.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut total = 0.0;
|
||||
for (term_sign, term) in terms {
|
||||
total += term_sign * eval_contextual_product(term.trim(), context)?;
|
||||
}
|
||||
Some(total)
|
||||
}
|
||||
|
||||
fn eval_contextual_product(term: &str, context: &LengthContext) -> Option<f32> {
|
||||
let mut result = None;
|
||||
let mut operator = '*';
|
||||
let mut depth = 0i32;
|
||||
let mut current = String::new();
|
||||
let mut factors = Vec::new();
|
||||
for character in term.chars() {
|
||||
match character {
|
||||
'(' => {
|
||||
depth += 1;
|
||||
current.push(character);
|
||||
}
|
||||
')' => {
|
||||
depth -= 1;
|
||||
current.push(character);
|
||||
}
|
||||
' ' if depth == 0 => {
|
||||
if !current.is_empty() {
|
||||
factors.push(std::mem::take(&mut current));
|
||||
}
|
||||
}
|
||||
_ => current.push(character),
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
factors.push(current);
|
||||
}
|
||||
for factor in &factors {
|
||||
if factor == "*" || factor == "/" {
|
||||
operator = factor.chars().next()?;
|
||||
continue;
|
||||
}
|
||||
let value = resolve_contextual(factor, context)?;
|
||||
result = Some(match result {
|
||||
None => value,
|
||||
Some(previous) if operator == '/' => previous / value,
|
||||
Some(previous) => previous * value,
|
||||
});
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Resolve a CSS length expression to px, recursively handling the small set
|
||||
/// of functional forms real stylesheets actually nest in practice:
|
||||
/// `var(--x, fallback)` (substitute the fallback; we track no custom
|
||||
@@ -1743,6 +1947,15 @@ fn two(value: &str) -> (&str, &str) {
|
||||
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(expression) = deferred_length_expression(v) {
|
||||
style.margin_expressions[idx] = Some(expression);
|
||||
style.margin_percent[idx] = None;
|
||||
style.margin_relative[idx] = None;
|
||||
style.margin_auto[idx] = false;
|
||||
set_margin_px(&mut style.margin, idx, 0.0);
|
||||
return;
|
||||
}
|
||||
style.margin_expressions[idx] = None;
|
||||
if let Some(frac) = percent_fraction(v) {
|
||||
style.margin_percent[idx] = Some(frac);
|
||||
style.margin_relative[idx] = None;
|
||||
@@ -1789,6 +2002,14 @@ fn set_margin_px(margin: &mut Edges, idx: usize, px: f32) {
|
||||
/// width during the top-down pass; a length is stored directly.
|
||||
fn set_padding_side(style: &mut LayoutStyle, idx: usize, value: &str) {
|
||||
let value = value.trim();
|
||||
if let Some(expression) = deferred_length_expression(value) {
|
||||
style.padding_expressions[idx] = Some(expression);
|
||||
style.padding_percent[idx] = None;
|
||||
style.padding_relative[idx] = None;
|
||||
set_padding_px(&mut style.padding, idx, 0.0);
|
||||
return;
|
||||
}
|
||||
style.padding_expressions[idx] = None;
|
||||
if let Some(frac) = percent_fraction(value) {
|
||||
style.padding_percent[idx] = Some(frac);
|
||||
style.padding_relative[idx] = None;
|
||||
@@ -2349,6 +2570,44 @@ mod tests {
|
||||
assert_eq!(shorthand.grid_template_columns.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_css_math_uses_runtime_geometry() {
|
||||
let context = (20.0, 16.0, 9.0, 10.0, 900.0);
|
||||
assert_eq!(
|
||||
resolve_contextual_length(
|
||||
"min(25vw,350px)",
|
||||
context.0,
|
||||
context.1,
|
||||
context.2,
|
||||
context.3,
|
||||
context.4,
|
||||
),
|
||||
Some(225.0)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_contextual_length(
|
||||
"clamp(200px,30vw,320px)",
|
||||
context.0,
|
||||
context.1,
|
||||
context.2,
|
||||
context.3,
|
||||
context.4,
|
||||
),
|
||||
Some(270.0)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_contextual_length(
|
||||
"calc(10vw + 2rem)",
|
||||
context.0,
|
||||
context.1,
|
||||
context.2,
|
||||
context.3,
|
||||
context.4,
|
||||
),
|
||||
Some(122.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ua_defaults_and_ignore_unknown() {
|
||||
let s = compute_style("span", Some("color: red; ; bogus: ; display: none"));
|
||||
|
||||
Reference in New Issue
Block a user