1use crate::buffer::{Buffer, Cell, CellDiffOption, CellWidth};
2use crate::layout::Rect;
3
4#[derive(Debug)]
10pub struct BufferDiff<'prev, 'next> {
11 next: &'next [Cell],
13 prev: &'prev [Cell],
15 area: Rect,
17 pos: usize,
19 trailing: Option<TrailingState>,
21}
22
23#[derive(Debug)]
25struct TrailingState {
26 next_index: usize,
27 end: usize,
28}
29
30impl<'prev, 'next> BufferDiff<'prev, 'next> {
31 pub(crate) fn new(prev: &'prev Buffer, next: &'next Buffer) -> Self {
39 assert!(
40 prev.area.x == next.area.x
41 && prev.area.y == next.area.y
42 && prev.area.width == next.area.width,
43 "buffer areas must have the same x, y, and width: prev={:?}, next={:?}",
44 prev.area,
45 next.area,
46 );
47
48 let mut area = prev.area;
49 area.height = area.height.min(next.area.height);
50
51 Self {
52 next: &next.content,
53 prev: &prev.content,
54 area,
55 pos: 0,
56 trailing: None,
57 }
58 }
59
60 const fn pos_of(&self, index: usize) -> (u16, u16) {
62 let w = self.area.width as usize;
63
64 let x = index % w + self.area.x as usize;
65 let y = index / w + self.area.y as usize;
66
67 (x as u16, y as u16)
68 }
69}
70
71impl<'next> Iterator for BufferDiff<'_, 'next> {
72 type Item = (u16, u16, &'next Cell);
73
74 fn next(&mut self) -> Option<Self::Item> {
75 if let Some(TrailingState {
77 ref mut next_index,
78 end,
79 }) = self.trailing
80 {
81 while *next_index < end {
82 let j = *next_index;
83 *next_index += 1;
84
85 if !is_skip(&self.next[j]) && self.prev[j].symbol() != self.next[j].symbol() {
90 let (tx, ty) = self.pos_of(j);
91 return Some((tx, ty, &self.next[j]));
92 }
93 }
94
95 self.pos = end;
97 self.trailing = None;
98 }
99
100 let len = self.next.len().min(self.prev.len());
101 while self.pos < len {
102 let i = self.pos;
103 self.pos += 1;
104
105 let current = &self.next[i];
106 let previous = &self.prev[i];
107
108 match current.diff_option {
109 CellDiffOption::Skip => {}
110 _ if is_skip(current) => {}
111
112 CellDiffOption::ForcedWidth(width) => {
113 self.pos = self
114 .pos
115 .saturating_add(width.get().saturating_sub(1) as usize);
116 if current != previous {
117 let (x, y) = self.pos_of(i);
118 return Some((x, y, &self.next[i]));
119 }
120 }
121 CellDiffOption::None | CellDiffOption::AlwaysUpdate => {
122 let cell_width = current.cell_width() as usize;
129 if matches!(current.diff_option, CellDiffOption::None) && current == previous {
130 self.pos += cell_width.saturating_sub(1);
132 continue;
133 }
134
135 let contains_vs16 =
140 cell_width > 1 && current.symbol().chars().any(|c| c == '\u{FE0F}');
141
142 if contains_vs16 {
143 let trailing_end = (i + cell_width).min(len);
144 self.trailing = Some(TrailingState {
145 next_index: i + 1,
146 end: trailing_end,
147 });
148 } else if cell_width > 1 {
149 self.pos += cell_width.saturating_sub(1);
150 } else {
151 }
153
154 let (x, y) = self.pos_of(i);
155 return Some((x, y, &self.next[i]));
156 }
157 }
158 }
159
160 None
161 }
162}
163
164#[allow(deprecated)]
166const fn is_skip(cell: &Cell) -> bool {
167 matches!(cell.diff_option, CellDiffOption::Skip)
168 || (cell.skip && matches!(cell.diff_option, CellDiffOption::None))
169}
170
171#[cfg(test)]
172mod tests {
173 use alloc::vec::Vec;
174 use core::num::NonZeroU16;
175
176 use compact_str::CompactString;
177
178 use super::*;
179 use crate::buffer::Buffer;
180 use crate::layout::Rect;
181
182 #[test]
183 fn empty_buffers_yield_no_diffs() {
184 let rect = Rect::new(0, 0, 5, 1);
185 let buf = Buffer::empty(rect);
186 let diff: Vec<_> = BufferDiff::new(&buf, &buf).collect();
187 assert!(diff.is_empty());
188 }
189
190 #[test]
191 fn identical_buffers_yield_no_diffs() {
192 let buf = Buffer::with_lines(["hello"]);
193 let diff: Vec<_> = BufferDiff::new(&buf, &buf).collect();
194 assert!(diff.is_empty());
195 }
196
197 #[test]
198 fn single_cell_change() {
199 let prev = Buffer::with_lines(["hello"]);
200 let next = Buffer::with_lines(["hallo"]);
201 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
202 assert_eq!(diff.len(), 1);
203 assert_eq!(diff[0].0, 1); assert_eq!(diff[0].1, 0); assert_eq!(diff[0].2.symbol(), "a");
206 }
207
208 #[test]
209 fn all_cells_changed() {
210 let prev = Buffer::with_lines(["aaa"]);
211 let next = Buffer::with_lines(["bbb"]);
212 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
213 assert_eq!(diff.len(), 3);
214 }
215
216 #[test]
217 fn skip_cells_are_skipped() {
218 let prev = Buffer::with_lines(["abc"]);
219 let mut next = Buffer::with_lines(["xyz"]);
220 next.content[1].diff_option = CellDiffOption::Skip;
221
222 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
223 assert_eq!(diff.len(), 2);
224 assert_eq!(diff[0].2.symbol(), "x");
225 assert_eq!(diff[1].2.symbol(), "z");
226 }
227
228 #[test]
229 fn always_update_cells_are_emitted_even_when_identical() {
230 let mut prev = Buffer::with_lines(["abc"]);
231 prev.content[1].diff_option = CellDiffOption::AlwaysUpdate;
232
233 let mut next = Buffer::with_lines(["abc"]);
234 next.content[1].diff_option = CellDiffOption::AlwaysUpdate;
235
236 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
237 assert_eq!(diff.len(), 1);
238 assert_eq!(diff[0].0, 1);
239 assert_eq!(diff[0].1, 0);
240 assert_eq!(diff[0].2.symbol(), "b");
241 }
242
243 #[test]
244 fn forced_width_skips_trailing() {
245 let prev = Buffer::with_lines(["abcd"]);
246 let mut next = Buffer::with_lines(["xbcd"]);
247 next.content[0].diff_option = CellDiffOption::ForcedWidth(NonZeroU16::new(2).unwrap());
248
249 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
250 assert_eq!(diff.len(), 1);
251 assert_eq!(diff[0].2.symbol(), "x");
252 }
253
254 #[test]
255 fn vs16_trailing_cell_unchanged() {
256 use crate::style::{Color, Style};
257
258 let rect = Rect::new(0, 0, 4, 1);
259 let mut prev = Buffer::empty(rect);
260 prev.set_string(0, 0, "⌨️", Style::new());
261 prev.set_string(2, 0, "ab", Style::new());
262
263 let mut next = Buffer::empty(rect);
264 next.set_string(0, 0, "⌨️", Style::new().fg(Color::Red));
265 next.set_string(2, 0, "ab", Style::new());
266
267 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
270 assert_eq!(diff.len(), 1);
271 assert_eq!(diff[0].0, 0);
272 assert_eq!(diff[0].1, 0);
273 }
274
275 #[test]
276 #[allow(deprecated)]
277 fn deprecated_skip_field_is_respected() {
278 let prev = Buffer::with_lines(["abc"]);
279 let mut next = Buffer::with_lines(["xyz"]);
280 next.content[1].skip = true;
281
282 let diff: CompactString = BufferDiff::new(&prev, &next)
283 .map(|(_, _, cell)| cell.symbol())
284 .collect();
285
286 assert_eq!(diff, "xz");
287 }
288
289 #[test]
290 #[allow(deprecated)]
291 fn forced_width_takes_precedence_over_deprecated_skip() {
292 let prev = Buffer::with_lines(["abcd"]);
293 let mut next = Buffer::with_lines(["xbcd"]);
294 next.content[0].skip = true;
295 next.content[0].diff_option = CellDiffOption::ForcedWidth(NonZeroU16::new(2).unwrap());
296
297 let diff: CompactString = BufferDiff::new(&prev, &next)
299 .map(|(_, _, cell)| cell.symbol())
300 .collect();
301
302 assert_eq!(diff, "x");
303 }
304
305 #[test]
306 #[should_panic(expected = "buffer areas must have the same x, y, and width")]
307 fn mismatched_widths_panics() {
308 let prev = Buffer::empty(Rect::new(0, 0, 5, 1));
309 let next = Buffer::empty(Rect::new(0, 0, 10, 1));
310 BufferDiff::new(&prev, &next);
311 }
312}