render: resolve contextual positioned lengths
This commit is contained in:
1 parent
d9f9202737
commit
ca551a996b
4 files changed
+243
-38
No files matched your search
@@ -676,11 +676,68 @@ pub fn layout_dom_with_images(
|
||||
style.height,
|
||||
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
||||
);
|
||||
for index in 0..4 {
|
||||
let Some(expression) =
|
||||
style.inset_expressions[index].as_deref()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let percent_base = if matches!(index, 1 | 3) {
|
||||
cb_w
|
||||
} else {
|
||||
viewport.1
|
||||
};
|
||||
style.inset[index] =
|
||||
crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
em_px,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
percent_base,
|
||||
)
|
||||
.map(crate::Dimension::Px);
|
||||
}
|
||||
for i in style.inset.iter_mut() {
|
||||
if let Some(d) = i {
|
||||
*i = Some(d.resolve(em_px, root_fs, vw, vh));
|
||||
}
|
||||
}
|
||||
// A fixed box is positioned against the initial containing
|
||||
// block, whose dimensions are the viewport, not the full
|
||||
// scrollable root element. Taffy attaches out-of-flow boxes to
|
||||
// a layout node, and the root node can grow to the document's
|
||||
// full content height. Make the CSS 2.1 stretch equation
|
||||
// explicit for the ubiquitous `position:fixed; inset:0` case
|
||||
// so overlays/canvases remain exactly viewport-sized.
|
||||
if style.position_fixed {
|
||||
if style.width == crate::Dimension::Auto {
|
||||
if let (
|
||||
Some(crate::Dimension::Px(right)),
|
||||
Some(crate::Dimension::Px(left)),
|
||||
) = (style.inset[1], style.inset[3])
|
||||
{
|
||||
style.width = crate::Dimension::Px(
|
||||
(viewport.0 - left - right).max(0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
if style.height == crate::Dimension::Auto {
|
||||
if let (
|
||||
Some(crate::Dimension::Px(top)),
|
||||
Some(crate::Dimension::Px(bottom)),
|
||||
) = (style.inset[0], style.inset[2])
|
||||
{
|
||||
style.height = crate::Dimension::Px(
|
||||
(viewport.1 - top - bottom).max(0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
child_cb_height_definite = matches!(
|
||||
style.height,
|
||||
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
||||
);
|
||||
}
|
||||
match &style.font_weight { Some(w) => inh.font_weight = Some(w.clone()), None => style.font_weight = inh.font_weight.clone() }
|
||||
match &style.font_family { Some(f) => inh.font_family = Some(f.clone()), None => style.font_family = inh.font_family.clone() }
|
||||
let is_table = tree
|
||||
|
||||
@@ -375,6 +375,8 @@ pub struct LayoutStyle {
|
||||
/// layout mode, but fixed boxes use the initial containing block.
|
||||
pub position_fixed: bool,
|
||||
pub inset: [Option<Dimension>; 4], // top, right, bottom, left
|
||||
/// Deferred functional inset expressions in top/right/bottom/left order.
|
||||
pub inset_expressions: [Option<String>; 4],
|
||||
|
||||
/// `overflow`/-x/-y other than `visible`: clips this element's descendants
|
||||
/// to its border box during paint. This is what makes the ubiquitous
|
||||
|
||||
@@ -625,13 +625,13 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
"top" => style.inset[0] = inset_dim(value),
|
||||
"right" => style.inset[1] = inset_dim(value),
|
||||
"bottom" => style.inset[2] = inset_dim(value),
|
||||
"left" => style.inset[3] = inset_dim(value),
|
||||
"top" => set_inset_side(style, 0, value),
|
||||
"right" => set_inset_side(style, 1, value),
|
||||
"bottom" => set_inset_side(style, 2, value),
|
||||
"left" => set_inset_side(style, 3, value),
|
||||
"inset" => {
|
||||
// 1-4 values, CSS shorthand order: all / v h / t h b / t r b l.
|
||||
let parts: Vec<crate::Dimension> = value.split_whitespace().map(dimension_value).collect();
|
||||
let parts = split_ws_paren(value);
|
||||
let (t, r, b, l) = match parts.as_slice() {
|
||||
[a] => (*a, *a, *a, *a),
|
||||
[v, h] => (*v, *h, *v, *h),
|
||||
@@ -639,7 +639,10 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
[t, r, b, l, ..] => (*t, *r, *b, *l),
|
||||
[] => return,
|
||||
};
|
||||
style.inset = [Some(t), Some(r), Some(b), Some(l)];
|
||||
set_inset_side(style, 0, t);
|
||||
set_inset_side(style, 1, r);
|
||||
set_inset_side(style, 2, b);
|
||||
set_inset_side(style, 3, l);
|
||||
}
|
||||
"overflow" | "overflow-x" | "overflow-y" => {
|
||||
style.overflow_hidden = value != "visible";
|
||||
@@ -1876,6 +1879,12 @@ struct LengthContext {
|
||||
|
||||
fn resolve_contextual(value: &str, context: &LengthContext) -> Option<f32> {
|
||||
let value = value.trim();
|
||||
if let Some(rest) = value.strip_prefix('(') {
|
||||
let end = find_matching_paren(rest)?;
|
||||
if end + 2 == value.len() {
|
||||
return eval_contextual_calc(&rest[..end], context);
|
||||
}
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix("var(") {
|
||||
let end = find_matching_paren(rest)?;
|
||||
let inner = &rest[..end];
|
||||
@@ -1895,7 +1904,7 @@ fn resolve_contextual(value: &str, context: &LengthContext) -> Option<f32> {
|
||||
let args = split_top_level(&rest[..end], ',');
|
||||
let mut values = args
|
||||
.iter()
|
||||
.filter_map(|arg| resolve_contextual(arg, context));
|
||||
.filter_map(|arg| eval_contextual_calc(arg, context));
|
||||
let mut best = values.next()?;
|
||||
for candidate in values {
|
||||
if (is_max && candidate > best) || (!is_max && candidate < best) {
|
||||
@@ -1908,9 +1917,9 @@ fn resolve_contextual(value: &str, context: &LengthContext) -> Option<f32> {
|
||||
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)?;
|
||||
let low = eval_contextual_calc(args[0], context)?;
|
||||
let preferred = eval_contextual_calc(args[1], context)?;
|
||||
let high = eval_contextual_calc(args[2], context)?;
|
||||
return Some(preferred.min(high).max(low));
|
||||
}
|
||||
}
|
||||
@@ -1963,6 +1972,22 @@ fn contextual_atom(value: &str, context: &LengthContext) -> Option<f32> {
|
||||
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("dvw")
|
||||
.or_else(|| lower.strip_suffix("svw"))
|
||||
.or_else(|| lower.strip_suffix("lvw"))
|
||||
.and_then(parse)
|
||||
{
|
||||
return Some(value * context.vw);
|
||||
}
|
||||
if let Some(value) = lower
|
||||
.strip_suffix("dvh")
|
||||
.or_else(|| lower.strip_suffix("svh"))
|
||||
.or_else(|| lower.strip_suffix("lvh"))
|
||||
.and_then(parse)
|
||||
{
|
||||
return Some(value * context.vh);
|
||||
}
|
||||
if let Some(value) = lower.strip_suffix("vw").and_then(parse) {
|
||||
return Some(value * context.vw);
|
||||
}
|
||||
@@ -2019,11 +2044,11 @@ fn eval_contextual_calc(expr: &str, context: &LengthContext) -> Option<f32> {
|
||||
}
|
||||
|
||||
fn eval_contextual_product(term: &str, context: &LengthContext) -> Option<f32> {
|
||||
let mut result = None;
|
||||
let mut result: Option<f32> = None;
|
||||
let mut operator = '*';
|
||||
let mut depth = 0i32;
|
||||
let mut current = String::new();
|
||||
let mut factors = Vec::new();
|
||||
let mut factors: Vec<(char, String)> = Vec::new();
|
||||
for character in term.chars() {
|
||||
match character {
|
||||
'(' => {
|
||||
@@ -2034,26 +2059,25 @@ fn eval_contextual_product(term: &str, context: &LengthContext) -> Option<f32> {
|
||||
depth -= 1;
|
||||
current.push(character);
|
||||
}
|
||||
' ' if depth == 0 => {
|
||||
if !current.is_empty() {
|
||||
factors.push(std::mem::take(&mut current));
|
||||
'*' | '/' if depth == 0 => {
|
||||
if current.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
factors.push((operator, std::mem::take(&mut current)));
|
||||
operator = character;
|
||||
}
|
||||
_ => current.push(character),
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
factors.push(current);
|
||||
}
|
||||
for factor in &factors {
|
||||
if factor == "*" || factor == "/" {
|
||||
operator = factor.chars().next()?;
|
||||
continue;
|
||||
if current.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
factors.push((operator, current));
|
||||
for (operator, factor) in &factors {
|
||||
let value = resolve_contextual(factor, context)?;
|
||||
result = Some(match result {
|
||||
None => value,
|
||||
Some(previous) if operator == '/' => previous / value,
|
||||
Some(previous) if *operator == '/' => previous / value,
|
||||
Some(previous) => previous * value,
|
||||
});
|
||||
}
|
||||
@@ -2069,6 +2093,12 @@ fn eval_contextual_product(term: &str, context: &LengthContext) -> Option<f32> {
|
||||
/// this function rather than assuming a flat expression.
|
||||
fn resolve_length(value: &str) -> Option<f32> {
|
||||
let v = value.trim();
|
||||
if let Some(rest) = v.strip_prefix('(') {
|
||||
let end = find_matching_paren(rest)?;
|
||||
if end + 2 == v.len() {
|
||||
return eval_calc(&rest[..end]);
|
||||
}
|
||||
}
|
||||
if let Some(rest) = v.strip_prefix("var(") {
|
||||
let end = find_matching_paren(rest)?;
|
||||
let inner = &rest[..end];
|
||||
@@ -2083,7 +2113,7 @@ fn resolve_length(value: &str) -> Option<f32> {
|
||||
let is_max = v.starts_with("max(");
|
||||
let end = find_matching_paren(rest)?;
|
||||
let args = split_top_level(&rest[..end], ',');
|
||||
let mut values = args.iter().filter_map(|a| resolve_length(a));
|
||||
let mut values = args.iter().filter_map(|a| eval_calc(a));
|
||||
let mut best = values.next()?;
|
||||
for val in values {
|
||||
if (is_max && val > best) || (!is_max && val < best) {
|
||||
@@ -2099,9 +2129,9 @@ fn resolve_length(value: &str) -> Option<f32> {
|
||||
let end = find_matching_paren(rest)?;
|
||||
let args = split_top_level(&rest[..end], ',');
|
||||
if args.len() == 3 {
|
||||
let lo = resolve_length(args[0].trim())?;
|
||||
let mid = resolve_length(args[1].trim())?;
|
||||
let hi = resolve_length(args[2].trim())?;
|
||||
let lo = eval_calc(args[0].trim())?;
|
||||
let mid = eval_calc(args[1].trim())?;
|
||||
let hi = eval_calc(args[2].trim())?;
|
||||
return Some(mid.min(hi).max(lo));
|
||||
}
|
||||
}
|
||||
@@ -2206,28 +2236,31 @@ fn eval_product(term: &str) -> Option<f32> {
|
||||
let mut op = '*';
|
||||
let mut depth = 0i32;
|
||||
let mut cur = String::new();
|
||||
let mut factors = Vec::new();
|
||||
let mut factors: Vec<(char, String)> = Vec::new();
|
||||
for c in term.chars() {
|
||||
match c {
|
||||
'(' => { depth += 1; cur.push(c); }
|
||||
')' => { depth -= 1; cur.push(c); }
|
||||
' ' if depth == 0 => {
|
||||
if !cur.is_empty() { factors.push(std::mem::take(&mut cur)); }
|
||||
'*' | '/' if depth == 0 => {
|
||||
if cur.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
factors.push((op, std::mem::take(&mut cur)));
|
||||
op = c;
|
||||
}
|
||||
_ => cur.push(c),
|
||||
}
|
||||
}
|
||||
if !cur.is_empty() { factors.push(cur); }
|
||||
|
||||
for tok in &factors {
|
||||
if tok == "*" || tok == "/" {
|
||||
op = tok.chars().next()?;
|
||||
continue;
|
||||
if cur.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
factors.push((op, cur));
|
||||
|
||||
for (op, tok) in &factors {
|
||||
let v = resolve_length(tok)?;
|
||||
result = Some(match result {
|
||||
None => v,
|
||||
Some(r) if op == '/' => r / v,
|
||||
Some(r) if *op == '/' => r / v,
|
||||
Some(r) => r * v,
|
||||
});
|
||||
}
|
||||
@@ -2258,6 +2291,17 @@ fn inset_dim(value: &str) -> Option<crate::Dimension> {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_inset_side(style: &mut LayoutStyle, index: usize, value: &str) {
|
||||
let value = value.trim();
|
||||
if let Some(expression) = deferred_length_expression(value) {
|
||||
style.inset[index] = None;
|
||||
style.inset_expressions[index] = Some(expression);
|
||||
} else {
|
||||
style.inset[index] = inset_dim(value);
|
||||
style.inset_expressions[index] = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute keyword font-sizes (the `medium`-anchored scale), for the handful
|
||||
/// of pages that still use them.
|
||||
fn font_size_keyword(v: &str) -> Option<f32> {
|
||||
@@ -2383,7 +2427,10 @@ fn is_font_size_token(value: &str) -> bool {
|
||||
{
|
||||
return true;
|
||||
}
|
||||
["px", "pt", "em", "ex", "rem", "vw", "vh", "vmin", "vmax", "%"]
|
||||
[
|
||||
"px", "pt", "em", "ex", "rem", "vw", "vh", "dvw", "dvh", "svw",
|
||||
"svh", "lvw", "lvh", "vmin", "vmax", "%",
|
||||
]
|
||||
.iter()
|
||||
.any(|unit| lower.strip_suffix(unit).and_then(|number| number.parse::<f32>().ok()).is_some())
|
||||
}
|
||||
@@ -2410,6 +2457,22 @@ fn dimension_value(tok: &str) -> crate::Dimension {
|
||||
if let Some(v) = lower.strip_suffix("ex").and_then(parse) { return Dimension::Ex(v); }
|
||||
if let Some(v) = lower.strip_suffix("vmin").and_then(parse) { return Dimension::Vmin(v); }
|
||||
if let Some(v) = lower.strip_suffix("vmax").and_then(parse) { return Dimension::Vmax(v); }
|
||||
if let Some(v) = lower
|
||||
.strip_suffix("dvw")
|
||||
.or_else(|| lower.strip_suffix("svw"))
|
||||
.or_else(|| lower.strip_suffix("lvw"))
|
||||
.and_then(parse)
|
||||
{
|
||||
return Dimension::Vw(v);
|
||||
}
|
||||
if let Some(v) = lower
|
||||
.strip_suffix("dvh")
|
||||
.or_else(|| lower.strip_suffix("svh"))
|
||||
.or_else(|| lower.strip_suffix("lvh"))
|
||||
.and_then(parse)
|
||||
{
|
||||
return Dimension::Vh(v);
|
||||
}
|
||||
if let Some(v) = lower.strip_suffix("vw").and_then(parse) { return Dimension::Vw(v); }
|
||||
if let Some(v) = lower.strip_suffix("vh").and_then(parse) { return Dimension::Vh(v); }
|
||||
if let Some(v) = lower.strip_suffix("px").and_then(parse) { return Dimension::Px(v); }
|
||||
@@ -3407,6 +3470,19 @@ mod tests {
|
||||
),
|
||||
Some(250.0)
|
||||
);
|
||||
let grouped = resolve_contextual_length(
|
||||
"calc(clamp(128px,92px + 7vw,188px) + (100vw - 48px)*43/440)",
|
||||
context.0,
|
||||
context.1,
|
||||
context.2,
|
||||
context.3,
|
||||
context.4,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(grouped - 238.263_64).abs() < 0.001,
|
||||
"grouped calc should preserve the nested subtraction: {grouped}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -102,6 +102,45 @@ fn relative_units_resolve_against_viewport_and_font_size() {
|
||||
assert!(hit, "expected a 500x160 box from 50vw/10em, rects: {:?}", layout.rects.values().map(|r| (r.width, r.height)).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_viewport_units_use_the_live_viewport() {
|
||||
let tree = parse_html(
|
||||
r#"<body style="margin:0"><div style="width:25dvw;height:15dvh"></div></body>"#,
|
||||
);
|
||||
let layout = layout_dom(&tree, (1000.0, 800.0));
|
||||
assert!(
|
||||
layout
|
||||
.rects
|
||||
.values()
|
||||
.any(|rect| (rect.width - 250.0).abs() < 0.01
|
||||
&& (rect.height - 120.0).abs() < 0.01),
|
||||
"dvw/dvh should resolve against the capture viewport"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_inset_box_uses_viewport_not_document_height() {
|
||||
let tree = parse_html(
|
||||
r#"
|
||||
<body style="margin:0">
|
||||
<div style="height:5000px"></div>
|
||||
<div id="overlay" style="position:fixed;inset:0">
|
||||
<canvas id="surface" style="width:100%;height:100%"></canvas>
|
||||
</div>
|
||||
</body>
|
||||
"#,
|
||||
);
|
||||
let layout = layout_dom(&tree, (900.0, 1000.0));
|
||||
for id in ["overlay", "surface"] {
|
||||
let rect = layout.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
assert!(
|
||||
(rect.width - 900.0).abs() < 0.01
|
||||
&& (rect.height - 1000.0).abs() < 0.01,
|
||||
"{id} should fill the viewport rather than the 5000px document: {rect:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_padding_top_reserves_aspect_ratio_box() {
|
||||
// The responsive aspect-ratio trick: an empty box with padding-top:56.25%
|
||||
@@ -999,3 +1038,34 @@ fn length_line_height_computes_before_inheritance() {
|
||||
Some(obscura_render::LineHeight::Ratio(2.0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_right_inset_uses_root_font_tokens() {
|
||||
let tree = parse_html(
|
||||
r#"
|
||||
<style>
|
||||
:root { --spacing: .25rem }
|
||||
body { margin: 0 }
|
||||
#header { position: relative; width: 900px; height: 80px }
|
||||
#search {
|
||||
position: absolute;
|
||||
right: calc(var(--spacing)*14);
|
||||
width: 100px;
|
||||
height: 36px;
|
||||
}
|
||||
</style>
|
||||
<div id="header"><button id="search">Search</button></div>
|
||||
"#,
|
||||
);
|
||||
let layout = layout_dom(&tree, (900.0, 1000.0));
|
||||
let search = tree.get_element_by_id("search").unwrap();
|
||||
assert_eq!(
|
||||
layout.styles[&search].inset[1],
|
||||
Some(obscura_render::Dimension::Px(56.0))
|
||||
);
|
||||
let rect = layout.rects[&search];
|
||||
assert!(
|
||||
(rect.x - 744.0).abs() < 0.01,
|
||||
"right inset should place 100px box at 900 - 56 - 100: {rect:?}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user