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 self.content.width()
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<'a, T> From<T> for Span<'a>
380where
381 T: Into<Cow<'a, str>>,
382{
383 fn from(s: T) -> Self {
384 Span::raw(s.into())
385 }
386}
387
388impl<'a> core::ops::Add<Self> for Span<'a> {
389 type Output = Line<'a>;
390
391 fn add(self, rhs: Self) -> Self::Output {
392 Line::from_iter([self, rhs])
393 }
394}
395
396impl Styled for Span<'_> {
397 type Item = Self;
398
399 fn style(&self) -> Style {
400 self.style
401 }
402
403 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
404 self.style(style)
405 }
406}
407
408impl Widget for Span<'_> {
409 fn render(self, area: Rect, buf: &mut Buffer) {
410 Widget::render(&self, area, buf);
411 }
412}
413
414impl Widget for &Span<'_> {
415 fn render(self, area: Rect, buf: &mut Buffer) {
416 let area = area.intersection(buf.area);
417 if area.is_empty() {
418 return;
419 }
420 let Rect { mut x, y, .. } = area;
421 for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
422 let symbol_width = grapheme.symbol.width();
423 let next_x = x.saturating_add(symbol_width as u16);
424 if next_x > area.right() {
425 break;
426 }
427
428 if i == 0 {
429 buf[(x, y)]
431 .set_symbol(grapheme.symbol)
432 .set_style(grapheme.style);
433 } else if x == area.x {
434 buf[(x, y)]
437 .append_symbol(grapheme.symbol)
438 .set_style(grapheme.style);
439 } else if symbol_width == 0 {
440 buf[(x - 1, y)]
442 .append_symbol(grapheme.symbol)
443 .set_style(grapheme.style);
444 } else {
445 buf[(x, y)]
447 .set_symbol(grapheme.symbol)
448 .set_style(grapheme.style);
449 }
450
451 for x_hidden in (x + 1)..next_x {
455 buf[(x_hidden, y)].reset();
458 }
459 x = next_x;
460 }
461 }
462}
463
464pub trait ToSpan {
472 fn to_span(&self) -> Span<'_>;
474}
475
476impl<T: fmt::Display> ToSpan for T {
482 fn to_span(&self) -> Span<'_> {
483 Span::raw(self.to_string())
484 }
485}
486
487impl fmt::Display for Span<'_> {
488 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489 for line in self.content.lines() {
490 fmt::Display::fmt(line, f)?;
491 }
492 Ok(())
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use alloc::string::String;
499 use alloc::{format, vec};
500
501 use rstest::{fixture, rstest};
502
503 use super::*;
504 use crate::buffer::Cell;
505 use crate::layout::Alignment;
506 use crate::style::Stylize;
507
508 #[fixture]
509 fn small_buf() -> Buffer {
510 Buffer::empty(Rect::new(0, 0, 10, 1))
511 }
512
513 #[test]
514 fn default() {
515 let span = Span::default();
516 assert_eq!(span.content, Cow::Borrowed(""));
517 assert_eq!(span.style, Style::default());
518 }
519
520 #[test]
521 fn raw_str() {
522 let span = Span::raw("test content");
523 assert_eq!(span.content, Cow::Borrowed("test content"));
524 assert_eq!(span.style, Style::default());
525 }
526
527 #[test]
528 fn raw_string() {
529 let content = String::from("test content");
530 let span = Span::raw(content.clone());
531 assert_eq!(span.content, Cow::Owned::<str>(content));
532 assert_eq!(span.style, Style::default());
533 }
534
535 #[test]
536 fn styled_str() {
537 let style = Style::new().red();
538 let span = Span::styled("test content", style);
539 assert_eq!(span.content, Cow::Borrowed("test content"));
540 assert_eq!(span.style, Style::new().red());
541 }
542
543 #[test]
544 fn styled_string() {
545 let content = String::from("test content");
546 let style = Style::new().green();
547 let span = Span::styled(content.clone(), style);
548 assert_eq!(span.content, Cow::Owned::<str>(content));
549 assert_eq!(span.style, style);
550 }
551
552 #[test]
553 fn set_content() {
554 let span = Span::default().content("test content");
555 assert_eq!(span.content, Cow::Borrowed("test content"));
556 }
557
558 #[test]
559 fn set_style() {
560 let span = Span::default().style(Style::new().green());
561 assert_eq!(span.style, Style::new().green());
562 }
563
564 #[test]
565 fn from_ref_str_borrowed_cow() {
566 let content = "test content";
567 let span = Span::from(content);
568 assert_eq!(span.content, Cow::Borrowed(content));
569 assert_eq!(span.style, Style::default());
570 }
571
572 #[test]
573 fn from_string_ref_str_borrowed_cow() {
574 let content = String::from("test content");
575 let span = Span::from(content.as_str());
576 assert_eq!(span.content, Cow::Borrowed(content.as_str()));
577 assert_eq!(span.style, Style::default());
578 }
579
580 #[test]
581 fn from_string_owned_cow() {
582 let content = String::from("test content");
583 let span = Span::from(content.clone());
584 assert_eq!(span.content, Cow::Owned::<str>(content));
585 assert_eq!(span.style, Style::default());
586 }
587
588 #[test]
589 fn from_ref_string_borrowed_cow() {
590 let content = String::from("test content");
591 let span = Span::from(&content);
592 assert_eq!(span.content, Cow::Borrowed(content.as_str()));
593 assert_eq!(span.style, Style::default());
594 }
595
596 #[test]
597 fn to_span() {
598 assert_eq!(42.to_span(), Span::raw("42"));
599 assert_eq!("test".to_span(), Span::raw("test"));
600 }
601
602 #[test]
603 fn reset_style() {
604 let span = Span::styled("test content", Style::new().green()).reset_style();
605 assert_eq!(span.style, Style::reset());
606 }
607
608 #[test]
609 fn patch_style() {
610 let span = Span::styled("test content", Style::new().green().on_yellow())
611 .patch_style(Style::new().red().bold());
612 assert_eq!(span.style, Style::new().red().on_yellow().bold());
613 }
614
615 #[test]
616 fn width() {
617 assert_eq!(Span::raw("").width(), 0);
618 assert_eq!(Span::raw("test").width(), 4);
619 assert_eq!(Span::raw("test content").width(), 12);
620 assert_eq!(Span::raw("test\ncontent").width(), 12);
622 }
623
624 #[test]
625 fn stylize() {
626 let span = Span::raw("test content").green();
627 assert_eq!(span.content, Cow::Borrowed("test content"));
628 assert_eq!(span.style, Style::new().green());
629
630 let span = Span::styled("test content", Style::new().green());
631 let stylized = span.on_yellow().bold();
632 assert_eq!(stylized.content, Cow::Borrowed("test content"));
633 assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
634 }
635
636 #[test]
637 fn display_span() {
638 let span = Span::raw("test content");
639 assert_eq!(format!("{span}"), "test content");
640 assert_eq!(format!("{span:.4}"), "test");
641 }
642
643 #[test]
644 fn display_newline_span() {
645 let span = Span::raw("test\ncontent");
646 assert_eq!(format!("{span}"), "testcontent");
647 }
648
649 #[test]
650 fn display_styled_span() {
651 let stylized_span = Span::styled("stylized test content", Style::new().green());
652 assert_eq!(format!("{stylized_span}"), "stylized test content");
653 assert_eq!(format!("{stylized_span:.8}"), "stylized");
654 }
655
656 #[test]
657 fn left_aligned() {
658 let span = Span::styled("Test Content", Style::new().green().italic());
659 let line = span.into_left_aligned_line();
660 assert_eq!(line.alignment, Some(Alignment::Left));
661 }
662
663 #[test]
664 fn centered() {
665 let span = Span::styled("Test Content", Style::new().green().italic());
666 let line = span.into_centered_line();
667 assert_eq!(line.alignment, Some(Alignment::Center));
668 }
669
670 #[test]
671 fn right_aligned() {
672 let span = Span::styled("Test Content", Style::new().green().italic());
673 let line = span.into_right_aligned_line();
674 assert_eq!(line.alignment, Some(Alignment::Right));
675 }
676
677 mod widget {
678 use rstest::rstest;
679
680 use super::*;
681
682 #[test]
683 fn render() {
684 let style = Style::new().green().on_yellow();
685 let span = Span::styled("test content", style);
686 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
687 span.render(buf.area, &mut buf);
688 let expected = Buffer::with_lines([Line::from(vec![
689 "test content".green().on_yellow(),
690 " ".into(),
691 ])]);
692 assert_eq!(buf, expected);
693 }
694
695 #[rstest]
696 #[case::x(20, 0)]
697 #[case::y(0, 20)]
698 #[case::both(20, 20)]
699 fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
700 let out_of_bounds = Rect::new(x, y, 10, 1);
701 Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
702 assert_eq!(small_buf, Buffer::empty(small_buf.area));
703 }
704
705 #[test]
708 fn render_truncates_too_long_content() {
709 let style = Style::new().green().on_yellow();
710 let span = Span::styled("test content", style);
711
712 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
713 span.render(Rect::new(0, 0, 5, 1), &mut buf);
714
715 let expected = Buffer::with_lines([Line::from(vec![
716 "test ".green().on_yellow(),
717 " ".into(),
718 ])]);
719 assert_eq!(buf, expected);
720 }
721
722 #[test]
725 fn render_patches_existing_style() {
726 let style = Style::new().green().on_yellow();
727 let span = Span::styled("test content", style);
728 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
729 buf.set_style(buf.area, Style::new().italic());
730 span.render(buf.area, &mut buf);
731 let expected = Buffer::with_lines([Line::from(vec![
732 "test content".green().on_yellow().italic(),
733 " ".italic(),
734 ])]);
735 assert_eq!(buf, expected);
736 }
737
738 #[test]
741 fn render_multi_width_symbol() {
742 let style = Style::new().green().on_yellow();
743 let span = Span::styled("test 😃 content", style);
744 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
745 span.render(buf.area, &mut buf);
746 let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
750 assert_eq!(buf, expected);
751 }
752
753 #[test]
756 fn render_multi_width_symbol_truncates_entire_symbol() {
757 let style = Style::new().green().on_yellow();
759 let span = Span::styled("test 😃 content", style);
760 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
761 span.render(buf.area, &mut buf);
762
763 let expected =
764 Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
765 assert_eq!(buf, expected);
766 }
767
768 #[test]
771 fn render_overflowing_area_truncates() {
772 let style = Style::new().green().on_yellow();
773 let span = Span::styled("test content", style);
774 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
775 span.render(Rect::new(10, 0, 20, 1), &mut buf);
776
777 let expected = Buffer::with_lines([Line::from(vec![
778 " ".into(),
779 "test ".green().on_yellow(),
780 ])]);
781 assert_eq!(buf, expected);
782 }
783
784 #[test]
785 fn render_first_zero_width() {
786 let span = Span::raw("\u{200B}abc");
787 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
788 span.render(buf.area, &mut buf);
789 assert_eq!(
790 buf.content(),
791 [Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
792 );
793 }
794
795 #[test]
796 fn render_second_zero_width() {
797 let span = Span::raw("a\u{200B}bc");
798 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
799 span.render(buf.area, &mut buf);
800 assert_eq!(
801 buf.content(),
802 [Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
803 );
804 }
805
806 #[test]
807 fn render_middle_zero_width() {
808 let span = Span::raw("ab\u{200B}c");
809 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
810 span.render(buf.area, &mut buf);
811 assert_eq!(
812 buf.content(),
813 [Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
814 );
815 }
816
817 #[test]
818 fn render_last_zero_width() {
819 let span = Span::raw("abc\u{200B}");
820 let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
821 span.render(buf.area, &mut buf);
822 assert_eq!(
823 buf.content(),
824 [Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
825 );
826 }
827
828 #[test]
829 fn render_with_newlines() {
830 let span = Span::raw("a\nb");
831 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
832 span.render(buf.area, &mut buf);
833 assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
834 }
835 }
836
837 #[test]
844 fn issue_1160() {
845 let span = Span::raw("Hello\u{200E}");
846 let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
847 span.render(buf.area, &mut buf);
848 assert_eq!(
849 buf.content(),
850 [
851 Cell::new("H"),
852 Cell::new("e"),
853 Cell::new("l"),
854 Cell::new("l"),
855 Cell::new("o\u{200E}"),
856 ]
857 );
858 }
859
860 #[test]
861 fn add() {
862 assert_eq!(
863 Span::default() + Span::default(),
864 Line::from(vec![Span::default(), Span::default()])
865 );
866
867 assert_eq!(
868 Span::default() + Span::raw("test"),
869 Line::from(vec![Span::default(), Span::raw("test")])
870 );
871
872 assert_eq!(
873 Span::raw("test") + Span::default(),
874 Line::from(vec![Span::raw("test"), Span::default()])
875 );
876
877 assert_eq!(
878 Span::raw("test") + Span::raw("content"),
879 Line::from(vec![Span::raw("test"), Span::raw("content")])
880 );
881 }
882
883 #[rstest]
884 #[case::default(Span::default(), "Span::default()")]
885 #[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
886 #[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
887 #[case::styled_italic(
888 Span::styled("test", Style::new().green().italic()),
889 r#"Span::from("test").green().italic()"#
890 )]
891 fn debug(#[case] span: Span, #[case] expected: &str) {
892 assert_eq!(format!("{span:?}"), expected);
893 }
894}