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