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
|
// em in non-font-size properties is relative to this element's
|
||||||
// OWN computed font-size; resolve every relative length now.
|
// OWN computed font-size; resolve every relative length now.
|
||||||
let em_px = style.font_size.unwrap_or(parent_fs);
|
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.width = style.width.resolve(em_px, root_fs, vw, vh);
|
||||||
style.height = style.height.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);
|
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,
|
// (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
|
// not a fraction of the height). Bake the resolved px back into
|
||||||
// `padding`/`margin`, which then feed taffy.
|
// `padding`/`margin`, which then feed taffy.
|
||||||
let cb_w = inh.cb_width;
|
|
||||||
for i in 0..4 {
|
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 Some(relative) = style.padding_relative[i] {
|
||||||
if let crate::Dimension::Px(px) =
|
if let crate::Dimension::Px(px) =
|
||||||
relative.resolve(em_px, root_fs, vw, vh)
|
relative.resolve(em_px, root_fs, vw, vh)
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ pub struct LayoutStyle {
|
|||||||
pub min_height: Dimension,
|
pub min_height: Dimension,
|
||||||
pub max_width: Dimension,
|
pub max_width: Dimension,
|
||||||
pub max_height: 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
|
/// `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
|
/// 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
|
/// 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
|
/// These retain their unit until the top-down pass knows the element font
|
||||||
/// size, root font size, and viewport dimensions.
|
/// size, root font size, and viewport dimensions.
|
||||||
pub margin_relative: [Option<Dimension>; 4],
|
pub margin_relative: [Option<Dimension>; 4],
|
||||||
|
/// Deferred `calc()`/`min()`/`max()`/`clamp()` margin expressions.
|
||||||
|
pub margin_expressions: [Option<String>; 4],
|
||||||
pub padding: Edges,
|
pub padding: Edges,
|
||||||
/// Percentage padding per side (top, right, bottom, left) as a 0..1
|
/// 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
|
/// 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),
|
/// Font- and viewport-relative padding lengths (top, right, bottom, left),
|
||||||
/// resolved alongside `margin_relative` during the top-down pass.
|
/// resolved alongside `margin_relative` during the top-down pass.
|
||||||
pub padding_relative: [Option<Dimension>; 4],
|
pub padding_relative: [Option<Dimension>; 4],
|
||||||
|
/// Deferred `calc()`/`min()`/`max()`/`clamp()` padding expressions.
|
||||||
|
pub padding_expressions: [Option<String>; 4],
|
||||||
pub border: Edges,
|
pub border: Edges,
|
||||||
/// `border-radius` (uniform; the first value of the shorthand). Rounds the
|
/// `border-radius` (uniform; the first value of the shorthand). Rounds the
|
||||||
/// background fill and border. In px after resolution.
|
/// background fill and border. In px after resolution.
|
||||||
|
|||||||
@@ -259,10 +259,12 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
}
|
}
|
||||||
"width" => {
|
"width" => {
|
||||||
style.width = dimension_value(value);
|
style.width = dimension_value(value);
|
||||||
|
style.size_expressions[0] = deferred_length_expression(value);
|
||||||
style.width_set = true;
|
style.width_set = true;
|
||||||
}
|
}
|
||||||
"height" => {
|
"height" => {
|
||||||
style.height = dimension_value(value);
|
style.height = dimension_value(value);
|
||||||
|
style.size_expressions[1] = deferred_length_expression(value);
|
||||||
style.height_set = true;
|
style.height_set = true;
|
||||||
}
|
}
|
||||||
"box-sizing" => {
|
"box-sizing" => {
|
||||||
@@ -276,10 +278,22 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
style.box_sizing
|
style.box_sizing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
"min-width" => style.min_width = dimension_value(value),
|
"min-width" => {
|
||||||
"min-height" => style.min_height = dimension_value(value),
|
style.min_width = dimension_value(value);
|
||||||
"max-width" => style.max_width = dimension_value(value),
|
style.size_expressions[2] = deferred_length_expression(value);
|
||||||
"max-height" => style.max_height = dimension_value(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),
|
"aspect-ratio" => style.aspect_ratio = parse_aspect_ratio(value),
|
||||||
"margin" => apply_margin_shorthand(style, value),
|
"margin" => apply_margin_shorthand(style, value),
|
||||||
"margin-top" => set_margin_side(style, 0, 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)
|
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
|
/// Resolve a CSS length expression to px, recursively handling the small set
|
||||||
/// of functional forms real stylesheets actually nest in practice:
|
/// of functional forms real stylesheets actually nest in practice:
|
||||||
/// `var(--x, fallback)` (substitute the fallback; we track no custom
|
/// `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) {
|
fn set_margin_side(style: &mut LayoutStyle, idx: usize, value: &str) {
|
||||||
let v = value.trim();
|
let v = value.trim();
|
||||||
let is_auto = v.eq_ignore_ascii_case("auto");
|
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) {
|
if let Some(frac) = percent_fraction(v) {
|
||||||
style.margin_percent[idx] = Some(frac);
|
style.margin_percent[idx] = Some(frac);
|
||||||
style.margin_relative[idx] = None;
|
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.
|
/// width during the top-down pass; a length is stored directly.
|
||||||
fn set_padding_side(style: &mut LayoutStyle, idx: usize, value: &str) {
|
fn set_padding_side(style: &mut LayoutStyle, idx: usize, value: &str) {
|
||||||
let value = value.trim();
|
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) {
|
if let Some(frac) = percent_fraction(value) {
|
||||||
style.padding_percent[idx] = Some(frac);
|
style.padding_percent[idx] = Some(frac);
|
||||||
style.padding_relative[idx] = None;
|
style.padding_relative[idx] = None;
|
||||||
@@ -2349,6 +2570,44 @@ mod tests {
|
|||||||
assert_eq!(shorthand.grid_template_columns.len(), 3);
|
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]
|
#[test]
|
||||||
fn ua_defaults_and_ignore_unknown() {
|
fn ua_defaults_and_ignore_unknown() {
|
||||||
let s = compute_style("span", Some("color: red; ; bogus: ; display: none"));
|
let s = compute_style("span", Some("color: red; ; bogus: ; display: none"));
|
||||||
|
|||||||
Reference in New Issue
Block a user