1use crate::grid::{display_width, Style};
6use std::borrow::Cow;
7
8#[derive(Clone, Debug, PartialEq, Eq, Default)]
10pub struct Span<'a> {
11 pub text: Cow<'a, str>,
12 pub style: Style,
13}
14
15impl<'a> Span<'a> {
16 pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
17 Self {
18 text: text.into(),
19 style: Style::default(),
20 }
21 }
22
23 pub fn styled(text: impl Into<Cow<'a, str>>, style: Style) -> Self {
24 Self {
25 text: text.into(),
26 style,
27 }
28 }
29
30 pub fn width(&self) -> u16 {
32 display_width(self.text.as_ref())
33 }
34}
35
36impl<'a> From<&'a str> for Span<'a> {
37 fn from(s: &'a str) -> Self {
38 Self::raw(s)
39 }
40}
41
42impl From<String> for Span<'_> {
43 fn from(s: String) -> Self {
44 Self::raw(Cow::Owned(s))
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Default)]
51pub struct Line<'a> {
52 pub spans: Vec<Span<'a>>,
53}
54
55impl<'a> Line<'a> {
56 pub fn new() -> Self {
57 Self { spans: Vec::new() }
58 }
59
60 pub fn from_spans<I: IntoIterator<Item = Span<'a>>>(spans: I) -> Self {
61 Self {
62 spans: spans.into_iter().collect(),
63 }
64 }
65
66 pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
67 Self::from_spans([Span::raw(text)])
68 }
69
70 pub fn push<S: Into<Span<'a>>>(mut self, span: S) -> Self {
72 self.spans.push(span.into());
73 self
74 }
75
76 pub fn width(&self) -> u16 {
78 self.spans
79 .iter()
80 .fold(0u16, |width, span| width.saturating_add(span.width()))
81 }
82}
83
84impl<'a> From<&'a str> for Line<'a> {
85 fn from(s: &'a str) -> Self {
86 Self::raw(s)
87 }
88}
89
90impl From<String> for Line<'_> {
91 fn from(s: String) -> Self {
92 Self::raw(Cow::Owned(s))
93 }
94}
95
96impl<'a> From<Span<'a>> for Line<'a> {
97 fn from(span: Span<'a>) -> Self {
98 Self::from_spans([span])
99 }
100}
101
102#[macro_export]
105macro_rules! line {
106 () => { $crate::line::Line::new() };
107 ($($span:expr),+ $(,)?) => {{
108 $crate::line::Line::from_spans([$(::core::convert::Into::<$crate::line::Span<'_>>::into($span)),+])
109 }};
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::grid::Color;
116
117 #[test]
118 fn span_from_str_is_default_style() {
119 let s: Span = "hi".into();
120 assert_eq!(s.text, "hi");
121 assert_eq!(s.style, Style::default());
122 assert_eq!(s.width(), 2);
123 }
124
125 #[test]
126 fn line_width_sums_spans() {
127 let l = Line::from_spans([
128 Span::raw("ab"),
129 Span::styled("cde", Style::new().fg(Color::Red)),
130 ]);
131 assert_eq!(l.width(), 5);
132 }
133
134 #[test]
135 fn line_macro_mixes_strs_and_spans() {
136 let l = line!["foo", " ", Span::styled("bar", Style::new().fg(Color::Red))];
137 assert_eq!(l.spans.len(), 3);
138 assert_eq!(l.spans[0].text, "foo");
139 assert_eq!(l.spans[1].text, " ");
140 assert_eq!(l.spans[2].text, "bar");
141 assert_eq!(l.spans[2].style.fg, Some(Color::Red));
142 }
143
144 #[test]
145 fn span_width_counts_two_columns_per_wide_char() {
146 assert_eq!(Span::raw("漢字").width(), 4);
148 assert_eq!(Span::raw("a漢b").width(), 4);
149 }
150
151 #[test]
152 fn span_width_is_zero_for_empty_text() {
153 assert_eq!(Span::raw("").width(), 0);
154 }
155
156 #[test]
157 fn line_width_zero_for_empty_and_for_all_empty_spans() {
158 assert_eq!(Line::new().width(), 0);
159 let l = Line::from_spans([Span::raw(""), Span::raw("")]);
160 assert_eq!(l.width(), 0);
161 }
162
163 #[test]
164 fn line_push_appends_a_span() {
165 let l = Line::new()
166 .push("a")
167 .push(Span::styled("b", Style::new().fg(Color::Red)));
168 assert_eq!(l.spans.len(), 2);
169 assert_eq!(l.spans[0].text, "a");
170 assert_eq!(l.spans[1].style.fg, Some(Color::Red));
171 }
172
173 #[test]
174 fn line_raw_wraps_text_in_a_single_default_span() {
175 let l = Line::raw("hello");
176 assert_eq!(l.spans.len(), 1);
177 assert_eq!(l.spans[0].text, "hello");
178 assert_eq!(l.spans[0].style, Style::default());
179 }
180}