1use astrelis_render::Color;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum LineStyle {
26 #[default]
28 Solid,
29 Dashed,
31 Dotted,
33 Wavy,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct UnderlineStyle {
40 pub color: Color,
42 pub thickness: f32,
44 pub style: LineStyle,
46 pub offset: f32,
48}
49
50impl UnderlineStyle {
51 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 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 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 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 pub fn with_offset(mut self, offset: f32) -> Self {
93 self.offset = offset;
94 self
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct StrikethroughStyle {
101 pub color: Color,
103 pub thickness: f32,
105 pub style: LineStyle,
107 pub offset: f32,
109}
110
111impl StrikethroughStyle {
112 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 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 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 pub fn with_offset(mut self, offset: f32) -> Self {
144 self.offset = offset;
145 self
146 }
147}
148
149#[derive(Debug, Clone, PartialEq)]
151pub struct TextDecoration {
152 pub underline: Option<UnderlineStyle>,
154 pub strikethrough: Option<StrikethroughStyle>,
156 pub background: Option<Color>,
158 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 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn underline(mut self, style: UnderlineStyle) -> Self {
181 self.underline = Some(style);
182 self
183 }
184
185 pub fn strikethrough(mut self, style: StrikethroughStyle) -> Self {
187 self.strikethrough = Some(style);
188 self
189 }
190
191 pub fn background(mut self, color: Color) -> Self {
193 self.background = Some(color);
194 self
195 }
196
197 pub fn background_padding_uniform(mut self, padding: f32) -> Self {
199 self.background_padding = [padding; 4];
200 self
201 }
202
203 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 pub fn has_decoration(&self) -> bool {
211 self.underline.is_some() || self.strikethrough.is_some() || self.background.is_some()
212 }
213
214 pub fn has_underline(&self) -> bool {
216 self.underline.is_some()
217 }
218
219 pub fn has_strikethrough(&self) -> bool {
221 self.strikethrough.is_some()
222 }
223
224 pub fn has_background(&self) -> bool {
226 self.background.is_some()
227 }
228}
229
230#[derive(Debug, Clone, PartialEq)]
234pub struct DecorationGeometry {
235 pub start: (f32, f32),
237 pub end: (f32, f32),
239 pub thickness: f32,
241 pub color: Color,
243 pub style: LineStyle,
245}
246
247impl DecorationGeometry {
248 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 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 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#[derive(Debug, Clone, PartialEq)]
283pub struct BackgroundGeometry {
284 pub rect: (f32, f32, f32, f32),
286 pub color: Color,
288}
289
290impl BackgroundGeometry {
291 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 pub fn as_rect(&self) -> (f32, f32, f32, f32) {
301 self.rect
302 }
303}
304
305pub 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq)]
373pub enum DecorationQuadType {
374 Background,
376 Underline {
378 thickness: f32,
380 },
381 Strikethrough {
383 thickness: f32,
385 },
386}
387
388#[derive(Debug, Clone, PartialEq)]
393pub struct DecorationQuad {
394 pub bounds: (f32, f32, f32, f32),
396 pub color: Color,
398 pub quad_type: DecorationQuadType,
400}
401
402impl DecorationQuad {
403 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 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 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 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 pub fn as_rect(&self) -> (f32, f32, f32, f32) {
450 self.bounds
451 }
452
453 pub fn is_background(&self) -> bool {
455 matches!(self.quad_type, DecorationQuadType::Background)
456 }
457
458 pub fn is_underline(&self) -> bool {
460 matches!(self.quad_type, DecorationQuadType::Underline { .. })
461 }
462
463 pub fn is_strikethrough(&self) -> bool {
465 matches!(self.quad_type, DecorationQuadType::Strikethrough { .. })
466 }
467}
468
469#[derive(Debug, Clone, Copy, PartialEq)]
471pub struct TextBounds {
472 pub x: f32,
474 pub y: f32,
476 pub width: f32,
478 pub height: f32,
480 pub baseline_offset: f32,
482}
483
484impl TextBounds {
485 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
497struct LineQuadParams {
499 x: f32,
501 y: f32,
503 width: f32,
505 thickness: f32,
507 color: Color,
509 style: LineStyle,
511 quad_type: DecorationQuadType,
513}
514
515fn 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 quads.push(DecorationQuad::new(
538 x, y, width, thickness, color, quad_type,
539 ));
540 }
541 LineStyle::Dashed => {
542 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 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 let wave_height = (thickness * 1.5).max(2.0); let wave_length = (thickness * 8.0).max(8.0); let segment_width = wave_length / 8.0; 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 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
619pub fn generate_decoration_quads(
662 bounds: &TextBounds,
663 decoration: &TextDecoration,
664) -> Vec<DecorationQuad> {
665 let mut quads = Vec::new();
666
667 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 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 if let Some(st_style) = decoration.strikethrough {
702 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 assert_eq!(x, -5.0); assert_eq!(width, 210.0); assert_eq!(height, 26.0); }
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 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 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 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 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 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 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 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 assert!(
939 quads.len() > 3,
940 "Combined decorations should generate multiple quads"
941 );
942
943 assert!(quads[0].is_background());
945 assert_eq!(quads[0].color, Color::YELLOW);
946 }
947}