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}