Skip to main content

termgrid_core/
render.rs

1use crate::{
2    ansi,
3    damage::{Damage, Rect},
4    BlitCell, BoxCharset, Cell, Frame, GlyphRegistry, Grid, RenderOp, Style, TruncateMode,
5};
6
7const DEFAULT_ELLIPSIS: &str = "…";
8const DAMAGE_MAX_RECTS: usize = 64;
9use crate::text::{
10    clip_to_cells_spans, clip_to_cells_text, ellipsis_to_cells_spans, ellipsis_to_cells_text,
11    normalize_spans, wrap_spans_wordwise, Span, WrapOpts,
12};
13use thiserror::Error;
14use unicode_segmentation::UnicodeSegmentation;
15
16#[derive(Debug, Error)]
17pub enum RenderError {
18    #[error("invalid grid size")]
19    InvalidGridSize,
20}
21
22/// A stateful renderer: owns a grid and applies frames to it.
23#[derive(Debug, Clone)]
24pub struct Renderer {
25    grid: Grid,
26    reg: GlyphRegistry,
27}
28
29impl Renderer {
30    pub fn new(width: u16, height: u16, reg: GlyphRegistry) -> Result<Self, RenderError> {
31        if width == 0 || height == 0 {
32            return Err(RenderError::InvalidGridSize);
33        }
34        Ok(Self {
35            grid: Grid::new(width, height),
36            reg,
37        })
38    }
39
40    pub fn grid(&self) -> &Grid {
41        &self.grid
42    }
43
44    pub fn grid_mut(&mut self) -> &mut Grid {
45        &mut self.grid
46    }
47
48    pub fn apply(&mut self, frame: &Frame) {
49        for op in &frame.ops {
50            self.apply_op(op);
51        }
52    }
53
54    /// Apply a frame and return the damaged regions.
55    ///
56    /// Damage is conservative and derived from the operations applied. It is
57    /// suitable for incremental terminal backends that want to minimize redraw
58    /// and flicker.
59    pub fn apply_with_damage(&mut self, frame: &Frame) -> Damage {
60        let mut dmg = Damage::empty();
61        for op in &frame.ops {
62            self.apply_op(op);
63            self.note_damage_for_op(op, &mut dmg);
64            if dmg.full_redraw {
65                break;
66            }
67        }
68        dmg
69    }
70
71    fn note_damage_for_op(&self, op: &RenderOp, dmg: &mut Damage) {
72        let w = self.grid.width;
73        let h = self.grid.height;
74
75        fn clip_rect(r: Rect, gw: u16, gh: u16) -> Rect {
76            if r.w == 0 || r.h == 0 {
77                return Rect::new(0, 0, 0, 0);
78            }
79            let x1 = r.x.min(gw);
80            let y1 = r.y.min(gh);
81            let x2 = r.right().min(gw);
82            let y2 = r.bottom().min(gh);
83            Rect::new(x1, y1, x2.saturating_sub(x1), y2.saturating_sub(y1))
84        }
85
86        let mut push = |r: Rect| {
87            let r = clip_rect(r, w, h);
88            dmg.push_rect(r, DAMAGE_MAX_RECTS);
89        };
90
91        match op {
92            RenderOp::Clear => {
93                dmg.full_redraw = true;
94                dmg.rects.clear();
95            }
96            RenderOp::ClearLine { y } => push(Rect::new(0, *y, w, 1)),
97            RenderOp::ClearEol { x, y } => push(Rect::new(*x, *y, w.saturating_sub(*x), 1)),
98            RenderOp::ClearBol { x, y } => push(Rect::new(0, *y, x.saturating_add(1), 1)),
99            RenderOp::ClearEos { x, y } => {
100                // First line from x..end, then remaining full lines.
101                push(Rect::new(*x, *y, w.saturating_sub(*x), 1));
102                if y.saturating_add(1) < h {
103                    push(Rect::new(
104                        0,
105                        y.saturating_add(1),
106                        w,
107                        h.saturating_sub(y.saturating_add(1)),
108                    ));
109                }
110            }
111            RenderOp::ClearRect { x, y, w: rw, h: rh } => push(Rect::new(*x, *y, *rw, *rh)),
112            RenderOp::FillRect {
113                x, y, w: rw, h: rh, ..
114            } => push(Rect::new(*x, *y, *rw, *rh)),
115            RenderOp::Put { x, y, text, .. } => {
116                let mw = crate::text::measure_cells_text(&self.reg, text) as u16;
117                push(Rect::new(*x, *y, mw, 1));
118            }
119            RenderOp::PutGlyph { x, y, glyph, .. } => {
120                let mw = crate::text::measure_cells_text(&self.reg, glyph) as u16;
121                push(Rect::new(*x, *y, mw, 1));
122            }
123            RenderOp::Label { x, y, w: lw, .. } => push(Rect::new(*x, *y, *lw, 1)),
124            RenderOp::LabelStyled { x, y, w: lw, .. } => push(Rect::new(*x, *y, *lw, 1)),
125            RenderOp::PutStyled { x, y, w: lw, .. } => push(Rect::new(*x, *y, *lw, 1)),
126            RenderOp::PutWrapped {
127                x, y, w: lw, text, ..
128            } => {
129                let lines = self.estimate_wrapped_lines(text, *lw);
130                push(Rect::new(*x, *y, *lw, lines));
131            }
132            RenderOp::PutWrappedStyled {
133                x,
134                y,
135                w: lw,
136                spans,
137                wrap_opts,
138                max_lines,
139            } => {
140                let lines = self.estimate_wrapped_spans_lines(spans, *lw, wrap_opts, *max_lines);
141                push(Rect::new(*x, *y, *lw, lines));
142            }
143            RenderOp::Blit {
144                x, y, w: bw, h: bh, ..
145            } => push(Rect::new(*x, *y, *bw, *bh)),
146            RenderOp::HLine { x, y, len, .. } => push(Rect::new(*x, *y, *len, 1)),
147            RenderOp::VLine { x, y, len, .. } => push(Rect::new(*x, *y, 1, *len)),
148            RenderOp::Box {
149                x, y, w: bw, h: bh, ..
150            } => push(Rect::new(*x, *y, *bw, *bh)),
151        }
152    }
153
154    fn estimate_wrapped_lines(&self, text: &str, w: u16) -> u16 {
155        if w == 0 {
156            return 0;
157        }
158        if text.is_empty() {
159            return 0;
160        }
161        let max_w = w.min(self.grid.width);
162        if max_w == 0 {
163            return 0;
164        }
165        let mut lines: u16 = 0;
166        for para in text.split(['\n', '\r']) {
167            if para.is_empty() {
168                continue;
169            }
170            for _ in wrap_text_wordwise(para, max_w, &self.reg) {
171                lines = lines.saturating_add(1);
172                if lines == u16::MAX {
173                    return lines;
174                }
175            }
176        }
177        lines.max(1)
178    }
179
180    fn estimate_wrapped_spans_lines(
181        &self,
182        spans: &[Span],
183        w: u16,
184        wrap_opts: &WrapOpts,
185        max_lines: Option<u16>,
186    ) -> u16 {
187        if w == 0 {
188            return 0;
189        }
190        let max_w = w.min(self.grid.width);
191        if max_w == 0 {
192            return 0;
193        }
194        let lines = wrap_spans_wordwise(&self.reg, spans, max_w as usize, wrap_opts);
195        let mut n = lines.len() as u16;
196        if let Some(lim) = max_lines {
197            n = n.min(lim);
198        }
199        n
200    }
201
202    #[cfg(feature = "debug-validate")]
203    #[inline]
204    fn debug_validate_after(&self, op: &RenderOp) {
205        if let Err(e) = self.grid.validate_invariants(&self.reg) {
206            // Pointer breadcrumb avoids requiring RenderOp: Debug while still attributing
207            // the invariant failure to a specific operation.
208            panic!(
209                "termgrid-core invariant violation after op_ptr={:p}: {:?}",
210                op, e
211            );
212        }
213    }
214
215    #[cfg(not(feature = "debug-validate"))]
216    #[inline]
217    fn debug_validate_after(&self, _op: &RenderOp) {
218        // No-op when invariant enforcement is disabled.
219    }
220
221    pub fn apply_op(&mut self, op: &RenderOp) {
222        match op {
223            RenderOp::Clear => self.grid.clear(),
224
225            RenderOp::ClearLine { y } => self.clear_line(*y),
226
227            RenderOp::ClearEol { x, y } => self.clear_eol(*x, *y),
228
229            RenderOp::ClearBol { x, y } => self.clear_bol(*x, *y),
230
231            RenderOp::ClearEos { x, y } => self.clear_eos(*x, *y),
232
233            RenderOp::ClearRect { x, y, w, h } => self.clear_rect(*x, *y, *w, *h),
234
235            RenderOp::Put { x, y, text, style } => {
236                let s: Style = *style;
237                self.grid.put_text(*x, *y, text, s, &self.reg);
238            }
239
240            RenderOp::PutGlyph { x, y, glyph, style } => {
241                let s: Style = *style;
242                self.grid.put_text(*x, *y, glyph, s, &self.reg);
243            }
244
245            RenderOp::Label {
246                x,
247                y,
248                w,
249                text,
250                style,
251                truncate,
252            } => {
253                self.label(*x, *y, *w, text, *style, *truncate);
254            }
255
256            RenderOp::LabelStyled {
257                x,
258                y,
259                w,
260                spans,
261                truncate,
262            } => {
263                self.put_styled(*x, *y, *w, spans, *truncate);
264            }
265
266            RenderOp::PutStyled {
267                x,
268                y,
269                w,
270                spans,
271                truncate,
272            } => {
273                self.put_styled(*x, *y, *w, spans, *truncate);
274            }
275
276            RenderOp::PutWrapped {
277                x,
278                y,
279                w,
280                text,
281                style,
282            } => {
283                self.put_wrapped(*x, *y, *w, text, *style);
284            }
285
286            RenderOp::PutWrappedStyled {
287                x,
288                y,
289                w,
290                spans,
291                wrap_opts,
292                max_lines,
293            } => {
294                self.put_wrapped_styled(*x, *y, *w, spans, wrap_opts, *max_lines);
295            }
296
297            RenderOp::Blit { x, y, w, h, cells } => {
298                self.blit(*x, *y, *w, *h, cells);
299            }
300
301            RenderOp::FillRect { x, y, w, h, style } => {
302                self.fill_rect(*x, *y, *w, *h, *style);
303            }
304
305            RenderOp::HLine {
306                x,
307                y,
308                len,
309                glyph,
310                style,
311            } => {
312                self.hline(*x, *y, *len, glyph, *style);
313            }
314
315            RenderOp::VLine {
316                x,
317                y,
318                len,
319                glyph,
320                style,
321            } => {
322                self.vline(*x, *y, *len, glyph, *style);
323            }
324
325            RenderOp::Box {
326                x,
327                y,
328                w,
329                h,
330                style,
331                charset,
332            } => {
333                self.draw_box(*x, *y, *w, *h, *style, *charset);
334            }
335        }
336        self.debug_validate_after(op);
337    }
338
339    fn clear_line(&mut self, y: u16) {
340        if y >= self.grid.height {
341            return;
342        }
343        let line = " ".repeat(self.grid.width as usize);
344        self.grid.put_text(0, y, &line, Style::plain(), &self.reg);
345    }
346
347    fn clear_eol(&mut self, x: u16, y: u16) {
348        if y >= self.grid.height || x >= self.grid.width {
349            return;
350        }
351
352        // If x is the continuation half of a wide glyph, blank the leading half too.
353        if x > 0 && matches!(self.grid.get(x, y), Some(Cell::Continuation)) {
354            self.grid.put_text(x - 1, y, " ", Style::plain(), &self.reg);
355        }
356
357        let count = (self.grid.width - x) as usize;
358        let line = " ".repeat(count);
359        self.grid.put_text(x, y, &line, Style::plain(), &self.reg);
360    }
361
362    fn clear_bol(&mut self, x: u16, y: u16) {
363        if y >= self.grid.height || x >= self.grid.width {
364            return;
365        }
366        let count = (x + 1) as usize;
367        let line = " ".repeat(count);
368        self.grid.put_text(0, y, &line, Style::plain(), &self.reg);
369    }
370
371    fn clear_eos(&mut self, x: u16, y: u16) {
372        if y >= self.grid.height || x >= self.grid.width {
373            return;
374        }
375        self.clear_eol(x, y);
376        for yy in (y + 1)..self.grid.height {
377            self.clear_line(yy);
378        }
379    }
380
381    fn clear_rect(&mut self, x: u16, y: u16, w: u16, h: u16) {
382        self.fill_rect(x, y, w, h, Style::plain());
383    }
384
385    fn label(&mut self, x: u16, y: u16, w: u16, text: &str, style: Style, truncate: TruncateMode) {
386        if w == 0 {
387            return;
388        }
389        if x >= self.grid.width || y >= self.grid.height {
390            return;
391        }
392        let max_w = w.min(self.grid.width - x);
393        if max_w == 0 {
394            return;
395        }
396
397        // Stop at the first newline, if present.
398        let line = text.split(['\n', '\r']).next().unwrap_or("");
399
400        let rendered = match truncate {
401            TruncateMode::Clip => clip_to_cells_text(&self.reg, line, max_w as usize).0,
402            TruncateMode::Ellipsis => {
403                ellipsis_to_cells_text(&self.reg, line, max_w as usize, DEFAULT_ELLIPSIS)
404            }
405        };
406
407        let spans = [Span::new(rendered, style)];
408        self.put_spans_line(x, y, max_w, &spans);
409    }
410
411    fn put_wrapped(&mut self, x: u16, y: u16, w: u16, text: &str, style: Style) {
412        if w == 0 {
413            return;
414        }
415        if x >= self.grid.width || y >= self.grid.height {
416            return;
417        }
418        let max_w = w.min(self.grid.width - x);
419        if max_w == 0 {
420            return;
421        }
422
423        let mut cy = y;
424        for para in text.split(['\n', '\r']) {
425            if cy >= self.grid.height {
426                break;
427            }
428            // Wrap each paragraph independently; blank paragraphs advance a line.
429            if para.is_empty() {
430                cy = cy.saturating_add(1);
431                continue;
432            }
433
434            for line in wrap_text_wordwise(para, max_w, &self.reg) {
435                if cy >= self.grid.height {
436                    break;
437                }
438                let spans = [Span::new(line, style)];
439                self.put_spans_line(x, cy, max_w, &spans);
440                cy = cy.saturating_add(1);
441            }
442            // Paragraph boundary: advance one line between paragraphs if there are more.
443            // The split() iterator discards delimiters; we keep it simple and do not add
444            // an extra blank line here beyond what empty paras already produce.
445        }
446    }
447
448    fn put_wrapped_styled(
449        &mut self,
450        x: u16,
451        y: u16,
452        w: u16,
453        spans: &[Span],
454        wrap_opts: &WrapOpts,
455        max_lines: Option<u16>,
456    ) {
457        if w == 0 {
458            return;
459        }
460        if x >= self.grid.width || y >= self.grid.height {
461            return;
462        }
463        let max_w = w.min(self.grid.width - x);
464        if max_w == 0 {
465            return;
466        }
467
468        let lines = wrap_spans_wordwise(&self.reg, spans, max_w as usize, wrap_opts);
469        let limit = max_lines.map(|v| v as usize);
470
471        let mut cy = y;
472        for (rendered, line_spans) in lines.into_iter().enumerate() {
473            if cy >= self.grid.height {
474                break;
475            }
476            if let Some(lim) = limit {
477                if rendered >= lim {
478                    break;
479                }
480            }
481            self.put_spans_line(x, cy, max_w, &line_spans);
482            cy = cy.saturating_add(1);
483        }
484    }
485
486    fn put_styled(&mut self, x: u16, y: u16, w: u16, spans: &[Span], truncate: TruncateMode) {
487        if w == 0 {
488            return;
489        }
490        if x >= self.grid.width || y >= self.grid.height {
491            return;
492        }
493        let max_w = w.min(self.grid.width - x);
494        if max_w == 0 {
495            return;
496        }
497
498        let spans = normalize_spans(spans);
499
500        let rendered_spans = match truncate {
501            TruncateMode::Clip => {
502                let (s, _clipped) = clip_to_cells_spans(&self.reg, &spans, max_w as usize);
503                s
504            }
505            TruncateMode::Ellipsis => {
506                // Deterministic: ellipsis is always rendered in plain style.
507                // Callers who want a styled ellipsis should construct it themselves
508                // by pre-ellipsizing spans before emitting render ops.
509                let ell = Span::new(DEFAULT_ELLIPSIS, Style::plain());
510                ellipsis_to_cells_spans(&self.reg, &spans, max_w as usize, &ell)
511            }
512        };
513
514        self.put_spans_line(x, y, max_w, &rendered_spans);
515    }
516
517    fn put_spans_line(&mut self, x: u16, y: u16, max_w: u16, spans: &[Span]) {
518        if max_w == 0 {
519            return;
520        }
521        let limit_x = x.saturating_add(max_w);
522
523        let mut cx = x;
524        for s in spans {
525            for g in s.text.graphemes(true) {
526                // Hard cap at the requested width, independent of grid width.
527                if cx >= limit_x {
528                    return;
529                }
530                if cx >= self.grid.width {
531                    return;
532                }
533                let gw = self.reg.width(g);
534                if gw == 0 {
535                    continue;
536                }
537                // Avoid placing a half-wide glyph either at the grid edge or beyond the op width.
538                if gw == 2 {
539                    if cx + 1 >= self.grid.width {
540                        return;
541                    }
542                    if cx + 1 >= limit_x {
543                        return;
544                    }
545                }
546                self.grid.put_text(cx, y, g, s.style, &self.reg);
547                cx = cx.saturating_add(gw as u16);
548            }
549        }
550    }
551
552    fn blit(&mut self, x: u16, y: u16, w: u16, h: u16, cells: &[Option<BlitCell>]) {
553        if w == 0 || h == 0 {
554            return;
555        }
556        if x >= self.grid.width || y >= self.grid.height {
557            return;
558        }
559
560        let max_w = w.min(self.grid.width.saturating_sub(x));
561        let max_h = h.min(self.grid.height.saturating_sub(y));
562        if max_w == 0 || max_h == 0 {
563            return;
564        }
565
566        let src_w = w as usize;
567
568        for sy in 0..max_h {
569            let ty = y.saturating_add(sy);
570            let mut sx: u16 = 0;
571
572            while sx < max_w {
573                let idx = sy as usize * src_w + sx as usize;
574                if idx >= cells.len() {
575                    break;
576                }
577
578                let tx = x.saturating_add(sx);
579                if tx >= self.grid.width {
580                    break;
581                }
582
583                if let Some(cell) = &cells[idx] {
584                    let style = cell.style;
585                    let glyph = cell.glyph.as_str();
586                    self.grid.put_text(tx, ty, glyph, style, &self.reg);
587
588                    let gw = self.reg.width(glyph) as u16;
589                    if gw == 2 {
590                        // A wide glyph consumes two cells; ignore the next source cell.
591                        sx = sx.saturating_add(2);
592                        continue;
593                    }
594                }
595                sx = sx.saturating_add(1);
596            }
597        }
598    }
599
600    fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, style: Style) {
601        if w == 0 || h == 0 {
602            return;
603        }
604        if x >= self.grid.width || y >= self.grid.height {
605            return;
606        }
607
608        let max_w = self.grid.width.saturating_sub(x);
609        let max_h = self.grid.height.saturating_sub(y);
610        let w = w.min(max_w);
611        let h = h.min(max_h);
612        if w == 0 || h == 0 {
613            return;
614        }
615
616        let line = " ".repeat(w as usize);
617        for dy in 0..h {
618            let yy = y.saturating_add(dy);
619            self.grid.put_text(x, yy, &line, style, &self.reg);
620        }
621    }
622
623    fn hline(&mut self, x: u16, y: u16, len_cells: u16, glyph: &str, style: Style) {
624        if len_cells == 0 {
625            return;
626        }
627        if y >= self.grid.height {
628            return;
629        }
630        if x >= self.grid.width {
631            return;
632        }
633
634        let gw = self.reg.width(glyph) as u16;
635        let gw = gw.max(1);
636
637        let mut cx = x;
638        let mut remaining = len_cells;
639        while remaining > 0 && cx < self.grid.width {
640            if gw == 2 {
641                if remaining < 2 {
642                    break;
643                }
644                if cx + 1 >= self.grid.width {
645                    break;
646                }
647                self.grid.put_text(cx, y, glyph, style, &self.reg);
648                cx = cx.saturating_add(2);
649                remaining = remaining.saturating_sub(2);
650            } else {
651                self.grid.put_text(cx, y, glyph, style, &self.reg);
652                cx = cx.saturating_add(1);
653                remaining = remaining.saturating_sub(1);
654            }
655        }
656    }
657
658    fn vline(&mut self, x: u16, y: u16, len_rows: u16, glyph: &str, style: Style) {
659        if len_rows == 0 {
660            return;
661        }
662        if x >= self.grid.width {
663            return;
664        }
665        if y >= self.grid.height {
666            return;
667        }
668
669        // If the glyph is wide, ensure we won't place half at the right edge.
670        if self.reg.width(glyph) == 2 && x + 1 >= self.grid.width {
671            return;
672        }
673
674        let max_len = self.grid.height.saturating_sub(y);
675        let len = len_rows.min(max_len);
676        for dy in 0..len {
677            let yy = y.saturating_add(dy);
678            self.grid.put_text(x, yy, glyph, style, &self.reg);
679        }
680    }
681
682    fn draw_box(&mut self, x: u16, y: u16, w: u16, h: u16, style: Style, charset: BoxCharset) {
683        if w < 2 || h < 2 {
684            return;
685        }
686        if x >= self.grid.width || y >= self.grid.height {
687            return;
688        }
689
690        let x1 = x.saturating_add(w - 1).min(self.grid.width - 1);
691        let y1 = y.saturating_add(h - 1).min(self.grid.height - 1);
692
693        if x1 <= x || y1 <= y {
694            return;
695        }
696
697        let (tl, tr, bl, br, hz, vt) = match charset {
698            BoxCharset::Ascii => ("+", "+", "+", "+", "-", "|"),
699            BoxCharset::UnicodeSingle => ("┌", "┐", "└", "┘", "─", "│"),
700            BoxCharset::UnicodeDouble => ("╔", "╗", "╚", "╝", "═", "║"),
701        };
702
703        // Corners
704        self.grid.put_text(x, y, tl, style, &self.reg);
705        self.grid.put_text(x1, y, tr, style, &self.reg);
706        self.grid.put_text(x, y1, bl, style, &self.reg);
707        self.grid.put_text(x1, y1, br, style, &self.reg);
708
709        // Horizontal lines
710        if x1 > x + 1 {
711            let count = (x1 - x - 1) as usize;
712            let line = hz.repeat(count);
713            self.grid.put_text(x + 1, y, &line, style, &self.reg);
714            self.grid.put_text(x + 1, y1, &line, style, &self.reg);
715        }
716
717        // Vertical lines
718        if y1 > y + 1 {
719            for yy in (y + 1)..y1 {
720                self.grid.put_text(x, yy, vt, style, &self.reg);
721                self.grid.put_text(x1, yy, vt, style, &self.reg);
722            }
723        }
724    }
725
726    /// Renders the current grid to an ANSI string.
727    pub fn to_ansi(&self) -> String {
728        ansi::grid_to_ansi(&self.grid)
729    }
730}
731
732fn clip_to_cells(text: &str, max_cells: u16, reg: &GlyphRegistry) -> String {
733    if max_cells == 0 {
734        return String::new();
735    }
736    let mut out = String::new();
737    let mut used: u16 = 0;
738    for g in UnicodeSegmentation::graphemes(text, true) {
739        let gw = reg.width(g) as u16;
740        if used + gw > max_cells {
741            break;
742        }
743        // Avoid placing half of a wide glyph in the last cell of the clip region.
744        if gw == 2 && used + 1 == max_cells {
745            break;
746        }
747        out.push_str(g);
748        used += gw.max(1);
749        if used >= max_cells {
750            break;
751        }
752    }
753    out
754}
755
756fn visible_width_cells(text: &str, reg: &GlyphRegistry) -> u16 {
757    let mut w: u16 = 0;
758    for g in UnicodeSegmentation::graphemes(text, true) {
759        if g == "\n" || g == "\r" {
760            break;
761        }
762        w = w.saturating_add(reg.width(g) as u16);
763    }
764    w
765}
766
767fn wrap_text_wordwise(text: &str, max_cells: u16, reg: &GlyphRegistry) -> Vec<String> {
768    // Tokenize into runs of whitespace vs non-whitespace graphemes.
769    #[derive(Clone)]
770    struct Tok {
771        s: String,
772        is_space: bool,
773    }
774
775    let mut toks: Vec<Tok> = Vec::new();
776    let mut cur = String::new();
777    let mut cur_is_space: Option<bool> = None;
778
779    for g in UnicodeSegmentation::graphemes(text, true) {
780        let is_space = g.chars().all(|c| c.is_whitespace());
781        match cur_is_space {
782            None => {
783                cur_is_space = Some(is_space);
784                cur.push_str(g);
785            }
786            Some(same) if same == is_space => {
787                cur.push_str(g);
788            }
789            Some(_) => {
790                toks.push(Tok {
791                    s: cur.clone(),
792                    is_space: cur_is_space.unwrap(),
793                });
794                cur.clear();
795                cur_is_space = Some(is_space);
796                cur.push_str(g);
797            }
798        }
799    }
800    if !cur.is_empty() {
801        toks.push(Tok {
802            s: cur,
803            is_space: cur_is_space.unwrap_or(false),
804        });
805    }
806
807    let mut lines: Vec<String> = Vec::new();
808    let mut line = String::new();
809    let mut used: u16 = 0;
810
811    fn push_line(lines: &mut Vec<String>, line: &mut String, used: &mut u16) {
812        lines.push(std::mem::take(line));
813        *used = 0;
814    }
815
816    let mut i = 0;
817    while i < toks.len() {
818        let tok = toks[i].clone();
819        if tok.is_space {
820            // Skip leading spaces.
821            if used == 0 {
822                i += 1;
823                continue;
824            }
825            // Collapse spaces to a single space to keep UI sane.
826            let s = " ";
827            let sw = reg.width(s) as u16;
828
829            // Avoid writing trailing spaces at end-of-line.
830            // If the next word would not fit after a space, wrap before the word.
831            // This keeps lines like "Hello" instead of "Hello ".
832            let mut next_word_w: Option<u16> = None;
833            for tok in toks.iter().skip(i + 1) {
834                if !tok.is_space {
835                    next_word_w = Some(visible_width_cells(&tok.s, reg));
836                    break;
837                }
838            }
839
840            if used + sw > max_cells {
841                push_line(&mut lines, &mut line, &mut used);
842            } else if let Some(ww) = next_word_w {
843                if used + sw + ww > max_cells {
844                    // Wrap before the next word; do not emit a trailing space.
845                    push_line(&mut lines, &mut line, &mut used);
846                } else {
847                    line.push_str(s);
848                    used += sw.max(1);
849                }
850            } else {
851                // Trailing whitespace at end of input: ignore.
852            }
853            i += 1;
854            continue;
855        }
856
857        let word = tok.s;
858        let ww = visible_width_cells(&word, reg);
859        if ww <= max_cells {
860            if used == 0 {
861                line.push_str(&word);
862                used = ww;
863            } else if used + ww <= max_cells {
864                line.push_str(&word);
865                used += ww;
866            } else {
867                push_line(&mut lines, &mut line, &mut used);
868                line.push_str(&word);
869                used = ww;
870            }
871            i += 1;
872            continue;
873        }
874
875        // Word longer than line: hard-break.
876        let mut remainder = word.as_str();
877        loop {
878            if remainder.is_empty() {
879                break;
880            }
881            if used != 0 {
882                push_line(&mut lines, &mut line, &mut used);
883            }
884            let part = clip_to_cells(remainder, max_cells, reg);
885            if part.is_empty() {
886                break;
887            }
888            line.push_str(&part);
889            used = visible_width_cells(&part, reg);
890            push_line(&mut lines, &mut line, &mut used);
891            remainder = &remainder[part.len()..];
892        }
893        i += 1;
894    }
895
896    if !line.is_empty() {
897        lines.push(line);
898    }
899    if lines.is_empty() {
900        lines.push(String::new());
901    }
902    lines
903}