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.map(|position| *position).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
106            .keys()
107            .last()
108            .and_then(|position| Some(*position))
109    }
110}
111
112/// Iterates through a subset of cells in the state.
113pub(crate) struct StateIter<'a> {
114    state: &'a State,
115    positions: Vec<Position>,
116    index: usize,
117}
118
119impl StateIter<'_> {
120    /// Create a new state iterator with the specified positions starting from the first position.
121    fn new(state: &State, positions: Vec<Position>) -> StateIter {
122        StateIter {
123            state,
124            positions,
125            index: 0,
126        }
127    }
128}
129
130impl<'a> Iterator for StateIter<'_> {
131    type Item = (Position, Option<Cell>);
132
133    fn next(&mut self) -> Option<Self::Item> {
134        if self.index < self.positions.len() {
135            let position = self.positions[self.index];
136            let cell = self
137                .state
138                .cells
139                .get(&position)
140                .and_then(|cell| Some(cell.clone()));
141
142            self.index += 1;
143            Some((position, cell))
144        } else {
145            None
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::{pos, Color, Position, Style};
153
154    use super::{Cell, State};
155
156    #[test]
157    fn state_set_text() {
158        let mut state = State::new();
159
160        state.set_text(pos!(0, 0), "A");
161        state.set_text(pos!(2, 0), "B");
162        state.set_text(pos!(1, 1), "C");
163
164        assert_eq!(3, state.cells.len());
165        assert_eq!(
166            Cell {
167                grapheme: "A".to_string(),
168                style: None
169            },
170            state.cells[&pos!(0, 0)]
171        );
172        assert_eq!(
173            Cell {
174                grapheme: "B".to_string(),
175                style: None
176            },
177            state.cells[&pos!(2, 0)]
178        );
179        assert_eq!(
180            Cell {
181                grapheme: "C".to_string(),
182                style: None
183            },
184            state.cells[&pos!(1, 1)]
185        );
186
187        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
188        assert_eq!(3, dirty_positions.len());
189        assert_eq!(pos!(0, 0), dirty_positions[0]);
190        assert_eq!(pos!(2, 0), dirty_positions[1]);
191        assert_eq!(pos!(1, 1), dirty_positions[2]);
192    }
193
194    #[test]
195    fn state_set_styled_text() {
196        let mut state = State::new();
197
198        state.set_styled_text(pos!(0, 0), "X", Style::new().set_bold(true));
199        state.set_styled_text(pos!(1, 3), "Y", Style::new().set_italic(true));
200        state.set_styled_text(pos!(2, 2), "Z", Style::new().set_foreground(Color::Blue));
201
202        assert_eq!(3, state.cells.len());
203        assert_eq!(
204            Cell {
205                grapheme: "X".to_string(),
206                style: Some(Style::new().set_bold(true)),
207            },
208            state.cells[&pos!(0, 0)],
209        );
210        assert_eq!(
211            Cell {
212                grapheme: "Y".to_string(),
213                style: Some(Style::new().set_italic(true)),
214            },
215            state.cells[&pos!(1, 3)],
216        );
217        assert_eq!(
218            Cell {
219                grapheme: "Z".to_string(),
220                style: Some(Style::new().set_foreground(Color::Blue)),
221            },
222            state.cells[&pos!(2, 2)],
223        );
224
225        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
226        assert_eq!(3, dirty_positions.len());
227        assert_eq!(pos!(0, 0), dirty_positions[0]);
228        assert_eq!(pos!(2, 2), dirty_positions[1]);
229        assert_eq!(pos!(1, 3), dirty_positions[2]);
230    }
231
232    #[test]
233    fn state_clear_line() {
234        let mut state = State::new();
235
236        state.set_text(pos!(0, 0), "A");
237        state.set_text(pos!(2, 0), "B");
238        state.set_text(pos!(1, 1), "C");
239        state.set_text(pos!(3, 1), "D");
240        state.clear_dirty();
241
242        assert_eq!(4, state.cells.len());
243        assert_eq!(
244            Cell {
245                grapheme: "A".to_string(),
246                style: None
247            },
248            state.cells[&pos!(0, 0)]
249        );
250        assert_eq!(
251            Cell {
252                grapheme: "B".to_string(),
253                style: None
254            },
255            state.cells[&pos!(2, 0)]
256        );
257        assert_eq!(
258            Cell {
259                grapheme: "C".to_string(),
260                style: None
261            },
262            state.cells[&pos!(1, 1)]
263        );
264        assert_eq!(
265            Cell {
266                grapheme: "D".to_string(),
267                style: None
268            },
269            state.cells[&pos!(3, 1)]
270        );
271
272        state.clear_line(1);
273
274        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
275        assert_eq!(2, dirty_positions.len());
276        assert_eq!(pos!(1, 1), dirty_positions[0]);
277        assert_eq!(pos!(3, 1), dirty_positions[1]);
278
279        let line_two_cell_count = state.cells.keys().filter(|pos| pos.y() == 1).count();
280        assert_eq!(0, line_two_cell_count);
281    }
282
283    #[test]
284    fn state_clear_dirty() {
285        let mut state = State::new();
286
287        state.set_text(pos!(0, 0), "A");
288        state.set_text(pos!(2, 0), "B");
289        state.set_text(pos!(1, 1), "C");
290
291        assert_eq!(3, state.cells.len());
292        assert_eq!(
293            Cell {
294                grapheme: "A".to_string(),
295                style: None
296            },
297            state.cells[&pos!(0, 0)]
298        );
299        assert_eq!(
300            Cell {
301                grapheme: "B".to_string(),
302                style: None
303            },
304            state.cells[&pos!(2, 0)]
305        );
306        assert_eq!(
307            Cell {
308                grapheme: "C".to_string(),
309                style: None
310            },
311            state.cells[&pos!(1, 1)]
312        );
313    }
314
315    #[test]
316    fn state_clear_rest_of_line() {
317        let mut state = State::new();
318
319        let content = ["ABC", "DEF", "GHI"];
320
321        for row in 0..content.len() {
322            let text = content[row];
323            for column in 0..text.len() {
324                state.set_text(
325                    pos!(column as u16, row as u16),
326                    text.get(column..column + 1).unwrap(),
327                );
328            }
329        }
330
331        state.clear_dirty();
332
333        assert_eq!(9, state.cells.len());
334
335        state.clear_rest_of_line(pos!(1, 1));
336
337        assert_eq!(7, state.cells.len());
338
339        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
340        assert_eq!(2, dirty_positions.len());
341        assert_eq!(pos!(1, 1), dirty_positions[0]);
342        assert_eq!(pos!(2, 1), dirty_positions[1]);
343
344        let line_two_cell_count = state.cells.keys().filter(|pos| pos.y() == 1).count();
345        assert_eq!(1, line_two_cell_count);
346    }
347
348    #[test]
349    fn state_clear_rest_of_interface() {
350        let mut state = State::new();
351
352        let content = ["ABC", "DEF", "GHI"];
353
354        for row in 0..content.len() {
355            let text = content[row];
356            for column in 0..text.len() {
357                state.set_text(
358                    pos!(column as u16, row as u16),
359                    text.get(column..column + 1).unwrap(),
360                );
361            }
362        }
363
364        state.clear_dirty();
365
366        assert_eq!(9, state.cells.len());
367
368        state.clear_rest_of_interface(pos!(1, 1));
369
370        assert_eq!(4, state.cells.len());
371
372        let dirty_positions: Vec<_> = state.dirty.clone().into_iter().collect();
373        assert_eq!(5, dirty_positions.len());
374        assert_eq!(pos!(1, 1), dirty_positions[0]);
375        assert_eq!(pos!(2, 1), dirty_positions[1]);
376        assert_eq!(pos!(0, 2), dirty_positions[2]);
377        assert_eq!(pos!(1, 2), dirty_positions[3]);
378        assert_eq!(pos!(2, 2), dirty_positions[4]);
379    }
380
381    #[test]
382    fn state_dirty_iter() {
383        let mut state = State::new();
384
385        state.set_text(pos!(0, 0), "A");
386        state.clear_dirty();
387
388        state.set_text(pos!(2, 0), "B");
389        state.set_text(pos!(1, 1), "C");
390        state.set_text(pos!(0, 2), "D");
391        state.clear_line(1);
392
393        let mut iter = state.dirty_iter();
394        assert_eq!(
395            Some((
396                pos!(2, 0),
397                Some(Cell {
398                    grapheme: "B".to_string(),
399                    style: None
400                })
401            )),
402            iter.next()
403        );
404        assert_eq!(Some((pos!(1, 1), None,)), iter.next());
405        assert_eq!(
406            Some((
407                pos!(0, 2),
408                Some(Cell {
409                    grapheme: "D".to_string(),
410                    style: None
411                })
412            )),
413            iter.next()
414        );
415        assert_eq!(None, iter.next());
416    }
417
418    #[test]
419    fn state_get_last_position() {
420        let mut state = State::new();
421
422        state.set_text(pos!(3, 1), "D");
423        state.set_text(pos!(1, 1), "C");
424        state.set_text(pos!(0, 0), "A");
425        state.set_text(pos!(2, 0), "B");
426
427        assert_eq!(pos!(3, 1), state.get_last_position().unwrap());
428    }
429}