tty_interface/
state.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{Position, Style};
4
5/// A cell in the terminal's column/line grid composed of text and optional style.
6#[derive(Debug, Clone, Eq, PartialEq)]
7pub(crate) struct Cell {
8    grapheme: String,
9    style: Option<Style>,
10}
11
12impl Cell {
13    /// This cell's text content.
14    pub(crate) fn grapheme(&self) -> &str {
15        &self.grapheme
16    }
17
18    /// If available, this cell's styling.
19    pub(crate) fn style(&self) -> Option<&Style> {
20        self.style.as_ref()
21    }
22}
23
24/// The terminal interface's contents with comparison capabilities.
25#[derive(Clone)]
26pub(crate) struct State {
27    cells: BTreeMap<Position, Cell>,
28    dirty: BTreeSet<Position>,
29}
30
31impl State {
32    /// Initialize a new, empty terminal state.
33    pub(crate) fn new() -> State {
34        State {
35            cells: BTreeMap::new(),
36            dirty: BTreeSet::new(),
37        }
38    }
39
40    /// Update a particular cell's grapheme.
41    pub(crate) fn set_text(&mut self, position: Position, grapheme: &str) {
42        self.handle_cell_update(position, grapheme, None);
43    }
44
45    /// Update a particular cell's grapheme and styling.
46    pub(crate) fn set_styled_text(&mut self, position: Position, grapheme: &str, style: Style) {
47        self.handle_cell_update(position, grapheme, Some(style));
48    }
49
50    /// Updates state and queues dirtied positions, if they've changed.
51    fn handle_cell_update(&mut self, position: Position, grapheme: &str, style: Option<Style>) {
52        let new_cell = Cell {
53            grapheme: grapheme.to_string(),
54            style,
55        };
56
57        // If this cell is unchanged, do not mark it dirty
58        if Some(&new_cell) == self.cells.get(&position) {
59            return;
60        }
61
62        self.dirty.insert(position);
63        self.cells.insert(position, new_cell);
64    }
65
66    /// Clears all cells in the specified line.
67    pub(crate) fn clear_line(&mut self, line: u16) {
68        self.handle_cell_clears(|position| position.y() == line);
69    }
70
71    /// Clears cells in the line from the specified position.
72    pub(crate) fn clear_rest_of_line(&mut self, from: Position) {
73        self.handle_cell_clears(|position| position.y() == from.y() && position.x() >= from.x());
74    }
75
76    /// Clears cells in the interface from the specified position.
77    pub(crate) fn clear_rest_of_interface(&mut self, from: Position) {
78        self.handle_cell_clears(|position| *position >= &from);
79    }
80
81    /// Clears cells matching the specified predicate, marking them dirtied for re-render.
82    fn handle_cell_clears<P: FnMut(&&Position) -> bool>(&mut self, filter_predicate: P) {
83        let cells = self.cells.keys();
84        let deleted_cells = cells.filter(filter_predicate);
85        let cell_positions: Vec<Position> = deleted_cells.copied().collect();
86
87        for position in cell_positions {
88            self.cells.remove(&position);
89            self.dirty.insert(position);
90        }
91    }
92
93    /// Marks any dirty cells as clean.
94    pub(crate) fn clear_dirty(&mut self) {
95        self.dirty.clear()
96    }
97
98    /// Create an iterator for this state's dirty cells.
99    pub(crate) fn dirty_iter(&self) -> StateIter<'_> {
100        StateIter::new(self, self.dirty.clone().into_iter().collect())
101    }
102
103    /// Get the last cell's position.
104    pub(crate) fn get_last_position(&self) -> Option<Position> {
105        self.cells.keys().last().copied()
106    }
107}
108
109/// Iterates through a subset of cells in the state.
110pub(crate) struct StateIter<'a> {
111    state: &'a State,
112    positions: Vec<Position>,
113    index: usize,
114}
115
116impl StateIter<'_> {
117    /// Create a new state iterator with the specified positions starting from the first position.
118    fn new(state: &State, positions: Vec<Position>) -> StateIter<'_> {
119        StateIter {
120            state,
121            positions,
122            index: 0,
123        }
124    }
125}
126
127impl Iterator for StateIter<'_> {
128    type Item = (Position, Option<Cell>);
129
130    fn next(&mut self) -> Option<Self::Item> {
131        if self.index < self.positions.len() {
132            let position = self.positions[self.index];
133            let cell = self.state.cells.get(&position).cloned();
134
135            self.index += 1;
136            Some((position, cell))
137        } else {
138            None
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use crate::{Color, Position, Style, pos};
146
147    use super::{Cell, State};
148
149    #[test]
150    fn state_set_text() {
151        let mut state = State::new();
152
153        state.set_text(pos!(0, 0), "A");
154        state.set_text(pos!(2, 0), "B");
155        state.set_text(pos!(1, 1), "C");
156
157        assert_eq!(3, state.cells.len());
158        assert_eq!(
159            Cell {
160                grapheme: "A".to_string(),
161                style: None
162            },
163            state.cells[&pos!(0, 0)]
164        );
165        assert_eq!(
166            Cell {
167                grapheme: "B".to_string(),
168                style: None
169            },
170            state.cells[&pos!(2, 0)]
171        );
172        assert_eq!(
173            Cell {
174                grapheme: "C".to_string(),
175                style: None
176            },
177            state.cells[&pos!(1, 1)]
178        );
179
180        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
181        assert_eq!(3, dirty_positions.len());
182        assert_eq!(pos!(0, 0), dirty_positions[0]);
183        assert_eq!(pos!(2, 0), dirty_positions[1]);
184        assert_eq!(pos!(1, 1), dirty_positions[2]);
185    }
186
187    #[test]
188    fn state_set_styled_text() {
189        let mut state = State::new();
190
191        state.set_styled_text(pos!(0, 0), "X", Style::new().set_bold(true));
192        state.set_styled_text(pos!(1, 3), "Y", Style::new().set_italic(true));
193        state.set_styled_text(pos!(2, 2), "Z", Style::new().set_foreground(Color::Blue));
194
195        assert_eq!(3, state.cells.len());
196        assert_eq!(
197            Cell {
198                grapheme: "X".to_string(),
199                style: Some(Style::new().set_bold(true)),
200            },
201            state.cells[&pos!(0, 0)],
202        );
203        assert_eq!(
204            Cell {
205                grapheme: "Y".to_string(),
206                style: Some(Style::new().set_italic(true)),
207            },
208            state.cells[&pos!(1, 3)],
209        );
210        assert_eq!(
211            Cell {
212                grapheme: "Z".to_string(),
213                style: Some(Style::new().set_foreground(Color::Blue)),
214            },
215            state.cells[&pos!(2, 2)],
216        );
217
218        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
219        assert_eq!(3, dirty_positions.len());
220        assert_eq!(pos!(0, 0), dirty_positions[0]);
221        assert_eq!(pos!(2, 2), dirty_positions[1]);
222        assert_eq!(pos!(1, 3), dirty_positions[2]);
223    }
224
225    #[test]
226    fn state_clear_line() {
227        let mut state = State::new();
228
229        state.set_text(pos!(0, 0), "A");
230        state.set_text(pos!(2, 0), "B");
231        state.set_text(pos!(1, 1), "C");
232        state.set_text(pos!(3, 1), "D");
233        state.clear_dirty();
234
235        assert_eq!(4, state.cells.len());
236        assert_eq!(
237            Cell {
238                grapheme: "A".to_string(),
239                style: None
240            },
241            state.cells[&pos!(0, 0)]
242        );
243        assert_eq!(
244            Cell {
245                grapheme: "B".to_string(),
246                style: None
247            },
248            state.cells[&pos!(2, 0)]
249        );
250        assert_eq!(
251            Cell {
252                grapheme: "C".to_string(),
253                style: None
254            },
255            state.cells[&pos!(1, 1)]
256        );
257        assert_eq!(
258            Cell {
259                grapheme: "D".to_string(),
260                style: None
261            },
262            state.cells[&pos!(3, 1)]
263        );
264
265        state.clear_line(1);
266
267        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
268        assert_eq!(2, dirty_positions.len());
269        assert_eq!(pos!(1, 1), dirty_positions[0]);
270        assert_eq!(pos!(3, 1), dirty_positions[1]);
271
272        let line_two_cell_count = state.cells.keys().filter(|pos| pos.y() == 1).count();
273        assert_eq!(0, line_two_cell_count);
274    }
275
276    #[test]
277    fn state_clear_dirty() {
278        let mut state = State::new();
279
280        state.set_text(pos!(0, 0), "A");
281        state.set_text(pos!(2, 0), "B");
282        state.set_text(pos!(1, 1), "C");
283
284        assert_eq!(3, state.cells.len());
285        assert_eq!(
286            Cell {
287                grapheme: "A".to_string(),
288                style: None
289            },
290            state.cells[&pos!(0, 0)]
291        );
292        assert_eq!(
293            Cell {
294                grapheme: "B".to_string(),
295                style: None
296            },
297            state.cells[&pos!(2, 0)]
298        );
299        assert_eq!(
300            Cell {
301                grapheme: "C".to_string(),
302                style: None
303            },
304            state.cells[&pos!(1, 1)]
305        );
306    }
307
308    #[test]
309    fn state_clear_rest_of_line() {
310        let mut state = State::new();
311
312        let content = ["ABC", "DEF", "GHI"];
313
314        for (row, text) in content.iter().enumerate() {
315            for column in 0..text.len() {
316                state.set_text(
317                    pos!(column as u16, row as u16),
318                    text.get(column..column + 1).unwrap(),
319                );
320            }
321        }
322
323        state.clear_dirty();
324
325        assert_eq!(9, state.cells.len());
326
327        state.clear_rest_of_line(pos!(1, 1));
328
329        assert_eq!(7, state.cells.len());
330
331        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
332        assert_eq!(2, dirty_positions.len());
333        assert_eq!(pos!(1, 1), dirty_positions[0]);
334        assert_eq!(pos!(2, 1), dirty_positions[1]);
335
336        let line_two_cell_count = state.cells.keys().filter(|pos| pos.y() == 1).count();
337        assert_eq!(1, line_two_cell_count);
338    }
339
340    #[test]
341    fn state_clear_rest_of_interface() {
342        let mut state = State::new();
343
344        let content = ["ABC", "DEF", "GHI"];
345
346        for (row, text) in content.iter().enumerate() {
347            for column in 0..text.len() {
348                state.set_text(
349                    pos!(column as u16, row as u16),
350                    text.get(column..column + 1).unwrap(),
351                );
352            }
353        }
354
355        state.clear_dirty();
356
357        assert_eq!(9, state.cells.len());
358
359        state.clear_rest_of_interface(pos!(1, 1));
360
361        assert_eq!(4, state.cells.len());
362
363        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
364        assert_eq!(5, dirty_positions.len());
365        assert_eq!(pos!(1, 1), dirty_positions[0]);
366        assert_eq!(pos!(2, 1), dirty_positions[1]);
367        assert_eq!(pos!(0, 2), dirty_positions[2]);
368        assert_eq!(pos!(1, 2), dirty_positions[3]);
369        assert_eq!(pos!(2, 2), dirty_positions[4]);
370    }
371
372    #[test]
373    fn state_dirty_iter() {
374        let mut state = State::new();
375
376        state.set_text(pos!(0, 0), "A");
377        state.clear_dirty();
378
379        state.set_text(pos!(2, 0), "B");
380        state.set_text(pos!(1, 1), "C");
381        state.set_text(pos!(0, 2), "D");
382        state.clear_line(1);
383
384        let mut iter = state.dirty_iter();
385        assert_eq!(
386            Some((
387                pos!(2, 0),
388                Some(Cell {
389                    grapheme: "B".to_string(),
390                    style: None
391                })
392            )),
393            iter.next()
394        );
395        assert_eq!(Some((pos!(1, 1), None,)), iter.next());
396        assert_eq!(
397            Some((
398                pos!(0, 2),
399                Some(Cell {
400                    grapheme: "D".to_string(),
401                    style: None
402                })
403            )),
404            iter.next()
405        );
406        assert_eq!(None, iter.next());
407    }
408
409    #[test]
410    fn state_get_last_position() {
411        let mut state = State::new();
412
413        state.set_text(pos!(3, 1), "D");
414        state.set_text(pos!(1, 1), "C");
415        state.set_text(pos!(0, 0), "A");
416        state.set_text(pos!(2, 0), "B");
417
418        assert_eq!(pos!(3, 1), state.get_last_position().unwrap());
419    }
420}