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

            
10
use 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).
19
const PX_PER_INCH: f32 = 96.0;
20
/// Centimetres per inch.
21
const CM_PER_INCH: f32 = 2.54;
22
/// Millimetres per inch.
23
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)]
32
pub 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)]
43
enum CalcFlatItem {
44
    Num(f32),
45
    Op(CalcOp),
46
}
47

            
48
/// Arithmetic operators.
49
#[derive(Clone, Copy, Debug, PartialEq)]
50
enum 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(…)`).
59
#[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
60
pub fn evaluate_calc(ctx: &CalcResolveContext, basis: f32) -> f32 {
61
    evaluate_calc_ast(ctx.items.as_slice(), basis, ctx.em_size, ctx.rem_size)
62
}
63

            
64
/// Stack-machine evaluator for a flat `CalcAstItem` slice.
65
///
66
/// `basis`    — the "100 %" reference value (e.g. containing-block width).
67
/// `em_size`  — element's computed font-size (for `em`).
68
/// `rem_size` — root element's computed font-size (for `rem`).
69
///
70
/// Two-pass approach with correct operator precedence:
71
///   Pass 1: evaluate `*` and `/`
72
///   Pass 2: evaluate `+` and `-`
73
/// Parenthesised sub-expressions are resolved recursively.
74
#[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
75
fn evaluate_calc_ast(
76
    items: &[CalcAstItem],
77
    basis: f32,
78
    em_size: f32,
79
    rem_size: f32,
80
) -> f32 {
81
    // Convert into a working vec of resolved numbers and operators.
82
    let mut flat: Vec<CalcFlatItem> = Vec::with_capacity(items.len());
83
    let mut i = 0;
84
    while i < items.len() {
85
        match &items[i] {
86
            CalcAstItem::Value(pv) => {
87
                flat.push(CalcFlatItem::Num(resolve_pixel_value(
88
                    pv, basis, em_size, rem_size,
89
                )));
90
            }
91
            CalcAstItem::Add => flat.push(CalcFlatItem::Op(CalcOp::Add)),
92
            CalcAstItem::Sub => flat.push(CalcFlatItem::Op(CalcOp::Sub)),
93
            CalcAstItem::Mul => flat.push(CalcFlatItem::Op(CalcOp::Mul)),
94
            CalcAstItem::Div => flat.push(CalcFlatItem::Op(CalcOp::Div)),
95
            CalcAstItem::BraceOpen => {
96
                // Find matching BraceClose and recurse
97
                let start = i + 1;
98
                let mut depth = 1u32;
99
                let mut j = start;
100
                while j < items.len() && depth > 0 {
101
                    match &items[j] {
102
                        CalcAstItem::BraceOpen => depth += 1,
103
                        CalcAstItem::BraceClose => depth -= 1,
104
                        _ => {}
105
                    }
106
                    if depth > 0 {
107
                        j += 1;
108
                    }
109
                }
110
                // items[start..j] is the inner sub-expression (excl. braces)
111
                let sub_val = evaluate_calc_ast(&items[start..j], basis, em_size, rem_size);
112
                flat.push(CalcFlatItem::Num(sub_val));
113
                i = j; // skip past the closing brace
114
            }
115
            CalcAstItem::BraceClose => { /* shouldn't happen at top level */ }
116
        }
117
        i += 1;
118
    }
119

            
120
    // Pass 1: resolve * and /
121
    let mut pass2: Vec<CalcFlatItem> = Vec::with_capacity(flat.len());
122
    let mut k = 0;
123
    while k < flat.len() {
124
        if let CalcFlatItem::Op(op @ (CalcOp::Mul | CalcOp::Div)) = &flat[k] {
125
            // Apply to previous Num in pass2 and next Num in flat
126
            if let (Some(CalcFlatItem::Num(lhs)), Some(CalcFlatItem::Num(rhs))) =
127
                (pass2.last(), flat.get(k + 1))
128
            {
129
                let result = match op {
130
                    CalcOp::Mul => lhs * rhs,
131
                    CalcOp::Div => {
132
                        if *rhs != 0.0 {
133
                            lhs / rhs
134
                        } else {
135
                            0.0
136
                        }
137
                    }
138
                    _ => unreachable!(),
139
                };
140
                *pass2.last_mut().unwrap() = CalcFlatItem::Num(result);
141
                k += 2; // skip operator + rhs
142
                continue;
143
            }
144
        }
145
        pass2.push(flat[k].clone());
146
        k += 1;
147
    }
148

            
149
    // Pass 2: resolve + and -
150
    let mut result = match pass2.first() {
151
        Some(CalcFlatItem::Num(v)) => *v,
152
        _ => return 0.0,
153
    };
154
    let mut m = 1;
155
    while m < pass2.len() {
156
        if let (CalcFlatItem::Op(op), Some(CalcFlatItem::Num(rhs))) =
157
            (&pass2[m], pass2.get(m + 1))
158
        {
159
            match op {
160
                CalcOp::Add => result += rhs,
161
                CalcOp::Sub => result -= rhs,
162
                _ => {} // already handled in pass 1
163
            }
164
            m += 2;
165
        } else {
166
            m += 1;
167
        }
168
    }
169

            
170
    result
171
}
172

            
173
/// Resolve a single `PixelValue` to `f32` pixels inside a `calc()` expression.
174
///
175
/// - `basis`    — the "100 %" reference (containing-block width or height)
176
/// - `em_size`  — element's computed font-size (for `em` units)
177
/// - `rem_size` — root element's computed font-size (for `rem` units)
178
131880
pub fn resolve_pixel_value(
179
131880
    pv: &PixelValue,
180
131880
    basis: f32,
181
131880
    em_size: f32,
182
131880
    rem_size: f32,
183
131880
) -> f32 {
184
131880
    match pv.metric {
185
131880
        SizeMetric::Px => pv.number.get(),
186
        SizeMetric::Pt => pv.number.get() * PT_TO_PX,
187
        SizeMetric::In => pv.number.get() * PX_PER_INCH,
188
        SizeMetric::Cm => pv.number.get() * PX_PER_INCH / CM_PER_INCH,
189
        SizeMetric::Mm => pv.number.get() * PX_PER_INCH / MM_PER_INCH,
190
        SizeMetric::Em => pv.number.get() * em_size,
191
        SizeMetric::Rem => pv.number.get() * rem_size,
192
        SizeMetric::Percent => basis * (pv.number.get() / 100.0),
193
        SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
194
            // Viewport units: fallback — proper resolution requires viewport context
195
            pv.number.get()
196
        }
197
    }
198
131880
}
199

            
200
/// Like `resolve_pixel_value`, but with proper viewport unit resolution.
201
#[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
202
pub fn resolve_pixel_value_with_viewport(
203
    pv: &PixelValue,
204
    basis: f32,
205
    em_size: f32,
206
    rem_size: f32,
207
    viewport_width: f32,
208
    viewport_height: f32,
209
) -> f32 {
210
    match pv.metric {
211
        SizeMetric::Vw => pv.number.get() / 100.0 * viewport_width,
212
        SizeMetric::Vh => pv.number.get() / 100.0 * viewport_height,
213
        SizeMetric::Vmin => pv.number.get() / 100.0 * viewport_width.min(viewport_height),
214
        SizeMetric::Vmax => pv.number.get() / 100.0 * viewport_width.max(viewport_height),
215
        _ => resolve_pixel_value(pv, basis, em_size, rem_size),
216
    }
217
}
218