Skip to main content

ferroui_base/media/transformation/
transform_parser.rs

1use crate::media::transformation::{TransformOperations, TransformOperationsBuilder};
2use crate::utilities::span_helpers::{try_parse_double, write_double, NumberStyles};
3use crate::utilities::{FormatError, MathUtilities};
4use crate::Matrix;
5use std::rc::Rc;
6
7/// Parses transform strings such as `translate(10px, 5px) rotate(45deg)`.
8pub struct TransformParser;
9
10const FUNCTION_MAPPING: [(&str, TransformFunction); 11] = [
11    ("translate", TransformFunction::Translate),
12    ("translateX", TransformFunction::TranslateX),
13    ("translateY", TransformFunction::TranslateY),
14    ("scale", TransformFunction::Scale),
15    ("scaleX", TransformFunction::ScaleX),
16    ("scaleY", TransformFunction::ScaleY),
17    ("skew", TransformFunction::Skew),
18    ("skewX", TransformFunction::SkewX),
19    ("skewY", TransformFunction::SkewY),
20    ("rotate", TransformFunction::Rotate),
21    ("matrix", TransformFunction::Matrix),
22];
23
24const UNIT_MAPPING: [(&str, Unit); 5] = [
25    ("deg", Unit::Degree),
26    ("grad", Unit::Gradian),
27    ("rad", Unit::Radian),
28    ("turn", Unit::Turn),
29    ("px", Unit::Pixel),
30];
31
32impl TransformParser {
33    pub fn parse(s: &str) -> Result<Rc<TransformOperations>, FormatError> {
34        let invalid_format = || FormatError::from_string(format!("Invalid transform string: '{s}'."));
35
36        if s.is_empty() {
37            return Err(invalid_format());
38        }
39
40        let mut span = s.trim();
41
42        if span.eq_ignore_ascii_case("none") {
43            return Ok(TransformOperations::identity());
44        }
45
46        let mut builder = TransformOperations::create_builder(0);
47
48        loop {
49            let begin_index = span.find('(');
50            let end_index = span.find(')');
51
52            let (Some(begin_index), Some(end_index)) = (begin_index, end_index) else {
53                return Err(invalid_format());
54            };
55            if end_index < begin_index {
56                return Err(invalid_format());
57            }
58
59            let name_part = span[..begin_index].trim();
60
61            let function = parse_transform_function(name_part);
62
63            if function == TransformFunction::Invalid {
64                return Err(invalid_format());
65            }
66
67            let value_part = span[begin_index + 1..end_index].trim();
68
69            parse_function(value_part, function, &mut builder)?;
70
71            span = &span[end_index + 1..];
72
73            if span.chars().all(char::is_whitespace) {
74                break;
75            }
76        }
77
78        Ok(builder.build())
79    }
80}
81
82fn parse_value(mut part: &str) -> Result<UnitValue, FormatError> {
83    let mut unit_index = None;
84
85    for (i, c) in part.char_indices() {
86        if c.is_ascii_digit() || c == '-' || c == '.' {
87            continue;
88        }
89
90        unit_index = Some(i);
91        break;
92    }
93
94    let mut unit = Unit::None;
95
96    if let Some(unit_index) = unit_index {
97        let unit_part = &part[unit_index..];
98
99        unit = parse_unit(unit_part)?;
100
101        part = &part[..unit_index];
102    }
103
104    let value = try_parse_double(part, NumberStyles::FLOAT).ok_or_else(|| FormatError::invalid_input(part))?;
105
106    Ok(UnitValue { unit, value })
107}
108
109fn parse_value_pair(part: &str, left_value: &mut UnitValue, right_value: &mut UnitValue) -> Result<usize, FormatError> {
110    if let Some(comma_index) = part.find(',') {
111        let left_part = part[..comma_index].trim();
112        let right_part = part[comma_index + 1..].trim();
113
114        *left_value = parse_value(left_part)?;
115        *right_value = parse_value(right_part)?;
116
117        return Ok(2);
118    }
119
120    *left_value = parse_value(part)?;
121
122    Ok(1)
123}
124
125fn parse_comma_delimited_values(mut part: &str, out_values: &mut [UnitValue]) -> Result<usize, FormatError> {
126    let mut value_index = 0;
127
128    loop {
129        if value_index >= out_values.len() {
130            return Err(FormatError::new("Too many provided values."));
131        }
132
133        let Some(comma_index) = part.find(',') else {
134            if !part.chars().all(char::is_whitespace) {
135                out_values[value_index] = parse_value(part)?;
136                value_index += 1;
137            }
138
139            break;
140        };
141
142        let value_part = part[..comma_index].trim();
143
144        out_values[value_index] = parse_value(value_part)?;
145        value_index += 1;
146
147        part = &part[comma_index + 1..];
148    }
149
150    Ok(value_index)
151}
152
153fn parse_function(
154    function_part: &str,
155    function: TransformFunction,
156    builder: &mut TransformOperationsBuilder,
157) -> Result<(), FormatError> {
158    match function {
159        TransformFunction::Scale | TransformFunction::ScaleX | TransformFunction::ScaleY => {
160            let mut scale_x = UnitValue::ONE;
161            let mut scale_y = UnitValue::ONE;
162
163            let count = parse_value_pair(function_part, &mut scale_x, &mut scale_y)?;
164
165            if count != 1 && (function == TransformFunction::ScaleX || function == TransformFunction::ScaleY) {
166                return Err(format_invalid_value_count(function, 1));
167            }
168
169            verify_zero_or_unit(function, &scale_x, Unit::None)?;
170            verify_zero_or_unit(function, &scale_y, Unit::None)?;
171
172            if function == TransformFunction::ScaleY {
173                scale_y = scale_x;
174                scale_x = UnitValue::ONE;
175            } else if function == TransformFunction::Scale && count == 1 {
176                scale_y = scale_x;
177            }
178
179            builder.append_scale(scale_x.value, scale_y.value);
180        }
181        TransformFunction::Skew | TransformFunction::SkewX | TransformFunction::SkewY => {
182            let mut skew_x = UnitValue::ZERO;
183            let mut skew_y = UnitValue::ZERO;
184
185            let count = parse_value_pair(function_part, &mut skew_x, &mut skew_y)?;
186
187            if count != 1 && (function == TransformFunction::SkewX || function == TransformFunction::SkewY) {
188                return Err(format_invalid_value_count(function, 1));
189            }
190
191            verify_zero_or_angle(function, &skew_x)?;
192            verify_zero_or_angle(function, &skew_y)?;
193
194            if function == TransformFunction::SkewY {
195                skew_y = skew_x;
196                skew_x = UnitValue::ZERO;
197            }
198
199            builder.append_skew(to_radians(&skew_x), to_radians(&skew_y));
200        }
201        TransformFunction::Rotate => {
202            let mut angle = UnitValue::ZERO;
203            let mut unused = UnitValue::ZERO;
204
205            let count = parse_value_pair(function_part, &mut angle, &mut unused)?;
206
207            if count != 1 {
208                return Err(format_invalid_value_count(function, 1));
209            }
210
211            verify_zero_or_angle(function, &angle)?;
212
213            builder.append_rotate(to_radians(&angle));
214        }
215        TransformFunction::Translate | TransformFunction::TranslateX | TransformFunction::TranslateY => {
216            let mut translate_x = UnitValue::ZERO;
217            let mut translate_y = UnitValue::ZERO;
218
219            let count = parse_value_pair(function_part, &mut translate_x, &mut translate_y)?;
220
221            if count != 1 && (function == TransformFunction::TranslateX || function == TransformFunction::TranslateY)
222            {
223                return Err(format_invalid_value_count(function, 1));
224            }
225
226            verify_zero_or_unit(function, &translate_x, Unit::Pixel)?;
227            verify_zero_or_unit(function, &translate_y, Unit::Pixel)?;
228
229            if function == TransformFunction::TranslateY {
230                translate_y = translate_x;
231                translate_x = UnitValue::ZERO;
232            }
233
234            builder.append_translate(translate_x.value, translate_y.value);
235        }
236        TransformFunction::Matrix => {
237            let mut values = [UnitValue::ZERO; 6];
238
239            let count = parse_comma_delimited_values(function_part, &mut values)?;
240
241            if count != 6 {
242                return Err(format_invalid_value_count(function, 6));
243            }
244
245            for value in &values {
246                verify_zero_or_unit(function, value, Unit::None)?;
247            }
248
249            let matrix = Matrix::new(
250                values[0].value,
251                values[1].value,
252                values[2].value,
253                values[3].value,
254                values[4].value,
255                values[5].value,
256            );
257
258            builder.append_matrix(matrix);
259        }
260        TransformFunction::Invalid => {}
261    }
262
263    Ok(())
264}
265
266fn verify_zero_or_unit(function: TransformFunction, value: &UnitValue, unit: Unit) -> Result<(), FormatError> {
267    let is_zero = value.unit == Unit::None && value.value == 0.0;
268
269    if !is_zero && value.unit != unit {
270        return Err(format_invalid_value(function, value));
271    }
272    Ok(())
273}
274
275fn verify_zero_or_angle(function: TransformFunction, value: &UnitValue) -> Result<(), FormatError> {
276    if value.value != 0.0 && !is_angle_unit(value.unit) {
277        return Err(format_invalid_value(function, value));
278    }
279    Ok(())
280}
281
282fn is_angle_unit(unit: Unit) -> bool {
283    matches!(unit, Unit::Radian | Unit::Gradian | Unit::Degree | Unit::Turn)
284}
285
286fn format_invalid_value(function: TransformFunction, value: &UnitValue) -> FormatError {
287    let unit_string = if value.unit == Unit::None { String::new() } else { format!("{:?}", value.unit) };
288    let mut number = String::new();
289    let _ = write_double(&mut number, value.value);
290
291    FormatError::from_string(format!("Invalid value {number} {unit_string} for {function:?}"))
292}
293
294fn format_invalid_value_count(function: TransformFunction, count: usize) -> FormatError {
295    FormatError::from_string(format!("Invalid format. {function:?} expects {count} value(s)."))
296}
297
298fn parse_unit(part: &str) -> Result<Unit, FormatError> {
299    for (name, unit) in UNIT_MAPPING {
300        if part.eq_ignore_ascii_case(name) {
301            return Ok(unit);
302        }
303    }
304
305    Err(FormatError::from_string(format!("Invalid unit: {part}")))
306}
307
308fn parse_transform_function(part: &str) -> TransformFunction {
309    for (name, transform_function) in FUNCTION_MAPPING {
310        if part.eq_ignore_ascii_case(name) {
311            return transform_function;
312        }
313    }
314
315    TransformFunction::Invalid
316}
317
318fn to_radians(value: &UnitValue) -> f64 {
319    match value.unit {
320        Unit::Radian => value.value,
321        Unit::Gradian => MathUtilities::grad2rad(value.value),
322        Unit::Degree => MathUtilities::deg2rad(value.value),
323        Unit::Turn => MathUtilities::turn2rad(value.value),
324        _ => value.value,
325    }
326}
327
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
329enum Unit {
330    None,
331    Pixel,
332    Radian,
333    Gradian,
334    Degree,
335    Turn,
336}
337
338#[derive(Clone, Copy, Debug)]
339struct UnitValue {
340    unit: Unit,
341    value: f64,
342}
343
344impl UnitValue {
345    const ZERO: UnitValue = UnitValue { unit: Unit::None, value: 0.0 };
346    const ONE: UnitValue = UnitValue { unit: Unit::None, value: 1.0 };
347}
348
349#[derive(Clone, Copy, Debug, PartialEq, Eq)]
350enum TransformFunction {
351    Invalid,
352    Translate,
353    TranslateX,
354    TranslateY,
355    Scale,
356    ScaleX,
357    ScaleY,
358    Skew,
359    SkewX,
360    SkewY,
361    Rotate,
362    Matrix,
363}