1use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
2
3use crate::{Alignment, Style, patch_style};
4
5#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6pub struct StyledChunk {
7 pub text: String,
8 pub style: Style,
9}
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct StyledLine {
13 pub chunks: Vec<StyledChunk>,
14 pub alignment: Alignment,
15 pub width: u16,
16}
17
18#[derive(Clone)]
19struct StyledToken {
20 text: String,
21 style: Style,
22 is_whitespace: bool,
23 width: u16,
24}
25
26#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
27pub struct Span {
28 pub content: String,
29 pub style: Style,
30}
31
32impl Span {
33 pub fn raw(content: impl Into<String>) -> Self {
34 Self {
35 content: content.into(),
36 style: Style::default(),
37 }
38 }
39
40 pub fn styled(content: impl Into<String>, style: Style) -> Self {
41 Self {
42 content: content.into(),
43 style,
44 }
45 }
46
47 pub fn style(mut self, style: Style) -> Self {
48 self.style = style;
49 self
50 }
51
52 pub fn width(&self) -> usize {
53 UnicodeWidthStr::width(self.content.as_str())
54 }
55}
56
57impl From<&str> for Span {
58 fn from(value: &str) -> Self {
59 Self::raw(value)
60 }
61}
62
63impl From<String> for Span {
64 fn from(value: String) -> Self {
65 Self::raw(value)
66 }
67}
68
69#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
70pub struct Line {
71 pub spans: Vec<Span>,
72 pub alignment: Option<Alignment>,
73}
74
75impl Line {
76 pub fn raw(content: impl Into<String>) -> Self {
77 Self {
78 spans: vec![Span::raw(content)],
79 alignment: None,
80 }
81 }
82
83 pub fn styled(content: impl Into<String>, style: Style) -> Self {
84 Self {
85 spans: vec![Span::styled(content, style)],
86 alignment: None,
87 }
88 }
89
90 pub fn alignment(mut self, alignment: Alignment) -> Self {
91 self.alignment = Some(alignment);
92 self
93 }
94
95 pub fn left_aligned(self) -> Self {
96 self.alignment(Alignment::Left)
97 }
98
99 pub fn centered(self) -> Self {
100 self.alignment(Alignment::Center)
101 }
102
103 pub fn right_aligned(self) -> Self {
104 self.alignment(Alignment::Right)
105 }
106
107 pub fn width(&self) -> usize {
108 self.spans.iter().map(Span::width).sum()
109 }
110
111 pub const fn height(&self) -> usize {
112 1
113 }
114
115 pub fn plain(&self) -> String {
116 self.spans
117 .iter()
118 .map(|span| span.content.as_str())
119 .collect::<String>()
120 }
121}
122
123impl From<&str> for Line {
124 fn from(value: &str) -> Self {
125 Self::raw(value)
126 }
127}
128
129impl From<String> for Line {
130 fn from(value: String) -> Self {
131 Self::raw(value)
132 }
133}
134
135impl From<Span> for Line {
136 fn from(value: Span) -> Self {
137 Self {
138 spans: vec![value],
139 alignment: None,
140 }
141 }
142}
143
144impl From<Vec<Span>> for Line {
145 fn from(value: Vec<Span>) -> Self {
146 Self {
147 spans: value,
148 alignment: None,
149 }
150 }
151}
152
153#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
154pub struct Text {
155 pub lines: Vec<Line>,
156 pub alignment: Option<Alignment>,
157}
158
159impl Text {
160 pub fn raw(content: impl Into<String>) -> Self {
161 Self::from(content.into())
162 }
163
164 pub fn styled(content: impl Into<String>, style: Style) -> Self {
165 Self::from(Line::styled(content, style))
166 }
167
168 pub fn alignment(mut self, alignment: Alignment) -> Self {
169 self.alignment = Some(alignment);
170 self
171 }
172
173 pub fn left_aligned(self) -> Self {
174 self.alignment(Alignment::Left)
175 }
176
177 pub fn centered(self) -> Self {
178 self.alignment(Alignment::Center)
179 }
180
181 pub fn right_aligned(self) -> Self {
182 self.alignment(Alignment::Right)
183 }
184
185 pub fn height(&self) -> usize {
186 self.lines.len().max(1)
187 }
188
189 pub fn width(&self) -> usize {
190 self.lines.iter().map(Line::width).max().unwrap_or(0)
191 }
192
193 pub fn is_empty(&self) -> bool {
194 self.lines.iter().all(|line| line.spans.is_empty())
195 }
196
197 pub fn plain(&self) -> String {
198 self.lines
199 .iter()
200 .map(Line::plain)
201 .collect::<Vec<_>>()
202 .join("\n")
203 }
204}
205
206impl From<&str> for Text {
207 fn from(value: &str) -> Self {
208 Self::from(value.to_string())
209 }
210}
211
212impl From<String> for Text {
213 fn from(value: String) -> Self {
214 let lines = if value.is_empty() {
215 vec![Line::default()]
216 } else {
217 value.split('\n').map(Line::raw).collect()
218 };
219
220 Self {
221 lines,
222 alignment: None,
223 }
224 }
225}
226
227impl From<Span> for Text {
228 fn from(value: Span) -> Self {
229 Self::from(Line::from(value))
230 }
231}
232
233impl From<Line> for Text {
234 fn from(value: Line) -> Self {
235 let alignment = value.alignment;
236 Self {
237 lines: vec![value],
238 alignment,
239 }
240 }
241}
242
243impl From<Vec<Line>> for Text {
244 fn from(value: Vec<Line>) -> Self {
245 let alignment = value.iter().find_map(|line| line.alignment);
246 Self {
247 lines: if value.is_empty() {
248 vec![Line::default()]
249 } else {
250 value
251 },
252 alignment,
253 }
254 }
255}
256
257pub fn display_width(text: &str) -> u16 {
258 text.chars()
259 .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0) as u16)
260 .sum()
261}
262
263pub fn display_width_prefix(text: &str, cursor: usize) -> u16 {
264 text.chars()
265 .take(cursor)
266 .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0) as u16)
267 .sum()
268}
269
270pub fn clip_to_width(content: &str, width: u16) -> String {
271 if width == 0 {
272 return String::new();
273 }
274
275 let mut clipped = String::new();
276 let mut used = 0u16;
277
278 for ch in content.chars() {
279 let char_width = (UnicodeWidthChar::width(ch).unwrap_or(0) as u16).max(1);
280 if used.saturating_add(char_width) > width {
281 break;
282 }
283 clipped.push(ch);
284 used = used.saturating_add(char_width);
285 }
286
287 clipped
288}
289
290pub fn wrap_plain_lines(content: &str, width: u16, trim_leading: bool) -> Vec<String> {
291 if width == 0 {
292 return Vec::new();
293 }
294
295 let mut lines = Vec::new();
296
297 for raw_line in content.split('\n') {
298 if raw_line.is_empty() {
299 lines.push(String::new());
300 continue;
301 }
302
303 let mut current = String::new();
304 let mut current_width = 0u16;
305
306 for ch in raw_line.chars() {
307 let char_width = (UnicodeWidthChar::width(ch).unwrap_or(0) as u16).max(1);
308 if current_width.saturating_add(char_width) > width && !current.is_empty() {
309 lines.push(current);
310 current = String::new();
311 current_width = 0;
312 if trim_leading && ch.is_whitespace() {
313 continue;
314 }
315 }
316
317 current.push(ch);
318 current_width = current_width.saturating_add(char_width);
319 }
320
321 lines.push(current);
322 }
323
324 if lines.is_empty() {
325 lines.push(String::new());
326 }
327
328 lines
329}
330
331pub fn styled_lines_from_text(
332 text: &Text,
333 base_style: Style,
334 fallback_alignment: Alignment,
335) -> Vec<StyledLine> {
336 let alignment = text.alignment.unwrap_or(fallback_alignment);
337 let mut lines: Vec<StyledLine> = text
338 .lines
339 .iter()
340 .map(|line| {
341 let chunks: Vec<StyledChunk> = line
342 .spans
343 .iter()
344 .map(|span| StyledChunk {
345 text: span.content.clone(),
346 style: patch_style(base_style, span.style),
347 })
348 .collect();
349 StyledLine {
350 width: chunks
351 .iter()
352 .map(|chunk| UnicodeWidthStr::width(chunk.text.as_str()) as u16)
353 .sum(),
354 chunks,
355 alignment: line.alignment.unwrap_or(alignment),
356 }
357 })
358 .collect();
359
360 if lines.is_empty() {
361 lines.push(StyledLine {
362 chunks: Vec::new(),
363 alignment,
364 width: 0,
365 });
366 }
367
368 lines
369}
370
371pub fn styled_line_from_line(line: &Line, base_style: Style) -> StyledLine {
372 styled_lines_from_text(&Text::from(line.clone()), base_style, Alignment::Left)
373 .into_iter()
374 .next()
375 .unwrap_or(StyledLine {
376 chunks: Vec::new(),
377 alignment: Alignment::Left,
378 width: 0,
379 })
380}
381
382pub fn styled_line_from_span(span: &Span, base_style: Style) -> StyledLine {
383 styled_line_from_line(&Line::from(span.clone()), base_style)
384}
385
386pub fn wrap_styled_lines(lines: &[StyledLine], width: u16, trim: bool) -> Vec<StyledLine> {
387 if width == 0 {
388 return Vec::new();
389 }
390
391 let mut wrapped = Vec::new();
392
393 for line in lines {
394 let mut current = StyledLine {
395 chunks: Vec::new(),
396 alignment: line.alignment,
397 width: 0,
398 };
399 for token in styled_tokens_from_line(line) {
400 if token.is_whitespace && trim && current.width == 0 {
401 continue;
402 }
403
404 if token.width <= width {
405 if current.width.saturating_add(token.width) > width && current.width > 0 {
406 wrapped.push(current);
407 current = StyledLine {
408 chunks: Vec::new(),
409 alignment: line.alignment,
410 width: 0,
411 };
412 if token.is_whitespace && trim {
413 continue;
414 }
415 }
416
417 append_token(&mut current, &token);
418 continue;
419 }
420
421 let mut token_text = String::new();
422 let mut token_width = 0u16;
423 for ch in token.text.chars() {
424 let char_width = (UnicodeWidthChar::width(ch).unwrap_or(0) as u16).max(1);
425
426 if token.is_whitespace && trim && current.width == 0 {
427 continue;
428 }
429
430 if current
431 .width
432 .saturating_add(token_width)
433 .saturating_add(char_width)
434 > width
435 && (current.width > 0 || token_width > 0)
436 {
437 if !token_text.is_empty() {
438 current.width = current.width.saturating_add(token_width);
439 push_chunk(&mut current.chunks, token_text.clone(), token.style);
440 token_text.clear();
441 token_width = 0;
442 }
443
444 wrapped.push(current);
445 current = StyledLine {
446 chunks: Vec::new(),
447 alignment: line.alignment,
448 width: 0,
449 };
450
451 if token.is_whitespace && trim {
452 continue;
453 }
454 }
455
456 token_text.push(ch);
457 token_width = token_width.saturating_add(char_width);
458 }
459
460 if !token_text.is_empty() {
461 current.width = current.width.saturating_add(token_width);
462 push_chunk(&mut current.chunks, token_text, token.style);
463 }
464 }
465
466 wrapped.push(current);
467 }
468
469 wrapped
470}
471
472fn styled_tokens_from_line(line: &StyledLine) -> Vec<StyledToken> {
473 let mut tokens = Vec::new();
474
475 for chunk in &line.chunks {
476 let mut token = String::new();
477 let mut token_is_whitespace = None;
478 let mut token_width = 0u16;
479
480 for ch in chunk.text.chars() {
481 let is_whitespace = ch.is_whitespace();
482 if token_is_whitespace.is_some() && token_is_whitespace != Some(is_whitespace) {
483 tokens.push(StyledToken {
484 text: token.clone(),
485 style: chunk.style,
486 is_whitespace: token_is_whitespace.unwrap_or(false),
487 width: token_width,
488 });
489 token.clear();
490 token_width = 0;
491 }
492
493 token_is_whitespace = Some(is_whitespace);
494 token.push(ch);
495 token_width = token_width
496 .saturating_add((UnicodeWidthChar::width(ch).unwrap_or(0) as u16).max(1));
497 }
498
499 if !token.is_empty() {
500 tokens.push(StyledToken {
501 text: token,
502 style: chunk.style,
503 is_whitespace: token_is_whitespace.unwrap_or(false),
504 width: token_width,
505 });
506 }
507 }
508
509 tokens
510}
511
512fn append_token(target: &mut StyledLine, token: &StyledToken) {
513 target.width = target.width.saturating_add(token.width);
514 push_chunk(&mut target.chunks, token.text.clone(), token.style);
515}
516
517fn push_chunk(chunks: &mut Vec<StyledChunk>, text: String, style: Style) {
518 if text.is_empty() {
519 return;
520 }
521
522 if let Some(last) = chunks.last_mut()
523 && last.style == style
524 {
525 last.text.push_str(&text);
526 return;
527 }
528
529 chunks.push(StyledChunk { text, style });
530}