Skip to main content

azul_layout/solver3/
calc.rs

1//! CSS `calc()` expression evaluator.
2//!
3//! This module implements a two-pass stack-machine evaluator for `calc()` expressions.
4//! It resolves `CalcAstItem` slices (flat, parenthesised AST) into a single `f32` pixel value.
5//!
6//! **Resolution context**: Em/rem units are resolved using per-node font sizes that are
7//! captured lazily during style translation and stored alongside the AST pointer passed
8//! to taffy. Percentages use the `basis` value provided by taffy (container width/height).
9
10use azul_css::props::{
11    basic::{
12        pixel::PT_TO_PX,
13        PixelValue, SizeMetric,
14    },
15    layout::dimensions::{CalcAstItem, CalcAstItemVec},
16};
17
18/// CSS reference pixels per inch (96 px/in per CSS spec).
19const PX_PER_INCH: f32 = 96.0;
20/// Centimetres per inch.
21const CM_PER_INCH: f32 = 2.54;
22/// Millimetres per inch.
23const MM_PER_INCH: f32 = 25.4;
24
25/// Font-size context captured at style-translation time and stored alongside the calc AST.
26///
27/// Taffy's `resolve_calc_value` callback only receives `(*const (), f32)` — no node id.
28/// We therefore bundle the per-node font sizes into the heap-pinned data that the opaque
29/// pointer references, so the evaluator can resolve `em` / `rem` correctly.
30#[derive(Debug, Clone)]
31#[repr(C)]
32pub struct CalcResolveContext {
33    /// The calc AST items (flat stack-machine representation).
34    pub items: CalcAstItemVec,
35    /// Element's computed `font-size` in px — used for `em` resolution.
36    pub em_size: f32,
37    /// Root element's computed `font-size` in px — used for `rem` resolution.
38    pub rem_size: f32,
39}
40
41/// Internal intermediate representation: a number or an operator (after value resolution).
42#[derive(Clone, Debug)]
43enum CalcFlatItem {
44    Num(f32),
45    Op(CalcOp),
46}
47
48/// Arithmetic operators.
49#[derive(Clone, Copy, Debug, PartialEq)]
50enum CalcOp {
51    Add,
52    Sub,
53    Mul,
54    Div,
55}
56
57/// Evaluate a `CalcResolveContext` using the given `basis` (the "100 %" reference value,
58/// e.g. containing-block width for `width: calc(…)`).
59pub fn evaluate_calc(ctx: &CalcResolveContext, basis: f32) -> f32 {
60    evaluate_calc_ast(ctx.items.as_slice(), basis, ctx.em_size, ctx.rem_size)
61}
62
63/// Stack-machine evaluator for a flat `CalcAstItem` slice.
64///
65/// `basis`    — the "100 %" reference value (e.g. containing-block width).
66/// `em_size`  — element's computed font-size (for `em`).
67/// `rem_size` — root element's computed font-size (for `rem`).
68///
69/// Two-pass approach with correct operator precedence:
70///   Pass 1: evaluate `*` and `/`
71///   Pass 2: evaluate `+` and `-`
72/// Parenthesised sub-expressions are resolved recursively.
73fn evaluate_calc_ast(
74    items: &[CalcAstItem],
75    basis: f32,
76    em_size: f32,
77    rem_size: f32,
78) -> f32 {
79    // Convert into a working vec of resolved numbers and operators.
80    let mut flat: Vec<CalcFlatItem> = Vec::with_capacity(items.len());
81    let mut i = 0;
82    while i < items.len() {
83        match &items[i] {
84            CalcAstItem::Value(pv) => {
85                flat.push(CalcFlatItem::Num(resolve_pixel_value(
86                    pv, basis, em_size, rem_size,
87                )));
88            }
89            CalcAstItem::Add => flat.push(CalcFlatItem::Op(CalcOp::Add)),
90            CalcAstItem::Sub => flat.push(CalcFlatItem::Op(CalcOp::Sub)),
91            CalcAstItem::Mul => flat.push(CalcFlatItem::Op(CalcOp::Mul)),
92            CalcAstItem::Div => flat.push(CalcFlatItem::Op(CalcOp::Div)),
93            CalcAstItem::BraceOpen => {
94                // Find matching BraceClose and recurse
95                let start = i + 1;
96                let mut depth = 1u32;
97                let mut j = start;
98                while j < items.len() && depth > 0 {
99                    match &items[j] {
100                        CalcAstItem::BraceOpen => depth += 1,
101                        CalcAstItem::BraceClose => depth -= 1,
102                        _ => {}
103                    }
104                    if depth > 0 {
105                        j += 1;
106                    }
107                }
108                // items[start..j] is the inner sub-expression (excl. braces)
109                let sub_val = evaluate_calc_ast(&items[start..j], basis, em_size, rem_size);
110                flat.push(CalcFlatItem::Num(sub_val));
111                i = j; // skip past the closing brace
112            }
113            CalcAstItem::BraceClose => { /* shouldn't happen at top level */ }
114        }
115        i += 1;
116    }
117
118    // Pass 1: resolve * and /
119    let mut pass2: Vec<CalcFlatItem> = Vec::with_capacity(flat.len());
120    let mut k = 0;
121    while k < flat.len() {
122        if let CalcFlatItem::Op(op @ (CalcOp::Mul | CalcOp::Div)) = &flat[k] {
123            // Apply to previous Num in pass2 and next Num in flat
124            if let (Some(CalcFlatItem::Num(lhs)), Some(CalcFlatItem::Num(rhs))) =
125                (pass2.last(), flat.get(k + 1))
126            {
127                let result = match op {
128                    CalcOp::Mul => lhs * rhs,
129                    CalcOp::Div => {
130                        if *rhs != 0.0 {
131                            lhs / rhs
132                        } else {
133                            0.0
134                        }
135                    }
136                    _ => unreachable!(),
137                };
138                *pass2.last_mut().unwrap() = CalcFlatItem::Num(result);
139                k += 2; // skip operator + rhs
140                continue;
141            }
142        }
143        pass2.push(flat[k].clone());
144        k += 1;
145    }
146
147    // Pass 2: resolve + and -
148    let mut result = match pass2.first() {
149        Some(CalcFlatItem::Num(v)) => *v,
150        _ => return 0.0,
151    };
152    let mut m = 1;
153    while m < pass2.len() {
154        if let (CalcFlatItem::Op(op), Some(CalcFlatItem::Num(rhs))) =
155            (&pass2[m], pass2.get(m + 1))
156        {
157            match op {
158                CalcOp::Add => result += rhs,
159                CalcOp::Sub => result -= rhs,
160                _ => {} // already handled in pass 1
161            }
162            m += 2;
163        } else {
164            m += 1;
165        }
166    }
167
168    result
169}
170
171/// Resolve a single `PixelValue` to `f32` pixels inside a `calc()` expression.
172///
173/// - `basis`    — the "100 %" reference (containing-block width or height)
174/// - `em_size`  — element's computed font-size (for `em` units)
175/// - `rem_size` — root element's computed font-size (for `rem` units)
176pub fn resolve_pixel_value(
177    pv: &PixelValue,
178    basis: f32,
179    em_size: f32,
180    rem_size: f32,
181) -> f32 {
182    match pv.metric {
183        SizeMetric::Px => pv.number.get(),
184        SizeMetric::Pt => pv.number.get() * PT_TO_PX,
185        SizeMetric::In => pv.number.get() * PX_PER_INCH,
186        SizeMetric::Cm => pv.number.get() * PX_PER_INCH / CM_PER_INCH,
187        SizeMetric::Mm => pv.number.get() * PX_PER_INCH / MM_PER_INCH,
188        SizeMetric::Em => pv.number.get() * em_size,
189        SizeMetric::Rem => pv.number.get() * rem_size,
190        SizeMetric::Percent => basis * (pv.number.get() / 100.0),
191        SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
192            // Viewport units: fallback — proper resolution requires viewport context
193            pv.number.get()
194        }
195    }
196}
197
198/// Like `resolve_pixel_value`, but with proper viewport unit resolution.
199pub fn resolve_pixel_value_with_viewport(
200    pv: &PixelValue,
201    basis: f32,
202    em_size: f32,
203    rem_size: f32,
204    viewport_width: f32,
205    viewport_height: f32,
206) -> f32 {
207    match pv.metric {
208        SizeMetric::Vw => pv.number.get() / 100.0 * viewport_width,
209        SizeMetric::Vh => pv.number.get() / 100.0 * viewport_height,
210        SizeMetric::Vmin => pv.number.get() / 100.0 * viewport_width.min(viewport_height),
211        SizeMetric::Vmax => pv.number.get() / 100.0 * viewport_width.max(viewport_height),
212        _ => resolve_pixel_value(pv, basis, em_size, rem_size),
213    }
214}
215