1use crate::registry::GlyphRegistry;
8use crate::{style::is_plain_style, Style};
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Span {
18 pub text: String,
19 #[serde(default, skip_serializing_if = "is_plain_style")]
20 pub style: Style,
21}
22
23impl Span {
24 pub fn new<T: Into<String>>(text: T, style: Style) -> Self {
29 Self {
30 text: text.into(),
31 style,
32 }
33 }
34}
35
36pub type Spans = Vec<Span>;
38
39pub fn spans_plain_text(spans: &[Span]) -> String {
41 let mut out = String::new();
42 for s in spans {
43 out.push_str(&s.text);
44 }
45 out
46}
47
48pub fn normalize_spans(spans: &[Span]) -> Vec<Span> {
52 let mut out: Vec<Span> = Vec::new();
53 for s in spans {
54 if s.text.is_empty() {
55 continue;
56 }
57 if let Some(last) = out.last_mut() {
58 if last.style == s.style {
59 last.text.push_str(&s.text);
60 continue;
61 }
62 }
63 out.push(s.clone());
64 }
65 out
66}
67
68pub fn measure_cells_text(registry: &GlyphRegistry, text: &str) -> usize {
73 use unicode_segmentation::UnicodeSegmentation;
74 let mut w: usize = 0;
75 for g in text.graphemes(true) {
76 w = w.saturating_add(registry.width(g) as usize);
77 }
78 w
79}
80
81pub fn measure_cells_spans(registry: &GlyphRegistry, spans: &[Span]) -> usize {
83 spans
84 .iter()
85 .map(|s| measure_cells_text(registry, &s.text))
86 .sum()
87}
88
89fn clip_text_to_cells_internal(
90 registry: &GlyphRegistry,
91 text: &str,
92 max_cells: usize,
93) -> (String, usize, bool) {
94 use unicode_segmentation::UnicodeSegmentation;
95
96 if max_cells == 0 || text.is_empty() {
97 return (String::new(), 0, !text.is_empty());
98 }
99
100 let mut out = String::new();
101 let mut used: usize = 0;
102 let mut clipped = false;
103
104 for g in text.graphemes(true) {
105 let gw = registry.width(g) as usize;
106 if used.saturating_add(gw) > max_cells {
107 clipped = true;
108 break;
109 }
110 used = used.saturating_add(gw);
111 out.push_str(g);
112 }
113
114 if !clipped {
116 if out.len() != text.len() {
119 clipped = true;
120 }
121 }
122
123 (out, used, clipped)
124}
125
126pub fn clip_to_cells_text(registry: &GlyphRegistry, text: &str, w: usize) -> (String, bool) {
130 let (out, _used, clipped) = clip_text_to_cells_internal(registry, text, w);
131 (out, clipped)
132}
133
134pub fn clip_to_cells_spans(
138 registry: &GlyphRegistry,
139 spans: &[Span],
140 w: usize,
141) -> (Vec<Span>, bool) {
142 if w == 0 {
143 return (Vec::new(), !spans.is_empty());
144 }
145
146 let mut out: Vec<Span> = Vec::new();
147 let mut used: usize = 0;
148 let mut clipped = false;
149
150 for s in spans {
151 if s.text.is_empty() {
152 continue;
153 }
154 let remaining = w.saturating_sub(used);
155 if remaining == 0 {
156 clipped = true;
157 break;
158 }
159 let (clipped_text, text_used, did_clip) =
160 clip_text_to_cells_internal(registry, &s.text, remaining);
161 if !clipped_text.is_empty() {
162 out.push(Span::new(clipped_text, s.style));
163 }
164 used = used.saturating_add(text_used);
165 if did_clip {
166 clipped = true;
167 break;
168 }
169 }
170
171 (normalize_spans(&out), clipped)
172}
173
174pub fn ellipsis_to_cells_text(
178 registry: &GlyphRegistry,
179 text: &str,
180 w: usize,
181 ellipsis: &str,
182) -> String {
183 if w == 0 {
184 return String::new();
185 }
186 if measure_cells_text(registry, text) <= w {
187 return text.to_string();
188 }
189
190 let ell_w = measure_cells_text(registry, ellipsis);
191 if ell_w >= w {
192 let (e, _clipped) = clip_to_cells_text(registry, ellipsis, w);
193 return e;
194 }
195
196 let avail = w.saturating_sub(ell_w);
197 let (prefix, _clipped) = clip_to_cells_text(registry, text, avail);
198 let mut out = prefix;
199 out.push_str(ellipsis);
200 out
201}
202
203pub fn ellipsis_to_cells_spans(
207 registry: &GlyphRegistry,
208 spans: &[Span],
209 w: usize,
210 ellipsis_span: &Span,
211) -> Vec<Span> {
212 if w == 0 {
213 return Vec::new();
214 }
215
216 if measure_cells_spans(registry, spans) <= w {
217 return normalize_spans(spans);
218 }
219
220 let ell_w = measure_cells_text(registry, &ellipsis_span.text);
221 if ell_w >= w {
222 let (t, _clipped) = clip_to_cells_text(registry, &ellipsis_span.text, w);
223 return normalize_spans(&[Span::new(t, ellipsis_span.style)]);
224 }
225
226 let avail = w.saturating_sub(ell_w);
227 let (mut prefix, _clipped) = clip_to_cells_spans(registry, spans, avail);
228 prefix.push(Span::new(ellipsis_span.text.clone(), ellipsis_span.style));
229 normalize_spans(&prefix)
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(default)]
238pub struct WrapOpts {
239 pub preserve_spaces: bool,
244
245 pub hard_break_long_tokens: bool,
248
249 pub trim_end: bool,
251
252 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub continuation_prefix: Option<Vec<Span>>,
262}
263
264impl Default for WrapOpts {
265 fn default() -> Self {
266 Self {
267 preserve_spaces: false,
268 hard_break_long_tokens: true,
269 trim_end: true,
270 continuation_prefix: None,
271 }
272 }
273}
274
275pub(crate) fn is_default_wrap_opts(o: &WrapOpts) -> bool {
276 *o == WrapOpts::default()
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
280enum TokenKind {
281 Text,
282 Space,
283 Newline,
284}
285
286#[derive(Clone, Debug, PartialEq, Eq)]
287struct Token {
288 text: String,
289 style: Style,
290 kind: TokenKind,
291}
292
293fn tokenize_spans(spans: &[Span], preserve_spaces: bool) -> Vec<Token> {
294 let spans = normalize_spans(spans);
295 let mut out: Vec<Token> = Vec::new();
296
297 for s in spans {
298 let mut first = true;
300 for part in s.text.split('\n') {
301 if !first {
302 out.push(Token {
303 text: "\n".to_string(),
304 style: s.style,
305 kind: TokenKind::Newline,
306 });
307 }
308 first = false;
309
310 if part.is_empty() {
311 continue;
312 }
313
314 let mut buf = String::new();
316 let mut in_space: Option<bool> = None;
317 for ch in part.chars() {
318 let is_space = ch.is_whitespace();
319 match in_space {
320 None => {
321 in_space = Some(is_space);
322 buf.push(ch);
323 }
324 Some(prev) if prev == is_space => {
325 buf.push(ch);
326 }
327 Some(prev) => {
328 if prev {
330 let txt = if preserve_spaces {
331 buf.clone()
332 } else {
333 " ".to_string()
334 };
335 out.push(Token {
336 text: txt,
337 style: s.style,
338 kind: TokenKind::Space,
339 });
340 } else {
341 out.push(Token {
342 text: buf.clone(),
343 style: s.style,
344 kind: TokenKind::Text,
345 });
346 }
347 buf.clear();
348 in_space = Some(is_space);
349 buf.push(ch);
350 }
351 }
352 }
353
354 if !buf.is_empty() {
355 if in_space.unwrap_or(false) {
356 let txt = if preserve_spaces {
357 buf
358 } else {
359 " ".to_string()
360 };
361 out.push(Token {
362 text: txt,
363 style: s.style,
364 kind: TokenKind::Space,
365 });
366 } else {
367 out.push(Token {
368 text: buf,
369 style: s.style,
370 kind: TokenKind::Text,
371 });
372 }
373 }
374 }
375 }
376
377 out
378}
379
380fn trim_trailing_spaces(registry: &GlyphRegistry, spans: &mut Vec<Span>) {
381 while let Some(last) = spans.last_mut() {
385 if last.text.is_empty() {
386 spans.pop();
387 continue;
388 }
389 let trimmed = last.text.trim_end_matches(&[' ', '\t', '\r'] as &[char]);
391 if trimmed.len() == last.text.len() {
392 break;
393 }
394 last.text = trimmed.to_string();
395 if last.text.is_empty() {
396 spans.pop();
397 }
398 }
399
400 let norm = normalize_spans(spans);
402 spans.clear();
403 spans.extend(norm);
404
405 let _ = registry;
407}
408
409fn push_token(line: &mut Vec<Span>, tok: &Token) {
410 if tok.text.is_empty() {
411 return;
412 }
413 line.push(Span::new(tok.text.clone(), tok.style));
414}
415
416fn push_span_text(line: &mut Vec<Span>, text: &str, style: Style) {
417 if text.is_empty() {
418 return;
419 }
420 line.push(Span::new(text.to_string(), style));
421}
422
423fn split_token_by_width<'a>(
424 registry: &GlyphRegistry,
425 text: &'a str,
426 max_cells: usize,
427) -> (String, &'a str) {
428 let (chunk, used, _clipped) = clip_text_to_cells_internal(registry, text, max_cells);
429 if chunk.is_empty() || used == 0 {
430 return (String::new(), text);
431 }
432 let rest = &text[chunk.len()..];
433 (chunk, rest)
434}
435
436fn hard_break_token(registry: &GlyphRegistry, tok: &Token, width: usize) -> Vec<Vec<Span>> {
437 let mut lines: Vec<Vec<Span>> = Vec::new();
440 let mut remaining = tok.text.as_str();
441
442 loop {
443 if remaining.is_empty() {
444 break;
445 }
446 let (chunk, used, _clipped) = clip_text_to_cells_internal(registry, remaining, width);
447 if chunk.is_empty() || used == 0 {
448 break;
450 }
451 lines.push(vec![Span::new(chunk.clone(), tok.style)]);
452 remaining = &remaining[chunk.len()..];
454 }
455
456 lines
457}
458
459pub fn wrap_spans_wordwise(
468 registry: &GlyphRegistry,
469 spans: &[Span],
470 width: usize,
471 opts: &WrapOpts,
472) -> Vec<Vec<Span>> {
473 if width == 0 {
474 return Vec::new();
475 }
476
477 let tokens = tokenize_spans(spans, opts.preserve_spaces);
478 let mut q: std::collections::VecDeque<Token> = tokens.into();
479
480 let mut lines: Vec<Vec<Span>> = Vec::new();
481 let mut line: Vec<Span> = Vec::new();
482 let mut line_w: usize = 0;
483
484 let begin_line = |line: &mut Vec<Span>, line_w: &mut usize, continuation: bool| {
485 if !continuation {
486 return;
487 }
488 let Some(prefix) = &opts.continuation_prefix else {
489 return;
490 };
491 let mut p = normalize_spans(prefix);
492 if p.is_empty() {
493 return;
494 }
495 let pw = measure_cells_spans(registry, &p);
496 if pw >= width {
498 return;
499 }
500 line.append(&mut p);
501 *line_w = line_w.saturating_add(pw);
502 };
503
504 let flush_line = |lines: &mut Vec<Vec<Span>>, line: &mut Vec<Span>, line_w: &mut usize| {
505 let mut out = normalize_spans(line);
506 if opts.trim_end {
507 trim_trailing_spaces(registry, &mut out);
508 }
509 lines.push(out);
510 line.clear();
511 *line_w = 0;
512 };
513
514 while let Some(tok) = q.pop_front() {
515 match tok.kind {
516 TokenKind::Newline => {
517 flush_line(&mut lines, &mut line, &mut line_w);
518 }
520 TokenKind::Space => {
521 if !opts.preserve_spaces {
522 if line.is_empty() {
524 continue;
525 }
526 }
527 let tok_w = measure_cells_text(registry, &tok.text);
528 if line_w.saturating_add(tok_w) > width {
529 flush_line(&mut lines, &mut line, &mut line_w);
530 begin_line(&mut line, &mut line_w, true);
531 } else {
535 push_token(&mut line, &tok);
536 line_w = line_w.saturating_add(tok_w);
537 }
538 }
539 TokenKind::Text => {
540 let tok_w = measure_cells_text(registry, &tok.text);
541
542 if tok_w > width {
543 if opts.hard_break_long_tokens {
544 if !line.is_empty() && line_w < width {
547 let avail = width - line_w;
548 let (first, rest) = split_token_by_width(registry, &tok.text, avail);
549 if !first.is_empty() {
550 push_span_text(&mut line, &first, tok.style);
551 line_w =
552 line_w.saturating_add(measure_cells_text(registry, &first));
553 }
554 flush_line(&mut lines, &mut line, &mut line_w);
555 if !rest.is_empty() {
556 q.push_front(Token {
557 text: rest.to_string(),
558 style: tok.style,
559 kind: TokenKind::Text,
560 });
561 }
562 continue;
563 }
564
565 if !line.is_empty() {
566 flush_line(&mut lines, &mut line, &mut line_w);
567 }
568
569 let broken = hard_break_token(registry, &tok, width);
570 for (i, b) in broken.into_iter().enumerate() {
571 if i == 0 {
572 lines.push(normalize_spans(&b));
573 continue;
574 }
575 if opts.continuation_prefix.is_some() {
577 let mut out: Vec<Span> = Vec::new();
578 let mut out_w: usize = 0;
579 begin_line(&mut out, &mut out_w, true);
580 out.extend(normalize_spans(&b));
581 lines.push(normalize_spans(&out));
582 } else {
583 lines.push(normalize_spans(&b));
584 }
585 }
586 continue;
587 } else {
588 if !line.is_empty() {
589 flush_line(&mut lines, &mut line, &mut line_w);
590 }
591 let (clipped, _did) = clip_to_cells_text(registry, &tok.text, width);
592 if !clipped.is_empty() {
593 lines.push(vec![Span::new(clipped, tok.style)]);
594 }
595 continue;
596 }
597 }
598
599 if line_w.saturating_add(tok_w) > width {
600 flush_line(&mut lines, &mut line, &mut line_w);
601 begin_line(&mut line, &mut line_w, true);
602 }
603 push_token(&mut line, &tok);
604 line_w = line_w.saturating_add(tok_w);
605 }
606 }
607 }
608
609 if !line.is_empty() || lines.is_empty() {
610 flush_line(&mut lines, &mut line, &mut line_w);
611 }
612
613 lines
614}
615
616pub fn apply_highlight(
633 spans: &[Span],
634 ranges: &[(usize, usize)],
635 highlight_style: Style,
636) -> Vec<Span> {
637 use unicode_segmentation::UnicodeSegmentation;
638
639 if spans.is_empty() || ranges.is_empty() {
640 return normalize_spans(spans);
641 }
642
643 let mut rs: Vec<(usize, usize)> = ranges.iter().copied().filter(|(s, e)| s < e).collect();
645 if rs.is_empty() {
646 return normalize_spans(spans);
647 }
648 rs.sort_by_key(|(s, _e)| *s);
649 let mut merged: Vec<(usize, usize)> = Vec::new();
650 for (s, e) in rs {
651 match merged.last_mut() {
652 None => merged.push((s, e)),
653 Some((_ls, le)) => {
654 if s <= *le {
655 *le = (*le).max(e);
656 } else {
657 merged.push((s, e));
658 }
659 }
660 }
661 }
662
663 let spans = normalize_spans(spans);
664 let mut out: Vec<Span> = Vec::new();
665
666 let mut global_g: usize = 0;
667 let mut r_idx: usize = 0;
668
669 for s in spans {
670 if s.text.is_empty() {
671 continue;
672 }
673
674 for g in s.text.graphemes(true) {
675 while r_idx < merged.len() && global_g >= merged[r_idx].1 {
677 r_idx += 1;
678 }
679
680 let in_range = if r_idx < merged.len() {
681 let (rs, re) = merged[r_idx];
682 global_g >= rs && global_g < re
683 } else {
684 false
685 };
686
687 let style = if in_range {
688 s.style.overlay(highlight_style)
689 } else {
690 s.style
691 };
692
693 if let Some(last) = out.last_mut() {
695 if last.style == style {
696 last.text.push_str(g);
697 } else {
698 out.push(Span::new(g.to_string(), style));
699 }
700 } else {
701 out.push(Span::new(g.to_string(), style));
702 }
703
704 global_g = global_g.saturating_add(1);
705 }
706 }
707
708 normalize_spans(&out)
709}