1use crate::font::{FontAttributes, FontStyle, FontWeight};
32use crate::text::{LineBreakConfig, Text, TextAlign, TextWrap, VerticalAlign};
33use astrelis_render::Color;
34
35#[derive(Debug, Clone)]
37pub struct TextSpan {
38 pub text: String,
40 pub style: TextSpanStyle,
42}
43
44impl TextSpan {
45 pub fn new(text: impl Into<String>, style: TextSpanStyle) -> Self {
47 Self {
48 text: text.into(),
49 style,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Default)]
56pub struct TextSpanStyle {
57 pub font_size: Option<f32>,
59 pub color: Option<Color>,
61 pub weight: Option<FontWeight>,
63 pub style: Option<FontStyle>,
65 pub font_family: Option<String>,
67 pub underline: bool,
69 pub strikethrough: bool,
71 pub background: Option<Color>,
73 pub scale: Option<f32>,
75}
76
77impl TextSpanStyle {
78 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn with_size(mut self, size: f32) -> Self {
85 self.font_size = Some(size);
86 self
87 }
88
89 pub fn with_color(mut self, color: Color) -> Self {
91 self.color = Some(color);
92 self
93 }
94
95 pub fn with_weight(mut self, weight: FontWeight) -> Self {
97 self.weight = Some(weight);
98 self
99 }
100
101 pub fn bold(mut self) -> Self {
103 self.weight = Some(FontWeight::Bold);
104 self
105 }
106
107 pub fn with_style(mut self, style: FontStyle) -> Self {
109 self.style = Some(style);
110 self
111 }
112
113 pub fn italic(mut self) -> Self {
115 self.style = Some(FontStyle::Italic);
116 self
117 }
118
119 pub fn with_family(mut self, family: impl Into<String>) -> Self {
121 self.font_family = Some(family.into());
122 self
123 }
124
125 pub fn with_underline(mut self, underline: bool) -> Self {
127 self.underline = underline;
128 self
129 }
130
131 pub fn with_strikethrough(mut self, strikethrough: bool) -> Self {
133 self.strikethrough = strikethrough;
134 self
135 }
136
137 pub fn with_background(mut self, color: Color) -> Self {
139 self.background = Some(color);
140 self
141 }
142
143 pub fn with_scale(mut self, scale: f32) -> Self {
145 self.scale = Some(scale);
146 self
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct RichText {
153 spans: Vec<TextSpan>,
155 default_font_size: f32,
157 default_color: Color,
159 default_font_attrs: FontAttributes,
161 align: TextAlign,
163 vertical_align: VerticalAlign,
165 wrap: TextWrap,
167 break_at_hyphens: bool,
169 max_width: Option<f32>,
171 max_height: Option<f32>,
173 line_height: f32,
175}
176
177impl RichText {
178 pub fn new() -> Self {
180 Self {
181 spans: Vec::new(),
182 default_font_size: 16.0,
183 default_color: Color::WHITE,
184 default_font_attrs: FontAttributes::default(),
185 align: TextAlign::Left,
186 vertical_align: VerticalAlign::Top,
187 wrap: TextWrap::Word,
188 break_at_hyphens: true,
189 max_width: None,
190 max_height: None,
191 line_height: 1.2,
192 }
193 }
194
195 pub fn push(&mut self, text: impl Into<String>, style: TextSpanStyle) {
197 self.spans.push(TextSpan::new(text, style));
198 }
199
200 pub fn push_str(&mut self, text: impl Into<String>) {
202 self.spans
203 .push(TextSpan::new(text, TextSpanStyle::default()));
204 }
205
206 pub fn push_bold(&mut self, text: impl Into<String>) {
208 self.spans
209 .push(TextSpan::new(text, TextSpanStyle::default().bold()));
210 }
211
212 pub fn push_italic(&mut self, text: impl Into<String>) {
214 self.spans
215 .push(TextSpan::new(text, TextSpanStyle::default().italic()));
216 }
217
218 pub fn push_colored(&mut self, text: impl Into<String>, color: Color) {
220 self.spans.push(TextSpan::new(
221 text,
222 TextSpanStyle::default().with_color(color),
223 ));
224 }
225
226 pub fn push_span(&mut self, span: TextSpan) {
228 self.spans.push(span);
229 }
230
231 pub fn spans(&self) -> &[TextSpan] {
233 &self.spans
234 }
235
236 pub fn set_default_font_size(&mut self, size: f32) {
238 self.default_font_size = size;
239 }
240
241 pub fn set_default_color(&mut self, color: Color) {
243 self.default_color = color;
244 }
245
246 pub fn set_default_font_attrs(&mut self, attrs: FontAttributes) {
248 self.default_font_attrs = attrs;
249 }
250
251 pub fn set_align(&mut self, align: TextAlign) {
253 self.align = align;
254 }
255
256 pub fn set_vertical_align(&mut self, align: VerticalAlign) {
258 self.vertical_align = align;
259 }
260
261 pub fn set_wrap(&mut self, wrap: TextWrap) {
263 self.wrap = wrap;
264 }
265
266 pub fn set_line_break(&mut self, config: LineBreakConfig) {
271 self.wrap = config.wrap;
272 self.break_at_hyphens = config.break_at_hyphens;
273 }
274
275 pub fn set_max_width(&mut self, width: Option<f32>) {
277 self.max_width = width;
278 }
279
280 pub fn set_max_height(&mut self, height: Option<f32>) {
282 self.max_height = height;
283 }
284
285 pub fn set_line_height(&mut self, height: f32) {
287 self.line_height = height;
288 }
289
290 pub fn full_text(&self) -> String {
292 self.spans.iter().map(|s| s.text.as_str()).collect()
293 }
294
295 pub fn to_text_segments(&self) -> Vec<(Text, TextSpanStyle)> {
301 let mut segments = Vec::new();
302
303 for span in &self.spans {
304 let mut text = Text::new(&span.text)
305 .size(
306 span.style
307 .font_size
308 .or(span.style.scale.map(|s| self.default_font_size * s))
309 .unwrap_or(self.default_font_size),
310 )
311 .color(span.style.color.unwrap_or(self.default_color))
312 .align(self.align)
313 .vertical_align(self.vertical_align)
314 .wrap(self.wrap)
315 .line_height(self.line_height);
316
317 if let Some(weight) = span.style.weight {
318 text = text.weight(weight);
319 } else {
320 text = text.weight(self.default_font_attrs.weight);
321 }
322
323 if let Some(style) = span.style.style {
324 text = text.style(style);
325 } else {
326 text = text.style(self.default_font_attrs.style);
327 }
328
329 if let Some(ref family) = span.style.font_family {
330 text = text.font(family.clone());
331 } else if !self.default_font_attrs.family.is_empty() {
332 text = text.font(self.default_font_attrs.family.clone());
333 }
334
335 if let Some(width) = self.max_width {
336 text = text.max_width(width);
337 }
338
339 if let Some(height) = self.max_height {
340 text = text.max_height(height);
341 }
342
343 segments.push((text, span.style.clone()));
344 }
345
346 segments
347 }
348
349 pub fn from_markup(markup: &str) -> Self {
363 let mut rich = RichText::new();
364 let mut current = String::new();
365 let mut chars = markup.chars().peekable();
366
367 while let Some(ch) = chars.next() {
368 match ch {
369 '*' => {
370 if chars.peek() == Some(&'*') {
371 chars.next(); if !current.is_empty() {
375 rich.push_str(current.clone());
376 current.clear();
377 }
378
379 let mut bold_text = String::new();
380 let mut found_end = false;
381
382 while let Some(ch) = chars.next() {
383 if ch == '*' && chars.peek() == Some(&'*') {
384 chars.next(); found_end = true;
386 break;
387 }
388 bold_text.push(ch);
389 }
390
391 if found_end {
392 rich.push_bold(bold_text);
393 } else {
394 current.push_str("**");
395 current.push_str(&bold_text);
396 }
397 } else {
398 if !current.is_empty() {
400 rich.push_str(current.clone());
401 current.clear();
402 }
403
404 let mut italic_text = String::new();
405 let mut found_end = false;
406
407 for ch in chars.by_ref() {
408 if ch == '*' {
409 found_end = true;
410 break;
411 }
412 italic_text.push(ch);
413 }
414
415 if found_end {
416 rich.push_italic(italic_text);
417 } else {
418 current.push('*');
419 current.push_str(&italic_text);
420 }
421 }
422 }
423 '_' => {
424 if chars.peek() == Some(&'_') {
425 chars.next(); if !current.is_empty() {
429 rich.push_str(current.clone());
430 current.clear();
431 }
432
433 let mut underline_text = String::new();
434 let mut found_end = false;
435
436 while let Some(ch) = chars.next() {
437 if ch == '_' && chars.peek() == Some(&'_') {
438 chars.next(); found_end = true;
440 break;
441 }
442 underline_text.push(ch);
443 }
444
445 if found_end {
446 rich.push(
447 underline_text,
448 TextSpanStyle::default().with_underline(true),
449 );
450 } else {
451 current.push_str("__");
452 current.push_str(&underline_text);
453 }
454 } else {
455 current.push(ch);
456 }
457 }
458 '~' => {
459 if chars.peek() == Some(&'~') {
460 chars.next(); if !current.is_empty() {
464 rich.push_str(current.clone());
465 current.clear();
466 }
467
468 let mut strike_text = String::new();
469 let mut found_end = false;
470
471 while let Some(ch) = chars.next() {
472 if ch == '~' && chars.peek() == Some(&'~') {
473 chars.next(); found_end = true;
475 break;
476 }
477 strike_text.push(ch);
478 }
479
480 if found_end {
481 rich.push(
482 strike_text,
483 TextSpanStyle::default().with_strikethrough(true),
484 );
485 } else {
486 current.push_str("~~");
487 current.push_str(&strike_text);
488 }
489 } else {
490 current.push(ch);
491 }
492 }
493 _ => {
494 current.push(ch);
495 }
496 }
497 }
498
499 if !current.is_empty() {
500 rich.push_str(current);
501 }
502
503 rich
504 }
505}
506
507impl Default for RichText {
508 fn default() -> Self {
509 Self::new()
510 }
511}
512
513pub struct RichTextBuilder {
515 rich_text: RichText,
516}
517
518impl RichTextBuilder {
519 pub fn new() -> Self {
521 Self {
522 rich_text: RichText::new(),
523 }
524 }
525
526 pub fn text(mut self, text: impl Into<String>) -> Self {
528 self.rich_text.push_str(text);
529 self
530 }
531
532 pub fn bold(mut self, text: impl Into<String>) -> Self {
534 self.rich_text.push_bold(text);
535 self
536 }
537
538 pub fn italic(mut self, text: impl Into<String>) -> Self {
540 self.rich_text.push_italic(text);
541 self
542 }
543
544 pub fn colored(mut self, text: impl Into<String>, color: Color) -> Self {
546 self.rich_text.push_colored(text, color);
547 self
548 }
549
550 pub fn span(mut self, text: impl Into<String>, style: TextSpanStyle) -> Self {
552 self.rich_text.push(text, style);
553 self
554 }
555
556 pub fn default_size(mut self, size: f32) -> Self {
558 self.rich_text.set_default_font_size(size);
559 self
560 }
561
562 pub fn default_color(mut self, color: Color) -> Self {
564 self.rich_text.set_default_color(color);
565 self
566 }
567
568 pub fn align(mut self, align: TextAlign) -> Self {
570 self.rich_text.set_align(align);
571 self
572 }
573
574 pub fn vertical_align(mut self, align: VerticalAlign) -> Self {
576 self.rich_text.set_vertical_align(align);
577 self
578 }
579
580 pub fn wrap(mut self, wrap: TextWrap) -> Self {
582 self.rich_text.set_wrap(wrap);
583 self
584 }
585
586 pub fn line_break(mut self, config: LineBreakConfig) -> Self {
591 self.rich_text.set_line_break(config);
592 self
593 }
594
595 pub fn max_width(mut self, width: f32) -> Self {
597 self.rich_text.set_max_width(Some(width));
598 self
599 }
600
601 pub fn max_height(mut self, height: f32) -> Self {
603 self.rich_text.set_max_height(Some(height));
604 self
605 }
606
607 pub fn line_height(mut self, height: f32) -> Self {
609 self.rich_text.set_line_height(height);
610 self
611 }
612
613 pub fn build(self) -> RichText {
615 self.rich_text
616 }
617}
618
619impl Default for RichTextBuilder {
620 fn default() -> Self {
621 Self::new()
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn test_rich_text_builder() {
631 let rich = RichTextBuilder::new()
632 .text("This is ")
633 .bold("bold")
634 .text(" and ")
635 .italic("italic")
636 .text(" text.")
637 .build();
638
639 assert_eq!(rich.spans().len(), 5);
640 assert_eq!(rich.full_text(), "This is bold and italic text.");
641 }
642
643 #[test]
644 fn test_markup_parsing_bold() {
645 let rich = RichText::from_markup("This is **bold** text.");
646 assert_eq!(rich.spans().len(), 3);
647 assert_eq!(rich.full_text(), "This is bold text.");
648
649 assert!(rich.spans()[1].style.weight == Some(FontWeight::Bold));
650 }
651
652 #[test]
653 fn test_markup_parsing_italic() {
654 let rich = RichText::from_markup("This is *italic* text.");
655 assert_eq!(rich.spans().len(), 3);
656 assert_eq!(rich.full_text(), "This is italic text.");
657
658 assert!(rich.spans()[1].style.style == Some(FontStyle::Italic));
659 }
660
661 #[test]
662 fn test_markup_parsing_underline() {
663 let rich = RichText::from_markup("This is __underlined__ text.");
664 assert_eq!(rich.spans().len(), 3);
665 assert_eq!(rich.full_text(), "This is underlined text.");
666
667 assert!(rich.spans()[1].style.underline);
668 }
669
670 #[test]
671 fn test_markup_parsing_strikethrough() {
672 let rich = RichText::from_markup("This is ~~strikethrough~~ text.");
673 assert_eq!(rich.spans().len(), 3);
674 assert_eq!(rich.full_text(), "This is strikethrough text.");
675
676 assert!(rich.spans()[1].style.strikethrough);
677 }
678
679 #[test]
680 fn test_markup_parsing_mixed() {
681 let rich = RichText::from_markup("This is **bold** and *italic* and __underlined__ text.");
682 assert_eq!(rich.spans().len(), 7);
683 }
684}