1use crate::buffer::Buffer;
2use crate::geom::{Pos, Rect, Size};
3use crate::style::Style;
4#[cfg(test)]
5use crate::style::Color;
6
7#[derive(Debug, Clone)]
11pub struct Span {
12 pub text: String,
13 pub style: Style,
14}
15
16impl Span {
17 pub fn new(text: impl Into<String>) -> Self {
19 Self { text: text.into(), style: Style::default() }
20 }
21
22 pub fn styled(text: impl Into<String>, style: Style) -> Self {
24 Self { text: text.into(), style }
25 }
26
27 pub fn width(&self) -> u16 {
29 self.text.chars()
30 .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16)
31 .sum()
32 }
33}
34
35impl From<String> for Span {
36 fn from(s: String) -> Self { Self { text: s, style: Style::default() } }
37}
38#[derive(Debug, Clone)]
42pub struct Line {
43 pub spans: Vec<Span>,
44}
45
46impl Line {
47 pub fn new(spans: impl IntoIterator<Item = Span>) -> Self {
48 Self { spans: spans.into_iter().collect() }
49 }
50
51 pub fn width(&self) -> u16 {
53 self.spans.iter().map(Span::width).sum()
54 }
55
56 pub fn render(&self, buf: &mut Buffer, pos: Pos, clip: Rect) -> u16 {
59 let mut x = pos.x;
60 for span in &self.spans {
61 if span.text.is_empty() { continue; }
62 buf.write_text(Pos { x, y: pos.y }, clip, &span.text, &span.style);
63 x = x.saturating_add(span.width());
64 }
65 x.saturating_sub(pos.x)
66 }
67}
68
69impl From<Span> for Line {
70 fn from(span: Span) -> Self { Self { spans: vec![span] } }
71}
72impl From<Vec<Span>> for Line {
75 fn from(spans: Vec<Span>) -> Self { Self { spans } }
76}
77
78#[derive(Debug, Clone)]
80pub struct Text {
81 pub lines: Vec<Line>,
82}
83
84impl Text {
85 pub fn new(lines: impl IntoIterator<Item = Line>) -> Self {
86 Self { lines: lines.into_iter().collect() }
87 }
88
89 pub fn height(&self) -> usize { self.lines.len() }
91
92 pub fn max_width(&self) -> u16 {
94 self.lines.iter().map(Line::width).max().unwrap_or(0)
95 }
96
97 pub fn first_text(&self) -> &str {
99 self.lines.first().and_then(|l| l.spans.first()).map(|s| s.text.as_str()).unwrap_or("")
100 }
101
102 pub fn render(&self, buf: &mut Buffer, rect: Rect) -> Size {
105 for (i, line) in self.lines.iter().enumerate() {
106 let y = rect.y.saturating_add(i as u16);
107 if y >= rect.y.saturating_add(rect.height) { break; }
108 line.render(buf, Pos { x: rect.x, y }, rect);
109 }
110 Size { width: self.max_width(), height: self.lines.len() as u16 }
111 }
112}
113
114impl From<&str> for Text {
117 fn from(s: &str) -> Self {
118 if s.is_empty() { return Self { lines: Vec::new() }; }
119 let lines: Vec<Line> = s.split('\n').map(|seg| Line::from(Span::new(seg))).collect();
120 Self { lines }
121 }
122}
123
124impl From<String> for Text {
125 fn from(s: String) -> Self { Text::from(s.as_str()) }
126}
127
128impl From<Line> for Text {
129 fn from(l: Line) -> Self { Self { lines: vec![l] } }
130}
131
132impl From<Vec<Line>> for Text {
133 fn from(lines: Vec<Line>) -> Self { Self { lines } }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::stylize::Stylize;
140
141 #[test]
144 fn test_span_new() {
145 let s = Span::new("hello");
146 assert_eq!(s.text, "hello");
147 }
148
149 #[test]
150 fn test_span_width() {
151 assert_eq!(Span::new("abc").width(), 3);
152 }
153
154 #[test]
157 fn test_line_from_span() {
158 let l = Line::from(Span::new("hello"));
159 assert_eq!(l.spans.len(), 1);
160 assert_eq!(l.spans[0].text, "hello");
161 }
162
163 #[test]
164 fn test_line_from_spans() {
165 let l = Line::from(vec![
166 Span::styled("A", Style::default().fg(Color::Red)),
167 Span::new("B"),
168 ]);
169 assert_eq!(l.spans.len(), 2);
170 assert_eq!(l.width(), 2);
171 }
172
173 #[test]
174 fn test_line_render() {
175 let mut buf = Buffer::new(Size { width: 10, height: 1 });
176 let line = Line::from(Span::new("hi"));
177 let w = line.render(&mut buf, Pos::default(), Rect { x: 0, y: 0, width: 10, height: 1 });
178 assert_eq!(w, 2);
179 assert_eq!(buf.cells[0].symbol, "h");
180 }
181
182 #[test]
185 fn test_text_from_str() {
186 let t = Text::from("hello");
187 assert_eq!(t.lines.len(), 1);
188 }
189
190 #[test]
191 fn test_text_from_line() {
192 let t = Text::from(Line::from(Span::new("hello")));
193 assert_eq!(t.lines.len(), 1);
194 }
195
196 #[test]
197 fn test_text_from_vec() {
198 let t = Text::from(vec![
199 Line::from(Span::new("line1")),
200 Line::from(Span::new("line2")),
201 ]);
202 assert_eq!(t.height(), 2);
203 }
204
205 #[test]
206 fn test_text_with_stylize() {
207 let t = Text::from(Line::from(vec![
208 "Error: ".red().bold(),
209 Span::new("not found"),
210 ]));
211 assert_eq!(t.lines[0].spans[0].style.fg, Some(crate::style::Color::Red));
212 assert!(t.lines[0].spans[0].style.bold);
213 }
214
215 #[test]
216 fn test_text_render() {
217 let mut buf = Buffer::new(Size { width: 20, height: 2 });
218 let t = Text::from(vec![
219 Line::from(Span::new("hello")),
220 Line::from(Span::new("world")),
221 ]);
222 t.render(&mut buf, Rect { x: 0, y: 0, width: 20, height: 2 });
223 assert_eq!(buf.cells[0].symbol, "h");
224 assert_eq!(&buf.cells[20].symbol, "w"); }
226
227 #[test]
228 fn test_line_width_multi_style() {
229 let line = Line::from(vec![
230 Span::styled("AB", Style::default().fg(Color::Red)),
231 Span::new("CD"),
232 ]);
233 assert_eq!(line.width(), 4);
234 }
235}