Skip to main content

astrelis_text/
decoration.rs

1//! Text decoration - underline, strikethrough, and background highlighting.
2//!
3//! This module provides text decoration capabilities for rich text rendering:
4//! - Underlines (solid, dashed, dotted, wavy)
5//! - Strikethrough
6//! - Background highlighting
7//!
8//! # Example
9//!
10//! ```ignore
11//! use astrelis_text::*;
12//!
13//! let decoration = TextDecoration::new()
14//!     .underline(UnderlineStyle::solid(Color::BLUE, 1.0))
15//!     .background(Color::YELLOW);
16//!
17//! let text = Text::new("Important text")
18//!     .decoration(decoration);
19//! ```
20
21use astrelis_render::Color;
22
23/// Line style for underlines and strikethrough.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum LineStyle {
26    /// Solid line
27    #[default]
28    Solid,
29    /// Dashed line
30    Dashed,
31    /// Dotted line
32    Dotted,
33    /// Wavy line (sine wave)
34    Wavy,
35}
36
37/// Underline style configuration.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct UnderlineStyle {
40    /// Line color
41    pub color: Color,
42    /// Line thickness in pixels
43    pub thickness: f32,
44    /// Line style (solid, dashed, dotted, wavy)
45    pub style: LineStyle,
46    /// Offset below baseline in pixels (positive = below)
47    pub offset: f32,
48}
49
50impl UnderlineStyle {
51    /// Create a solid underline.
52    pub fn solid(color: Color, thickness: f32) -> Self {
53        Self {
54            color,
55            thickness,
56            style: LineStyle::Solid,
57            offset: 2.0,
58        }
59    }
60
61    /// Create a dashed underline.
62    pub fn dashed(color: Color, thickness: f32) -> Self {
63        Self {
64            color,
65            thickness,
66            style: LineStyle::Dashed,
67            offset: 2.0,
68        }
69    }
70
71    /// Create a dotted underline.
72    pub fn dotted(color: Color, thickness: f32) -> Self {
73        Self {
74            color,
75            thickness,
76            style: LineStyle::Dotted,
77            offset: 2.0,
78        }
79    }
80
81    /// Create a wavy underline.
82    pub fn wavy(color: Color, thickness: f32) -> Self {
83        Self {
84            color,
85            thickness,
86            style: LineStyle::Wavy,
87            offset: 2.0,
88        }
89    }
90
91    /// Set the offset below baseline.
92    pub fn with_offset(mut self, offset: f32) -> Self {
93        self.offset = offset;
94        self
95    }
96}
97
98/// Strikethrough style configuration.
99#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct StrikethroughStyle {
101    /// Line color
102    pub color: Color,
103    /// Line thickness in pixels
104    pub thickness: f32,
105    /// Line style (solid, dashed, dotted)
106    pub style: LineStyle,
107    /// Offset from baseline in pixels (0 = centered on text)
108    pub offset: f32,
109}
110
111impl StrikethroughStyle {
112    /// Create a solid strikethrough.
113    pub fn solid(color: Color, thickness: f32) -> Self {
114        Self {
115            color,
116            thickness,
117            style: LineStyle::Solid,
118            offset: 0.0,
119        }
120    }
121
122    /// Create a dashed strikethrough.
123    pub fn dashed(color: Color, thickness: f32) -> Self {
124        Self {
125            color,
126            thickness,
127            style: LineStyle::Dashed,
128            offset: 0.0,
129        }
130    }
131
132    /// Create a dotted strikethrough.
133    pub fn dotted(color: Color, thickness: f32) -> Self {
134        Self {
135            color,
136            thickness,
137            style: LineStyle::Dotted,
138            offset: 0.0,
139        }
140    }
141
142    /// Set the offset from baseline.
143    pub fn with_offset(mut self, offset: f32) -> Self {
144        self.offset = offset;
145        self
146    }
147}
148
149/// Text decoration configuration.
150#[derive(Debug, Clone, PartialEq)]
151pub struct TextDecoration {
152    /// Underline style
153    pub underline: Option<UnderlineStyle>,
154    /// Strikethrough style
155    pub strikethrough: Option<StrikethroughStyle>,
156    /// Background highlight color
157    pub background: Option<Color>,
158    /// Background padding (left, top, right, bottom)
159    pub background_padding: [f32; 4],
160}
161
162impl Default for TextDecoration {
163    fn default() -> Self {
164        Self {
165            underline: None,
166            strikethrough: None,
167            background: None,
168            background_padding: [0.0, 0.0, 0.0, 0.0],
169        }
170    }
171}
172
173impl TextDecoration {
174    /// Create a new empty decoration.
175    pub fn new() -> Self {
176        Self::default()
177    }
178
179    /// Set underline style.
180    pub fn underline(mut self, style: UnderlineStyle) -> Self {
181        self.underline = Some(style);
182        self
183    }
184
185    /// Set strikethrough style.
186    pub fn strikethrough(mut self, style: StrikethroughStyle) -> Self {
187        self.strikethrough = Some(style);
188        self
189    }
190
191    /// Set background highlight color.
192    pub fn background(mut self, color: Color) -> Self {
193        self.background = Some(color);
194        self
195    }
196
197    /// Set background padding (uniform).
198    pub fn background_padding_uniform(mut self, padding: f32) -> Self {
199        self.background_padding = [padding; 4];
200        self
201    }
202
203    /// Set background padding (left, top, right, bottom).
204    pub fn background_padding_ltrb(mut self, left: f32, top: f32, right: f32, bottom: f32) -> Self {
205        self.background_padding = [left, top, right, bottom];
206        self
207    }
208
209    /// Check if any decoration is set.
210    pub fn has_decoration(&self) -> bool {
211        self.underline.is_some() || self.strikethrough.is_some() || self.background.is_some()
212    }
213
214    /// Check if underline is set.
215    pub fn has_underline(&self) -> bool {
216        self.underline.is_some()
217    }
218
219    /// Check if strikethrough is set.
220    pub fn has_strikethrough(&self) -> bool {
221        self.strikethrough.is_some()
222    }
223
224    /// Check if background is set.
225    pub fn has_background(&self) -> bool {
226        self.background.is_some()
227    }
228}
229
230/// Geometry for rendering decorations.
231///
232/// This is typically generated per line or per text span.
233#[derive(Debug, Clone, PartialEq)]
234pub struct DecorationGeometry {
235    /// Line start position (x, y)
236    pub start: (f32, f32),
237    /// Line end position (x, y)
238    pub end: (f32, f32),
239    /// Line thickness
240    pub thickness: f32,
241    /// Line color
242    pub color: Color,
243    /// Line style
244    pub style: LineStyle,
245}
246
247impl DecorationGeometry {
248    /// Create a new decoration geometry.
249    pub fn new(
250        start: (f32, f32),
251        end: (f32, f32),
252        thickness: f32,
253        color: Color,
254        style: LineStyle,
255    ) -> Self {
256        Self {
257            start,
258            end,
259            thickness,
260            color,
261            style,
262        }
263    }
264
265    /// Get the line length.
266    pub fn length(&self) -> f32 {
267        let dx = self.end.0 - self.start.0;
268        let dy = self.end.1 - self.start.1;
269        (dx * dx + dy * dy).sqrt()
270    }
271
272    /// Get the center point.
273    pub fn center(&self) -> (f32, f32) {
274        (
275            (self.start.0 + self.end.0) / 2.0,
276            (self.start.1 + self.end.1) / 2.0,
277        )
278    }
279}
280
281/// Background highlight geometry.
282#[derive(Debug, Clone, PartialEq)]
283pub struct BackgroundGeometry {
284    /// Rectangle bounds (x, y, width, height)
285    pub rect: (f32, f32, f32, f32),
286    /// Background color
287    pub color: Color,
288}
289
290impl BackgroundGeometry {
291    /// Create a new background geometry.
292    pub fn new(x: f32, y: f32, width: f32, height: f32, color: Color) -> Self {
293        Self {
294            rect: (x, y, width, height),
295            color,
296        }
297    }
298
299    /// Get the rectangle as (x, y, width, height).
300    pub fn as_rect(&self) -> (f32, f32, f32, f32) {
301        self.rect
302    }
303}
304
305/// Generate decoration geometry for a line of text.
306///
307/// # Arguments
308///
309/// * `decoration` - The decoration configuration
310/// * `baseline_y` - Y coordinate of the text baseline
311/// * `line_start_x` - X coordinate where the line starts
312/// * `line_end_x` - X coordinate where the line ends
313/// * `line_height` - Height of the line
314///
315/// # Returns
316///
317/// Tuple of (background, underlines, strikethroughs)
318pub fn generate_decoration_geometry(
319    decoration: &TextDecoration,
320    baseline_y: f32,
321    line_start_x: f32,
322    line_end_x: f32,
323    line_height: f32,
324) -> (
325    Option<BackgroundGeometry>,
326    Option<DecorationGeometry>,
327    Option<DecorationGeometry>,
328) {
329    let mut background = None;
330    let mut underline = None;
331    let mut strikethrough = None;
332
333    // Background
334    if let Some(bg_color) = decoration.background {
335        let padding = &decoration.background_padding;
336        let x = line_start_x - padding[0];
337        let y = baseline_y - line_height + padding[1];
338        let width = (line_end_x - line_start_x) + padding[0] + padding[2];
339        let height = line_height + padding[1] + padding[3];
340
341        background = Some(BackgroundGeometry::new(x, y, width, height, bg_color));
342    }
343
344    // Underline
345    if let Some(ul_style) = decoration.underline {
346        let y = baseline_y + ul_style.offset;
347        underline = Some(DecorationGeometry::new(
348            (line_start_x, y),
349            (line_end_x, y),
350            ul_style.thickness,
351            ul_style.color,
352            ul_style.style,
353        ));
354    }
355
356    // Strikethrough
357    if let Some(st_style) = decoration.strikethrough {
358        let y = baseline_y - (line_height / 2.0) + st_style.offset;
359        strikethrough = Some(DecorationGeometry::new(
360            (line_start_x, y),
361            (line_end_x, y),
362            st_style.thickness,
363            st_style.color,
364            st_style.style,
365        ));
366    }
367
368    (background, underline, strikethrough)
369}
370
371/// Type of decoration quad for rendering.
372#[derive(Debug, Clone, Copy, PartialEq)]
373pub enum DecorationQuadType {
374    /// Background highlight quad.
375    Background,
376    /// Underline quad.
377    Underline {
378        /// Line thickness in pixels.
379        thickness: f32,
380    },
381    /// Strikethrough quad.
382    Strikethrough {
383        /// Line thickness in pixels.
384        thickness: f32,
385    },
386}
387
388/// A quad for rendering text decorations.
389///
390/// This is the unified output format for all decoration types.
391/// The renderer generates these quads and submits them for rendering.
392#[derive(Debug, Clone, PartialEq)]
393pub struct DecorationQuad {
394    /// Quad bounds (x, y, width, height) in logical pixels.
395    pub bounds: (f32, f32, f32, f32),
396    /// Quad color.
397    pub color: Color,
398    /// Type of decoration this quad represents.
399    pub quad_type: DecorationQuadType,
400}
401
402impl DecorationQuad {
403    /// Create a new decoration quad.
404    pub fn new(
405        x: f32,
406        y: f32,
407        width: f32,
408        height: f32,
409        color: Color,
410        quad_type: DecorationQuadType,
411    ) -> Self {
412        Self {
413            bounds: (x, y, width, height),
414            color,
415            quad_type,
416        }
417    }
418
419    /// Create a background quad.
420    pub fn background(x: f32, y: f32, width: f32, height: f32, color: Color) -> Self {
421        Self::new(x, y, width, height, color, DecorationQuadType::Background)
422    }
423
424    /// Create an underline quad.
425    pub fn underline(x: f32, y: f32, width: f32, thickness: f32, color: Color) -> Self {
426        Self::new(
427            x,
428            y,
429            width,
430            thickness,
431            color,
432            DecorationQuadType::Underline { thickness },
433        )
434    }
435
436    /// Create a strikethrough quad.
437    pub fn strikethrough(x: f32, y: f32, width: f32, thickness: f32, color: Color) -> Self {
438        Self::new(
439            x,
440            y,
441            width,
442            thickness,
443            color,
444            DecorationQuadType::Strikethrough { thickness },
445        )
446    }
447
448    /// Get the bounds as (x, y, width, height).
449    pub fn as_rect(&self) -> (f32, f32, f32, f32) {
450        self.bounds
451    }
452
453    /// Check if this is a background quad.
454    pub fn is_background(&self) -> bool {
455        matches!(self.quad_type, DecorationQuadType::Background)
456    }
457
458    /// Check if this is an underline quad.
459    pub fn is_underline(&self) -> bool {
460        matches!(self.quad_type, DecorationQuadType::Underline { .. })
461    }
462
463    /// Check if this is a strikethrough quad.
464    pub fn is_strikethrough(&self) -> bool {
465        matches!(self.quad_type, DecorationQuadType::Strikethrough { .. })
466    }
467}
468
469/// Text bounds information needed for decoration geometry generation.
470#[derive(Debug, Clone, Copy, PartialEq)]
471pub struct TextBounds {
472    /// X position of text (left edge).
473    pub x: f32,
474    /// Y position of text (top edge).
475    pub y: f32,
476    /// Width of text.
477    pub width: f32,
478    /// Height of text (line height).
479    pub height: f32,
480    /// Baseline Y offset from top (ascent).
481    pub baseline_offset: f32,
482}
483
484impl TextBounds {
485    /// Create new text bounds.
486    pub fn new(x: f32, y: f32, width: f32, height: f32, baseline_offset: f32) -> Self {
487        Self {
488            x,
489            y,
490            width,
491            height,
492            baseline_offset,
493        }
494    }
495}
496
497/// Parameters for generating line decoration quads.
498struct LineQuadParams {
499    /// Starting X position of the line.
500    x: f32,
501    /// Y position (center) of the line.
502    y: f32,
503    /// Total width of the line.
504    width: f32,
505    /// Line thickness in pixels.
506    thickness: f32,
507    /// Line color.
508    color: Color,
509    /// Line style (Solid, Dashed, Dotted, Wavy).
510    style: LineStyle,
511    /// Type of decoration quad (Underline or Strikethrough).
512    quad_type: DecorationQuadType,
513}
514
515/// Generate line quads for a given line style.
516///
517/// This helper function generates the appropriate quads for different line styles:
518/// - Solid: Single rectangular quad
519/// - Dashed: Multiple rectangular quads with gaps
520/// - Dotted: Multiple small square quads
521/// - Wavy: Multiple rectangular quads forming a sine wave pattern
522fn generate_line_quads(quads: &mut Vec<DecorationQuad>, params: &LineQuadParams) {
523    let LineQuadParams {
524        x,
525        y,
526        width,
527        thickness,
528        color,
529        style,
530        quad_type,
531    } = params;
532    let (x, y, width, thickness, color, style, quad_type) =
533        (*x, *y, *width, *thickness, *color, *style, *quad_type);
534    match style {
535        LineStyle::Solid => {
536            // Single solid quad
537            quads.push(DecorationQuad::new(
538                x, y, width, thickness, color, quad_type,
539            ));
540        }
541        LineStyle::Dashed => {
542            // Dashed line: dash_length = 4 * thickness, gap_length = 2 * thickness
543            let dash_length = (4.0 * thickness).max(3.0);
544            let gap_length = (2.0 * thickness).max(2.0);
545            let segment_length = dash_length + gap_length;
546
547            let mut current_x = x;
548            while current_x < x + width {
549                let remaining = (x + width) - current_x;
550                let dash_width = dash_length.min(remaining);
551
552                if dash_width > 0.5 {
553                    quads.push(DecorationQuad::new(
554                        current_x, y, dash_width, thickness, color, quad_type,
555                    ));
556                }
557
558                current_x += segment_length;
559            }
560        }
561        LineStyle::Dotted => {
562            // Dotted line: dots are squares with size = thickness, spaced by 2 * thickness
563            let dot_size = thickness;
564            let dot_spacing = (2.0 * thickness).max(2.0);
565            let segment_length = dot_size + dot_spacing;
566
567            let mut current_x = x;
568            while current_x < x + width {
569                let remaining = (x + width) - current_x;
570                let dot_width = dot_size.min(remaining);
571
572                if dot_width > 0.5 {
573                    quads.push(DecorationQuad::new(
574                        current_x, y, dot_width, thickness, color, quad_type,
575                    ));
576                }
577
578                current_x += segment_length;
579            }
580        }
581        LineStyle::Wavy => {
582            // Wavy line: sine wave pattern
583            // Wave parameters
584            let wave_height = (thickness * 1.5).max(2.0); // Amplitude of the wave
585            let wave_length = (thickness * 8.0).max(8.0); // One complete wave cycle
586            let segment_width = wave_length / 8.0; // Divide wave into segments for smooth curve
587
588            let mut current_x = x;
589            let mut segment_index = 0;
590
591            while current_x < x + width {
592                let remaining = (x + width) - current_x;
593                let seg_width = segment_width.min(remaining);
594
595                if seg_width > 0.5 {
596                    // Calculate Y offset based on sine wave
597                    let phase = segment_index as f32 * segment_width / wave_length
598                        * 2.0
599                        * std::f32::consts::PI;
600                    let y_offset = phase.sin() * wave_height * 0.5;
601
602                    quads.push(DecorationQuad::new(
603                        current_x,
604                        y + y_offset,
605                        seg_width,
606                        thickness,
607                        color,
608                        quad_type,
609                    ));
610                }
611
612                current_x += segment_width;
613                segment_index += 1;
614            }
615        }
616    }
617}
618
619/// Generate decoration quads from text bounds and decoration configuration.
620///
621/// This function generates all the quads needed to render decorations for a piece of text.
622/// It returns a Vec of DecorationQuad that can be rendered using the decoration pipeline.
623///
624/// The order of quads in the returned Vec is:
625/// 1. Background quads (rendered first, behind text)
626/// 2. Underline quads (rendered after text)
627/// 3. Strikethrough quads (rendered after text)
628///
629/// Supports all line styles:
630/// - **Solid**: Continuous line
631/// - **Dashed**: Alternating dashes and gaps
632/// - **Dotted**: Series of dots
633/// - **Wavy**: Sine wave pattern
634///
635/// # Arguments
636///
637/// * `bounds` - The text bounds (position, size, baseline)
638/// * `decoration` - The decoration configuration
639///
640/// # Returns
641///
642/// A Vec of DecorationQuad to render
643///
644/// # Example
645///
646/// ```ignore
647/// use astrelis_text::{TextDecoration, UnderlineStyle, TextBounds, generate_decoration_quads, LineStyle};
648///
649/// let bounds = TextBounds::new(10.0, 20.0, 100.0, 24.0, 18.0);
650///
651/// // Solid underline
652/// let decoration = TextDecoration::new()
653///     .underline(UnderlineStyle::solid(Color::BLUE, 1.0));
654///
655/// // Wavy underline
656/// let decoration = TextDecoration::new()
657///     .underline(UnderlineStyle::wavy(Color::RED, 1.5));
658///
659/// let quads = generate_decoration_quads(&bounds, &decoration);
660/// ```
661pub fn generate_decoration_quads(
662    bounds: &TextBounds,
663    decoration: &TextDecoration,
664) -> Vec<DecorationQuad> {
665    let mut quads = Vec::new();
666
667    // Background (rendered first, behind text)
668    if let Some(bg_color) = decoration.background {
669        let padding = &decoration.background_padding;
670        let x = bounds.x - padding[0];
671        let y = bounds.y - padding[1];
672        let width = bounds.width + padding[0] + padding[2];
673        let height = bounds.height + padding[1] + padding[3];
674
675        quads.push(DecorationQuad::background(x, y, width, height, bg_color));
676    }
677
678    // Underline (rendered after text)
679    if let Some(ul_style) = decoration.underline {
680        let baseline_y = bounds.y + bounds.baseline_offset;
681        let y = baseline_y + ul_style.offset;
682        let x = bounds.x;
683        let width = bounds.width;
684        let thickness = ul_style.thickness;
685
686        generate_line_quads(
687            &mut quads,
688            &LineQuadParams {
689                x,
690                y,
691                width,
692                thickness,
693                color: ul_style.color,
694                style: ul_style.style,
695                quad_type: DecorationQuadType::Underline { thickness },
696            },
697        );
698    }
699
700    // Strikethrough (rendered after text)
701    if let Some(st_style) = decoration.strikethrough {
702        // Strikethrough at ~40% of line height from baseline (approximately middle of x-height)
703        let baseline_y = bounds.y + bounds.baseline_offset;
704        let y = baseline_y - (bounds.height * 0.35) + st_style.offset;
705        let x = bounds.x;
706        let width = bounds.width;
707        let thickness = st_style.thickness;
708
709        generate_line_quads(
710            &mut quads,
711            &LineQuadParams {
712                x,
713                y,
714                width,
715                thickness,
716                color: st_style.color,
717                style: st_style.style,
718                quad_type: DecorationQuadType::Strikethrough { thickness },
719            },
720        );
721    }
722
723    quads
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729
730    #[test]
731    fn test_line_style_default() {
732        assert_eq!(LineStyle::default(), LineStyle::Solid);
733    }
734
735    #[test]
736    fn test_underline_style_solid() {
737        let style = UnderlineStyle::solid(Color::RED, 1.0);
738        assert_eq!(style.color, Color::RED);
739        assert_eq!(style.thickness, 1.0);
740        assert_eq!(style.style, LineStyle::Solid);
741        assert_eq!(style.offset, 2.0);
742    }
743
744    #[test]
745    fn test_underline_style_wavy() {
746        let style = UnderlineStyle::wavy(Color::BLUE, 2.0).with_offset(3.0);
747        assert_eq!(style.color, Color::BLUE);
748        assert_eq!(style.thickness, 2.0);
749        assert_eq!(style.style, LineStyle::Wavy);
750        assert_eq!(style.offset, 3.0);
751    }
752
753    #[test]
754    fn test_strikethrough_style_solid() {
755        let style = StrikethroughStyle::solid(Color::BLACK, 1.5);
756        assert_eq!(style.color, Color::BLACK);
757        assert_eq!(style.thickness, 1.5);
758        assert_eq!(style.style, LineStyle::Solid);
759        assert_eq!(style.offset, 0.0);
760    }
761
762    #[test]
763    fn test_text_decoration_default() {
764        let decoration = TextDecoration::default();
765        assert!(!decoration.has_decoration());
766        assert!(!decoration.has_underline());
767        assert!(!decoration.has_strikethrough());
768        assert!(!decoration.has_background());
769    }
770
771    #[test]
772    fn test_text_decoration_builder() {
773        let decoration = TextDecoration::new()
774            .underline(UnderlineStyle::solid(Color::RED, 1.0))
775            .strikethrough(StrikethroughStyle::solid(Color::BLACK, 1.0))
776            .background(Color::YELLOW);
777
778        assert!(decoration.has_decoration());
779        assert!(decoration.has_underline());
780        assert!(decoration.has_strikethrough());
781        assert!(decoration.has_background());
782    }
783
784    #[test]
785    fn test_decoration_geometry() {
786        let geom =
787            DecorationGeometry::new((0.0, 0.0), (100.0, 0.0), 1.0, Color::RED, LineStyle::Solid);
788        assert_eq!(geom.length(), 100.0);
789        assert_eq!(geom.center(), (50.0, 0.0));
790    }
791
792    #[test]
793    fn test_background_geometry() {
794        let geom = BackgroundGeometry::new(10.0, 20.0, 100.0, 50.0, Color::YELLOW);
795        assert_eq!(geom.as_rect(), (10.0, 20.0, 100.0, 50.0));
796        assert_eq!(geom.color, Color::YELLOW);
797    }
798
799    #[test]
800    fn test_generate_decoration_geometry() {
801        let decoration = TextDecoration::new()
802            .underline(UnderlineStyle::solid(Color::RED, 1.0))
803            .strikethrough(StrikethroughStyle::solid(Color::BLACK, 1.0))
804            .background(Color::YELLOW);
805
806        let (bg, ul, st) = generate_decoration_geometry(&decoration, 100.0, 0.0, 200.0, 20.0);
807
808        assert!(bg.is_some());
809        assert!(ul.is_some());
810        assert!(st.is_some());
811
812        let bg = bg.unwrap();
813        assert_eq!(bg.color, Color::YELLOW);
814
815        let ul = ul.unwrap();
816        assert_eq!(ul.color, Color::RED);
817        assert_eq!(ul.start.0, 0.0);
818        assert_eq!(ul.end.0, 200.0);
819
820        let st = st.unwrap();
821        assert_eq!(st.color, Color::BLACK);
822        assert_eq!(st.start.0, 0.0);
823        assert_eq!(st.end.0, 200.0);
824    }
825
826    #[test]
827    fn test_background_padding() {
828        let decoration = TextDecoration::new()
829            .background(Color::YELLOW)
830            .background_padding_ltrb(5.0, 3.0, 5.0, 3.0);
831
832        let (bg, _, _) = generate_decoration_geometry(&decoration, 100.0, 0.0, 200.0, 20.0);
833
834        let bg = bg.unwrap();
835        let (x, _y, width, height) = bg.as_rect();
836
837        // Check padding is applied
838        assert_eq!(x, -5.0); // left padding
839        assert_eq!(width, 210.0); // original 200 + left 5 + right 5
840        assert_eq!(height, 26.0); // original 20 + top 3 + bottom 3
841    }
842
843    #[test]
844    fn test_solid_line_style() {
845        let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
846        let decoration = TextDecoration::new().underline(UnderlineStyle::solid(Color::RED, 1.0));
847
848        let quads = generate_decoration_quads(&bounds, &decoration);
849
850        // Solid line should generate exactly 1 quad
851        assert_eq!(quads.len(), 1);
852        assert!(quads[0].is_underline());
853        assert_eq!(quads[0].color, Color::RED);
854    }
855
856    #[test]
857    fn test_dashed_line_style() {
858        let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
859        let decoration = TextDecoration::new().underline(UnderlineStyle::dashed(Color::BLUE, 2.0));
860
861        let quads = generate_decoration_quads(&bounds, &decoration);
862
863        // Dashed line should generate multiple quads (dashes with gaps)
864        assert!(
865            quads.len() > 1,
866            "Dashed line should generate multiple quads"
867        );
868        assert!(quads[0].is_underline());
869        assert_eq!(quads[0].color, Color::BLUE);
870    }
871
872    #[test]
873    fn test_dotted_line_style() {
874        let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
875        let decoration = TextDecoration::new().underline(UnderlineStyle::dotted(Color::GREEN, 1.5));
876
877        let quads = generate_decoration_quads(&bounds, &decoration);
878
879        // Dotted line should generate multiple quads (dots with gaps)
880        assert!(
881            quads.len() > 1,
882            "Dotted line should generate multiple quads"
883        );
884        assert!(quads[0].is_underline());
885        assert_eq!(quads[0].color, Color::GREEN);
886    }
887
888    #[test]
889    fn test_wavy_line_style() {
890        let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
891        let decoration = TextDecoration::new().underline(UnderlineStyle::wavy(Color::YELLOW, 1.0));
892
893        let quads = generate_decoration_quads(&bounds, &decoration);
894
895        // Wavy line should generate multiple quads forming a wave
896        assert!(quads.len() > 1, "Wavy line should generate multiple quads");
897        assert!(quads[0].is_underline());
898        assert_eq!(quads[0].color, Color::YELLOW);
899
900        // Verify that y positions vary (wave effect)
901        if quads.len() >= 2 {
902            let y_positions: Vec<f32> = quads.iter().map(|q| q.bounds.1).collect();
903            let all_same = y_positions.windows(2).all(|w| w[0] == w[1]);
904            assert!(!all_same, "Wavy line should have varying y positions");
905        }
906    }
907
908    #[test]
909    fn test_strikethrough_line_styles() {
910        let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
911
912        // Test solid strikethrough
913        let decoration =
914            TextDecoration::new().strikethrough(StrikethroughStyle::solid(Color::BLACK, 1.0));
915        let quads = generate_decoration_quads(&bounds, &decoration);
916        assert_eq!(quads.len(), 1);
917        assert!(quads[0].is_strikethrough());
918
919        // Test dashed strikethrough
920        let decoration =
921            TextDecoration::new().strikethrough(StrikethroughStyle::dashed(Color::BLACK, 1.0));
922        let quads = generate_decoration_quads(&bounds, &decoration);
923        assert!(quads.len() > 1);
924        assert!(quads[0].is_strikethrough());
925    }
926
927    #[test]
928    fn test_combined_decorations_with_line_styles() {
929        let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
930        let decoration = TextDecoration::new()
931            .background(Color::YELLOW)
932            .underline(UnderlineStyle::wavy(Color::RED, 1.0))
933            .strikethrough(StrikethroughStyle::dashed(Color::BLACK, 1.0));
934
935        let quads = generate_decoration_quads(&bounds, &decoration);
936
937        // Should have: 1 background + multiple underline + multiple strikethrough
938        assert!(
939            quads.len() > 3,
940            "Combined decorations should generate multiple quads"
941        );
942
943        // First quad should be background
944        assert!(quads[0].is_background());
945        assert_eq!(quads[0].color, Color::YELLOW);
946    }
947}