ailloli_ui_terminal_core/
damage.rs

1//! Renderer-facing terminal damage tracking.
2
3use serde::{Deserialize, Serialize};
4
5/// Accumulated terminal regions and presentation properties requiring redraw.
6///
7/// Line indices are zero-based, unique, and sorted when added through methods;
8/// public mutation or deserialization can bypass those invariants. No row bound
9/// is stored or checked.
10///
11/// # Examples
12///
13/// ```
14/// use ailloli_ui_terminal_core::TerminalDamage;
15/// let mut damage = TerminalDamage::clean();
16/// damage.mark_line(3);
17/// assert_eq!(damage.dirty_lines, vec![3]);
18/// ```
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct TerminalDamage {
21    /// `true` requests repaint of the complete grid.
22    pub full: bool,
23    /// Sorted unique zero-based rows dirtied through the API.
24    pub dirty_lines: Vec<usize>,
25    /// `true` requests cursor repaint.
26    pub cursor_dirty: bool,
27    /// `true` requests title refresh.
28    pub title_dirty: bool,
29}
30
31impl TerminalDamage {
32    /// Creates a snapshot with no pending grid, cursor, or title damage.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use ailloli_ui_terminal_core::TerminalDamage;
38    /// let damage = TerminalDamage::clean();
39    /// assert!(!damage.full && damage.dirty_lines.is_empty());
40    /// ```
41    pub fn clean() -> Self {
42        Self {
43            full: false,
44            dirty_lines: Vec::new(),
45            cursor_dirty: false,
46            title_dirty: false,
47        }
48    }
49
50    /// Creates a full-grid snapshot with cursor and title both dirty.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use ailloli_ui_terminal_core::TerminalDamage;
56    /// let damage = TerminalDamage::full();
57    /// assert!(damage.full && damage.cursor_dirty && damage.title_dirty);
58    /// ```
59    pub fn full() -> Self {
60        Self {
61            full: true,
62            dirty_lines: Vec::new(),
63            cursor_dirty: true,
64            title_dirty: true,
65        }
66    }
67
68    /// Requests full-grid and cursor repaint and clears individual rows.
69    ///
70    /// The existing `title_dirty` value is preserved.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use ailloli_ui_terminal_core::TerminalDamage;
76    /// let mut damage = TerminalDamage::clean();
77    /// damage.mark_line(2);
78    /// damage.mark_full();
79    /// assert!(damage.full && damage.cursor_dirty && damage.dirty_lines.is_empty());
80    /// ```
81    pub fn mark_full(&mut self) {
82        self.full = true;
83        self.dirty_lines.clear();
84        self.cursor_dirty = true;
85    }
86
87    /// Adds one zero-based line index, maintaining sorted uniqueness.
88    ///
89    /// Values outside the actual terminal height are accepted.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use ailloli_ui_terminal_core::TerminalDamage;
95    /// let mut damage = TerminalDamage::clean();
96    /// damage.mark_line(4); damage.mark_line(1); damage.mark_line(4);
97    /// assert_eq!(damage.dirty_lines, vec![1, 4]);
98    /// ```
99    pub fn mark_line(&mut self, line: usize) {
100        if !self.dirty_lines.contains(&line) {
101            self.dirty_lines.push(line);
102            self.dirty_lines.sort_unstable();
103        }
104    }
105
106    /// Adds every line in an inclusive range.
107    ///
108    /// `start > end_inclusive` is an empty no-op. Work and storage are linear
109    /// in the range length, and indices are not bounded to a screen height.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use ailloli_ui_terminal_core::TerminalDamage;
115    /// let mut damage = TerminalDamage::clean();
116    /// damage.mark_range(2, 4);
117    /// assert_eq!(damage.dirty_lines, vec![2, 3, 4]);
118    /// ```
119    pub fn mark_range(&mut self, start: usize, end_inclusive: usize) {
120        for line in start..=end_inclusive {
121            self.mark_line(line);
122        }
123    }
124
125    /// Requests a cursor repaint.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use ailloli_ui_terminal_core::TerminalDamage;
131    /// let mut damage = TerminalDamage::clean(); damage.mark_cursor();
132    /// assert!(damage.cursor_dirty);
133    /// ```
134    pub fn mark_cursor(&mut self) {
135        self.cursor_dirty = true;
136    }
137
138    /// Requests a title refresh.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use ailloli_ui_terminal_core::TerminalDamage;
144    /// let mut damage = TerminalDamage::clean(); damage.mark_title();
145    /// assert!(damage.title_dirty);
146    /// ```
147    pub fn mark_title(&mut self) {
148        self.title_dirty = true;
149    }
150
151    /// Discards every pending damage flag and dirty row.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use ailloli_ui_terminal_core::TerminalDamage;
157    /// let mut damage = TerminalDamage::full(); damage.reset();
158    /// assert_eq!(damage, TerminalDamage::clean());
159    /// ```
160    pub fn reset(&mut self) {
161        *self = Self::clean();
162    }
163}
164
165impl Default for TerminalDamage {
166    fn default() -> Self {
167        Self::full()
168    }
169}