Skip to main content

lumen_engine/expr/
builtins.rs

1use std::sync::{Mutex, OnceLock};
2
3use crate::{
4    error::{ExpressionError, LumenError},
5    expr::{
6        ExpressionContext,
7        ast::{BuiltinFn, ExprNode, ExpressionValue},
8        eval::evaluate_expr,
9    },
10    node::{NodeId, NodeKind, PropertyValue},
11};
12
13pub fn evaluate_builtin(
14    builtin: BuiltinFn,
15    args: &[ExpressionValue],
16    ctx: &ExpressionContext<'_>,
17) -> crate::Result<ExpressionValue> {
18    let error = |details: String| {
19        LumenError::Expression(ExpressionError::Evaluate {
20            path: ctx.path.clone(),
21            details,
22        })
23    };
24    let expect_len = |expected: usize| {
25        if args.len() == expected {
26            Ok(())
27        } else {
28            Err(error(format!(
29                "builtin expects {expected} arguments, got {}",
30                args.len()
31            )))
32        }
33    };
34    let expect_min_len = |minimum: usize| {
35        if args.len() >= minimum {
36            Ok(())
37        } else {
38            Err(error(format!(
39                "builtin expects at least {minimum} arguments, got {}",
40                args.len()
41            )))
42        }
43    };
44
45    match builtin {
46        BuiltinFn::Min => {
47            expect_min_len(2)?;
48            let mut value =
49                to_number(&args[0], error("min expects numeric arguments".to_string()))?;
50            for arg in &args[1..] {
51                value = value.min(to_number(
52                    arg,
53                    error("min expects numeric arguments".to_string()),
54                )?);
55            }
56            Ok(ExpressionValue::Number(value))
57        }
58        BuiltinFn::Max => {
59            expect_min_len(2)?;
60            let mut value =
61                to_number(&args[0], error("max expects numeric arguments".to_string()))?;
62            for arg in &args[1..] {
63                value = value.max(to_number(
64                    arg,
65                    error("max expects numeric arguments".to_string()),
66                )?);
67            }
68            Ok(ExpressionValue::Number(value))
69        }
70        BuiltinFn::Abs => unary_numeric(args, &expect_len, |value| value.abs()),
71        BuiltinFn::Floor => unary_numeric(args, &expect_len, |value| value.floor()),
72        BuiltinFn::Ceil => unary_numeric(args, &expect_len, |value| value.ceil()),
73        BuiltinFn::Round => unary_numeric(args, &expect_len, |value| value.round()),
74        BuiltinFn::Sin => unary_numeric(args, &expect_len, |value| value.sin()),
75        BuiltinFn::Cos => unary_numeric(args, &expect_len, |value| value.cos()),
76        BuiltinFn::Fract => unary_numeric(args, &expect_len, |value| value.fract()),
77        BuiltinFn::Clamp => {
78            expect_len(3)?;
79            let value = to_number(
80                &args[0],
81                error("clamp expects numeric arguments".to_string()),
82            )?;
83            let min = to_number(
84                &args[1],
85                error("clamp expects numeric arguments".to_string()),
86            )?;
87            let max = to_number(
88                &args[2],
89                error("clamp expects numeric arguments".to_string()),
90            )?;
91            if min > max {
92                return Err(error("clamp requires min <= max".to_string()));
93            }
94            Ok(ExpressionValue::Number(value.clamp(min, max)))
95        }
96        BuiltinFn::Lerp => interpolate_linear(args, &expect_len, &error, "lerp"),
97        BuiltinFn::Pow => {
98            expect_len(2)?;
99            let lhs = to_number(&args[0], error("pow expects numeric arguments".to_string()))?;
100            let rhs = to_number(&args[1], error("pow expects numeric arguments".to_string()))?;
101            Ok(ExpressionValue::Number(lhs.powf(rhs)))
102        }
103        BuiltinFn::Mod => {
104            expect_len(2)?;
105            let lhs = to_number(&args[0], error("mod expects numeric arguments".to_string()))?;
106            let rhs = to_number(&args[1], error("mod expects numeric arguments".to_string()))?;
107            if rhs.abs() <= f64::EPSILON {
108                return Err(error("mod divisor must be non-zero".to_string()));
109            }
110            Ok(ExpressionValue::Number(lhs % rhs))
111        }
112        BuiltinFn::Smoothstep => {
113            expect_len(3)?;
114            let edge0 = to_number(
115                &args[0],
116                error("smoothstep expects numeric arguments".to_string()),
117            )?;
118            let edge1 = to_number(
119                &args[1],
120                error("smoothstep expects numeric arguments".to_string()),
121            )?;
122            let x = to_number(
123                &args[2],
124                error("smoothstep expects numeric arguments".to_string()),
125            )?;
126            if (edge1 - edge0).abs() <= f64::EPSILON {
127                return Ok(ExpressionValue::Number(0.0));
128            }
129            let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
130            Ok(ExpressionValue::Number(t * t * (3.0 - 2.0 * t)))
131        }
132        BuiltinFn::Linear => interpolate_linear(args, &expect_len, &error, "linear"),
133        BuiltinFn::Step => {
134            expect_len(3)?;
135            let start = to_number(
136                &args[0],
137                error("step expects numeric arguments".to_string()),
138            )?;
139            let end = to_number(
140                &args[1],
141                error("step expects numeric arguments".to_string()),
142            )?;
143            let t = to_number(
144                &args[2],
145                error("step expects numeric arguments".to_string()),
146            )?;
147            Ok(ExpressionValue::Number(if t >= 1.0 { end } else { start }))
148        }
149        BuiltinFn::TextHeight => Err(error(
150            "text_height node references should be evaluated before calling this helper"
151                .to_string(),
152        )),
153        BuiltinFn::TextWidth => Err(error(
154            "text_width node references should be evaluated before calling this helper".to_string(),
155        )),
156        BuiltinFn::Uppercase => {
157            expect_len(1)?;
158            let input = to_string(
159                &args[0],
160                error("uppercase expects a string argument".to_string()),
161            )?;
162            Ok(ExpressionValue::String(input.to_uppercase()))
163        }
164        BuiltinFn::Lowercase => {
165            expect_len(1)?;
166            let input = to_string(
167                &args[0],
168                error("lowercase expects a string argument".to_string()),
169            )?;
170            Ok(ExpressionValue::String(input.to_lowercase()))
171        }
172    }
173}
174
175fn interpolate_linear(
176    args: &[ExpressionValue],
177    expect_len: &dyn Fn(usize) -> crate::Result<()>,
178    error: &dyn Fn(String) -> LumenError,
179    builtin_name: &'static str,
180) -> crate::Result<ExpressionValue> {
181    expect_len(3)?;
182    let start = to_number(
183        &args[0],
184        error(format!("{builtin_name} expects numeric arguments")),
185    )?;
186    let end = to_number(
187        &args[1],
188        error(format!("{builtin_name} expects numeric arguments")),
189    )?;
190    let t = to_number(
191        &args[2],
192        error(format!("{builtin_name} expects numeric arguments")),
193    )?;
194    Ok(ExpressionValue::Number(start + (end - start) * t))
195}
196
197fn unary_numeric(
198    args: &[ExpressionValue],
199    expect_len: &dyn Fn(usize) -> crate::Result<()>,
200    f: impl Fn(f64) -> f64,
201) -> crate::Result<ExpressionValue> {
202    expect_len(1)?;
203    let value = to_number(
204        &args[0],
205        LumenError::Expression(ExpressionError::Evaluate {
206            path: None,
207            details: "builtin expects numeric argument".to_string(),
208        }),
209    )?;
210    Ok(ExpressionValue::Number(f(value)))
211}
212
213fn to_number(value: &ExpressionValue, error: LumenError) -> crate::Result<f64> {
214    match value {
215        ExpressionValue::Number(number) => Ok(*number),
216        ExpressionValue::Boolean(boolean) => Ok(if *boolean { 1.0 } else { 0.0 }),
217        ExpressionValue::String(text) => text.parse::<f64>().map_err(|_| error),
218    }
219}
220
221fn to_string(value: &ExpressionValue, _error: LumenError) -> crate::Result<String> {
222    match value {
223        ExpressionValue::String(text) => Ok(text.clone()),
224        ExpressionValue::Number(number) => Ok(number.to_string()),
225        ExpressionValue::Boolean(boolean) => Ok(boolean.to_string()),
226    }
227}
228
229pub fn evaluate_text_measure_builtin(
230    builtin: BuiltinFn,
231    args: &[ExprNode],
232    ctx: &ExpressionContext<'_>,
233) -> crate::Result<ExpressionValue> {
234    let builtin_name = match builtin {
235        BuiltinFn::TextHeight => "text_height",
236        BuiltinFn::TextWidth => "text_width",
237        _ => unreachable!(),
238    };
239    if args.is_empty() || args.len() > 4 {
240        return Err(text_measure_error(
241            ctx,
242            format!("{builtin_name} expects 1 to 4 arguments"),
243        ));
244    }
245
246    if let Some(ExprNode::Node(node_id)) = args.first() {
247        if args.len() != 1 {
248            return Err(text_measure_error(
249                ctx,
250                format!("{builtin_name}(node(id)) does not accept override arguments"),
251            ));
252        }
253        let measurement = measure_text_node(*node_id, ctx)?;
254        return Ok(ExpressionValue::Number(match builtin {
255            BuiltinFn::TextHeight => f64::from(measurement.height),
256            BuiltinFn::TextWidth => f64::from(measurement.width),
257            _ => unreachable!(),
258        }));
259    }
260
261    let text = args
262        .first()
263        .map(|arg| {
264            to_string(
265                &evaluate_expr(arg, ctx)?,
266                text_measure_error(ctx, format!("{builtin_name} expects a string argument")),
267            )
268        })
269        .transpose()?
270        .unwrap_or_default();
271    let font_size = args
272        .get(1)
273        .map(|arg| {
274            to_number(
275                &evaluate_expr(arg, ctx)?,
276                text_measure_error(
277                    ctx,
278                    format!("{builtin_name} optional second arg must be numeric font size"),
279                ),
280            )
281        })
282        .transpose()?
283        .unwrap_or(16.0);
284    let max_width = args
285        .get(2)
286        .map(|arg| {
287            to_number(
288                &evaluate_expr(arg, ctx)?,
289                text_measure_error(
290                    ctx,
291                    format!("{builtin_name} optional third arg must be numeric max width"),
292                ),
293            )
294        })
295        .transpose()?
296        .unwrap_or(0.0);
297    let font_family = args
298        .get(3)
299        .map(|arg| {
300            to_string(
301                &evaluate_expr(arg, ctx)?,
302                text_measure_error(
303                    ctx,
304                    format!("{builtin_name} optional fourth arg must be a font family string"),
305                ),
306            )
307        })
308        .transpose()?
309        .unwrap_or_else(|| "sans-serif".to_string());
310    let measurement = measure_text_value(&text, &font_family, font_size as f32, max_width as f32)?;
311    Ok(ExpressionValue::Number(match builtin {
312        BuiltinFn::TextHeight => f64::from(measurement.height),
313        BuiltinFn::TextWidth => f64::from(measurement.width),
314        _ => unreachable!(),
315    }))
316}
317
318fn measure_text_node(
319    node_id: NodeId,
320    ctx: &ExpressionContext<'_>,
321) -> crate::Result<lumen_text::TextMeasurement> {
322    let graph = ctx.graph.ok_or_else(|| {
323        text_measure_error(
324            ctx,
325            format!("no graph available to resolve text node `{}`", node_id.0),
326        )
327    })?;
328    let node = graph
329        .nodes
330        .get(&node_id)
331        .ok_or_else(|| text_measure_error(ctx, format!("text node `{}` not found", node_id.0)))?;
332    let NodeKind::Text(text) = node else {
333        return Err(text_measure_error(
334            ctx,
335            format!("node `{}` is not a text node", node_id.0),
336        ));
337    };
338
339    let content = resolve_string(
340        node_id,
341        "content",
342        &text.params.content.to_property_value(),
343        ctx,
344    )?;
345    let font_family = resolve_string(
346        node_id,
347        "font_family",
348        &text.params.font_family.to_property_value(),
349        ctx,
350    )?;
351    let font_size = resolve_number(
352        node_id,
353        "font_size",
354        &text.params.font_size.to_property_value(),
355        ctx,
356    )? as f32;
357    let max_width = resolve_number(
358        node_id,
359        "max_width",
360        &text.params.max_width.to_property_value(),
361        ctx,
362    )? as f32;
363    measure_text_value(&content, &font_family, font_size, max_width)
364}
365
366fn measure_text_value(
367    content: &str,
368    font_family: &str,
369    font_size: f32,
370    max_width: f32,
371) -> crate::Result<lumen_text::TextMeasurement> {
372    static TEXT_SYSTEM: OnceLock<Mutex<lumen_text::TextSystem>> = OnceLock::new();
373    let mut text_system = TEXT_SYSTEM
374        .get_or_init(|| Mutex::new(lumen_text::TextSystem::new()))
375        .lock()
376        .map_err(|_| {
377            LumenError::Expression(ExpressionError::Evaluate {
378                path: None,
379                details: "text system lock was poisoned".to_string(),
380            })
381        })?;
382    let mut request = lumen_text::TextLayoutRequest::new(content);
383    request.font_family = font_family.to_string();
384    request.font_size = font_size;
385    request.max_width = (max_width > 0.0).then_some(max_width);
386    Ok(text_system.measure(&request))
387}
388
389fn resolve_string(
390    node_id: NodeId,
391    property: &str,
392    value: &PropertyValue,
393    ctx: &ExpressionContext<'_>,
394) -> crate::Result<String> {
395    match value {
396        PropertyValue::String(text) => Ok(text.clone()),
397        _ => Err(text_measure_error(
398            ctx,
399            format!(
400                "text node `{}` property `{property}` must be a string",
401                node_id.0
402            ),
403        )),
404    }
405}
406
407fn resolve_number(
408    node_id: NodeId,
409    property: &str,
410    value: &PropertyValue,
411    ctx: &ExpressionContext<'_>,
412) -> crate::Result<f64> {
413    match value {
414        PropertyValue::Float(number) => Ok(*number),
415        PropertyValue::Int(number) => Ok(*number as f64),
416        _ => Err(text_measure_error(
417            ctx,
418            format!(
419                "text node `{}` property `{property}` must be numeric",
420                node_id.0
421            ),
422        )),
423    }
424}
425
426fn text_measure_error(ctx: &ExpressionContext<'_>, details: String) -> LumenError {
427    LumenError::Expression(ExpressionError::Evaluate {
428        path: ctx.path.clone(),
429        details,
430    })
431}