1#![deny(missing_docs)]
2#![warn(clippy::pedantic, clippy::nursery, clippy::arithmetic_side_effects)]
3use alloc::borrow::Cow;
4use alloc::string::{String, ToString};
5use alloc::vec;
6use alloc::vec::Vec;
7use core::fmt;
8
9use unicode_truncate::UnicodeTruncateStr;
10use unicode_width::UnicodeWidthStr;
11
12use crate::buffer::Buffer;
13use crate::layout::{Alignment, Rect};
14use crate::style::{Style, Styled};
15use crate::text::{Span, StyledGrapheme, Text};
16use crate::widgets::Widget;
17
18#[derive(Default, Clone, Eq, PartialEq, Hash)]
184pub struct Line<'a> {
185 pub style: Style,
187
188 pub alignment: Option<Alignment>,
190
191 pub spans: Vec<Span<'a>>,
193}
194
195impl fmt::Debug for Line<'_> {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 if self.spans.is_empty() {
198 f.write_str("Line::default()")?;
199 } else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
200 f.write_str(r#"Line::from(""#)?;
201 f.write_str(&self.spans[0].content)?;
202 f.write_str(r#"")"#)?;
203 } else if self.spans.len() == 1 {
204 f.write_str("Line::from(")?;
205 self.spans[0].fmt(f)?;
206 f.write_str(")")?;
207 } else {
208 f.write_str("Line::from_iter(")?;
209 f.debug_list().entries(&self.spans).finish()?;
210 f.write_str(")")?;
211 }
212 self.style.fmt_stylize(f)?;
213 match self.alignment {
214 Some(Alignment::Left) => write!(f, ".left_aligned()"),
215 Some(Alignment::Center) => write!(f, ".centered()"),
216 Some(Alignment::Right) => write!(f, ".right_aligned()"),
217 None => Ok(()),
218 }
219 }
220}
221
222fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
223 match content.into() {
224 Cow::Borrowed(s) => s.lines().map(Span::raw).collect(),
225 Cow::Owned(s) => s.lines().map(|v| Span::raw(v.to_string())).collect(),
226 }
227}
228
229impl<'a> Line<'a> {
230 pub fn raw<T>(content: T) -> Self
252 where
253 T: Into<Cow<'a, str>>,
254 {
255 Self {
256 spans: cow_to_spans(content),
257 ..Default::default()
258 }
259 }
260
261 pub fn styled<T, S>(content: T, style: S) -> Self
287 where
288 T: Into<Cow<'a, str>>,
289 S: Into<Style>,
290 {
291 Self {
292 spans: cow_to_spans(content),
293 style: style.into(),
294 ..Default::default()
295 }
296 }
297
298 #[must_use = "method moves the value of self and returns the modified value"]
313 pub fn spans<I>(mut self, spans: I) -> Self
314 where
315 I: IntoIterator,
316 I::Item: Into<Span<'a>>,
317 {
318 self.spans = spans.into_iter().map(Into::into).collect();
319 self
320 }
321
322 #[must_use = "method moves the value of self and returns the modified value"]
343 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
344 self.style = style.into();
345 self
346 }
347
348 #[must_use = "method moves the value of self and returns the modified value"]
368 pub fn alignment(self, alignment: Alignment) -> Self {
369 Self {
370 alignment: Some(alignment),
371 ..self
372 }
373 }
374
375 #[must_use = "method moves the value of self and returns the modified value"]
389 pub fn left_aligned(self) -> Self {
390 self.alignment(Alignment::Left)
391 }
392
393 #[must_use = "method moves the value of self and returns the modified value"]
407 pub fn centered(self) -> Self {
408 self.alignment(Alignment::Center)
409 }
410
411 #[must_use = "method moves the value of self and returns the modified value"]
425 pub fn right_aligned(self) -> Self {
426 self.alignment(Alignment::Right)
427 }
428
429 #[must_use]
441 pub fn width(&self) -> usize {
442 UnicodeWidthStr::width(self)
443 }
444
445 pub fn styled_graphemes<S: Into<Style>>(
477 &'a self,
478 base_style: S,
479 ) -> impl Iterator<Item = StyledGrapheme<'a>> {
480 let style = base_style.into().patch(self.style);
481 self.spans
482 .iter()
483 .flat_map(move |span| span.styled_graphemes(style))
484 }
485
486 #[must_use = "method moves the value of self and returns the modified value"]
512 pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
513 self.style = self.style.patch(style);
514 self
515 }
516
517 #[must_use = "method moves the value of self and returns the modified value"]
535 pub fn reset_style(self) -> Self {
536 self.patch_style(Style::reset())
537 }
538
539 pub fn iter(&self) -> core::slice::Iter<'_, Span<'a>> {
541 self.spans.iter()
542 }
543
544 pub fn iter_mut(&mut self) -> core::slice::IterMut<'_, Span<'a>> {
546 self.spans.iter_mut()
547 }
548
549 pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
564 self.spans.push(span.into());
565 }
566}
567
568impl UnicodeWidthStr for Line<'_> {
569 fn width(&self) -> usize {
570 self.spans.iter().map(UnicodeWidthStr::width).sum()
571 }
572
573 fn width_cjk(&self) -> usize {
574 self.spans.iter().map(UnicodeWidthStr::width_cjk).sum()
575 }
576}
577
578impl<'a> IntoIterator for Line<'a> {
579 type Item = Span<'a>;
580 type IntoIter = alloc::vec::IntoIter<Span<'a>>;
581
582 fn into_iter(self) -> Self::IntoIter {
583 self.spans.into_iter()
584 }
585}
586
587impl<'a> IntoIterator for &'a Line<'a> {
588 type Item = &'a Span<'a>;
589 type IntoIter = core::slice::Iter<'a, Span<'a>>;
590
591 fn into_iter(self) -> Self::IntoIter {
592 self.iter()
593 }
594}
595
596impl<'a> IntoIterator for &'a mut Line<'a> {
597 type Item = &'a mut Span<'a>;
598 type IntoIter = core::slice::IterMut<'a, Span<'a>>;
599
600 fn into_iter(self) -> Self::IntoIter {
601 self.iter_mut()
602 }
603}
604
605impl From<String> for Line<'_> {
606 fn from(s: String) -> Self {
607 Self::raw(s)
608 }
609}
610
611impl<'a> From<&'a str> for Line<'a> {
612 fn from(s: &'a str) -> Self {
613 Self::raw(s)
614 }
615}
616
617impl<'a> From<Cow<'a, str>> for Line<'a> {
618 fn from(s: Cow<'a, str>) -> Self {
619 Self::raw(s)
620 }
621}
622
623impl<'a> From<Vec<Span<'a>>> for Line<'a> {
624 fn from(spans: Vec<Span<'a>>) -> Self {
625 Self {
626 spans,
627 ..Default::default()
628 }
629 }
630}
631
632impl<'a, 'b, T> From<&'b [T]> for Line<'a>
633where
634 T: Into<Span<'a>> + Clone,
635{
636 fn from(value: &'b [T]) -> Self {
637 Self {
638 spans: value.iter().cloned().map(Into::into).collect(),
639 ..Default::default()
640 }
641 }
642}
643
644impl<'a> From<Span<'a>> for Line<'a> {
645 fn from(span: Span<'a>) -> Self {
646 Self::from(vec![span])
647 }
648}
649
650impl<'a> From<Line<'a>> for String {
651 fn from(line: Line<'a>) -> Self {
652 line.iter().fold(Self::new(), |mut acc, s| {
653 acc.push_str(s.content.as_ref());
654 acc
655 })
656 }
657}
658
659impl<'a, T> FromIterator<T> for Line<'a>
660where
661 T: Into<Span<'a>>,
662{
663 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
664 Self::from(iter.into_iter().map(Into::into).collect::<Vec<_>>())
665 }
666}
667
668impl<'a> core::ops::Add<Span<'a>> for Line<'a> {
670 type Output = Self;
671
672 fn add(mut self, rhs: Span<'a>) -> Self::Output {
673 self.spans.push(rhs);
674 self
675 }
676}
677
678impl<'a> core::ops::Add<Self> for Line<'a> {
680 type Output = Text<'a>;
681
682 fn add(self, rhs: Self) -> Self::Output {
683 Text::from(vec![self, rhs])
684 }
685}
686
687impl<'a> core::ops::AddAssign<Span<'a>> for Line<'a> {
688 fn add_assign(&mut self, rhs: Span<'a>) {
689 self.spans.push(rhs);
690 }
691}
692
693impl<'a> Extend<Span<'a>> for Line<'a> {
694 fn extend<T: IntoIterator<Item = Span<'a>>>(&mut self, iter: T) {
695 self.spans.extend(iter);
696 }
697}
698
699impl Widget for Line<'_> {
700 fn render(self, area: Rect, buf: &mut Buffer) {
701 Widget::render(&self, area, buf);
702 }
703}
704
705impl Widget for &Line<'_> {
706 fn render(self, area: Rect, buf: &mut Buffer) {
707 self.render_with_alignment(area, buf, None);
708 }
709}
710
711impl Line<'_> {
712 pub(crate) fn render_with_alignment(
715 &self,
716 area: Rect,
717 buf: &mut Buffer,
718 parent_alignment: Option<Alignment>,
719 ) {
720 let area = area.intersection(buf.area);
721 if area.is_empty() {
722 return;
723 }
724 let area = Rect { height: 1, ..area };
725 let line_width = self.width();
726 if line_width == 0 {
727 return;
728 }
729
730 buf.set_style(area, self.style);
731
732 let alignment = self.alignment.or(parent_alignment);
733
734 let area_width = usize::from(area.width);
735 let can_render_complete_line = line_width <= area_width;
736 if can_render_complete_line {
737 let indent_width = match alignment {
738 Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
739 Some(Alignment::Right) => area_width.saturating_sub(line_width),
740 Some(Alignment::Left) | None => 0,
741 };
742 let indent_width = u16::try_from(indent_width).unwrap_or(u16::MAX);
743 let area = area.indent_x(indent_width);
744 render_spans(&self.spans, area, buf, 0);
745 } else {
746 let skip_width = match alignment {
749 Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
750 Some(Alignment::Right) => line_width.saturating_sub(area_width),
751 Some(Alignment::Left) | None => 0,
752 };
753 render_spans(&self.spans, area, buf, skip_width);
754 }
755 }
756}
757
758fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_width: usize) {
760 for (span, span_width, offset) in spans_after_width(spans, span_skip_width) {
761 area = area.indent_x(offset);
762 if area.is_empty() {
763 break;
764 }
765 span.render(area, buf);
766 let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
767 area = area.indent_x(span_width);
768 }
769}
770
771fn spans_after_width<'a>(
774 spans: &'a [Span],
775 mut skip_width: usize,
776) -> impl Iterator<Item = (Span<'a>, usize, u16)> {
777 spans
778 .iter()
779 .map(|span| (span, span.width()))
780 .filter_map(move |(span, span_width)| {
782 if skip_width >= span_width {
785 skip_width = skip_width.saturating_sub(span_width);
786 return None;
787 }
788
789 let available_width = span_width.saturating_sub(skip_width);
792 skip_width = 0; Some((span, span_width, available_width))
794 })
795 .map(|(span, span_width, available_width)| {
796 if span_width <= available_width {
797 return (span.clone(), span_width, 0u16);
799 }
800 let (content, actual_width) = span.content.unicode_truncate_start(available_width);
803
804 let first_grapheme_offset = available_width.saturating_sub(actual_width);
807 let first_grapheme_offset = u16::try_from(first_grapheme_offset).unwrap_or(u16::MAX);
808 (
809 Span::styled(content, span.style),
810 actual_width,
811 first_grapheme_offset,
812 )
813 })
814}
815
816pub trait ToLine {
824 fn to_line(&self) -> Line<'_>;
826}
827
828impl<T: fmt::Display> ToLine for T {
834 fn to_line(&self) -> Line<'_> {
835 Line::from(self.to_string())
836 }
837}
838
839impl fmt::Display for Line<'_> {
840 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
841 for span in &self.spans {
842 write!(f, "{span}")?;
843 }
844 Ok(())
845 }
846}
847
848impl Styled for Line<'_> {
849 type Item = Self;
850
851 fn style(&self) -> Style {
852 self.style
853 }
854
855 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
856 self.style(style)
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 #[cfg(not(feature = "std"))]
863 extern crate std;
864
865 use alloc::format;
866 use core::iter;
867 use std::dbg;
868
869 use rstest::{fixture, rstest};
870
871 use super::*;
872 use crate::style::{Color, Modifier, Stylize};
873
874 #[fixture]
875 fn small_buf() -> Buffer {
876 Buffer::empty(Rect::new(0, 0, 10, 1))
877 }
878
879 #[test]
880 fn raw_str() {
881 let line = Line::raw("test content");
882 assert_eq!(line.spans, [Span::raw("test content")]);
883 assert_eq!(line.alignment, None);
884
885 let line = Line::raw("a\nb");
886 assert_eq!(line.spans, [Span::raw("a"), Span::raw("b")]);
887 assert_eq!(line.alignment, None);
888 }
889
890 #[test]
891 fn styled_str() {
892 let style = Style::new().yellow();
893 let content = "Hello, world!";
894 let line = Line::styled(content, style);
895 assert_eq!(line.spans, [Span::raw(content)]);
896 assert_eq!(line.style, style);
897 }
898
899 #[test]
900 fn styled_string() {
901 let style = Style::new().yellow();
902 let content = String::from("Hello, world!");
903 let line = Line::styled(content.clone(), style);
904 assert_eq!(line.spans, [Span::raw(content)]);
905 assert_eq!(line.style, style);
906 }
907
908 #[test]
909 fn styled_cow() {
910 let style = Style::new().yellow();
911 let content = Cow::from("Hello, world!");
912 let line = Line::styled(content.clone(), style);
913 assert_eq!(line.spans, [Span::raw(content)]);
914 assert_eq!(line.style, style);
915 }
916
917 #[test]
918 fn spans_vec() {
919 let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
920 assert_eq!(
921 line.spans,
922 vec![
923 Span::styled("Hello", Style::new().blue()),
924 Span::styled(" world!", Style::new().green()),
925 ]
926 );
927 }
928
929 #[test]
930 fn spans_iter() {
931 let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {i}")));
932 assert_eq!(
933 line.spans,
934 vec![
935 Span::raw("Item 1"),
936 Span::raw("Item 2"),
937 Span::raw("Item 3"),
938 ]
939 );
940 }
941
942 #[test]
943 fn style() {
944 let line = Line::default().style(Style::new().red());
945 assert_eq!(line.style, Style::new().red());
946 }
947
948 #[test]
949 fn alignment() {
950 let line = Line::from("This is left").alignment(Alignment::Left);
951 assert_eq!(Some(Alignment::Left), line.alignment);
952
953 let line = Line::from("This is default");
954 assert_eq!(None, line.alignment);
955 }
956
957 #[test]
958 fn width() {
959 let line = Line::from(vec![
960 Span::styled("My", Style::default().fg(Color::Yellow)),
961 Span::raw(" text"),
962 ]);
963 assert_eq!(7, line.width());
964
965 let empty_line = Line::default();
966 assert_eq!(0, empty_line.width());
967 }
968
969 #[test]
970 fn patch_style() {
971 let raw_line = Line::styled("foobar", Color::Yellow);
972 let styled_line = Line::styled("foobar", (Color::Yellow, Modifier::ITALIC));
973
974 assert_ne!(raw_line, styled_line);
975
976 let raw_line = raw_line.patch_style(Modifier::ITALIC);
977 assert_eq!(raw_line, styled_line);
978 }
979
980 #[test]
981 fn reset_style() {
982 let line =
983 Line::styled("foobar", Style::default().yellow().on_red().italic()).reset_style();
984
985 assert_eq!(Style::reset(), line.style);
986 }
987
988 #[test]
989 fn stylize() {
990 assert_eq!(Line::default().green().style, Color::Green.into());
991 assert_eq!(
992 Line::default().on_green().style,
993 Style::new().bg(Color::Green)
994 );
995 assert_eq!(Line::default().italic().style, Modifier::ITALIC.into());
996 }
997
998 #[test]
999 fn from_string() {
1000 let s = String::from("Hello, world!");
1001 let line = Line::from(s);
1002 assert_eq!(line.spans, [Span::from("Hello, world!")]);
1003
1004 let s = String::from("Hello\nworld!");
1005 let line = Line::from(s);
1006 assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
1007 }
1008
1009 #[test]
1010 fn from_str() {
1011 let s = "Hello, world!";
1012 let line = Line::from(s);
1013 assert_eq!(line.spans, [Span::from("Hello, world!")]);
1014
1015 let s = "Hello\nworld!";
1016 let line = Line::from(s);
1017 assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
1018 }
1019
1020 #[test]
1021 fn to_line() {
1022 let line = 42.to_line();
1023 assert_eq!(line.spans, [Span::from("42")]);
1024 }
1025
1026 #[test]
1027 fn from_vec() {
1028 let spans = vec![
1029 Span::styled("Hello,", Style::default().fg(Color::Red)),
1030 Span::styled(" world!", Style::default().fg(Color::Green)),
1031 ];
1032 let line = Line::from(spans.clone());
1033 assert_eq!(line.spans, spans);
1034 }
1035
1036 #[test]
1037 fn from_slice_of_spans() {
1038 let slice = [
1039 Span::from("Hello"),
1040 Span::from("world!"),
1041 Span::from("Extra"),
1042 ];
1043 let line = Line::from(&slice[0..2]);
1044 let line2 = Line::from(&slice[1..]);
1045 assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
1046 assert_eq!(line2.spans, vec![Span::from("world!"), Span::from("Extra")]);
1047 }
1048
1049 #[test]
1050 fn from_slice_of_strs() {
1051 let slice = ["Hello", "world!", "Extra"];
1052 let line = Line::from(&slice[0..2]);
1053 let line2 = Line::from(&slice[1..]);
1054 assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
1055 assert_eq!(line2.spans, vec![Span::from("world!"), Span::from("Extra")]);
1056 }
1057
1058 #[test]
1059 fn from_slice_of_strings() {
1060 let slice = [
1061 "Hello".to_string(),
1062 "world!".to_string(),
1063 "Extra".to_string(),
1064 ];
1065 let line = Line::from(&slice[0..2]);
1066 let line2 = Line::from(&slice[1..]);
1067 assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
1068 assert_eq!(line2.spans, vec![Span::from("world!"), Span::from("Extra")]);
1069 }
1070
1071 #[test]
1072 fn from_iter() {
1073 let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
1074 assert_eq!(
1075 line.spans,
1076 vec![
1077 Span::styled("Hello", Style::new().blue()),
1078 Span::styled(" world!", Style::new().green()),
1079 ]
1080 );
1081 }
1082
1083 #[test]
1084 fn collect() {
1085 let line: Line = iter::once("Hello".blue())
1086 .chain(iter::once(" world!".green()))
1087 .collect();
1088 assert_eq!(
1089 line.spans,
1090 vec![
1091 Span::styled("Hello", Style::new().blue()),
1092 Span::styled(" world!", Style::new().green()),
1093 ]
1094 );
1095 }
1096
1097 #[test]
1098 fn from_span() {
1099 let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
1100 let line = Line::from(span.clone());
1101 assert_eq!(line.spans, [span]);
1102 }
1103
1104 #[test]
1105 fn add_span() {
1106 assert_eq!(
1107 Line::raw("Red").red() + Span::raw("blue").blue(),
1108 Line {
1109 spans: vec![Span::raw("Red"), Span::raw("blue").blue()],
1110 style: Style::new().red(),
1111 alignment: None,
1112 },
1113 );
1114 }
1115
1116 #[test]
1117 fn add_line() {
1118 assert_eq!(
1119 Line::raw("Red").red() + Line::raw("Blue").blue(),
1120 Text {
1121 lines: vec![Line::raw("Red").red(), Line::raw("Blue").blue()],
1122 style: Style::default(),
1123 alignment: None,
1124 }
1125 );
1126 }
1127
1128 #[test]
1129 fn add_assign_span() {
1130 let mut line = Line::raw("Red").red();
1131 line += Span::raw("Blue").blue();
1132 assert_eq!(
1133 line,
1134 Line {
1135 spans: vec![Span::raw("Red"), Span::raw("Blue").blue()],
1136 style: Style::new().red(),
1137 alignment: None,
1138 },
1139 );
1140 }
1141
1142 #[test]
1143 fn extend() {
1144 let mut line = Line::from("Hello, ");
1145 line.extend([Span::raw("world!")]);
1146 assert_eq!(line.spans, [Span::raw("Hello, "), Span::raw("world!")]);
1147
1148 let mut line = Line::from("Hello, ");
1149 line.extend([Span::raw("world! "), Span::raw("How are you?")]);
1150 assert_eq!(
1151 line.spans,
1152 [
1153 Span::raw("Hello, "),
1154 Span::raw("world! "),
1155 Span::raw("How are you?")
1156 ]
1157 );
1158 }
1159
1160 #[test]
1161 fn into_string() {
1162 let line = Line::from(vec![
1163 Span::styled("Hello,", Style::default().fg(Color::Red)),
1164 Span::styled(" world!", Style::default().fg(Color::Green)),
1165 ]);
1166 let s: String = line.into();
1167 assert_eq!(s, "Hello, world!");
1168 }
1169
1170 #[test]
1171 fn styled_graphemes() {
1172 const RED: Style = Style::new().red();
1173 const GREEN: Style = Style::new().green();
1174 const BLUE: Style = Style::new().blue();
1175 const RED_ON_WHITE: Style = Style::new().red().on_white();
1176 const GREEN_ON_WHITE: Style = Style::new().green().on_white();
1177 const BLUE_ON_WHITE: Style = Style::new().blue().on_white();
1178
1179 let line = Line::from(vec![
1180 Span::styled("He", RED),
1181 Span::styled("ll", GREEN),
1182 Span::styled("o!", BLUE),
1183 ]);
1184 let styled_graphemes = line
1185 .styled_graphemes(Style::new().bg(Color::White))
1186 .collect::<Vec<StyledGrapheme>>();
1187 assert_eq!(
1188 styled_graphemes,
1189 vec![
1190 StyledGrapheme::new("H", RED_ON_WHITE),
1191 StyledGrapheme::new("e", RED_ON_WHITE),
1192 StyledGrapheme::new("l", GREEN_ON_WHITE),
1193 StyledGrapheme::new("l", GREEN_ON_WHITE),
1194 StyledGrapheme::new("o", BLUE_ON_WHITE),
1195 StyledGrapheme::new("!", BLUE_ON_WHITE),
1196 ],
1197 );
1198 }
1199
1200 #[test]
1201 fn display_line_from_vec() {
1202 let line_from_vec = Line::from(vec![Span::raw("Hello,"), Span::raw(" world!")]);
1203
1204 assert_eq!(format!("{line_from_vec}"), "Hello, world!");
1205 }
1206
1207 #[test]
1208 fn display_styled_line() {
1209 let styled_line = Line::styled("Hello, world!", Style::new().green().italic());
1210
1211 assert_eq!(format!("{styled_line}"), "Hello, world!");
1212 }
1213
1214 #[test]
1215 fn display_line_from_styled_span() {
1216 let styled_span = Span::styled("Hello, world!", Style::new().green().italic());
1217 let line_from_styled_span = Line::from(styled_span);
1218
1219 assert_eq!(format!("{line_from_styled_span}"), "Hello, world!");
1220 }
1221
1222 #[test]
1223 fn left_aligned() {
1224 let line = Line::from("Hello, world!").left_aligned();
1225 assert_eq!(line.alignment, Some(Alignment::Left));
1226 }
1227
1228 #[test]
1229 fn centered() {
1230 let line = Line::from("Hello, world!").centered();
1231 assert_eq!(line.alignment, Some(Alignment::Center));
1232 }
1233
1234 #[test]
1235 fn right_aligned() {
1236 let line = Line::from("Hello, world!").right_aligned();
1237 assert_eq!(line.alignment, Some(Alignment::Right));
1238 }
1239
1240 #[test]
1241 pub fn push_span() {
1242 let mut line = Line::from("A");
1243 line.push_span(Span::raw("B"));
1244 line.push_span("C");
1245 assert_eq!(
1246 line.spans,
1247 vec![Span::raw("A"), Span::raw("B"), Span::raw("C")]
1248 );
1249 }
1250
1251 mod widget {
1252 use unicode_segmentation::UnicodeSegmentation;
1253 use unicode_width::UnicodeWidthStr;
1254
1255 use super::*;
1256 use crate::buffer::Cell;
1257
1258 const BLUE: Style = Style::new().blue();
1259 const GREEN: Style = Style::new().green();
1260 const ITALIC: Style = Style::new().italic();
1261
1262 #[fixture]
1263 fn hello_world() -> Line<'static> {
1264 Line::from(vec![
1265 Span::styled("Hello ", BLUE),
1266 Span::styled("world!", GREEN),
1267 ])
1268 .style(ITALIC)
1269 }
1270
1271 #[test]
1272 fn render() {
1273 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
1274 hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
1275 let mut expected = Buffer::with_lines(["Hello world! "]);
1276 expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
1277 expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
1278 expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
1279 assert_eq!(buf, expected);
1280 }
1281
1282 #[rstest]
1283 fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
1284 let out_of_bounds = Rect::new(20, 20, 10, 1);
1285 hello_world.render(out_of_bounds, &mut small_buf);
1286 assert_eq!(small_buf, Buffer::empty(small_buf.area));
1287 }
1288
1289 #[test]
1290 fn render_only_styles_line_area() {
1291 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
1292 hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
1293 let mut expected = Buffer::with_lines(["Hello world! "]);
1294 expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
1295 expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
1296 expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
1297 assert_eq!(buf, expected);
1298 }
1299
1300 #[test]
1301 fn render_only_styles_first_line() {
1302 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 2));
1303 hello_world().render(buf.area, &mut buf);
1304 let mut expected = Buffer::with_lines(["Hello world! ", " "]);
1305 expected.set_style(Rect::new(0, 0, 20, 1), ITALIC);
1306 expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
1307 expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
1308 assert_eq!(buf, expected);
1309 }
1310
1311 #[test]
1312 fn render_truncates() {
1313 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
1314 Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
1315 assert_eq!(buf, Buffer::with_lines(["Hello "]));
1316 }
1317
1318 #[test]
1319 fn render_centered() {
1320 let line = hello_world().alignment(Alignment::Center);
1321 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
1322 line.render(Rect::new(0, 0, 15, 1), &mut buf);
1323 let mut expected = Buffer::with_lines([" Hello world! "]);
1324 expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
1325 expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
1326 expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
1327 assert_eq!(buf, expected);
1328 }
1329
1330 #[test]
1331 fn render_right_aligned() {
1332 let line = hello_world().alignment(Alignment::Right);
1333 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
1334 line.render(Rect::new(0, 0, 15, 1), &mut buf);
1335 let mut expected = Buffer::with_lines([" Hello world!"]);
1336 expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
1337 expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
1338 expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
1339 assert_eq!(buf, expected);
1340 }
1341
1342 #[test]
1343 fn render_truncates_left() {
1344 let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
1345 Line::from("Hello world")
1346 .left_aligned()
1347 .render(buf.area, &mut buf);
1348 assert_eq!(buf, Buffer::with_lines(["Hello"]));
1349 }
1350
1351 #[test]
1352 fn render_truncates_right() {
1353 let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
1354 Line::from("Hello world")
1355 .right_aligned()
1356 .render(buf.area, &mut buf);
1357 assert_eq!(buf, Buffer::with_lines(["world"]));
1358 }
1359
1360 #[test]
1361 fn render_truncates_center() {
1362 let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
1363 Line::from("Hello world")
1364 .centered()
1365 .render(buf.area, &mut buf);
1366 assert_eq!(buf, Buffer::with_lines(["lo wo"]));
1367 }
1368
1369 #[test]
1372 fn regression_1032() {
1373 let line = Line::from(
1374 "🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する",
1375 );
1376 let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
1377 line.render(buf.area, &mut buf);
1378 assert_eq!(
1379 buf,
1380 Buffer::with_lines([
1381 "🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
1382 ])
1383 );
1384 }
1385
1386 #[test]
1391 fn crab_emoji_width() {
1392 let crab = "🦀";
1393 assert_eq!(crab.len(), 4); assert_eq!(crab.chars().count(), 1);
1395 assert_eq!(crab.graphemes(true).count(), 1);
1396 assert_eq!(crab.width(), 2); }
1398
1399 #[rstest]
1402 #[case::left_4(Alignment::Left, 4, "1234")]
1403 #[case::left_5(Alignment::Left, 5, "1234 ")]
1404 #[case::left_6(Alignment::Left, 6, "1234🦀")]
1405 #[case::left_7(Alignment::Left, 7, "1234🦀7")]
1406 #[case::right_4(Alignment::Right, 4, "7890")]
1407 #[case::right_5(Alignment::Right, 5, " 7890")]
1408 #[case::right_6(Alignment::Right, 6, "🦀7890")]
1409 #[case::right_7(Alignment::Right, 7, "4🦀7890")]
1410 fn render_truncates_emoji(
1411 #[case] alignment: Alignment,
1412 #[case] buf_width: u16,
1413 #[case] expected: &str,
1414 ) {
1415 let line = Line::from("1234🦀7890").alignment(alignment);
1416 let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
1417 line.render(buf.area, &mut buf);
1418 assert_eq!(buf, Buffer::with_lines([expected]));
1419 }
1420
1421 #[rstest]
1429 #[case::center_6_0(6, 0, "")]
1430 #[case::center_6_1(6, 1, " ")] #[case::center_6_2(6, 2, "🦀")]
1432 #[case::center_6_3(6, 3, "b🦀")]
1433 #[case::center_6_4(6, 4, "b🦀c")]
1434 #[case::center_7_0(7, 0, "")]
1435 #[case::center_7_1(7, 1, " ")] #[case::center_7_2(7, 2, "🦀")]
1437 #[case::center_7_3(7, 3, "🦀c")]
1438 #[case::center_7_4(7, 4, "b🦀c")]
1439 #[case::center_8_0(8, 0, "")]
1440 #[case::center_8_1(8, 1, " ")] #[case::center_8_2(8, 2, " c")] #[case::center_8_3(8, 3, "🦀c")]
1443 #[case::center_8_4(8, 4, "🦀cd")]
1444 #[case::center_8_5(8, 5, "b🦀cd")]
1445 #[case::center_9_0(9, 0, "")]
1446 #[case::center_9_1(9, 1, "c")]
1447 #[case::center_9_2(9, 2, " c")] #[case::center_9_3(9, 3, " cd")]
1449 #[case::center_9_4(9, 4, "🦀cd")]
1450 #[case::center_9_5(9, 5, "🦀cde")]
1451 #[case::center_9_6(9, 6, "b🦀cde")]
1452 fn render_truncates_emoji_center(
1453 #[case] line_width: u16,
1454 #[case] buf_width: u16,
1455 #[case] expected: &str,
1456 ) {
1457 let value = match line_width {
1462 6 => "ab🦀cd",
1463 7 => "ab🦀cde",
1464 8 => "ab🦀cdef",
1465 9 => "ab🦀cdefg",
1466 _ => unreachable!(),
1467 };
1468 let line = Line::from(value).centered();
1469 let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
1470 line.render(buf.area, &mut buf);
1471 assert_eq!(buf, Buffer::with_lines([expected]));
1472 }
1473
1474 #[rstest]
1480 #[case::left(Alignment::Left, "XXa🦀bcXXX")]
1481 #[case::center(Alignment::Center, "XX🦀bc🦀XX")]
1482 #[case::right(Alignment::Right, "XXXbc🦀dXX")]
1483 fn render_truncates_away_from_0x0(#[case] alignment: Alignment, #[case] expected: &str) {
1484 let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).alignment(alignment);
1485 let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
1487 let area = Rect::new(2, 0, 6, 1);
1488 line.render(area, &mut buf);
1489 assert_eq!(buf, Buffer::with_lines([expected]));
1490 }
1491
1492 #[rstest]
1496 #[case::right_4(4, "c🦀d")]
1497 #[case::right_5(5, "bc🦀d")]
1498 #[case::right_6(6, "Xbc🦀d")]
1499 #[case::right_7(7, "🦀bc🦀d")]
1500 #[case::right_8(8, "a🦀bc🦀d")]
1501 fn render_right_aligned_multi_span(#[case] buf_width: u16, #[case] expected: &str) {
1502 let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).right_aligned();
1503 let area = Rect::new(0, 0, buf_width, 1);
1504 let mut buf = Buffer::filled(area, Cell::new("X"));
1506 line.render(buf.area, &mut buf);
1507 assert_eq!(buf, Buffer::with_lines([expected]));
1508 }
1509
1510 #[test]
1516 fn flag_emoji() {
1517 let str = "🇺🇸1234";
1518 assert_eq!(str.len(), 12); assert_eq!(str.chars().count(), 6); assert_eq!(str.graphemes(true).count(), 5); assert_eq!(str.width(), 6); }
1523
1524 #[rstest]
1527 #[case::flag_1(1, " ")]
1528 #[case::flag_2(2, "🇺🇸")]
1529 #[case::flag_3(3, "🇺🇸1")]
1530 #[case::flag_4(4, "🇺🇸12")]
1531 #[case::flag_5(5, "🇺🇸123")]
1532 #[case::flag_6(6, "🇺🇸1234")]
1533 #[case::flag_7(7, "🇺🇸1234 ")]
1534 fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
1535 let line = Line::from("🇺🇸1234");
1536 let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
1537 line.render(buf.area, &mut buf);
1538 assert_eq!(buf, Buffer::with_lines([expected]));
1539 }
1540
1541 #[rstest]
1543 #[case::left(Alignment::Left, "This is some content with a some")]
1544 #[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
1545 fn render_truncates_very_long_line_of_many_spans(
1546 #[case] alignment: Alignment,
1547 #[case] expected: &str,
1548 ) {
1549 let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
1550 let min_width = usize::from(u16::MAX).saturating_add(1);
1551
1552 let factor = min_width.div_ceil(part.len());
1554
1555 let line = Line::from(vec![Span::raw(part); factor]).alignment(alignment);
1556
1557 dbg!(line.width());
1558 assert!(line.width() >= min_width);
1559
1560 let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
1561 line.render(buf.area, &mut buf);
1562 assert_eq!(buf, Buffer::with_lines([expected]));
1563 }
1564
1565 #[rstest]
1567 #[case::left(Alignment::Left, "This is some content with a some")]
1568 #[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
1569 fn render_truncates_very_long_single_span_line(
1570 #[case] alignment: Alignment,
1571 #[case] expected: &str,
1572 ) {
1573 let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
1574 let min_width = usize::from(u16::MAX).saturating_add(1);
1575
1576 let factor = min_width.div_ceil(part.len());
1578
1579 let line = Line::from(vec![Span::raw(part.repeat(factor))]).alignment(alignment);
1580
1581 dbg!(line.width());
1582 assert!(line.width() >= min_width);
1583
1584 let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
1585 line.render(buf.area, &mut buf);
1586 assert_eq!(buf, Buffer::with_lines([expected]));
1587 }
1588
1589 #[test]
1590 fn render_with_newlines() {
1591 let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
1592 Line::from("Hello\nworld!").render(Rect::new(0, 0, 11, 1), &mut buf);
1593 assert_eq!(buf, Buffer::with_lines(["Helloworld!"]));
1594 }
1595 }
1596
1597 mod iterators {
1598 use super::*;
1599
1600 #[fixture]
1602 fn hello_world() -> Line<'static> {
1603 Line::from(vec![
1604 Span::styled("Hello ", Color::Blue),
1605 Span::styled("world!", Color::Green),
1606 ])
1607 }
1608
1609 #[rstest]
1610 fn iter(hello_world: Line<'_>) {
1611 let mut iter = hello_world.iter();
1612 assert_eq!(iter.next(), Some(&Span::styled("Hello ", Color::Blue)));
1613 assert_eq!(iter.next(), Some(&Span::styled("world!", Color::Green)));
1614 assert_eq!(iter.next(), None);
1615 }
1616
1617 #[rstest]
1618 fn iter_mut(mut hello_world: Line<'_>) {
1619 let mut iter = hello_world.iter_mut();
1620 assert_eq!(iter.next(), Some(&mut Span::styled("Hello ", Color::Blue)));
1621 assert_eq!(iter.next(), Some(&mut Span::styled("world!", Color::Green)));
1622 assert_eq!(iter.next(), None);
1623 }
1624
1625 #[rstest]
1626 fn into_iter(hello_world: Line<'_>) {
1627 let mut iter = hello_world.into_iter();
1628 assert_eq!(iter.next(), Some(Span::styled("Hello ", Color::Blue)));
1629 assert_eq!(iter.next(), Some(Span::styled("world!", Color::Green)));
1630 assert_eq!(iter.next(), None);
1631 }
1632
1633 #[rstest]
1634 fn into_iter_ref(hello_world: Line<'_>) {
1635 let mut iter = (&hello_world).into_iter();
1636 assert_eq!(iter.next(), Some(&Span::styled("Hello ", Color::Blue)));
1637 assert_eq!(iter.next(), Some(&Span::styled("world!", Color::Green)));
1638 assert_eq!(iter.next(), None);
1639 }
1640
1641 #[test]
1642 fn into_iter_mut_ref() {
1643 let mut hello_world = Line::from(vec![
1644 Span::styled("Hello ", Color::Blue),
1645 Span::styled("world!", Color::Green),
1646 ]);
1647 let mut iter = (&mut hello_world).into_iter();
1648 assert_eq!(iter.next(), Some(&mut Span::styled("Hello ", Color::Blue)));
1649 assert_eq!(iter.next(), Some(&mut Span::styled("world!", Color::Green)));
1650 assert_eq!(iter.next(), None);
1651 }
1652
1653 #[rstest]
1654 fn for_loop_ref(hello_world: Line<'_>) {
1655 let mut result = String::new();
1656 for span in &hello_world {
1657 result.push_str(span.content.as_ref());
1658 }
1659 assert_eq!(result, "Hello world!");
1660 }
1661
1662 #[rstest]
1663 fn for_loop_mut_ref() {
1664 let mut hello_world = Line::from(vec![
1665 Span::styled("Hello ", Color::Blue),
1666 Span::styled("world!", Color::Green),
1667 ]);
1668 let mut result = String::new();
1669 for span in &mut hello_world {
1670 result.push_str(span.content.as_ref());
1671 }
1672 assert_eq!(result, "Hello world!");
1673 }
1674
1675 #[rstest]
1676 fn for_loop_into(hello_world: Line<'_>) {
1677 let mut result = String::new();
1678 for span in hello_world {
1679 result.push_str(span.content.as_ref());
1680 }
1681 assert_eq!(result, "Hello world!");
1682 }
1683 }
1684
1685 #[rstest]
1686 #[case::empty(Line::default(), "Line::default()")]
1687 #[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
1688 #[case::styled(
1689 Line::styled("Hello, world!", Color::Yellow),
1690 r#"Line::from("Hello, world!").yellow()"#
1691 )]
1692 #[case::styled_complex(
1693 Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
1694 r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
1695 )]
1696 #[case::styled_span(
1697 Line::from(Span::styled("Hello, world!", Color::Yellow)),
1698 r#"Line::from(Span::from("Hello, world!").yellow())"#
1699 )]
1700 #[case::styled_line_and_span(
1701 Line::from(vec![
1702 Span::styled("Hello", Color::Yellow),
1703 Span::styled(" world!", Color::Green),
1704 ]).italic(),
1705 r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
1706 )]
1707 #[case::spans_vec(
1708 Line::from(vec![
1709 Span::styled("Hello", Color::Blue),
1710 Span::styled(" world!", Color::Green),
1711 ]),
1712 r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
1713 )]
1714 #[case::left_aligned(
1715 Line::from("Hello, world!").left_aligned(),
1716 r#"Line::from("Hello, world!").left_aligned()"#
1717 )]
1718 #[case::centered(
1719 Line::from("Hello, world!").centered(),
1720 r#"Line::from("Hello, world!").centered()"#
1721 )]
1722 #[case::right_aligned(
1723 Line::from("Hello, world!").right_aligned(),
1724 r#"Line::from("Hello, world!").right_aligned()"#
1725 )]
1726 fn debug(#[case] line: Line, #[case] expected: &str) {
1727 assert_eq!(format!("{line:?}"), expected);
1728 }
1729}