1use crate::buffer::{Buffer, Cell, CellDiffOption, CellWidth};
2use crate::layout::Rect;
3use crate::style::{Color, Modifier};
4
5#[derive(Debug)]
11pub struct BufferDiff<'prev, 'next> {
12 next: &'next [Cell],
14 prev: &'prev [Cell],
16 area: Rect,
18 pos: usize,
20 trailing: Option<TrailingState>,
25}
26
27#[derive(Debug)]
29struct TrailingState {
30 next_index: usize,
31 end: usize,
32 force: bool,
39}
40
41const VISIBLE_ON_BLANK: Modifier = Modifier::REVERSED
43 .union(Modifier::UNDERLINED)
44 .union(Modifier::SLOW_BLINK)
45 .union(Modifier::RAPID_BLINK)
46 .union(Modifier::CROSSED_OUT);
47
48impl<'prev, 'next> BufferDiff<'prev, 'next> {
49 pub(crate) fn new(prev: &'prev Buffer, next: &'next Buffer) -> Self {
57 assert!(
58 prev.area.x == next.area.x
59 && prev.area.y == next.area.y
60 && prev.area.width == next.area.width,
61 "buffer areas must have the same x, y, and width: prev={:?}, next={:?}",
62 prev.area,
63 next.area,
64 );
65
66 let mut area = prev.area;
67 area.height = area.height.min(next.area.height);
68
69 Self {
70 next: &next.content,
71 prev: &prev.content,
72 area,
73 pos: 0,
74 trailing: None,
75 }
76 }
77
78 const fn pos_of(&self, index: usize) -> (u16, u16) {
80 let w = self.area.width as usize;
81
82 let x = index % w + self.area.x as usize;
83 let y = index / w + self.area.y as usize;
84
85 (x as u16, y as u16)
86 }
87}
88
89impl<'next> Iterator for BufferDiff<'_, 'next> {
90 type Item = (u16, u16, &'next Cell);
91
92 fn next(&mut self) -> Option<Self::Item> {
93 let len = self.next.len().min(self.prev.len());
94
95 if let Some(TrailingState {
97 next_index,
98 end,
99 force,
100 }) = &mut self.trailing
101 {
102 while *next_index < *end {
103 let j = *next_index;
104 let cell_width = self.next[j].cell_width().max(1) as usize;
107 *next_index += cell_width;
108 *end = (*end).max(*next_index).min(len);
109
110 if !is_skip(&self.next[j])
111 && (*force || self.prev[j].symbol() != self.next[j].symbol())
112 {
113 let (tx, ty) = self.pos_of(j);
114 return Some((tx, ty, &self.next[j]));
115 }
116 }
117
118 self.pos = *end;
120 self.trailing = None;
121 }
122 while self.pos < len {
123 let i = self.pos;
124 self.pos += 1;
125
126 let current = &self.next[i];
127 let previous = &self.prev[i];
128
129 match current.diff_option {
130 CellDiffOption::Skip => {}
131 _ if is_skip(current) => {}
132
133 CellDiffOption::ForcedWidth(width) => {
134 self.pos = self
135 .pos
136 .saturating_add(width.get().saturating_sub(1) as usize);
137 if current != previous {
138 let (x, y) = self.pos_of(i);
139 return Some((x, y, &self.next[i]));
140 }
141 }
142 CellDiffOption::None | CellDiffOption::AlwaysUpdate => {
143 let cell_width = current.cell_width() as usize;
150 if matches!(current.diff_option, CellDiffOption::None) && current == previous {
151 self.pos += cell_width.saturating_sub(1);
153 continue;
154 }
155
156 let previous_width = previous.cell_width() as usize;
157
158 let contains_vs16 =
163 cell_width > 1 && current.symbol().chars().any(|c| c == '\u{FE0F}');
164
165 if contains_vs16 {
166 let trailing_end = (i + cell_width).min(len);
167 self.trailing = Some(TrailingState {
168 next_index: i + 1,
169 end: trailing_end,
170 force: false,
171 });
172 } else if cell_width > 1 {
173 self.pos += cell_width.saturating_sub(1);
174 } else if previous_width > cell_width
175 && (previous.bg != Color::Reset
176 || previous.modifier.intersects(VISIBLE_ON_BLANK))
177 {
178 self.trailing = Some(TrailingState {
183 next_index: i + 1,
184 end: i + previous_width,
185 force: true,
186 });
187 } else {
188 }
190
191 let (x, y) = self.pos_of(i);
192 return Some((x, y, &self.next[i]));
193 }
194 }
195 }
196
197 None
198 }
199}
200
201#[allow(deprecated)]
203const fn is_skip(cell: &Cell) -> bool {
204 matches!(cell.diff_option, CellDiffOption::Skip)
205 || (cell.skip && matches!(cell.diff_option, CellDiffOption::None))
206}
207
208#[cfg(test)]
209mod tests {
210 use alloc::vec::Vec;
211 use core::num::NonZeroU16;
212
213 use compact_str::CompactString;
214
215 use super::*;
216 use crate::buffer::Buffer;
217 use crate::layout::Rect;
218 use crate::style::Color;
219
220 #[test]
221 fn empty_buffers_yield_no_diffs() {
222 let rect = Rect::new(0, 0, 5, 1);
223 let buf = Buffer::empty(rect);
224 let diff: Vec<_> = BufferDiff::new(&buf, &buf).collect();
225 assert!(diff.is_empty());
226 }
227
228 #[test]
229 fn identical_buffers_yield_no_diffs() {
230 let buf = Buffer::with_lines(["hello"]);
231 let diff: Vec<_> = BufferDiff::new(&buf, &buf).collect();
232 assert!(diff.is_empty());
233 }
234
235 #[test]
236 fn single_cell_change() {
237 let prev = Buffer::with_lines(["hello"]);
238 let next = Buffer::with_lines(["hallo"]);
239 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
240 assert_eq!(diff.len(), 1);
241 assert_eq!(diff[0].0, 1); assert_eq!(diff[0].1, 0); assert_eq!(diff[0].2.symbol(), "a");
244 }
245
246 #[test]
247 fn all_cells_changed() {
248 let prev = Buffer::with_lines(["aaa"]);
249 let next = Buffer::with_lines(["bbb"]);
250 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
251 assert_eq!(diff.len(), 3);
252 }
253
254 #[test]
255 fn skip_cells_are_skipped() {
256 let prev = Buffer::with_lines(["abc"]);
257 let mut next = Buffer::with_lines(["xyz"]);
258 next.content[1].diff_option = CellDiffOption::Skip;
259
260 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
261 assert_eq!(diff.len(), 2);
262 assert_eq!(diff[0].2.symbol(), "x");
263 assert_eq!(diff[1].2.symbol(), "z");
264 }
265
266 #[test]
267 fn always_update_cells_are_emitted_even_when_identical() {
268 let mut prev = Buffer::with_lines(["abc"]);
269 prev.content[1].diff_option = CellDiffOption::AlwaysUpdate;
270
271 let mut next = Buffer::with_lines(["abc"]);
272 next.content[1].diff_option = CellDiffOption::AlwaysUpdate;
273
274 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
275 assert_eq!(diff.len(), 1);
276 assert_eq!(diff[0].0, 1);
277 assert_eq!(diff[0].1, 0);
278 assert_eq!(diff[0].2.symbol(), "b");
279 }
280
281 #[test]
282 fn forced_width_skips_trailing() {
283 let prev = Buffer::with_lines(["abcd"]);
284 let mut next = Buffer::with_lines(["xbcd"]);
285 next.content[0].diff_option = CellDiffOption::ForcedWidth(NonZeroU16::new(2).unwrap());
286
287 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
288 assert_eq!(diff.len(), 1);
289 assert_eq!(diff[0].2.symbol(), "x");
290 }
291
292 #[test]
293 fn vs16_trailing_cell_unchanged() {
294 use crate::style::{Color, Style};
295
296 let rect = Rect::new(0, 0, 4, 1);
297 let mut prev = Buffer::empty(rect);
298 prev.set_string(0, 0, "⌨️", Style::new());
299 prev.set_string(2, 0, "ab", Style::new());
300
301 let mut next = Buffer::empty(rect);
302 next.set_string(0, 0, "⌨️", Style::new().fg(Color::Red));
303 next.set_string(2, 0, "ab", Style::new());
304
305 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
308 assert_eq!(diff.len(), 1);
309 assert_eq!(diff[0].0, 0);
310 assert_eq!(diff[0].1, 0);
311 }
312
313 #[test]
314 #[allow(deprecated)]
315 fn deprecated_skip_field_is_respected() {
316 let prev = Buffer::with_lines(["abc"]);
317 let mut next = Buffer::with_lines(["xyz"]);
318 next.content[1].skip = true;
319
320 let diff: CompactString = BufferDiff::new(&prev, &next)
321 .map(|(_, _, cell)| cell.symbol())
322 .collect();
323
324 assert_eq!(diff, "xz");
325 }
326
327 #[test]
328 #[allow(deprecated)]
329 fn forced_width_takes_precedence_over_deprecated_skip() {
330 let prev = Buffer::with_lines(["abcd"]);
331 let mut next = Buffer::with_lines(["xbcd"]);
332 next.content[0].skip = true;
333 next.content[0].diff_option = CellDiffOption::ForcedWidth(NonZeroU16::new(2).unwrap());
334
335 let diff: CompactString = BufferDiff::new(&prev, &next)
337 .map(|(_, _, cell)| cell.symbol())
338 .collect();
339
340 assert_eq!(diff, "x");
341 }
342
343 #[test]
357 fn uncovered_trailing_cells_emitted_when_wide_char_style_changes() {
358 use crate::style::{Color, Style};
359
360 let rect = Rect::new(0, 0, 12, 1);
362 let mut prev = Buffer::empty(rect);
363 prev.set_string(0, 0, "你好,世界!", Style::new().bg(Color::Blue));
364
365 let mut next = Buffer::empty(rect);
366 next.set_string(0, 0, "Hello", Style::default());
367
368 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
369
370 for x in [5u16, 7, 9, 11] {
376 assert!(
377 diff.iter().any(|(dx, dy, _)| *dx == x && *dy == 0),
378 "expected update for trailing cell at x={x} (blue bg not cleared otherwise)"
379 );
380 }
381 }
382
383 #[test]
392 fn uncovered_trailing_cells_emitted_when_wide_chars_partially_replaced() {
393 use crate::style::{Color, Style};
394
395 let rect = Rect::new(0, 0, 12, 1);
396 let mut prev = Buffer::empty(rect);
397 prev.set_string(0, 0, "你好,世界!", Style::new().bg(Color::Blue));
398
399 let mut next = Buffer::empty(rect);
400 next.set_string(0, 0, "喵呜www", Style::new().bg(Color::Blue));
401
402 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
403
404 assert!(
408 diff.iter().any(|(dx, dy, _)| *dx == 7 && *dy == 0),
409 "expected update for trailing cell at x=7 (blue bg not cleared otherwise)"
410 );
411 }
412
413 #[test]
414 fn no_force_update_when_wide_char_has_default_style() {
415 use crate::style::Style;
416
417 let rect = Rect::new(0, 0, 12, 1);
418 let mut prev = Buffer::empty(rect);
419 prev.set_string(0, 0, "你好,世界!", Style::default());
420
421 let mut next = Buffer::empty(rect);
422 next.set_string(0, 0, "Hello", Style::default());
423
424 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
425
426 for x in [5u16, 7, 9, 11] {
431 assert!(
432 !diff.iter().any(|(dx, dy, _)| *dx == x && *dy == 0),
433 "expected no update for trailing cell at x={x}"
434 );
435 }
436 }
437
438 #[test]
439 fn shrinking_wide_glyph_clears_trailing_cell() {
440 let rect = Rect::new(0, 0, 2, 1);
448
449 let mut prev = Buffer::empty(rect);
450 prev.set_string(0, 0, "+", crate::style::Style::new());
451 prev.cell_mut((0, 0)).unwrap().set_bg(Color::Red);
453
454 assert_eq!(prev.content[0].symbol(), "+");
455
456 assert_eq!(prev.content[1].symbol(), " ");
458
459 let next = Buffer::empty(rect);
461 assert_eq!(next.content[1], prev.content[1]); let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
464
465 assert_eq!(diff.len(), 2, "both columns must be redrawn, got {diff:?}");
466 assert_eq!((diff[0].0, diff[0].1), (0, 0));
467 assert_eq!(diff[0].2.symbol(), " ");
468
469 assert_eq!((diff[1].0, diff[1].1), (1, 0));
472 assert_eq!(diff[1].2.symbol(), " ");
473 }
474
475 #[test]
476 fn shrinking_wide_glyph_clears_trailing_background() {
477 use crate::style::{Color, Style};
478
479 let rect = Rect::new(0, 0, 2, 1);
483
484 let mut prev = Buffer::empty(rect);
485 prev.set_string(0, 0, "+", Style::new().bg(Color::Blue));
486
487 let next = Buffer::empty(rect); let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
490
491 assert!(
492 diff.iter().any(|(x, y, _)| *x == 1 && *y == 0),
493 "trailing cell (1,0) must be redrawn to clear the leftover background, got {diff:?}"
494 );
495 }
496
497 #[test]
511 fn trailing_range_cell_style_change_is_emitted() {
512 use crate::style::{Color, Style};
513
514 let rect = Rect::new(0, 0, 3, 1);
515 let mut prev = Buffer::empty(rect);
516 prev.set_string(0, 0, "你", Style::new().bg(Color::Blue));
517 prev.content[1]
519 .set_symbol("X")
520 .set_fg(Color::Red)
521 .set_bg(Color::Reset);
522
523 let mut next = Buffer::empty(rect);
524 next.set_string(0, 0, "a", Style::default());
525 next.content[1]
527 .set_symbol("X")
528 .set_fg(Color::Green)
529 .set_bg(Color::Reset);
530
531 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
532
533 assert!(
534 diff.iter().any(|(x, y, _)| *x == 1 && *y == 0),
535 "col 1 style change (Red→Green) must be emitted even though symbol is unchanged; got {diff:?}"
536 );
537 }
538
539 #[test]
549 fn trailing_range_wide_char_trailing_not_corrupted() {
550 use crate::style::{Color, Style};
551
552 let rect = Rect::new(0, 0, 4, 1);
553 let mut prev = Buffer::empty(rect);
554 prev.set_string(0, 0, "你好", Style::new().bg(Color::Blue));
555 let mut next = Buffer::empty(rect);
558 next.set_string(0, 0, "a", Style::new().bg(Color::Blue));
559 next.set_string(1, 0, "好", Style::new().bg(Color::Blue));
560 let diff: Vec<_> = BufferDiff::new(&prev, &next).collect();
563
564 assert!(
567 !diff.iter().any(|(x, y, _)| *x == 2 && *y == 0),
568 "trailing cell of '好' must not receive a spurious EMPTY write; got {diff:?}"
569 );
570 assert!(
572 diff.iter()
573 .any(|(x, y, cell)| *x == 1 && *y == 0 && cell.symbol() == "好"),
574 "'好' at col 1 must be emitted; got {diff:?}"
575 );
576 }
577
578 #[test]
579 #[should_panic(expected = "buffer areas must have the same x, y, and width")]
580 fn mismatched_widths_panics() {
581 let prev = Buffer::empty(Rect::new(0, 0, 5, 1));
582 let next = Buffer::empty(Rect::new(0, 0, 10, 1));
583 BufferDiff::new(&prev, &next);
584 }
585}