1use crate::geometry::Alignment;
4use crate::style::{Style, Stylize};
5use alloc::borrow::Cow;
6use alloc::string::{String, ToString};
7use alloc::vec::Vec;
8use core::fmt;
9use unicode_width::UnicodeWidthStr;
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub struct Span<'a> {
29 pub content: Cow<'a, str>,
31 pub style: Style,
33}
34
35impl<'a> Span<'a> {
36 #[must_use]
38 pub fn raw<T: Into<Cow<'a, str>>>(content: T) -> Self {
39 Self {
40 content: content.into(),
41 style: Style::default(),
42 }
43 }
44
45 #[must_use]
47 pub fn styled<T: Into<Cow<'a, str>>>(content: T, style: Style) -> Self {
48 Self {
49 content: content.into(),
50 style,
51 }
52 }
53
54 #[must_use]
56 pub fn width(&self) -> usize {
57 self.content.width()
58 }
59
60 #[must_use]
62 pub fn into_owned(self) -> Span<'static> {
63 Span {
64 content: Cow::Owned(self.content.into_owned()),
65 style: self.style,
66 }
67 }
68
69 #[must_use]
71 pub fn patch_style(mut self, style: Style) -> Self {
72 self.style = self.style.patch(style);
73 self
74 }
75}
76
77impl<'a> From<&'a str> for Span<'a> {
78 fn from(s: &'a str) -> Self {
79 Self::raw(s)
80 }
81}
82
83impl From<String> for Span<'static> {
84 fn from(s: String) -> Self {
85 Self::raw(s)
86 }
87}
88
89impl<'a> Stylize for Span<'a> {
90 fn style(mut self, style: Style) -> Self {
91 self.style = self.style.patch(style);
92 self
93 }
94}
95
96impl fmt::Display for Span<'_> {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "{}", self.content)
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash)]
118#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
119pub struct Line<'a> {
120 pub spans: Vec<Span<'a>>,
122 pub alignment: Alignment,
124 pub style: Style,
126}
127
128impl<'a> Line<'a> {
129 #[must_use]
131 pub fn new() -> Self {
132 Self {
133 spans: Vec::new(),
134 alignment: Alignment::Start,
135 style: Style::default(),
136 }
137 }
138
139 #[must_use]
141 pub fn from_spans(spans: Vec<Span<'a>>) -> Self {
142 Self {
143 spans,
144 alignment: Alignment::Start,
145 style: Style::default(),
146 }
147 }
148
149 #[must_use]
151 pub fn styled<T: Into<Cow<'a, str>>>(content: T, style: Style) -> Self {
152 Self {
153 spans: alloc::vec![Span::styled(content, style)],
154 alignment: Alignment::Start,
155 style: Style::default(),
156 }
157 }
158
159 #[must_use]
161 pub fn alignment(mut self, alignment: Alignment) -> Self {
162 self.alignment = alignment;
163 self
164 }
165
166 #[must_use]
168 pub fn width(&self) -> usize {
169 self.spans.iter().map(Span::width).sum()
170 }
171
172 #[must_use]
184 pub fn truncate(self, max_width: usize, ellipsis: Option<&str>) -> Line<'a> {
185 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
186
187 let current_width = self.width();
188 if current_width <= max_width {
189 return self;
190 }
191
192 let ellipsis_str = ellipsis.unwrap_or("...");
193 let ellipsis_width = ellipsis_str.width();
194
195 if ellipsis_width >= max_width {
196 return Line::from(ellipsis_str.to_string());
197 }
198
199 let target_width = max_width.saturating_sub(ellipsis_width);
200 let mut new_spans = alloc::vec::Vec::new();
201 let mut accumulated_width = 0;
202
203 for span in self.spans {
204 let span_width = span.width();
205
206 if accumulated_width + span_width <= target_width {
207 accumulated_width += span_width;
208 new_spans.push(span);
209 } else {
210 let remaining = target_width.saturating_sub(accumulated_width);
211 if remaining > 0 {
212 let mut truncated = alloc::string::String::new();
213 let mut w = 0;
214 for c in span.content.chars() {
215 let cw = c.width().unwrap_or(0);
216 if w + cw <= remaining {
217 truncated.push(c);
218 w += cw;
219 } else {
220 break;
221 }
222 }
223 if !truncated.is_empty() {
224 new_spans.push(Span::styled(truncated, span.style));
225 }
226 }
227 break;
228 }
229 }
230
231 new_spans.push(Span::raw(ellipsis_str.to_string()));
232
233 Line {
234 spans: new_spans,
235 alignment: self.alignment,
236 style: self.style,
237 }
238 }
239
240 #[must_use]
242 pub fn into_owned(self) -> Line<'static> {
243 Line {
244 spans: self.spans.into_iter().map(Span::into_owned).collect(),
245 alignment: self.alignment,
246 style: self.style,
247 }
248 }
249
250 #[must_use]
252 pub fn patch_style(mut self, style: Style) -> Self {
253 self.style = self.style.patch(style);
254 self
255 }
256
257 pub fn push_span(&mut self, span: Span<'a>) {
259 self.spans.push(span);
260 }
261}
262
263impl<'a> Default for Line<'a> {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl<'a> From<&'a str> for Line<'a> {
270 fn from(s: &'a str) -> Self {
271 Self::from_spans(alloc::vec![Span::raw(s)])
272 }
273}
274
275impl From<String> for Line<'static> {
276 fn from(s: String) -> Self {
277 Self::from_spans(alloc::vec![Span::raw(s)])
278 }
279}
280
281impl<'a> From<Span<'a>> for Line<'a> {
282 fn from(span: Span<'a>) -> Self {
283 Self::from_spans(alloc::vec![span])
284 }
285}
286
287impl<'a> From<Vec<Span<'a>>> for Line<'a> {
288 fn from(spans: Vec<Span<'a>>) -> Self {
289 Self::from_spans(spans)
290 }
291}
292
293impl<'a> Stylize for Line<'a> {
294 fn style(self, style: Style) -> Self {
295 self.patch_style(style)
296 }
297}
298
299impl fmt::Display for Line<'_> {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 for span in &self.spans {
302 write!(f, "{span}")?;
303 }
304 Ok(())
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Hash)]
324#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
325pub struct Text<'a> {
326 pub lines: Vec<Line<'a>>,
328 pub style: Style,
330}
331
332impl<'a> Text<'a> {
333 #[must_use]
335 pub fn new() -> Self {
336 Self {
337 lines: Vec::new(),
338 style: Style::default(),
339 }
340 }
341
342 #[must_use]
344 pub fn from_lines(lines: Vec<Line<'a>>) -> Self {
345 Self {
346 lines,
347 style: Style::default(),
348 }
349 }
350
351 #[must_use]
353 pub fn styled<T: Into<Cow<'a, str>>>(content: T, style: Style) -> Self {
354 let content = content.into();
355 let lines: Vec<Line<'a>> = match content {
356 Cow::Borrowed(s) => s.lines().map(|line| Line::styled(line, style)).collect(),
357 Cow::Owned(s) => s
358 .lines()
359 .map(|line| Line::styled(line.to_string(), style))
360 .collect(),
361 };
362 Self { lines, style }
363 }
364
365 #[must_use]
367 pub fn width(&self) -> usize {
368 self.lines.iter().map(Line::width).max().unwrap_or(0)
369 }
370
371 #[must_use]
373 pub fn height(&self) -> usize {
374 self.lines.len()
375 }
376
377 #[must_use]
379 pub fn into_owned(self) -> Text<'static> {
380 Text {
381 lines: self.lines.into_iter().map(Line::into_owned).collect(),
382 style: self.style,
383 }
384 }
385
386 #[must_use]
388 pub fn patch_style(mut self, style: Style) -> Self {
389 self.style = self.style.patch(style);
390 self
391 }
392
393 pub fn push_line(&mut self, line: Line<'a>) {
395 self.lines.push(line);
396 }
397
398 pub fn extend_lines(&mut self, lines: impl IntoIterator<Item = Line<'a>>) {
400 self.lines.extend(lines);
401 }
402}
403
404impl<'a> Default for Text<'a> {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410impl<'a> From<&'a str> for Text<'a> {
411 fn from(s: &'a str) -> Self {
412 let lines = s.lines().map(Line::from).collect();
413 Self {
414 lines,
415 style: Style::default(),
416 }
417 }
418}
419
420impl From<String> for Text<'static> {
421 fn from(s: String) -> Self {
422 let lines: Vec<Line<'static>> =
423 s.lines().map(|line| Line::from(line.to_string())).collect();
424 Self {
425 lines,
426 style: Style::default(),
427 }
428 }
429}
430
431impl<'a> From<Line<'a>> for Text<'a> {
432 fn from(line: Line<'a>) -> Self {
433 Self::from_lines(alloc::vec![line])
434 }
435}
436
437impl<'a> From<Vec<Line<'a>>> for Text<'a> {
438 fn from(lines: Vec<Line<'a>>) -> Self {
439 Self::from_lines(lines)
440 }
441}
442
443impl<'a> Stylize for Text<'a> {
444 fn style(self, style: Style) -> Self {
445 self.patch_style(style)
446 }
447}
448
449impl fmt::Display for Text<'_> {
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 for (i, line) in self.lines.iter().enumerate() {
452 if i > 0 {
453 writeln!(f)?;
454 }
455 write!(f, "{line}")?;
456 }
457 Ok(())
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::style::Color;
465
466 #[test]
467 fn test_span_width() {
468 let span = Span::raw("Hello");
469 assert_eq!(span.width(), 5);
470 }
471
472 #[test]
473 fn test_line_width() {
474 let line = Line::from(vec![Span::raw("Hello "), Span::raw("World")]);
475 assert_eq!(line.width(), 11);
476 }
477
478 #[test]
479 fn test_text_dimensions() {
480 let text = Text::from("Hello\nWorld\n!");
481 assert_eq!(text.height(), 3);
482 assert_eq!(text.width(), 5);
483 }
484
485 #[test]
486 fn test_stylize() {
487 use crate::style::Stylize;
488 let span = Span::raw("test").red().bold();
489 assert_eq!(span.style.fg, Some(Color::Red));
490 }
491}