1use alloc::borrow::Cow;
2use alloc::string::ToString;
3use core::fmt;
4
5use unicode_segmentation::UnicodeSegmentation;
6use unicode_width::UnicodeWidthStr;
7
8use crate::buffer::Buffer;
9use crate::layout::Rect;
10use crate::style::{Style, Styled};
11use crate::text::{Line, StyledGrapheme};
12use crate::widgets::Widget;
13
14#[derive(Default, Clone, Eq, PartialEq, Hash)]
99pub struct Span<'a> {
100 pub style: Style,
102 pub content: Cow<'a, str>,
104}
105
106impl fmt::Debug for Span<'_> {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 if self.content.is_empty() {
109 write!(f, "Span::default()")?;
110 } else {
111 write!(f, "Span::from({:?})", self.content)?;
112 }
113 if self.style != Style::default() {
114 self.style.fmt_stylize(f)?;
115 }
116 Ok(())
117 }
118}
119
120impl<'a> Span<'a> {
121 pub fn raw<T>(content: T) -> Self
132 where
133 T: Into<Cow<'a, str>>,
134 {
135 Self {
136 content: content.into(),
137 style: Style::default(),
138 }
139 }
140
141 pub fn styled<T, S>(content: T, style: S) -> Self
162 where
163 T: Into<Cow<'a, str>>,
164 S: Into<Style>,
165 {
166 Self {
167 content: content.into(),
168 style: style.into(),
169 }
170 }
171
172 #[must_use = "method moves the value of self and returns the modified value"]
187 pub fn content<T>(mut self, content: T) -> Self
188 where
189 T: Into<Cow<'a, str>>,
190 {
191 self.content = content.into();
192 self
193 }
194
195 #[must_use = "method moves the value of self and returns the modified value"]
216 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
217 self.style = style.into();
218 self
219 }
220
221 #[must_use = "method moves the value of self and returns the modified value"]
241 pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
242 self.style = self.style.patch(style);
243 self
244 }
245
246 #[must_use = "method moves the value of self and returns the modified value"]
266 pub fn reset_style(self) -> Self {
267 self.patch_style(Style::reset())
268 }
269
270 pub fn width(&self) -> usize {
272 UnicodeWidthStr::width(self)
273 }
274
275 pub fn styled_graphemes<S: Into<Style>>(
307 &'a self,
308 base_style: S,
309 ) -> impl Iterator<Item = StyledGrapheme<'a>> {
310 let style = base_style.into().patch(self.style);
311 self.content
312 .as_ref()
313 .graphemes(true)
314 .filter(|g| !g.contains(char::is_control))
315 .map(move |g| StyledGrapheme { symbol: g, style })
316 }
317
318 #[must_use = "method moves the value of self and returns the modified value"]
328 pub fn into_left_aligned_line(self) -> Line<'a> {
329 Line::from(self).left_aligned()
330 }
331
332 #[expect(clippy::wrong_self_convention)]
333 #[deprecated = "use `into_left_aligned_line()` instead"]
334 pub fn to_left_aligned_line(self) -> Line<'a> {
335 self.into_left_aligned_line()
336 }
337
338 #[must_use = "method moves the value of self and returns the modified value"]
348 pub fn into_centered_line(self) -> Line<'a> {
349 Line::from(self).centered()
350 }
351
352 #[expect(clippy::wrong_self_convention)]
353 #[deprecated = "use `into_centered_line()` instead"]
354 pub fn to_centered_line(self) -> Line<'a> {
355 self.into_centered_line()
356 }
357
358 #[must_use = "method moves the value of self and returns the modified value"]
368 pub fn into_right_aligned_line(self) -> Line<'a> {
369 Line::from(self).right_aligned()
370 }
371
372 #[expect(clippy::wrong_self_convention)]
373 #[deprecated = "use `into_right_aligned_line()` instead"]
374 pub fn to_right_aligned_line(self) -> Line<'a> {
375 self.into_right_aligned_line()
376 }
377}
378
379impl UnicodeWidthStr for Span<'_> {
380 fn width(&self) -> usize {
381 self.content.width()
382 }
383
384 fn width_cjk(&self) -> usize {
385 self.content.width_cjk()
386 }
387}
388
389impl<'a, T> From<T> for Span<'a>
390where
391 T: Into<Cow<'a, str>>,
392{
393 fn from(s: T) -> Self {
394 Span::raw(s.into())
395 }
396}
397
398impl<'a> core::ops::Add<Self> for Span<'a> {
399 type Output = Line<'a>;
400
401 fn add(self, rhs: Self) -> Self::Output {
402 Line::from_iter([self, rhs])
403 }
404}
405
406impl Styled for Span<'_> {
407 type Item = Self;
408
409 fn style(&self) -> Style {
410 self.style
411 }
412
413 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
414 self.style(style)
415 }
416}
417
418impl Widget for Span<'_> {
419 fn render(self, area: Rect, buf: &mut Buffer) {
420 Widget::render(&self, area, buf);
421 }
422}
423
424impl Widget for &Span<'_> {
425 fn render(self, area: Rect, buf: &mut Buffer) {
426 let area = area.intersection(buf.area);
427 if area.is_empty() {
428 return;
429 }
430 let Rect { mut x, y, .. } = area;
431 for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
432 let symbol_width = grapheme.symbol.width();
433 let next_x = x.saturating_add(symbol_width as u16);
434 if next_x > area.right() {
435 break;
436 }
437
438 if i == 0 {
439 buf[(x, y)]
441 .set_symbol(grapheme.symbol)
442 .set_style(grapheme.style);
443 } else if x == area.x {
444 buf[(x, y)]
447 .append_symbol(grapheme.symbol)
448 .set_style(grapheme.style);
449 } else if symbol_width == 0 {
450 buf[(x - 1, y)]
452 .append_symbol(grapheme.symbol)
453 .set_style(grapheme.style);
454 } else {
455 buf[(x, y)]
457 .set_symbol(grapheme.symbol)
458 .set_style(grapheme.style);
459 }
460
461 for x_hidden in (x + 1)..next_x {
465 buf[(x_hidden, y)].reset();
468 }
469 x = next_x;
470 }
471 }
472}
473
474pub trait ToSpan {
482 fn to_span(&self) -> Span<'_>;
484}
485
486impl<T: fmt::Display> ToSpan for T {
492 fn to_span(&self) -> Span<'_> {
493 Span::raw(self.to_string())
494 }
495}
496
497impl fmt::Display for Span<'_> {
498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499 for line in self.content.lines() {
500 fmt::Display::fmt(line, f)?;
501 }
502 Ok(())
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use alloc::string::String;
509 use alloc::{format, vec};
510
511 use rstest::{fixture, rstest};
512
513 use super::*;
514 use crate::buffer::Cell;
515 use crate::layout::Alignment;
516 use crate::style::Stylize;
517
518 #[fixture]
519 fn small_buf() -> Buffer {
520 Buffer::empty(Rect::new(0, 0, 10, 1))
521 }
522
523 #[test]
524 fn default() {
525 let span = Span::default();
526 assert_eq!(span.content, Cow::Borrowed(""));
527 assert_eq!(span.style, Style::default());
528 }
529
530 #[test]
531 fn raw_str() {
532 let span = Span::raw("test content");
533 assert_eq!(span.content, Cow::Borrowed("test content"));
534 assert_eq!(span.style, Style::default());
535 }
536
537 #[test]
538 fn raw_string() {
539 let content = String::from("test content");
540 let span = Span::raw(content.clone());
541 assert_eq!(span.content, Cow::Owned::<str>(content));
542 assert_eq!(span.style, Style::default());
543 }
544
545 #[test]
546 fn styled_str() {
547 let style = Style::new().red();
548 let span = Span::styled("test content", style);
549 assert_eq!(span.content, Cow::Borrowed("test content"));
550 assert_eq!(span.style, Style::new().red());
551 }
552
553 #[test]
554 fn styled_string() {
555 let content = String::from("test content");
556 let style = Style::new().green();
557 let span = Span::styled(content.clone(), style);
558 assert_eq!(span.content, Cow::Owned::<str>(content));
559 assert_eq!(span.style, style);
560 }
561
562 #[test]
563 fn set_content() {
564 let span = Span::default().content("test content");
565 assert_eq!(span.content, Cow::Borrowed("test content"));
566 }
567
568 #[test]
569 fn set_style() {
570 let span = Span::default().style(Style::new().green());
571 assert_eq!(span.style, Style::new().green());
572 }
573
574 #[test]
575 fn from_ref_str_borrowed_cow() {
576 let content = "test content";
577 let span = Span::from(content);
578 assert_eq!(span.content, Cow::Borrowed(content));
579 assert_eq!(span.style, Style::default());
580 }
581
582 #[test]
583 fn from_string_ref_str_borrowed_cow() {
584 let content = String::from("test content");
585 let span = Span::from(content.as_str());
586 assert_eq!(span.content, Cow::Borrowed(content.as_str()));
587 assert_eq!(span.style, Style::default());
588 }
589
590 #[test]
591 fn from_string_owned_cow() {
592 let content = String::from("test content");
593 let span = Span::from(content.clone());
594 assert_eq!(span.content, Cow::Owned::<str>(content));
595 assert_eq!(span.style, Style::default());
596 }
597
598 #[test]
599 fn from_ref_string_borrowed_cow() {
600 let content = String::from("test content");
601 let span = Span::from(&content);
602 assert_eq!(span.content, Cow::Borrowed(content.as_str()));
603 assert_eq!(span.style, Style::default());
604 }
605
606 #[test]
607 fn to_span() {
608 assert_eq!(42.to_span(), Span::raw("42"));
609 assert_eq!("test".to_span(), Span::raw("test"));
610 }
611
612 #[test]
613 fn reset_style() {
614 let span = Span::styled("test content", Style::new().green()).reset_style();
615 assert_eq!(span.style, Style::reset());
616 }
617
618 #[test]
619 fn patch_style() {
620 let span = Span::styled("test content", Style::new().green().on_yellow())
621 .patch_style(Style::new().red().bold());
622 assert_eq!(span.style, Style::new().red().on_yellow().bold());
623 }
624
625 #[test]
626 fn width() {
627 assert_eq!(Span::raw("").width(), 0);
628 assert_eq!(Span::raw("test").width(), 4);
629 assert_eq!(Span::raw("test content").width(), 12);
630 assert_eq!(Span::raw("test\ncontent").width(), 12);
632 }
633
634 #[test]
635 fn stylize() {
636 let span = Span::raw("test content").green();
637 assert_eq!(span.content, Cow::Borrowed("test content"));
638 assert_eq!(span.style, Style::new().green());
639
640 let span = Span::styled("test content", Style::new().green());
641 let stylized = span.on_yellow().bold();
642 assert_eq!(stylized.content, Cow::Borrowed("test content"));
643 assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
644 }
645
646 #[test]
647 fn display_span() {
648 let span = Span::raw("test content");
649 assert_eq!(format!("{span}"), "test content");
650 assert_eq!(format!("{span:.4}"), "test");
651 }
652
653 #[test]
654 fn display_newline_span() {
655 let span = Span::raw("test\ncontent");
656 assert_eq!(format!("{span}"), "testcontent");
657 }
658
659 #[test]
660 fn display_styled_span() {
661 let stylized_span = Span::styled("stylized test content", Style::new().green());
662 assert_eq!(format!("{stylized_span}"), "stylized test content");
663 assert_eq!(format!("{stylized_span:.8}"), "stylized");
664 }
665
666 #[test]
667 fn left_aligned() {
668 let span = Span::styled("Test Content", Style::new().green().italic());
669 let line = span.into_left_aligned_line();
670 assert_eq!(line.alignment, Some(Alignment::Left));
671 }
672
673 #[test]
674 fn centered() {
675 let span = Span::styled("Test Content", Style::new().green().italic());
676 let line = span.into_centered_line();
677 assert_eq!(line.alignment, Some(Alignment::Center));
678 }
679
680 #[test]
681 fn right_aligned() {
682 let span = Span::styled("Test Content", Style::new().green().italic());
683 let line = span.into_right_aligned_line();
684 assert_eq!(line.alignment, Some(Alignment::Right));
685 }
686
687 mod widget {
688 use rstest::rstest;
689
690 use super::*;
691
692 #[test]
693 fn render() {
694 let style = Style::new().green().on_yellow();
695 let span = Span::styled("test content", style);
696 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
697 span.render(buf.area, &mut buf);
698 let expected = Buffer::with_lines([Line::from(vec![
699 "test content".green().on_yellow(),
700 " ".into(),
701 ])]);
702 assert_eq!(buf, expected);
703 }
704
705 #[rstest]
706 #[case::x(20, 0)]
707 #[case::y(0, 20)]
708 #[case::both(20, 20)]
709 fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
710 let out_of_bounds = Rect::new(x, y, 10, 1);
711 Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
712 assert_eq!(small_buf, Buffer::empty(small_buf.area));
713 }
714
715 #[test]
718 fn render_truncates_too_long_content() {
719 let style = Style::new().green().on_yellow();
720 let span = Span::styled("test content", style);
721
722 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
723 span.render(Rect::new(0, 0, 5, 1), &mut buf);
724
725 let expected = Buffer::with_lines([Line::from(vec![
726 "test ".green().on_yellow(),
727 " ".into(),
728 ])]);
729 assert_eq!(buf, expected);
730 }
731
732 #[test]
735 fn render_patches_existing_style() {
736 let style = Style::new().green().on_yellow();
737 let span = Span::styled("test content", style);
738 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
739 buf.set_style(buf.area, Style::new().italic());
740 span.render(buf.area, &mut buf);
741 let expected = Buffer::with_lines([Line::from(vec![
742 "test content".green().on_yellow().italic(),
743 " ".italic(),
744 ])]);
745 assert_eq!(buf, expected);
746 }
747
748 #[test]
751 fn render_multi_width_symbol() {
752 let style = Style::new().green().on_yellow();
753 let span = Span::styled("test 😃 content", style);
754 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
755 span.render(buf.area, &mut buf);
756 let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
760 assert_eq!(buf, expected);
761 }
762
763 #[test]
766 fn render_multi_width_symbol_truncates_entire_symbol() {
767 let style = Style::new().green().on_yellow();
769 let span = Span::styled("test 😃 content", style);
770 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
771 span.render(buf.area, &mut buf);
772
773 let expected =
774 Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
775 assert_eq!(buf, expected);
776 }
777
778 #[test]
781 fn render_overflowing_area_truncates() {
782 let style = Style::new().green().on_yellow();
783 let span = Span::styled("test content", style);
784 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
785 span.render(Rect::new(10, 0, 20, 1), &mut buf);
786
787 let expected = Buffer::with_lines([Line::from(vec![
788 " ".into(),
789 "test ".green().on_yellow(),
790 ])]);
791 assert_eq!(buf, expected);
792 }
793
794 #[test]
795 fn render_first_zero_width() {
796 let span = Span::raw("\u{200B}abc");
797 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
798 span.render(buf.area, &mut buf);
799 assert_eq!(
800 buf.content(),
801 [Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
802 );
803 }
804
805 #[test]
806 fn render_second_zero_width() {
807 let span = Span::raw("a\u{200B}bc");
808 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
809 span.render(buf.area, &mut buf);
810 assert_eq!(
811 buf.content(),
812 [Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
813 );
814 }
815
816 #[test]
817 fn render_middle_zero_width() {
818 let span = Span::raw("ab\u{200B}c");
819 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
820 span.render(buf.area, &mut buf);
821 assert_eq!(
822 buf.content(),
823 [Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
824 );
825 }
826
827 #[test]
828 fn render_last_zero_width() {
829 let span = Span::raw("abc\u{200B}");
830 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
831 span.render(buf.area, &mut buf);
832 assert_eq!(
833 buf.content(),
834 [Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
835 );
836 }
837
838 #[test]
839 fn render_with_newlines() {
840 let span = Span::raw("a\nb");
841 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
842 span.render(buf.area, &mut buf);
843 assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
844 }
845 }
846
847 #[test]
854 fn issue_1160() {
855 let span = Span::raw("Hello\u{200E}");
856 let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
857 span.render(buf.area, &mut buf);
858 assert_eq!(
859 buf.content(),
860 [
861 Cell::new("H"),
862 Cell::new("e"),
863 Cell::new("l"),
864 Cell::new("l"),
865 Cell::new("o\u{200E}"),
866 ]
867 );
868 }
869
870 #[test]
871 fn add() {
872 assert_eq!(
873 Span::default() + Span::default(),
874 Line::from(vec![Span::default(), Span::default()])
875 );
876
877 assert_eq!(
878 Span::default() + Span::raw("test"),
879 Line::from(vec![Span::default(), Span::raw("test")])
880 );
881
882 assert_eq!(
883 Span::raw("test") + Span::default(),
884 Line::from(vec![Span::raw("test"), Span::default()])
885 );
886
887 assert_eq!(
888 Span::raw("test") + Span::raw("content"),
889 Line::from(vec![Span::raw("test"), Span::raw("content")])
890 );
891 }
892
893 #[rstest]
894 #[case::default(Span::default(), "Span::default()")]
895 #[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
896 #[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
897 #[case::styled_italic(
898 Span::styled("test", Style::new().green().italic()),
899 r#"Span::from("test").green().italic()"#
900 )]
901 fn debug(#[case] span: Span, #[case] expected: &str) {
902 assert_eq!(format!("{span:?}"), expected);
903 }
904}