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).
19pub(super) const PX_PER_INCH: f32 = 96.0;
20/// Centimetres per inch.
21pub(super) const CM_PER_INCH: f32 = 2.54;
22/// Millimetres per inch.
23pub(super) const 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// Tiny enum (Num(f32) = 4B vs Op(CalcOp) = 1B); the "large" variant is a bare
41// f32, so boxing it would add a pointer + heap allocation — strictly worse than
42// the few bytes of size disparity. Accepted.
43#[allow(variant_size_differences)]
44/// Internal intermediate representation: a number or an operator (after value resolution).
45#[derive(Clone, Debug)]
46enum CalcFlatItem {
47    Num(f32),
48    Op(CalcOp),
49}
50
51/// Arithmetic operators.
52#[derive(Clone, Copy, Debug, PartialEq)]
53enum CalcOp {
54    Add,
55    Sub,
56    Mul,
57    Div,
58}
59
60/// Evaluate a `CalcResolveContext` using the given `basis` (the "100 %" reference value,
61/// e.g. containing-block width for `width: calc(…)`).
62#[inline(never)] // M12.7: keep out of calc_used_size — its loop/jump-table inlined into the huge fn forces a remill PC-dispatch loop that mis-delivers auto_w
63#[must_use] pub fn evaluate_calc(ctx: &CalcResolveContext, basis: f32) -> f32 {
64    evaluate_calc_ast(ctx.items.as_slice(), basis, ctx.em_size, ctx.rem_size)
65}
66
67/// Stack-machine evaluator for a flat `CalcAstItem` slice.
68///
69/// `basis`    — the "100 %" reference value (e.g. containing-block width).
70/// `em_size`  — element's computed font-size (for `em`).
71/// `rem_size` — root element's computed font-size (for `rem`).
72///
73/// Two-pass approach with correct operator precedence:
74///   Pass 1: evaluate `*` and `/`
75///   Pass 2: evaluate `+` and `-`
76/// Parenthesised sub-expressions are resolved recursively.
77#[inline(never)] // M12.7: keep out of calc_used_size — its loop/jump-table inlined into the huge fn forces a remill PC-dispatch loop that mis-delivers auto_w
78fn evaluate_calc_ast(
79    items: &[CalcAstItem],
80    basis: f32,
81    em_size: f32,
82    rem_size: f32,
83) -> f32 {
84    // Convert into a working vec of resolved numbers and operators.
85    let mut flat: Vec<CalcFlatItem> = Vec::with_capacity(items.len());
86    let mut i = 0;
87    while i < items.len() {
88        match &items[i] {
89            CalcAstItem::Value(pv) => {
90                flat.push(CalcFlatItem::Num(resolve_pixel_value(
91                    pv, basis, em_size, rem_size,
92                )));
93            }
94            CalcAstItem::Add => flat.push(CalcFlatItem::Op(CalcOp::Add)),
95            CalcAstItem::Sub => flat.push(CalcFlatItem::Op(CalcOp::Sub)),
96            CalcAstItem::Mul => flat.push(CalcFlatItem::Op(CalcOp::Mul)),
97            CalcAstItem::Div => flat.push(CalcFlatItem::Op(CalcOp::Div)),
98            CalcAstItem::BraceOpen => {
99                // Find matching BraceClose and recurse
100                let start = i + 1;
101                let mut depth = 1u32;
102                let mut j = start;
103                while j < items.len() && depth > 0 {
104                    match &items[j] {
105                        CalcAstItem::BraceOpen => depth += 1,
106                        CalcAstItem::BraceClose => depth -= 1,
107                        _ => {}
108                    }
109                    if depth > 0 {
110                        j += 1;
111                    }
112                }
113                // items[start..j] is the inner sub-expression (excl. braces)
114                let sub_val = evaluate_calc_ast(&items[start..j], basis, em_size, rem_size);
115                flat.push(CalcFlatItem::Num(sub_val));
116                i = j; // skip past the closing brace
117            }
118            CalcAstItem::BraceClose => { /* shouldn't happen at top level */ }
119        }
120        i += 1;
121    }
122
123    // Pass 1: resolve * and /
124    let mut pass2: Vec<CalcFlatItem> = Vec::with_capacity(flat.len());
125    let mut k = 0;
126    while k < flat.len() {
127        if let CalcFlatItem::Op(op @ (CalcOp::Mul | CalcOp::Div)) = &flat[k] {
128            // Apply to previous Num in pass2 and next Num in flat
129            if let (Some(CalcFlatItem::Num(lhs)), Some(CalcFlatItem::Num(rhs))) =
130                (pass2.last(), flat.get(k + 1))
131            {
132                let result = match op {
133                    CalcOp::Mul => lhs * rhs,
134                    CalcOp::Div => {
135                        if *rhs == 0.0 {
136                            0.0
137                        } else {
138                            lhs / rhs
139                        }
140                    }
141                    _ => unreachable!(),
142                };
143                *pass2.last_mut().unwrap() = CalcFlatItem::Num(result);
144                k += 2; // skip operator + rhs
145                continue;
146            }
147        }
148        pass2.push(flat[k].clone());
149        k += 1;
150    }
151
152    // Pass 2: resolve + and -
153    let mut result = match pass2.first() {
154        Some(CalcFlatItem::Num(v)) => *v,
155        _ => return 0.0,
156    };
157    let mut m = 1;
158    while m < pass2.len() {
159        if let (CalcFlatItem::Op(op), Some(CalcFlatItem::Num(rhs))) =
160            (&pass2[m], pass2.get(m + 1))
161        {
162            match op {
163                CalcOp::Add => result += rhs,
164                CalcOp::Sub => result -= rhs,
165                _ => {} // already handled in pass 1
166            }
167            m += 2;
168        } else {
169            m += 1;
170        }
171    }
172
173    result
174}
175
176/// Resolve a single `PixelValue` to `f32` pixels inside a `calc()` expression.
177///
178/// - `basis`    — the "100 %" reference (containing-block width or height)
179/// - `em_size`  — element's computed font-size (for `em` units)
180/// - `rem_size` — root element's computed font-size (for `rem` units)
181///
182/// NOTE: this variant has no viewport context, so viewport units (`vw`/`vh`/
183/// `vmin`/`vmax`) fall back to their raw number (i.e. `50vw` → `50px`). Callers
184/// that may see viewport units must use [`resolve_pixel_value_with_viewport`]
185/// (or [`resolve_pixel_value_no_percent_with_viewport`]) instead.
186#[must_use] pub fn resolve_pixel_value(
187    pv: &PixelValue,
188    basis: f32,
189    em_size: f32,
190    rem_size: f32,
191) -> f32 {
192    match pv.metric {
193        SizeMetric::Px => pv.number.get(),
194        SizeMetric::Pt => pv.number.get() * PT_TO_PX,
195        SizeMetric::In => pv.number.get() * PX_PER_INCH,
196        SizeMetric::Cm => pv.number.get() * PX_PER_INCH / CM_PER_INCH,
197        SizeMetric::Mm => pv.number.get() * PX_PER_INCH / MM_PER_INCH,
198        SizeMetric::Em => pv.number.get() * em_size,
199        SizeMetric::Rem => pv.number.get() * rem_size,
200        SizeMetric::Percent => basis * (pv.number.get() / 100.0),
201        SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
202            // Viewport units: fallback — proper resolution requires viewport context
203            pv.number.get()
204        }
205    }
206}
207
208/// Like `resolve_pixel_value`, but with proper viewport unit resolution.
209#[inline(never)] // M12.7: keep out of calc_used_size — its loop/jump-table inlined into the huge fn forces a remill PC-dispatch loop that mis-delivers auto_w
210#[must_use] pub fn resolve_pixel_value_with_viewport(
211    pv: &PixelValue,
212    basis: f32,
213    em_size: f32,
214    rem_size: f32,
215    viewport_width: f32,
216    viewport_height: f32,
217) -> f32 {
218    match pv.metric {
219        SizeMetric::Vw => pv.number.get() / 100.0 * viewport_width,
220        SizeMetric::Vh => pv.number.get() / 100.0 * viewport_height,
221        SizeMetric::Vmin => pv.number.get() / 100.0 * viewport_width.min(viewport_height),
222        SizeMetric::Vmax => pv.number.get() / 100.0 * viewport_width.max(viewport_height),
223        _ => resolve_pixel_value(pv, basis, em_size, rem_size),
224    }
225}
226
227/// Resolve a `PixelValue` to pixels, returning `None` for percentage and viewport units.
228#[must_use] pub fn resolve_pixel_value_no_percent(
229    pv: &PixelValue,
230    em_size: f32,
231    rem_size: f32,
232) -> Option<f32> {
233    match pv.metric {
234        SizeMetric::Px => Some(pv.number.get()),
235        SizeMetric::Pt => Some(pv.number.get() * PT_TO_PX),
236        SizeMetric::In => Some(pv.number.get() * PX_PER_INCH),
237        SizeMetric::Cm => Some(pv.number.get() * PX_PER_INCH / CM_PER_INCH),
238        SizeMetric::Mm => Some(pv.number.get() * PX_PER_INCH / MM_PER_INCH),
239        SizeMetric::Em => Some(pv.number.get() * em_size),
240        SizeMetric::Rem => Some(pv.number.get() * rem_size),
241        SizeMetric::Percent
242        | SizeMetric::Vw
243        | SizeMetric::Vh
244        | SizeMetric::Vmin
245        | SizeMetric::Vmax => None,
246    }
247}
248
249/// Like `resolve_pixel_value_no_percent`, but resolves viewport units using
250/// the provided viewport dimensions. Returns `None` only for percentages.
251#[must_use] pub fn resolve_pixel_value_no_percent_with_viewport(
252    pv: &PixelValue,
253    em_size: f32,
254    rem_size: f32,
255    viewport_width: f32,
256    viewport_height: f32,
257) -> Option<f32> {
258    match pv.metric {
259        SizeMetric::Vw => Some(pv.number.get() / 100.0 * viewport_width),
260        SizeMetric::Vh => Some(pv.number.get() / 100.0 * viewport_height),
261        SizeMetric::Vmin => Some(pv.number.get() / 100.0 * viewport_width.min(viewport_height)),
262        SizeMetric::Vmax => Some(pv.number.get() / 100.0 * viewport_width.max(viewport_height)),
263        _ => resolve_pixel_value_no_percent(pv, em_size, rem_size),
264    }
265}
266