Skip to main content

ply_engine/
text_styling.rs

1use rustc_hash::FxHashMap;
2use std::f32::consts::PI;
3use crate::color::Color;
4
5#[derive(Debug, Clone)]
6pub struct StyledSegment {
7    pub text: String,
8    pub styles: Vec<String>,
9}
10
11#[derive(Debug, Clone, Copy)]
12pub struct Transform {
13    pub x: f32, 
14    pub y: f32,
15    pub scale_x: f32, 
16    pub scale_y: f32,
17    pub rotation: f32,
18}
19
20impl Default for Transform {
21    fn default() -> Self { 
22        Self { 
23            x: 0.0, 
24            y: 0.0, 
25            scale_x: 1.0, 
26            scale_y: 1.0, 
27            rotation: 0.0 
28        } 
29    }
30}
31
32pub fn parse_text_lines(lines: Vec<String>) -> Result<Vec<Vec<StyledSegment>>, String> {
33    let mut result_lines: Vec<Vec<StyledSegment>> = Vec::new();
34    let mut style_stack: Vec<String> = Vec::new();
35    
36    let mut in_style_def = false;
37    let mut escaped = false;
38    
39    let mut text_buffer = String::new();
40    let mut style_buffer = String::new();
41
42    for line in lines {
43        let mut line_segments: Vec<StyledSegment> = Vec::new();
44        
45        for c in line.chars() {
46            if escaped {
47                if in_style_def {
48                    style_buffer.push(c);
49                } else {
50                    text_buffer.push(c);
51                }
52                escaped = false;
53                continue;
54            }
55
56            match c {
57                '\\' => {
58                    escaped = true;
59                }
60                '{' => {
61                    if in_style_def {
62                        style_buffer.push(c); 
63                    } else {
64                        if !text_buffer.is_empty() {
65                            line_segments.push(StyledSegment {
66                                text: text_buffer.clone(),
67                                styles: style_stack.clone(),
68                            });
69                            text_buffer.clear();
70                        }
71                        in_style_def = true;
72                    }
73                }
74                '|' => {
75                    if in_style_def {
76                        style_stack.push(style_buffer.clone());
77                        style_buffer.clear();
78                        in_style_def = false;
79                    } else {
80                        text_buffer.push(c);
81                    }
82                }
83                '}' => {
84                    if in_style_def {
85                        style_buffer.push(c);
86                    } else {
87                        if !text_buffer.is_empty() {
88                            line_segments.push(StyledSegment {
89                                text: text_buffer.clone(),
90                                styles: style_stack.clone(),
91                            });
92                            text_buffer.clear();
93                        }
94                        
95                        if style_stack.pop().is_none() {
96                            return Err(format!("Error: '}}' found with no open style on this line: {}", line));
97                        }
98                    }
99                }
100                ' ' => {
101                    if in_style_def {
102                        return Err(format!("Error: Whitespace not allowed in style definition on this line: {}", line));
103                    } else {
104                        text_buffer.push(c);
105                    }
106                }
107                _ => {
108                    if in_style_def {
109                        style_buffer.push(c);
110                    } else {
111                        text_buffer.push(c);
112                    }
113                }
114            }
115        }
116
117        if !text_buffer.is_empty() {
118            line_segments.push(StyledSegment {
119                text: text_buffer.clone(),
120                styles: style_stack.clone(),
121            });
122            text_buffer.clear();
123        }
124        
125        result_lines.push(line_segments);
126    }
127
128    if in_style_def {
129        return Err("Error: Ended inside a style definition.".to_string());
130    }
131    if !style_stack.is_empty() {
132        return Err(format!("Error: Ended with {} unclosed styles.", style_stack.len()));
133    }
134
135    Ok(result_lines)
136}
137
138/// Render styled text segments with a custom default color for unstyled text.
139pub fn render_styled_text<F1, F2>(
140    segments: &[StyledSegment], 
141    time: f64, 
142    font_size: f32,
143    default_color: Color,
144    animation_tracker: &mut FxHashMap<String, (usize, f64)>,
145    total_char_index: &mut usize,
146    mut render_fn: F1,
147    mut render_shadow_fn: F2
148) where 
149    F1: FnMut(&str, Transform, Color),
150    F2: FnMut(&str, Transform, Color)
151{
152    let named_colors: FxHashMap<String, Color> = [
153        ("white", (255.0, 255.0, 255.0)), ("black", (0.0, 0.0, 0.0)),
154        ("lightgray", (191.25, 191.25, 191.25)), ("darkgray", (94.35, 94.35, 94.35)),
155        ("red", (229.5, 0.0, 0.0)), ("orange", (255.0, 140.25, 0.0)),
156        ("yellow", (255.0, 214.2, 0.0)), ("lime", (0.0, 204.0, 0.0)),
157        ("green", (0.0, 127.5, 0.0)), ("cyan", (0.0, 204.0, 204.0)),
158        ("lightblue", (51.0, 153.0, 255.0)), ("blue", (0.0, 51.0, 204.0)),
159        ("purple", (114.75, 38.25, 196.35)), ("magenta", (204.0, 0.0, 204.0)),
160        ("brown", (137.7, 68.85, 17.85)), ("pink", (255.0, 102.0, 168.3)),
161    ].iter().map(|(k, (r,g,b))| (k.to_string(), Color::rgba(*r, *g, *b, 255.0))).collect();
162
163    let parse_float = |s: &str| s.parse::<f32>().unwrap_or(0.0);
164    let parse_color = |s: &str| -> Color {
165        if let Some(c) = named_colors.get(&s.to_lowercase()) { return *c; }
166        if s.starts_with('#') {
167            let hex = s.trim_start_matches('#');
168            if let Ok(val) = u32::from_str_radix(hex, 16) {
169                let r = ((val >> 16) & 0xFF) as f32;
170                let g = ((val >> 8) & 0xFF) as f32;
171                let b = (val & 0xFF) as f32;
172                return Color::rgba(r, g, b, 255.0);
173            }
174        }
175        if s.starts_with('(') && s.ends_with(')') {
176            let inner = &s[1..s.len()-1];
177            let parts: Vec<f32> = inner.split(',').map(|p| parse_float(p.trim())).collect();
178            if parts.len() >= 3 {
179                return Color::rgba(parts[0], parts[1], parts[2], 255.0);
180            }
181        }
182        Color::rgba(255.0, 255.0, 255.0, 255.0)
183    };
184
185    for segment in segments {
186        let mut has_effects = false;
187        for style_str in &segment.styles {
188            let mut parts = style_str.split('_');
189            let first_part = parts.next().unwrap_or("");
190            let (cmd, _) = if let Some(idx) = first_part.find('=') {
191                (&first_part[..idx], Some(&first_part[idx+1..]))
192            } else {
193                (first_part, None)
194            };
195            if cmd != "color" && cmd != "opacity" && !cmd.is_empty() {
196                has_effects = true;
197                break;
198            }
199        }
200
201        if !has_effects {
202            let mut color = default_color;
203            let mut opacity_mult = 1.0;
204            
205            for style_str in &segment.styles {
206                let mut parts = style_str.split('_');
207                let first_part = parts.next().unwrap_or("");
208                let (cmd, first_arg_val) = if let Some(idx) = first_part.find('=') {
209                    (&first_part[..idx], Some(&first_part[idx+1..]))
210                } else {
211                    (first_part, None)
212                };
213                
214                let val = if let Some(v) = first_arg_val {
215                    v
216                } else {
217                    parts.next().unwrap_or("")
218                };
219
220                if cmd == "color" {
221                    color = parse_color(val);
222                } else if cmd == "opacity" {
223                    opacity_mult *= parse_float(val);
224                }
225            }
226            
227            color.a *= opacity_mult;
228            render_fn(&segment.text, Transform::default(), color);
229            *total_char_index += segment.text.chars().count();
230            continue;
231        }
232
233        for char_obj in segment.text.chars() {
234            let global_char_idx = *total_char_index as f32;
235
236            let mut tr = Transform::default();
237            let mut color = default_color;
238            let mut opacity_mult = 1.0;
239            let mut shadow_opts: Option<(Color, f32, f32, f32, f32)> = None;
240            let mut skip_render = false;
241            let mut render_char = char_obj.to_string();
242            
243            for style_str in &segment.styles {
244                let mut parts = style_str.split('_');
245                let first_part = parts.next().unwrap_or("");
246                
247                let (cmd, first_arg_val) = if let Some(idx) = first_part.find('=') {
248                    (&first_part[..idx], Some(&first_part[idx+1..]))
249                } else {
250                    (first_part, None)
251                };
252
253                let mut args: FxHashMap<&str, &str> = parts.map(|arg| {
254                    let mut kv = arg.split('=');
255                    (kv.next().unwrap_or(""), kv.next().unwrap_or(""))
256                }).collect();
257                
258                if let Some(val) = first_arg_val {
259                    args.insert("", val);
260                }
261
262                let get_f = |k: &str, def: f32| args.get(k).map(|v| parse_float(v)).unwrap_or(def);
263
264                if cmd == "hide" { skip_render = true; break; }
265
266                let anim_id = args.get("id").unwrap_or(&"");
267                if !anim_id.is_empty() {
268                    let anim_key = anim_id.to_string();
269                    let (start_index, start_time) = {
270                        let entry = animation_tracker.entry(anim_key.clone()).or_insert((*total_char_index, time));
271                        (entry.0, entry.1)
272                    };
273                    if *total_char_index < start_index {
274                        animation_tracker.insert(anim_key, (*total_char_index, start_time));
275                    }
276                    let delay = get_f("delay", 0.0);
277                    
278                    let elapsed = ((time - start_time) as f32 - delay).max(0.0);
279                    let relative_idx = global_char_idx - start_index as f32;
280                    
281                    let is_in = args.contains_key("in");
282                    let is_out = args.contains_key("out");
283                    
284                    if is_in || is_out {
285                        match cmd {
286                            "type" => {
287                                let speed = get_f("speed", 8.0);
288                                let chars_processed = elapsed * speed;
289                                let cursor = args.get("cursor").unwrap_or(&"");
290                                if is_in {
291                                    if relative_idx >= chars_processed {
292                                        if !cursor.is_empty() && relative_idx > 0.0 && (relative_idx - 1.0) < chars_processed {
293                                            render_char = cursor.to_string();
294                                        } else {
295                                            skip_render = true;
296                                        }
297                                    }
298                                } else {
299                                    if relative_idx < chars_processed { skip_render = true; }
300                                }
301                            },
302                            "fade" => {
303                                let speed = get_f("speed", 3.0);
304                                let trail = get_f("trail", 3.0);
305                                let progress = (elapsed * speed - relative_idx) / trail;
306                                let mut alpha = progress.clamp(0.0, 1.0);
307                                if is_out { alpha = 1.0 - alpha; }
308                                opacity_mult *= alpha;
309                            },
310                            "scale" => {
311                                let speed = get_f("speed", 3.0);
312                                let trail = get_f("trail", 3.0);
313                                let progress = (elapsed * speed - relative_idx) / trail;
314                                let mut s = progress.clamp(0.0, 1.0);
315                                if is_out { s = 1.0 - s; }
316                                tr.scale_x *= s;
317                                tr.scale_y *= s;
318                            }
319                            _ => {}
320                        }
321                    } else {
322                        panic!("Animation style '{}' requires either 'in' or 'out' argument.", cmd);
323                    }
324                }
325
326                if skip_render { break; }
327
328                if cmd == "transform" {
329                    if let Some(v) = args.get("translate") {
330                        let nums: Vec<f32> = v.split(',').map(parse_float).collect();
331                        tr.x += nums.get(0).unwrap_or(&0.0) * font_size; 
332                        tr.y += nums.get(1).unwrap_or(&0.0) * font_size;
333                    }
334                    if let Some(v) = args.get("scale") {
335                        let nums: Vec<f32> = v.split(',').map(parse_float).collect();
336                        tr.scale_x *= nums.get(0).unwrap_or(&1.0);
337                        tr.scale_y *= nums.get(1).unwrap_or(nums.get(0).unwrap_or(&1.0));
338                    }
339                    tr.rotation += get_f("rotate", 0.0);
340                }
341
342                if cmd == "wave" {
343                    let w = get_f("w", 3.0);
344                    let f = if args.contains_key("s") { get_f("s", 0.0) / w } else { get_f("f", 0.5) };
345                    let a = get_f("a", 0.3) * font_size;
346                    let p = get_f("p", 0.0);
347                    let r = get_f("r", 0.0);
348                    
349                    let arg = 2.0 * PI * (f * time as f32 + global_char_idx / w + p);
350                    let disp = arg.cos() * a;
351                    
352                    let rad = r.to_radians();
353                    tr.x += -disp * rad.sin();
354                    tr.y += disp * rad.cos();
355                }
356
357                if cmd == "pulse" {
358                    let w = get_f("w", 2.0);
359                    let f = if args.contains_key("s") { get_f("s", 0.0) / w } else { get_f("f", 0.6) };
360                    let a = get_f("a", 0.15);
361                    let p = get_f("p", 0.0);
362                    
363                    let arg = 2.0 * PI * (f * time as f32 + global_char_idx / w + p);
364                    let scale_delta = 1.0 + arg.cos() * a;
365                    tr.scale_x *= scale_delta;
366                    tr.scale_y *= scale_delta;
367                }
368
369                if cmd == "swing" {
370                    let w = get_f("w", 3.0);
371                    let f = if args.contains_key("s") { get_f("s", 0.0) / w } else { get_f("f", 0.5) };
372                    let a = get_f("a", 8.0);
373                    let p = get_f("p", 0.0);
374                    
375                    let arg = 2.0 * PI * (f * time as f32 + global_char_idx / w + p);
376                    tr.rotation += arg.sin() * a;
377                }
378
379                if cmd == "jitter" {
380                    let seed = (time as f32 * 20.0).floor() + global_char_idx * 13.37;
381                    let rand_x = (seed.sin() * 43758.5453).fract();
382                    let rand_y = ((seed + 7.1).cos() * 23421.632).fract();
383                    
384                    let radii_str = args.get("radii").unwrap_or(&"0.1,0.1");
385                    let rads: Vec<f32> = radii_str.split(',').map(parse_float).collect();
386                    let rx = rads.get(0).unwrap_or(&0.5) * font_size;
387                    let ry = rads.get(1).unwrap_or(rads.get(0).unwrap_or(&0.5)) * font_size;
388                    let rot = get_f("rotation", 0.0).to_radians();
389
390                    let jx = (rand_x - 0.5) * 2.0 * rx;
391                    let jy = (rand_y - 0.5) * 2.0 * ry;
392
393                    tr.x += jx * rot.cos() - jy * rot.sin();
394                    tr.y += jx * rot.sin() + jy * rot.cos();
395                }
396
397                if cmd == "gradient" {
398                    let speed = get_f("speed", 1.0);
399                    let stops_str = args.get("stops").unwrap_or(&"0:#FF0000,1:#FF9A00,2:#D0DE21,3:#4FDC4A,4:#3FDAD8,5:#2FC9E2,6:#1C7FEE,7:#5F15F2,8:#BA0CF8,9:#FB07D9,10:#FF0000");
400                    
401                    let stops: Vec<(f32, Color)> = stops_str.split(',').map(|pair| {
402                        let mut kv = pair.split(':');
403                        let pos = kv.next().unwrap_or("0").parse::<f32>().unwrap_or(0.0);
404                        let col = parse_color(kv.next().unwrap_or("white"));
405                        (pos, col)
406                    }).collect();
407                    
408                    if !stops.is_empty() {
409                        let cycle_len = stops.last().unwrap().0;
410                        let current_pos = (global_char_idx - time as f32 * speed).rem_euclid(cycle_len);
411                        
412                        let mut c1 = stops[0].1;
413                        let mut c2 = stops[0].1;
414                        let mut t = 0.0;
415
416                        for i in 0..stops.len()-1 {
417                            if current_pos >= stops[i].0 && current_pos <= stops[i+1].0 {
418                                c1 = stops[i].1;
419                                c2 = stops[i+1].1;
420                                let span = stops[i+1].0 - stops[i].0;
421                                t = if span > 0.0 { (current_pos - stops[i].0) / span } else { 0.0 };
422                                break;
423                            }
424                        }
425
426                        if current_pos > stops.last().unwrap().0 {
427                            c1 = stops.last().unwrap().1;
428                            c2 = stops[0].1;
429                            let span = cycle_len - stops.last().unwrap().0;
430                            t = (current_pos - stops.last().unwrap().0) / span;
431                        }
432                        
433                        color.r = c1.r + (c2.r - c1.r) * t;
434                        color.g = c1.g + (c2.g - c1.g) * t;
435                        color.b = c1.b + (c2.b - c1.b) * t;
436                    }
437                }
438
439                if cmd == "opacity" {
440                    if let Some(v) = args.get("") {
441                        opacity_mult *= parse_float(v);
442                    }
443                }
444
445                if cmd == "color" {
446                    if let Some(v) = args.get("") {
447                        color = parse_color(v);
448                    }
449                }
450
451                if cmd == "shadow" {
452                    let color_str = args.get("color").unwrap_or(&"black");
453                    let sc = parse_color(color_str);
454                    let off_str = args.get("offset").unwrap_or(&"-0.3,0.3");
455                    let offs: Vec<f32> = off_str.split(',').map(parse_float).collect();
456                    let ox = offs.get(0).unwrap_or(&-0.3) * font_size;
457                    let oy = offs.get(1).unwrap_or(&0.3) * font_size;
458                    
459                    let scl_str = args.get("scale").unwrap_or(&"1");
460                    let scls: Vec<f32> = scl_str.split(',').map(parse_float).collect();
461                    let sx = *scls.get(0).unwrap_or(&1.0);
462                    let sy = *scls.get(1).unwrap_or(&sx);
463                    
464                    shadow_opts = Some((sc, ox, oy, sx, sy));
465                }
466            }
467
468            if !skip_render {
469                color.a *= opacity_mult;
470                
471                if let Some((sc, ox, oy, ssx, ssy)) = shadow_opts {
472                    let mut shadow_tr = tr;
473                    shadow_tr.x += ox;
474                    shadow_tr.y += oy;
475                    shadow_tr.scale_x *= ssx;
476                    shadow_tr.scale_y *= ssy;
477                    
478                    let shadow_final_color = Color::rgba(sc.r, sc.g, sc.b, sc.a * opacity_mult);
479                    render_shadow_fn(&render_char, shadow_tr, shadow_final_color);
480                }
481                
482                render_fn(&render_char, tr, color);
483            }
484            *total_char_index += 1;
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    const WHITE: Color = Color { r: 255.0, g: 255.0, b: 255.0, a: 255.0 };
494
495    #[test]
496    fn test_render_simple_text() {
497        let lines = vec!["Hello".to_string()];
498        let segments = parse_text_lines(lines).unwrap();
499        let mut tracker = FxHashMap::default();
500        let mut rendered = Vec::new();
501        
502        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
503            |c, tr, col| rendered.push((c.to_string(), tr, col)),
504            |_, _, _| {});
505        
506        assert_eq!(rendered.len(), 1, "Rendered length should be 1 for 'Hello' (optimized)");
507        assert_eq!(rendered[0].0, "Hello", "First text should be 'Hello'");
508        assert_eq!(rendered[0].1.scale_x, 1.0, "Default scale_x should be 1.0");
509        assert_eq!(rendered[0].1.scale_y, 1.0, "Default scale_y should be 1.0");
510    }
511
512    #[test]
513    fn test_render_color_named() {
514        let lines = vec!["{color=red|R}".to_string()];
515        let segments = parse_text_lines(lines).unwrap();
516        let mut tracker = FxHashMap::default();
517        let mut rendered = Vec::new();
518        
519        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
520            |c, tr, col| rendered.push((c.to_string(), tr, col)),
521            |_, _, _| {});
522        
523        assert_eq!(rendered[0].0, "R", "Char should be 'R'");
524        assert!((rendered[0].2.r - 230.0).abs() < 1.0, "Named color red r value wrong? {:?}", rendered);
525        assert!(rendered[0].2.g < 1.0, "Named color red g value wrong? {:?}", rendered);
526        assert!(rendered[0].2.b < 1.0, "Named color red b value wrong? {:?}", rendered);
527    }
528
529    #[test]
530    fn test_render_color_hex() {
531        let lines = vec!["{color=#FF0000|R}".to_string()];
532        let segments = parse_text_lines(lines).unwrap();
533        let mut tracker = FxHashMap::default();
534        let mut rendered = Vec::new();
535        
536        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
537            |c, tr, col| rendered.push((c.to_string(), tr, col)),
538            |_, _, _| {});
539        
540        assert!((rendered[0].2.r - 255.0).abs() < 1.0, "Hex color r value wrong? {:?}", rendered);
541        assert!(rendered[0].2.g < 1.0, "Hex color g value wrong? {:?}", rendered);
542        assert!(rendered[0].2.b < 1.0, "Hex color b value wrong? {:?}", rendered);
543    }
544
545    #[test]
546    fn test_render_color_rgb() {
547        let lines = vec!["{color=(255,128,0)|O}".to_string()];
548        let segments = parse_text_lines(lines).unwrap();
549        let mut tracker = FxHashMap::default();
550        let mut rendered = Vec::new();
551        
552        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
553            |c, tr, col| rendered.push((c.to_string(), tr, col)),
554            |_, _, _| {});
555        
556        assert!((rendered[0].2.r - 255.0).abs() < 1.0, "RGB color r value wrong? {:?}", rendered);
557        assert!((rendered[0].2.g - 128.0).abs() < 1.0, "RGB color g value wrong? {:?}", rendered);
558        assert!(rendered[0].2.b < 1.0, "RGB color b value wrong? {:?}", rendered);
559    }
560
561    #[test]
562    fn test_render_opacity() {
563        let lines = vec!["{opacity=0.5|A}".to_string()];
564        let segments = parse_text_lines(lines).unwrap();
565        let mut tracker = FxHashMap::default();
566        let mut rendered = Vec::new();
567        
568        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
569            |c, tr, col| rendered.push((c.to_string(), tr, col)),
570            |_, _, _| {});
571        
572        assert!((rendered[0].2.a - 127.5).abs() < 1.0, "Opacity value wrong? {:?}", rendered);
573    }
574
575    #[test]
576    fn test_render_transform_translate() {
577        let lines = vec!["{transform_translate=0.5,0.5|A}".to_string()];
578        let segments = parse_text_lines(lines).unwrap();
579        let mut tracker = FxHashMap::default();
580        let mut rendered = Vec::new();
581        
582        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
583            |c, tr, col| rendered.push((c.to_string(), tr, col)),
584            |_, _, _| {});
585        
586        assert!((rendered[0].1.x - 8.0).abs() < 0.01, "Translate x wrong? {:?}", rendered); // 0.5 * 16.0
587        assert!((rendered[0].1.y - 8.0).abs() < 0.01, "Translate y wrong? {:?}", rendered);
588    }
589
590    #[test]
591    fn test_render_transform_scale() {
592        let lines = vec!["{transform_scale=2.0|A}".to_string()];
593        let segments = parse_text_lines(lines).unwrap();
594        let mut tracker = FxHashMap::default();
595        let mut rendered = Vec::new();
596        
597        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
598            |c, tr, col| rendered.push((c.to_string(), tr, col)),
599            |_, _, _| {});
600        
601        assert!((rendered[0].1.scale_x - 2.0).abs() < 0.01, "Scale x wrong? {:?}", rendered);
602        assert!((rendered[0].1.scale_y - 2.0).abs() < 0.01, "Scale y wrong? {:?}", rendered);
603    }
604
605    #[test]
606    fn test_render_transform_scale_xy() {
607        let lines = vec!["{transform_scale=2.0,0.5|A}".to_string()];
608        let segments = parse_text_lines(lines).unwrap();
609        let mut tracker = FxHashMap::default();
610        let mut rendered = Vec::new();
611        
612        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
613            |c, tr, col| rendered.push((c.to_string(), tr, col)),
614            |_, _, _| {});
615        
616        assert!((rendered[0].1.scale_x - 2.0).abs() < 0.01, "Scale x wrong? {:?}", rendered);
617        assert!((rendered[0].1.scale_y - 0.5).abs() < 0.01, "Scale y wrong? {:?}", rendered);
618    }
619
620    #[test]
621    fn test_render_transform_rotate() {
622        let lines = vec!["{transform_rotate=45|A}".to_string()];
623        let segments = parse_text_lines(lines).unwrap();
624        let mut tracker = FxHashMap::default();
625        let mut rendered = Vec::new();
626        
627        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
628            |c, tr, col| rendered.push((c.to_string(), tr, col)),
629            |_, _, _| {});
630        
631        assert!((rendered[0].1.rotation - 45.0).abs() < 0.01, "Rotate value wrong? {:?}", rendered);
632    }
633
634    #[test]
635    fn test_render_wave_effect() {
636        let lines = vec!["{wave|ABC}".to_string()];
637        let segments = parse_text_lines(lines).unwrap();
638        let mut tracker = FxHashMap::default();
639        let mut rendered = Vec::new();
640        
641        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
642            |c, tr, col| rendered.push((c.to_string(), tr, col)),
643            |_, _, _| {});
644        
645        assert_eq!(rendered.len(), 3, "Wave effect rendered length wrong? {:?}", rendered);
646        assert_ne!(rendered[0].1.y, rendered[1].1.y, "Wave effect Y position not different? {:?}", rendered);
647    }
648
649    #[test]
650    fn test_render_wave_with_params() {
651        let lines = vec!["{wave_w=2.0_f=1.0_a=0.5|AB}".to_string()];
652        let segments = parse_text_lines(lines).unwrap();
653        let mut tracker = FxHashMap::default();
654        let mut rendered = Vec::new();
655        
656        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
657            |c, tr, col| rendered.push((c.to_string(), tr, col)),
658            |_, _, _| {});
659        
660        assert_eq!(rendered.len(), 2, "Wave effect rendered length wrong? {:?}", rendered);
661        assert!(rendered[0].1.y.abs() <= 8.0, "Wave effect Y position amplitude wrong? {:?}", rendered);
662    }
663
664    #[test]
665    fn test_render_pulse_effect() {
666        let lines = vec!["{pulse|ABC}".to_string()];
667        let segments = parse_text_lines(lines).unwrap();
668        let mut tracker = FxHashMap::default();
669        let mut rendered = Vec::new();
670        
671        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
672            |c, tr, col| rendered.push((c.to_string(), tr, col)),
673            |_, _, _| {});
674        
675        assert_eq!(rendered.len(), 3, "Pulse effect rendered length wrong? {:?}", rendered);
676        assert_ne!(rendered[0].1.scale_x, rendered[1].1.scale_x, "Pulse effect scale not different? {:?}", rendered);
677    }
678
679    #[test]
680    fn test_render_swing_effect() {
681        let lines = vec!["{swing|ABC}".to_string()];
682        let segments = parse_text_lines(lines).unwrap();
683        let mut tracker = FxHashMap::default();
684        let mut rendered = Vec::new();
685        
686        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
687            |c, tr, col| rendered.push((c.to_string(), tr, col)),
688            |_, _, _| {});
689        
690        assert_eq!(rendered.len(), 3, "Swing effect rendered length wrong? {:?}", rendered);
691        assert_ne!(rendered[0].1.rotation, rendered[1].1.rotation, "Swing effect rotation not different? {:?}", rendered);
692    }
693
694    #[test]
695    fn test_render_jitter_effect() {
696        let lines = vec!["{jitter_radii=0.1,0.1|A}".to_string()];
697        let segments = parse_text_lines(lines).unwrap();
698        let mut tracker = FxHashMap::default();
699        let mut rendered_t1 = Vec::new();
700        let mut rendered_t2 = Vec::new();
701        
702        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
703            |c, tr, col| rendered_t1.push((c.to_string(), tr, col)),
704            |_, _, _| {});
705        
706        render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
707            |c, tr, col| rendered_t2.push((c.to_string(), tr, col)),
708            |_, _, _| {});
709        
710        assert_ne!(rendered_t1[0].1.x, rendered_t2[0].1.x, "Jitter effect X position not different? {:?} {:?}", rendered_t1, rendered_t2);
711    }
712
713    #[test]
714    fn test_render_gradient_effect() {
715        let lines = vec!["{gradient_stops=0:#FF0000,3:#0000FF|ABC}".to_string()];
716        let segments = parse_text_lines(lines).unwrap();
717        let mut tracker = FxHashMap::default();
718        let mut rendered = Vec::new();
719        
720        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
721            |c, tr, col| rendered.push((c.to_string(), tr, col)),
722            |_, _, _| {});
723        
724        assert_eq!(rendered.len(), 3, "Gradient effect rendered length wrong? {:?}", rendered);
725        assert!(rendered[0].2.r > 0.5, "Gradient effect first char color not correct? {:?}", rendered);
726        assert!(rendered[2].2.b > rendered[0].2.b, "Gradient effect color not correct? {:?}", rendered);
727    }
728
729    #[test]
730    fn test_render_hide_effect() {
731        let lines = vec!["{hide|ABC}".to_string()];
732        let segments = parse_text_lines(lines).unwrap();
733        let mut tracker = FxHashMap::default();
734        let mut rendered = Vec::new();
735        
736        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
737            |c, tr, col| rendered.push((c.to_string(), tr, col)),
738            |_, _, _| {});
739        
740        assert_eq!(rendered.len(), 0, "Hide effect rendered length wrong? {:?}", rendered);
741    }
742
743    #[test]
744    fn test_render_shadow_effect() {
745        let lines = vec!["{shadow|A}".to_string()];
746        let segments = parse_text_lines(lines).unwrap();
747        let mut tracker = FxHashMap::default();
748        let mut rendered = Vec::new();
749        let mut shadows = Vec::new();
750        
751        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
752            |c, tr, col| rendered.push((c.to_string(), tr, col)),
753            |c, tr, col| shadows.push((c.to_string(), tr, col)));
754        
755        assert_eq!(rendered.len(), 1, "Shadow effect rendered length wrong? {:?}", rendered);
756        assert_eq!(shadows.len(), 1, "Shadow effect shadows length wrong? {:?}", shadows);
757        assert_eq!(shadows[0].0, "A", "Shadow effect char wrong? {:?}", shadows);
758        assert!(shadows[0].2.r < 0.1, "Shadow effect color r value wrong? {:?}", shadows);
759    }
760
761    #[test]
762    fn test_render_shadow_with_color() {
763        let lines = vec!["{shadow_color=red|A}".to_string()];
764        let segments = parse_text_lines(lines).unwrap();
765        let mut tracker = FxHashMap::default();
766        let mut shadows = Vec::new();
767        
768        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
769            |_, _, _| {},
770            |c, tr, col| shadows.push((c.to_string(), tr, col)));
771        
772        assert!(shadows[0].2.r > 0.5, "Shadow color r value wrong? {:?}", shadows);
773    }
774
775    #[test]
776    fn test_render_shadow_offset() {
777        let lines = vec!["{shadow_offset=0.5,0.5|A}".to_string()];
778        let segments = parse_text_lines(lines).unwrap();
779        let mut tracker = FxHashMap::default();
780        let mut rendered = Vec::new();
781        let mut shadows = Vec::new();
782        
783        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
784            |c, tr, col| rendered.push((c.to_string(), tr, col)),
785            |c, tr, col| shadows.push((c.to_string(), tr, col)));
786        
787        assert!((shadows[0].1.x - 8.0).abs() < 0.01, "Shadow offset x wrong? {:?}", shadows);
788        assert!((shadows[0].1.y - 8.0).abs() < 0.01, "Shadow offset y wrong? {:?}", shadows);
789    }
790
791    #[test]
792    fn test_render_type_animation() {
793        let lines = vec!["{type_in_id=t1_cursor=\\||ABC}".to_string()];
794        let segments = parse_text_lines(lines).unwrap();
795        let mut tracker = FxHashMap::default();
796        let mut rendered = Vec::new();
797        
798        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
799            |c, tr, col| rendered.push((c.to_string(), tr, col)),
800            |_, _, _| {});
801        
802        assert_eq!(rendered.len(), 0, "Type animation at time 0 should show nothing");
803        
804        rendered.clear();
805        render_styled_text(&segments[0], 0.1, 16.0, WHITE, &mut tracker, &mut 0,
806            |c, tr, col| rendered.push((c.to_string(), tr, col)),
807            |_, _, _| {});
808        
809        assert!(rendered.len() > 0, "Type animation after time should show chars");
810
811        assert!(rendered[rendered.len()-1].0 == "|", "Type animation cursor should be present? {:?}", rendered);
812    }
813
814    #[test]
815    fn test_render_fade_animation() {
816        let lines = vec!["{fade_in_id=f1|A}".to_string()];
817        let segments = parse_text_lines(lines).unwrap();
818        let mut tracker = FxHashMap::default();
819        let mut rendered = Vec::new();
820        
821        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
822            |c, tr, col| rendered.push((c.to_string(), tr, col)),
823            |_, _, _| {});
824        
825        assert!(rendered.len() > 0, "Fade animation should render something");
826        assert!(rendered[0].2.a < 0.1, "Fade animation alpha at time 0 should be low? {:?}", rendered);
827        
828        rendered.clear();
829        render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
830            |c, tr, col| rendered.push((c.to_string(), tr, col)),
831            |_, _, _| {});
832        
833        assert!(rendered[0].2.a > 0.9, "Fade animation alpha after time should be high? {:?}", rendered);
834    }
835
836    #[test]
837    fn test_render_scale_animation() {
838        let lines = vec!["{scale_in_id=s1|A}".to_string()];
839        let segments = parse_text_lines(lines).unwrap();
840        let mut tracker = FxHashMap::default();
841        let mut rendered = Vec::new();
842        
843        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
844            |c, tr, col| rendered.push((c.to_string(), tr, col)),
845            |_, _, _| {});
846        
847        assert!(rendered[0].1.scale_x < 0.1, "Scale animation scale_x at time 0 should be small? {:?}", rendered);
848        
849        rendered.clear();
850        render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
851            |c, tr, col| rendered.push((c.to_string(), tr, col)),
852            |_, _, _| {});
853        
854        assert!(rendered[0].1.scale_x > 0.9, "Scale animation scale_x after time should be large? {:?}", rendered);
855    }
856
857    #[test]
858    fn test_render_nested_wave_pulse() {
859        let lines = vec!["{wave|{pulse|ABC}}".to_string()];
860        let segments = parse_text_lines(lines).unwrap();
861        let mut tracker = FxHashMap::default();
862        let mut rendered = Vec::new();
863        
864        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
865            |c, tr, col| rendered.push((c.to_string(), tr, col)),
866            |_, _, _| {});
867        
868        assert_eq!(rendered.len(), 3, "Nested wave/pulse rendered length wrong? {:?}", rendered);
869        assert_ne!(rendered[0].1.y, 0.0, "Wave effect y not applied? {:?}", rendered);
870        assert_ne!(rendered[0].1.scale_x, 1.0, "Pulse effect scale_x not applied? {:?}", rendered);
871    }
872
873    #[test]
874    fn test_render_nested_color_wave() {
875        let lines = vec!["{color=red|{wave|ABC}}".to_string()];
876        let segments = parse_text_lines(lines).unwrap();
877        let mut tracker = FxHashMap::default();
878        let mut rendered = Vec::new();
879        
880        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
881            |c, tr, col| rendered.push((c.to_string(), tr, col)),
882            |_, _, _| {});
883        
884        assert_eq!(rendered.len(), 3, "Nested color/wave rendered length wrong? {:?}", rendered);
885        assert!(rendered[0].2.r > 0.5, "Nested color effect r value wrong? {:?}", rendered);
886        assert!(rendered[1].2.r > 0.5, "Nested color effect r value wrong? {:?}", rendered);
887        assert_ne!(rendered[0].1.y, rendered[1].1.y, "Nested wave effect y not different? {:?}", rendered);
888    }
889
890    #[test]
891    fn test_render_multiple_same_effect_nested() {
892        let lines = vec!["{wave|A{wave|B}C}".to_string()];
893        let segments = parse_text_lines(lines).unwrap();
894        let mut tracker = FxHashMap::default();
895        let mut rendered = Vec::new();
896        
897        render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
898            |c, tr, col| rendered.push((c.to_string(), tr, col)),
899            |_, _, _| {});
900        
901        assert_eq!(rendered.len(), 3, "Multiple nested wave rendered length wrong? {:?}", rendered);
902        let b_offset = rendered[1].1.y;
903        let a_offset = rendered[0].1.y;
904        assert_ne!(b_offset, a_offset, "Nested wave offsets not different? {:?}", rendered);
905    }
906
907    #[test]
908    fn test_render_gradient_over_time() {
909        let lines = vec!["{gradient_speed=10|AB}".to_string()];
910        let segments = parse_text_lines(lines).unwrap();
911        let mut tracker = FxHashMap::default();
912        let mut rendered_t1 = Vec::new();
913        let mut rendered_t2 = Vec::new();
914        
915        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
916            |c, tr, col| rendered_t1.push((c.to_string(), tr, col)),
917            |_, _, _| {});
918        
919        render_styled_text(&segments[0], 0.1, 16.0, WHITE, &mut tracker, &mut 0,
920            |c, tr, col| rendered_t2.push((c.to_string(), tr, col)),
921            |_, _, _| {});
922        
923        assert_ne!(rendered_t1[0].2.r, rendered_t2[0].2.r, "Gradient color r value should change over time");
924    }
925
926    #[test]
927    fn test_render_all_effects_combined() {
928        let lines = vec!["{wave|{pulse|{swing|{color=cyan|ABC}}}}".to_string()];
929        let segments = parse_text_lines(lines).unwrap();
930        let mut tracker = FxHashMap::default();
931        let mut rendered = Vec::new();
932        
933        render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
934            |c, tr, col| rendered.push((c.to_string(), tr, col)),
935            |_, _, _| {});
936        
937        assert_eq!(rendered.len(), 3, "All effects combined rendered length wrong? {:?}", rendered);
938        assert_ne!(rendered[0].1.y, 0.0, "Wave effect y not applied? {:?}", rendered);
939        assert_ne!(rendered[0].1.scale_x, 1.0, "Pulse effect scale_x not applied? {:?}", rendered);
940        assert_ne!(rendered[0].1.rotation, 0.0, "Swing effect rotation not applied? {:?}", rendered);
941        assert!(rendered[0].2.g > 0.5 && rendered[0].2.b > 0.5, "Cyan color effect not applied? {:?}", rendered);
942    }
943
944    #[test]
945    fn test_render_color_overwrite_nested() {
946        let lines = vec!["{color=red|A{color=blue|B}C}".to_string()];
947        let segments = parse_text_lines(lines).unwrap();
948        let mut tracker = FxHashMap::default();
949        let mut rendered = Vec::new();
950        
951        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
952            |c, tr, col| rendered.push((c.to_string(), tr, col)),
953            |_, _, _| {});
954        
955        assert_eq!(rendered.len(), 3, "Color overwrite nested rendered length wrong? {:?}", rendered);
956        assert!(rendered[0].2.r > 0.5 && rendered[0].2.b < 0.5, "Outer color red not applied to A? {:?}", rendered);
957        assert!(rendered[1].2.b > 0.5 && rendered[1].2.r < 0.5, "Inner color blue not applied to B? {:?}", rendered);
958        assert!(rendered[2].2.r > 0.5 && rendered[2].2.b < 0.5, "Outer color red not applied to C? {:?}", rendered);
959    }
960
961    #[test]
962    fn test_render_transform_accumulation() {
963        let lines = vec!["{transform_translate=0.5,0|{transform_translate=0,0.5|A}}".to_string()];
964        let segments = parse_text_lines(lines).unwrap();
965        let mut tracker = FxHashMap::default();
966        let mut rendered = Vec::new();
967        
968        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
969            |c, tr, col| rendered.push((c.to_string(), tr, col)),
970            |_, _, _| {});
971        
972        assert!((rendered[0].1.x - 8.0).abs() < 0.01, "Transform accumulation x wrong? {:?}", rendered);
973        assert!((rendered[0].1.y - 8.0).abs() < 0.01, "Transform accumulation y wrong? {:?}", rendered);
974    }
975
976    #[test]
977    fn test_render_opacity_accumulation() {
978        let lines = vec!["{opacity=0.5|{opacity=0.5|A}}".to_string()];
979        let segments = parse_text_lines(lines).unwrap();
980        let mut tracker = FxHashMap::default();
981        let mut rendered = Vec::new();
982        
983        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
984            |c, tr, col| rendered.push((c.to_string(), tr, col)),
985            |_, _, _| {});
986        
987        assert!((rendered[0].2.a - 63.75).abs() < 1.0, "Opacity accumulation wrong? {:?}", rendered);
988    }
989
990    #[test]
991    fn test_render_shadow_with_transform() {
992        let lines = vec!["{transform_scale=2|{shadow|A}}".to_string()];
993        let segments = parse_text_lines(lines).unwrap();
994        let mut tracker = FxHashMap::default();
995        let mut rendered = Vec::new();
996        let mut shadows = Vec::new();
997        
998        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
999            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1000            |c, tr, col| shadows.push((c.to_string(), tr, col)));
1001        
1002        assert!((rendered[0].1.scale_x - 2.0).abs() < 0.01, "Shadow with transform scale_x wrong? {:?}", rendered);
1003        assert!((shadows[0].1.scale_x - 2.0).abs() < 0.01, "Shadow with transform scale_x wrong? {:?}", shadows);
1004    }
1005
1006    #[test]
1007    fn test_render_empty_text() {
1008        let lines = vec!["".to_string()];
1009        let segments = parse_text_lines(lines).unwrap();
1010        let mut tracker = FxHashMap::default();
1011        let mut rendered = Vec::new();
1012        
1013        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1014            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1015            |_, _, _| {});
1016        
1017        assert_eq!(rendered.len(), 0, "Empty text rendered length wrong? {:?}", rendered);
1018    }
1019
1020    #[test]
1021    fn test_render_unicode_with_effects() {
1022        let lines = vec!["{wave|你好🌍}".to_string()];
1023        let segments = parse_text_lines(lines).unwrap();
1024        let mut tracker = FxHashMap::default();
1025        let mut rendered = Vec::new();
1026        
1027        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1028            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1029            |_, _, _| {});
1030        
1031        assert_eq!(rendered.len(), 3, "Unicode with effects rendered length wrong? {:?}", rendered);
1032        assert_eq!(rendered[0].0, "你", "First unicode char wrong? {:?}", rendered);
1033        assert_eq!(rendered[1].0, "好", "Second unicode char wrong? {:?}", rendered);
1034        assert_eq!(rendered[2].0, "🌍", "Third unicode char wrong? {:?}", rendered);
1035    }
1036
1037    #[test]
1038    fn test_render_type_out_animation() {
1039        let lines = vec!["{type_out_id=t2|ABC}".to_string()];
1040        let segments = parse_text_lines(lines).unwrap();
1041        let mut tracker = FxHashMap::default();
1042        let mut rendered = Vec::new();
1043        
1044        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1045            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1046            |_, _, _| {});
1047        
1048        assert_eq!(rendered.len(), 3, "Type out animation at time 0 should show all chars");
1049        
1050        rendered.clear();
1051        render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
1052            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1053            |_, _, _| {});
1054        
1055        assert_eq!(rendered.len(), 0, "Type out animation after time should hide chars");
1056    }
1057
1058    #[test]
1059    fn test_render_fade_out_animation() {
1060        let lines = vec!["{fade_out_id=f2|A}".to_string()];
1061        let segments = parse_text_lines(lines).unwrap();
1062        let mut tracker = FxHashMap::default();
1063        let mut rendered = Vec::new();
1064        
1065        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1066            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1067            |_, _, _| {});
1068        
1069        assert!(rendered.len() > 0);
1070        assert!(rendered[0].2.a > 0.9, "Fade out animation alpha at time 0 should be high? {:?}", rendered);
1071        
1072        rendered.clear();
1073        render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
1074            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1075            |_, _, _| {});
1076        
1077        assert!(rendered[0].2.a < 0.1, "Fade out animation alpha after time should be low? {:?}", rendered);
1078    }
1079
1080    #[test]
1081    fn test_render_scale_out_animation() {
1082        let lines = vec!["{scale_out_id=s2|A}".to_string()];
1083        let segments = parse_text_lines(lines).unwrap();
1084        let mut tracker = FxHashMap::default();
1085        let mut rendered = Vec::new();
1086        
1087        render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1088            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1089            |_, _, _| {});
1090        
1091        assert!(rendered[0].1.scale_x > 0.9, "Scale out animation scale_x at time 0 should be large? {:?}", rendered);
1092        
1093        rendered.clear();
1094        render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
1095            |c, tr, col| rendered.push((c.to_string(), tr, col)),
1096            |_, _, _| {});
1097        
1098        assert!(rendered[0].1.scale_x < 0.1, "Scale out animation scale_x after time should be small? {:?}", rendered);
1099    }
1100}