Skip to main content

emux_render/
lib.rs

1//! Rendering primitives: text output, cursor drawing, and damage tracking.
2
3pub mod cursor;
4pub mod damage;
5pub mod text;
6
7use std::io::{self, Write};
8
9use crossterm::{
10    cursor as ct_cursor, style, QueueableCommand,
11};
12use emux_term::Screen;
13
14use crate::cursor::cursor_style;
15use crate::damage::DamageTracker;
16use crate::text::render_row;
17
18/// Terminal renderer with damage tracking for efficient redraws.
19pub struct Renderer {
20    damage: DamageTracker,
21    last_cols: usize,
22    last_rows: usize,
23}
24
25impl Renderer {
26    /// Create a new renderer for a terminal of the given size.
27    pub fn new(cols: usize, rows: usize) -> Self {
28        Self {
29            damage: DamageTracker::new(rows),
30            last_cols: cols,
31            last_rows: rows,
32        }
33    }
34
35    /// Render the screen to the given writer, only updating dirty rows.
36    pub fn render<W: Write>(&mut self, writer: &mut W, screen: &Screen) -> io::Result<()> {
37        let cols = screen.cols();
38        let rows = screen.rows();
39
40        // Detect size changes
41        if cols != self.last_cols || rows != self.last_rows {
42            self.resize(cols, rows);
43        }
44
45        if !self.damage.needs_redraw() {
46            return Ok(());
47        }
48
49        // Hide cursor during rendering
50        writer.queue(ct_cursor::Hide)?;
51
52        let dirty = self.damage.dirty_rows();
53        for row in dirty {
54            if row >= rows {
55                continue;
56            }
57
58            // Move to start of this row
59            writer.queue(ct_cursor::MoveTo(0, row as u16))?;
60
61            // Get the row cells from the grid
62            let grid_row = screen.grid.row(row);
63            let spans = render_row(&grid_row.cells, cols);
64
65            for (content_style, text) in spans {
66                writer.queue(style::ResetColor)?;
67                writer.queue(style::SetStyle(content_style))?;
68                writer.queue(style::Print(&text))?;
69            }
70        }
71
72        // Reset style
73        writer.queue(style::ResetColor)?;
74        writer.queue(style::SetAttribute(style::Attribute::Reset))?;
75
76        // Position cursor
77        let cursor = &screen.cursor;
78        writer.queue(ct_cursor::MoveTo(cursor.col as u16, cursor.row as u16))?;
79
80        // Show/hide cursor and set shape
81        if cursor.visible {
82            writer.queue(ct_cursor::Show)?;
83            writer.queue(cursor_style(cursor.shape))?;
84        } else {
85            writer.queue(ct_cursor::Hide)?;
86        }
87
88        writer.flush()?;
89        self.damage.clear();
90
91        Ok(())
92    }
93
94    /// Force a full redraw on the next render call.
95    pub fn force_redraw(&mut self) {
96        self.damage.mark_all();
97    }
98
99    /// Resize the renderer to match new terminal dimensions.
100    pub fn resize(&mut self, cols: usize, rows: usize) {
101        self.last_cols = cols;
102        self.last_rows = rows;
103        self.damage.resize(rows);
104    }
105}