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::TextBlock {
126                x, y, w: rw, h: rh, ..
127            } => push(Rect::new(*x, *y, *rw, *rh)),
128            RenderOp::TextBlockStyled {
129                x, y, w: rw, h: rh, ..
130            } => push(Rect::new(*x, *y, *rw, *rh)),
131            RenderOp::Blit {
132                x, y, w: bw, h: bh, ..
133            } => push(Rect::new(*x, *y, *bw, *bh)),
134            RenderOp::Box {
135                x, y, w: bw, h: bh, ..
136            } => push(Rect::new(*x, *y, *bw, *bh)),
137        }
138    }
139
140    #[cfg(feature = "debug-validate")]
141    #[inline]
142    fn debug_validate_after(&self, op: &RenderOp) {
143        if let Err(e) = self.grid.validate_invariants(&self.reg) {
144            // Pointer breadcrumb avoids requiring RenderOp: Debug while still attributing
145            // the invariant failure to a specific operation.
146            panic!(
147                "termgrid-core invariant violation after op_ptr={:p}: {:?}",
148                op, e
149            );
150        }
151    }
152
153    #[cfg(not(feature = "debug-validate"))]
154    #[inline]
155    fn debug_validate_after(&self, _op: &RenderOp) {
156        // No-op when invariant enforcement is disabled.
157    }
158
159    pub fn apply_op(&mut self, op: &RenderOp) {
160        match op {
161            RenderOp::Clear => self.grid.clear(),
162
163            RenderOp::ClearLine { y } => self.clear_line(*y),
164
165            RenderOp::ClearEol { x, y } => self.clear_eol(*x, *y),
166
167            RenderOp::ClearBol { x, y } => self.clear_bol(*x, *y),
168
169            RenderOp::ClearEos { x, y } => self.clear_eos(*x, *y),
170
171            RenderOp::ClearRect { x, y, w, h } => self.clear_rect(*x, *y, *w, *h),
172
173            RenderOp::Put { x, y, text, style } => {
174                let s: Style = *style;
175                self.grid.put_text(*x, *y, text, s, &self.reg);
176            }
177
178            RenderOp::PutGlyph { x, y, glyph, style } => {
179                let s: Style = *style;
180                self.grid.put_text(*x, *y, glyph, s, &self.reg);
181            }
182
183            RenderOp::Label {
184                x,
185                y,
186                w,
187                text,
188                style,
189                truncate,
190            } => {
191                self.label(*x, *y, *w, text, *style, *truncate);
192            }
193
194            RenderOp::LabelStyled {
195                x,
196                y,
197                w,
198                spans,
199                truncate,
200            } => {
201                self.put_styled(*x, *y, *w, spans, *truncate);
202            }
203
204            RenderOp::TextBlock {
205                x,
206                y,
207                w,
208                h,
209                text,
210                style,
211                wrap,
212            } => {
213                let spans = [Span::new(text.as_str(), *style)];
214                self.put_wrapped_styled(*x, *y, *w, &spans, wrap, Some(*h));
215            }
216
217            RenderOp::TextBlockStyled {
218                x,
219                y,
220                w,
221                h,
222                spans,
223                wrap,
224            } => {
225                self.put_wrapped_styled(*x, *y, *w, spans, wrap, Some(*h));
226            }
227
228            RenderOp::Blit { x, y, w, h, cells } => {
229                self.blit(*x, *y, *w, *h, cells);
230            }
231
232            RenderOp::FillRect {
233                x,
234                y,
235                w,
236                h,
237                glyph,
238                style,
239            } => {
240                self.fill_rect(*x, *y, *w, *h, glyph, *style);
241            }
242
243            RenderOp::Box {
244                x,
245                y,
246                w,
247                h,
248                style,
249                charset,
250            } => {
251                self.draw_box(*x, *y, *w, *h, *style, *charset);
252            }
253        }
254        self.debug_validate_after(op);
255    }
256
257    fn clear_line(&mut self, y: u16) {
258        if y >= self.grid.height {
259            return;
260        }
261        let line = " ".repeat(self.grid.width as usize);
262        self.grid.put_text(0, y, &line, Style::plain(), &self.reg);
263    }
264
265    fn clear_eol(&mut self, x: u16, y: u16) {
266        if y >= self.grid.height || x >= self.grid.width {
267            return;
268        }
269
270        // If x is the continuation half of a wide glyph, blank the leading half too.
271        if x > 0 && matches!(self.grid.get(x, y), Some(Cell::Continuation)) {
272            self.grid.put_text(x - 1, y, " ", Style::plain(), &self.reg);
273        }
274
275        let count = (self.grid.width - x) as usize;
276        let line = " ".repeat(count);
277        self.grid.put_text(x, y, &line, Style::plain(), &self.reg);
278    }
279
280    fn clear_bol(&mut self, x: u16, y: u16) {
281        if y >= self.grid.height || x >= self.grid.width {
282            return;
283        }
284        let count = (x + 1) as usize;
285        let line = " ".repeat(count);
286        self.grid.put_text(0, y, &line, Style::plain(), &self.reg);
287    }
288
289    fn clear_eos(&mut self, x: u16, y: u16) {
290        if y >= self.grid.height || x >= self.grid.width {
291            return;
292        }
293        self.clear_eol(x, y);
294        for yy in (y + 1)..self.grid.height {
295            self.clear_line(yy);
296        }
297    }
298
299    fn clear_rect(&mut self, x: u16, y: u16, w: u16, h: u16) {
300        self.fill_rect(x, y, w, h, " ", Style::plain());
301    }
302
303    fn label(&mut self, x: u16, y: u16, w: u16, text: &str, style: Style, truncate: TruncateMode) {
304        if w == 0 {
305            return;
306        }
307        if x >= self.grid.width || y >= self.grid.height {
308            return;
309        }
310        let max_w = w.min(self.grid.width - x);
311        if max_w == 0 {
312            return;
313        }
314
315        // Stop at the first newline, if present.
316        let line = text.split(['\n', '\r']).next().unwrap_or("");
317
318        let rendered = match truncate {
319            TruncateMode::Clip => clip_to_cells_text(&self.reg, line, max_w as usize).0,
320            TruncateMode::Ellipsis => {
321                ellipsis_to_cells_text(&self.reg, line, max_w as usize, DEFAULT_ELLIPSIS)
322            }
323        };
324
325        let spans = [Span::new(rendered, style)];
326        self.put_spans_line(x, y, max_w, &spans);
327    }
328
329    // NOTE: plain text wrapping is implemented by converting text into a single span
330    // and delegating to `wrap_spans_wordwise` so WrapOpts behavior is consistent.
331
332    fn put_wrapped_styled(
333        &mut self,
334        x: u16,
335        y: u16,
336        w: u16,
337        spans: &[Span],
338        wrap_opts: &WrapOpts,
339        max_lines: Option<u16>,
340    ) {
341        if w == 0 {
342            return;
343        }
344        if x >= self.grid.width || y >= self.grid.height {
345            return;
346        }
347        let max_w = w.min(self.grid.width - x);
348        if max_w == 0 {
349            return;
350        }
351
352        let lines = wrap_spans_wordwise(&self.reg, spans, max_w as usize, wrap_opts);
353        let limit = max_lines.map(|v| v as usize);
354
355        let mut cy = y;
356        for (rendered, line_spans) in lines.into_iter().enumerate() {
357            if cy >= self.grid.height {
358                break;
359            }
360            if let Some(lim) = limit {
361                if rendered >= lim {
362                    break;
363                }
364            }
365            self.put_spans_line(x, cy, max_w, &line_spans);
366            cy = cy.saturating_add(1);
367        }
368    }
369
370    fn put_styled(&mut self, x: u16, y: u16, w: u16, spans: &[Span], truncate: TruncateMode) {
371        if w == 0 {
372            return;
373        }
374        if x >= self.grid.width || y >= self.grid.height {
375            return;
376        }
377        let max_w = w.min(self.grid.width - x);
378        if max_w == 0 {
379            return;
380        }
381
382        let spans = normalize_spans(spans);
383
384        let rendered_spans = match truncate {
385            TruncateMode::Clip => {
386                let (s, _clipped) = clip_to_cells_spans(&self.reg, &spans, max_w as usize);
387                s
388            }
389            TruncateMode::Ellipsis => {
390                // Deterministic: ellipsis is always rendered in plain style.
391                // Callers who want a styled ellipsis should construct it themselves
392                // by pre-ellipsizing spans before emitting render ops.
393                let ell = Span::new(DEFAULT_ELLIPSIS, Style::plain());
394                ellipsis_to_cells_spans(&self.reg, &spans, max_w as usize, &ell)
395            }
396        };
397
398        self.put_spans_line(x, y, max_w, &rendered_spans);
399    }
400
401    fn put_spans_line(&mut self, x: u16, y: u16, max_w: u16, spans: &[Span]) {
402        if max_w == 0 {
403            return;
404        }
405        let limit_x = x.saturating_add(max_w);
406
407        let mut cx = x;
408        for s in spans {
409            for g in s.text.graphemes(true) {
410                // Hard cap at the requested width, independent of grid width.
411                if cx >= limit_x {
412                    return;
413                }
414                if cx >= self.grid.width {
415                    return;
416                }
417                let gw = self.reg.width(g);
418                if gw == 0 {
419                    continue;
420                }
421                // Avoid placing a half-wide glyph either at the grid edge or beyond the op width.
422                if gw == 2 {
423                    if cx + 1 >= self.grid.width {
424                        return;
425                    }
426                    if cx + 1 >= limit_x {
427                        return;
428                    }
429                }
430                self.grid.put_text(cx, y, g, s.style, &self.reg);
431                cx = cx.saturating_add(gw as u16);
432            }
433        }
434    }
435
436    fn blit(&mut self, x: u16, y: u16, w: u16, h: u16, cells: &[Option<BlitCell>]) {
437        if w == 0 || h == 0 {
438            return;
439        }
440        if x >= self.grid.width || y >= self.grid.height {
441            return;
442        }
443
444        let max_w = w.min(self.grid.width.saturating_sub(x));
445        let max_h = h.min(self.grid.height.saturating_sub(y));
446        if max_w == 0 || max_h == 0 {
447            return;
448        }
449
450        let src_w = w as usize;
451
452        for sy in 0..max_h {
453            let ty = y.saturating_add(sy);
454            let mut sx: u16 = 0;
455
456            while sx < max_w {
457                let idx = sy as usize * src_w + sx as usize;
458                if idx >= cells.len() {
459                    break;
460                }
461
462                let tx = x.saturating_add(sx);
463                if tx >= self.grid.width {
464                    break;
465                }
466
467                if let Some(cell) = &cells[idx] {
468                    let style = cell.style;
469                    let glyph = cell.glyph.as_str();
470                    self.grid.put_text(tx, ty, glyph, style, &self.reg);
471
472                    let gw = self.reg.width(glyph) as u16;
473                    if gw == 2 {
474                        // A wide glyph consumes two cells; ignore the next source cell.
475                        sx = sx.saturating_add(2);
476                        continue;
477                    }
478                }
479                sx = sx.saturating_add(1);
480            }
481        }
482    }
483
484    fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, glyph: &str, style: Style) {
485        if w == 0 || h == 0 {
486            return;
487        }
488        if x >= self.grid.width || y >= self.grid.height {
489            return;
490        }
491
492        let gw = self.reg.width(glyph) as u16;
493        let gw = gw.max(1);
494        // Avoid placing half of a wide glyph at the right edge.
495        if gw == 2 && x + 1 >= self.grid.width {
496            return;
497        }
498
499        let max_w = self.grid.width.saturating_sub(x);
500        let max_h = self.grid.height.saturating_sub(y);
501        let w = w.min(max_w);
502        let h = h.min(max_h);
503        if w == 0 || h == 0 {
504            return;
505        }
506
507        for dy in 0..h {
508            let yy = y.saturating_add(dy);
509            let mut cx = x;
510            let mut remaining = w;
511
512            while remaining > 0 && cx < self.grid.width {
513                if gw == 2 {
514                    if remaining < 2 {
515                        break;
516                    }
517                    if cx + 1 >= self.grid.width {
518                        break;
519                    }
520                    self.grid.put_text(cx, yy, glyph, style, &self.reg);
521                    cx = cx.saturating_add(2);
522                    remaining = remaining.saturating_sub(2);
523                } else {
524                    self.grid.put_text(cx, yy, glyph, style, &self.reg);
525                    cx = cx.saturating_add(1);
526                    remaining = remaining.saturating_sub(1);
527                }
528            }
529        }
530    }
531
532    fn draw_box(&mut self, x: u16, y: u16, w: u16, h: u16, style: Style, charset: BoxCharset) {
533        if w < 2 || h < 2 {
534            return;
535        }
536        if x >= self.grid.width || y >= self.grid.height {
537            return;
538        }
539
540        let x1 = x.saturating_add(w - 1).min(self.grid.width - 1);
541        let y1 = y.saturating_add(h - 1).min(self.grid.height - 1);
542
543        if x1 <= x || y1 <= y {
544            return;
545        }
546
547        let (tl, tr, bl, br, hz, vt) = match charset {
548            BoxCharset::Ascii => ("+", "+", "+", "+", "-", "|"),
549            BoxCharset::UnicodeSingle => ("┌", "┐", "└", "┘", "─", "│"),
550            BoxCharset::UnicodeDouble => ("╔", "╗", "╚", "╝", "═", "║"),
551        };
552
553        // Corners
554        self.grid.put_text(x, y, tl, style, &self.reg);
555        self.grid.put_text(x1, y, tr, style, &self.reg);
556        self.grid.put_text(x, y1, bl, style, &self.reg);
557        self.grid.put_text(x1, y1, br, style, &self.reg);
558
559        // Horizontal lines
560        if x1 > x + 1 {
561            let count = (x1 - x - 1) as usize;
562            let line = hz.repeat(count);
563            self.grid.put_text(x + 1, y, &line, style, &self.reg);
564            self.grid.put_text(x + 1, y1, &line, style, &self.reg);
565        }
566
567        // Vertical lines
568        if y1 > y + 1 {
569            for yy in (y + 1)..y1 {
570                self.grid.put_text(x, yy, vt, style, &self.reg);
571                self.grid.put_text(x1, yy, vt, style, &self.reg);
572            }
573        }
574    }
575
576    /// Renders the current grid to an ANSI string.
577    pub fn to_ansi(&self) -> String {
578        ansi::grid_to_ansi(&self.grid)
579    }
580}