ratatui_toolkit/primitives/termtui/
row.rs

1//! Terminal row representation
2
3use crate::primitives::termtui::attrs::Attrs;
4use crate::primitives::termtui::cell::Cell;
5
6/// A row of terminal cells
7#[derive(Clone, Debug)]
8pub struct Row {
9    /// Cells in this row
10    cells: Vec<Cell>,
11    /// Number of cells that have been written to
12    size: u16,
13    /// Whether this row wraps to the next line
14    wrapped: bool,
15}
16
17impl Row {
18    /// Create a new row with the given width
19    pub fn new(width: u16) -> Self {
20        Self {
21            cells: (0..width).map(|_| Cell::new()).collect(),
22            size: 0,
23            wrapped: false,
24        }
25    }
26
27    /// Create a new row with specific attributes
28    pub fn new_with_attrs(width: u16, attrs: Attrs) -> Self {
29        Self {
30            cells: (0..width).map(|_| Cell::with_attrs(attrs)).collect(),
31            size: 0,
32            wrapped: false,
33        }
34    }
35
36    /// Get the width of this row
37    pub fn width(&self) -> u16 {
38        self.cells.len() as u16
39    }
40
41    /// Get a cell at the given column
42    pub fn get(&self, col: u16) -> Option<&Cell> {
43        self.cells.get(col as usize)
44    }
45
46    /// Get a mutable cell at the given column
47    pub fn get_mut(&mut self, col: u16) -> Option<&mut Cell> {
48        // Track size (rightmost written cell)
49        if col >= self.size {
50            self.size = col + 1;
51        }
52        self.cells.get_mut(col as usize)
53    }
54
55    /// Insert a cell at the given column, shifting others right
56    pub fn insert(&mut self, col: u16, cell: Cell) {
57        let col = col as usize;
58        if col < self.cells.len() {
59            self.cells.insert(col, cell);
60            self.cells.pop(); // Keep same width
61        }
62    }
63
64    /// Remove a cell at the given column, shifting others left
65    pub fn remove(&mut self, col: u16) {
66        let col = col as usize;
67        if col < self.cells.len() {
68            self.cells.remove(col);
69            self.cells.push(Cell::new()); // Keep same width
70        }
71    }
72
73    /// Clear the row
74    pub fn clear(&mut self) {
75        for cell in &mut self.cells {
76            cell.clear();
77        }
78        self.size = 0;
79        self.wrapped = false;
80    }
81
82    /// Clear cells from start to end (exclusive)
83    pub fn erase(&mut self, start: u16, end: u16) {
84        let start = start as usize;
85        let end = (end as usize).min(self.cells.len());
86
87        for cell in &mut self.cells[start..end] {
88            cell.clear();
89        }
90    }
91
92    /// Resize the row
93    pub fn resize(&mut self, new_width: u16) {
94        let new_width = new_width as usize;
95        if new_width > self.cells.len() {
96            self.cells.resize_with(new_width, Cell::new);
97        } else {
98            self.cells.truncate(new_width);
99        }
100    }
101
102    /// Check if this row wraps to the next line
103    pub fn wrapped(&self) -> bool {
104        self.wrapped
105    }
106
107    /// Set whether this row wraps
108    pub fn set_wrapped(&mut self, wrapped: bool) {
109        self.wrapped = wrapped;
110    }
111
112    /// Check if a column is a wide character continuation
113    pub fn is_wide_continuation(&self, col: u16) -> bool {
114        self.cells
115            .get(col as usize)
116            .map(|c| c.is_wide_continuation())
117            .unwrap_or(false)
118    }
119
120    /// Clear wide character at position (both cells)
121    pub fn clear_wide(&mut self, col: u16) {
122        let col = col as usize;
123        if col < self.cells.len() {
124            // Check if this is a wide char
125            if self.cells[col].width() == 2 && col + 1 < self.cells.len() {
126                self.cells[col].clear();
127                self.cells[col + 1].clear();
128            } else if col > 0 && self.cells[col].is_wide_continuation() {
129                // This is continuation, clear the previous cell too
130                self.cells[col - 1].clear();
131                self.cells[col].clear();
132            } else {
133                self.cells[col].clear();
134            }
135        }
136    }
137
138    /// Write cell contents to a string (for text extraction)
139    pub fn write_contents(&self, output: &mut String, start: u16, end: u16) {
140        let start = start as usize;
141        let end = (end as usize).min(self.cells.len());
142
143        for cell in &self.cells[start..end] {
144            if !cell.is_wide_continuation() {
145                output.push_str(cell.text());
146            }
147        }
148    }
149
150    /// Get trimmed contents (no trailing spaces)
151    pub fn contents_trimmed(&self) -> String {
152        let mut output = String::new();
153        self.write_contents(&mut output, 0, self.width());
154        output.trim_end().to_string()
155    }
156
157    /// Iterate over cells
158    pub fn cells(&self) -> impl Iterator<Item = &Cell> {
159        self.cells.iter()
160    }
161
162    /// Get the number of cells actually written to
163    pub fn used_width(&self) -> u16 {
164        self.size
165    }
166}
167
168impl Default for Row {
169    fn default() -> Self {
170        Self::new(80)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_row_new() {
180        let row = Row::new(80);
181        assert_eq!(row.width(), 80);
182        assert!(!row.wrapped());
183    }
184
185    #[test]
186    fn test_row_get_set() {
187        let mut row = Row::new(80);
188
189        if let Some(cell) = row.get_mut(5) {
190            cell.set_text("X");
191        }
192
193        assert_eq!(row.get(5).map(|c| c.text()), Some("X"));
194    }
195
196    #[test]
197    fn test_row_clear() {
198        let mut row = Row::new(80);
199
200        if let Some(cell) = row.get_mut(5) {
201            cell.set_text("X");
202        }
203
204        row.clear();
205        assert_eq!(row.get(5).map(|c| c.text()), Some(" "));
206    }
207
208    #[test]
209    fn test_row_erase() {
210        let mut row = Row::new(80);
211
212        for i in 0..10 {
213            if let Some(cell) = row.get_mut(i) {
214                cell.set_text("X");
215            }
216        }
217
218        row.erase(3, 7);
219        assert_eq!(row.get(2).map(|c| c.text()), Some("X"));
220        assert_eq!(row.get(3).map(|c| c.text()), Some(" "));
221        assert_eq!(row.get(6).map(|c| c.text()), Some(" "));
222        assert_eq!(row.get(7).map(|c| c.text()), Some("X"));
223    }
224
225    #[test]
226    fn test_row_contents() {
227        let mut row = Row::new(80);
228
229        for (i, c) in "Hello".chars().enumerate() {
230            if let Some(cell) = row.get_mut(i as u16) {
231                cell.set_text(c.to_string());
232            }
233        }
234
235        let contents = row.contents_trimmed();
236        assert_eq!(contents, "Hello");
237    }
238
239    #[test]
240    fn test_row_wrapped() {
241        let mut row = Row::new(80);
242        assert!(!row.wrapped());
243
244        row.set_wrapped(true);
245        assert!(row.wrapped());
246    }
247}