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}