1use crate::style::Style;
7use std::borrow::Cow;
8use unicode_segmentation::UnicodeSegmentation;
9use unicode_width::UnicodeWidthStr;
10
11#[derive(Debug, Clone, PartialEq)]
13pub struct Span {
14 pub text: Cow<'static, str>,
16 pub style: Style,
18 pub link: Option<String>,
20}
21
22impl Span {
23 pub fn raw<S: Into<Cow<'static, str>>>(text: S) -> Self {
25 Span {
26 text: text.into(),
27 style: Style::new(),
28 link: None,
29 }
30 }
31
32 pub fn styled<S: Into<Cow<'static, str>>>(text: S, style: Style) -> Self {
34 Span {
35 text: text.into(),
36 style,
37 link: None,
38 }
39 }
40
41 pub fn linked<S: Into<Cow<'static, str>>>(text: S, style: Style, url: String) -> Self {
43 Span {
44 text: text.into(),
45 style,
46 link: Some(url),
47 }
48 }
49
50 pub fn width(&self) -> usize {
52 UnicodeWidthStr::width(self.text.as_ref())
53 }
54
55 pub fn is_empty(&self) -> bool {
57 self.text.is_empty()
58 }
59}
60
61impl<S: Into<Cow<'static, str>>> From<S> for Span {
62 fn from(text: S) -> Self {
63 Span::raw(text)
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum Alignment {
70 #[default]
72 Left,
73 Center,
75 Right,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum Overflow {
82 #[default]
84 Wrap,
85 Ellipsis,
87 Truncate,
89 Visible,
91}
92
93#[derive(Debug, Clone, Default)]
95pub struct Text {
96 pub spans: Vec<Span>,
98 pub alignment: Alignment,
100 pub overflow: Overflow,
102 pub style: Style,
104}
105
106impl Text {
107 pub fn new() -> Self {
109 Text::default()
110 }
111
112 pub fn plain<S: Into<Cow<'static, str>>>(text: S) -> Self {
114 Text {
115 spans: vec![Span::raw(text)],
116 ..Default::default()
117 }
118 }
119
120 pub fn styled<S: Into<Cow<'static, str>>>(text: S, style: Style) -> Self {
122 Text {
123 spans: vec![Span::styled(text, style)],
124 style,
125 ..Default::default()
126 }
127 }
128
129 pub fn from_spans<I: IntoIterator<Item = Span>>(spans: I) -> Self {
131 Text {
132 spans: spans.into_iter().collect(),
133 ..Default::default()
134 }
135 }
136
137 pub fn push_span(&mut self, span: Span) {
139 self.spans.push(span);
140 }
141
142 pub fn push<S: Into<Cow<'static, str>>>(&mut self, text: S) {
144 self.spans.push(Span::raw(text));
145 }
146
147 pub fn push_styled<S: Into<Cow<'static, str>>>(&mut self, text: S, style: Style) {
149 self.spans.push(Span::styled(text, style));
150 }
151
152 pub fn alignment(mut self, alignment: Alignment) -> Self {
154 self.alignment = alignment;
155 self
156 }
157
158 pub fn overflow(mut self, overflow: Overflow) -> Self {
160 self.overflow = overflow;
161 self
162 }
163
164 pub fn no_wrap(self) -> Self {
168 self.overflow(Overflow::Visible)
169 }
170
171 pub fn style(mut self, style: Style) -> Self {
173 self.style = style;
174 self
175 }
176
177 pub fn width(&self) -> usize {
179 self.spans.iter().map(|s| s.width()).sum()
180 }
181
182 pub fn plain_text(&self) -> String {
184 self.spans.iter().map(|s| s.text.as_ref()).collect()
185 }
186
187 pub fn is_empty(&self) -> bool {
189 self.spans.is_empty() || self.spans.iter().all(|s| s.is_empty())
190 }
191
192 pub fn wrap(&self, width: usize) -> Vec<Vec<Span>> {
194 if width == 0 {
195 return vec![];
196 }
197
198 match self.overflow {
199 Overflow::Visible => vec![self.spans.clone()],
200 Overflow::Truncate | Overflow::Ellipsis => {
201 vec![self.truncate_spans(width, self.overflow == Overflow::Ellipsis)]
202 }
203 Overflow::Wrap => self.wrap_spans(width),
204 }
205 }
206
207 fn truncate_spans(&self, width: usize, ellipsis: bool) -> Vec<Span> {
208 let mut result = Vec::new();
209 let mut remaining_width = if ellipsis {
210 width.saturating_sub(1)
211 } else {
212 width
213 };
214
215 for span in &self.spans {
216 if remaining_width == 0 {
217 break;
218 }
219
220 let span_width = span.width();
221 if span_width <= remaining_width {
222 result.push(span.clone());
223 remaining_width -= span_width;
224 } else {
225 let truncated = truncate_str(&span.text, remaining_width);
227 result.push(Span::styled(truncated.to_string(), span.style));
228 remaining_width = 0;
229 }
230 }
231
232 if ellipsis && self.width() > width {
233 result.push(Span::raw("…"));
234 }
235
236 result
237 }
238
239 fn wrap_spans(&self, max_width: usize) -> Vec<Vec<Span>> {
240 let mut lines: Vec<Vec<Span>> = Vec::new();
241 let mut current_line: Vec<Span> = Vec::new();
242 let mut current_width = 0;
243
244 for span in &self.spans {
245 let words = split_into_words(&span.text);
246
247 for (word, trailing_space) in words {
248 let word_width = UnicodeWidthStr::width(word);
249 let space_width = if trailing_space { 1 } else { 0 };
250 let total_width = word_width + space_width;
251
252 if current_width + word_width <= max_width {
254 let text = if trailing_space {
255 format!("{word} ")
256 } else {
257 word.to_string()
258 };
259 current_line.push(Span::styled(text, span.style));
260 current_width += total_width;
261 } else if word_width > max_width {
262 if !current_line.is_empty() {
264 lines.push(std::mem::take(&mut current_line));
265 current_width = 0;
266 }
267
268 let broken = break_word(word, max_width);
270 for (i, part) in broken.iter().enumerate() {
271 if i > 0 {
272 lines.push(std::mem::take(&mut current_line));
273 }
274 current_line.push(Span::styled(part.to_string(), span.style));
275 current_width = UnicodeWidthStr::width(part.as_str());
276 }
277
278 if trailing_space && current_width < max_width {
279 current_line.push(Span::styled(" ", span.style));
280 current_width += 1;
281 }
282 } else {
283 if !current_line.is_empty() {
285 lines.push(std::mem::take(&mut current_line));
286 }
287 let text = if trailing_space {
288 format!("{word} ")
289 } else {
290 word.to_string()
291 };
292 current_line.push(Span::styled(text, span.style));
293 current_width = total_width;
294 }
295 }
296 }
297
298 if !current_line.is_empty() {
299 lines.push(current_line);
300 }
301
302 if lines.is_empty() {
303 lines.push(Vec::new());
304 }
305
306 lines
307 }
308
309 pub fn align_line(&self, line: Vec<Span>, width: usize) -> Vec<Span> {
311 let line_width: usize = line.iter().map(|s| s.width()).sum();
312
313 if line_width >= width {
314 return line;
315 }
316
317 let padding = width - line_width;
318
319 match self.alignment {
320 Alignment::Left => {
321 line
324 }
325 Alignment::Right => {
326 let mut result = vec![Span::raw(" ".repeat(padding))];
327 result.extend(line);
328 result
329 }
330 Alignment::Center => {
331 let left_pad = padding / 2;
332 let right_pad = padding - left_pad;
333 let mut result = vec![Span::raw(" ".repeat(left_pad))];
334 result.extend(line);
335 result.push(Span::raw(" ".repeat(right_pad)));
336 result
337 }
338 }
339 }
340}
341
342impl<S: Into<Cow<'static, str>>> From<S> for Text {
343 fn from(text: S) -> Self {
344 Text::plain(text)
345 }
346}
347
348fn truncate_str(s: &str, max_width: usize) -> &str {
350 let mut width = 0;
351 let mut end = 0;
352
353 for grapheme in s.graphemes(true) {
354 let grapheme_width = UnicodeWidthStr::width(grapheme);
355 if width + grapheme_width > max_width {
356 break;
357 }
358 width += grapheme_width;
359 end += grapheme.len();
360 }
361
362 &s[..end]
363}
364
365fn split_into_words(s: &str) -> Vec<(&str, bool)> {
367 let mut words = Vec::new();
368 let mut word_start = None;
369 let mut leading_spaces = 0;
370
371 for (i, c) in s.char_indices() {
373 if c.is_whitespace() {
374 leading_spaces = i + c.len_utf8();
375 } else {
376 break;
377 }
378 }
379
380 let chars_to_process = if leading_spaces > 0 {
383 &s[leading_spaces..]
384 } else {
385 s
386 };
387
388 for (i, c) in chars_to_process.char_indices() {
389 if c.is_whitespace() {
390 if let Some(start) = word_start {
391 let word = &chars_to_process[start..i];
392 let final_word = if start == 0 && leading_spaces > 0 {
394 word
396 } else {
397 word
398 };
399 words.push((final_word, true));
400 word_start = None;
401 }
402 } else if word_start.is_none() {
403 word_start = Some(i);
404 }
405 }
406
407 if let Some(start) = word_start {
408 words.push((&chars_to_process[start..], false));
409 }
410
411 if leading_spaces > 0 && !words.is_empty() {
414 let mut result = vec![(&s[..leading_spaces], false)];
420 result.extend(words);
421 return result;
422 } else if leading_spaces > 0 && words.is_empty() {
423 return vec![(s, false)];
425 }
426
427 words
428}
429
430fn break_word(word: &str, max_width: usize) -> Vec<String> {
432 let mut parts = Vec::new();
433 let mut current = String::new();
434 let mut current_width = 0;
435
436 for grapheme in word.graphemes(true) {
437 let grapheme_width = UnicodeWidthStr::width(grapheme);
438
439 if current_width + grapheme_width > max_width && !current.is_empty() {
440 parts.push(std::mem::take(&mut current));
441 current_width = 0;
442 }
443
444 current.push_str(grapheme);
445 current_width += grapheme_width;
446 }
447
448 if !current.is_empty() {
449 parts.push(current);
450 }
451
452 parts
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_span_width() {
461 assert_eq!(Span::raw("hello").width(), 5);
462 assert_eq!(Span::raw("你好").width(), 4); assert_eq!(Span::raw("").width(), 0);
464 }
465
466 #[test]
467 fn test_text_plain() {
468 let text = Text::plain("Hello, World!");
469 assert_eq!(text.plain_text(), "Hello, World!");
470 assert_eq!(text.width(), 13);
471 }
472
473 #[test]
474 fn test_text_wrap_simple() {
475 let text = Text::plain("hello world");
476 let lines = text.wrap(6);
477 assert_eq!(lines.len(), 2);
478 assert_eq!(lines[0][0].text, "hello ");
479 assert_eq!(lines[1][0].text, "world");
480 }
481
482 #[test]
483 fn test_text_wrap_long_word() {
484 let text = Text::plain("supercalifragilistic");
485 let lines = text.wrap(10);
486 assert!(lines.len() > 1);
487 }
488
489 #[test]
490 fn test_truncate_ellipsis() {
491 let text = Text::plain("Hello, World!").overflow(Overflow::Ellipsis);
492 let lines = text.wrap(8);
493 let plain: String = lines[0].iter().map(|s| s.text.as_ref()).collect();
494 assert!(plain.ends_with('…'));
495 assert!(UnicodeWidthStr::width(plain.as_str()) <= 8);
497 }
498
499 #[test]
500 fn test_alignment_left() {
501 let text = Text::plain("hi").alignment(Alignment::Left);
502 let lines = text.wrap(10);
503 let aligned = text.align_line(lines[0].clone(), 10);
504 let plain: String = aligned.iter().map(|s| s.text.as_ref()).collect();
505 assert_eq!(plain, "hi");
507 }
508
509 #[test]
510 fn test_alignment_right() {
511 let text = Text::plain("hi").alignment(Alignment::Right);
512 let lines = text.wrap(10);
513 let aligned = text.align_line(lines[0].clone(), 10);
514 let plain: String = aligned.iter().map(|s| s.text.as_ref()).collect();
515 assert_eq!(plain, " hi");
516 }
517
518 #[test]
519 fn test_alignment_center() {
520 let text = Text::plain("hi").alignment(Alignment::Center);
521 let lines = text.wrap(10);
522 let aligned = text.align_line(lines[0].clone(), 10);
523 let plain: String = aligned.iter().map(|s| s.text.as_ref()).collect();
524 assert_eq!(plain, " hi ");
525 }
526}