1use crate::Sink;
6use std::collections::HashMap;
7use std::io::{self, Write as _};
8
9use katui_proto::kitty::{
10 Delete, IdSpace, ImageId as KittyImage, Place, PlacementId as KittyPlacement,
11};
12use katui_proto::placeholder::placeholder_row;
13use katui_proto::probe::CellPixels;
14use katui_scene::cell::Position;
15use katui_scene::placement::CellRect;
16use katui_scene::surface::{PixelRect, Surface, diff_rects};
17
18use crate::anchor::Anchor;
19
20use super::{Stash, damage_is_dense, transmit_pixels, write_whole_image};
21use crate::cells::move_cursor;
22use crate::kitty::crop_rect;
23
24#[derive(Debug)]
38pub struct PatchedKitty {
39 base: u32,
40 tile_side: u32,
41 budget: usize,
42 prev: Option<Surface>,
43 base_shown: Option<KittyImage>,
44 tile_generations: HashMap<u32, bool>,
45 grid: Grid,
46 cell: CellPixels,
47 stash: Option<Box<dyn Stash>>,
48 repaint: bool,
49}
50
51#[derive(Debug)]
54enum Grid {
55 Plain,
56 Anchored(Anchor),
57 Placeholders,
58}
59
60impl PatchedKitty {
61 #[must_use]
65 pub fn new(base: u32, cell: CellPixels) -> Self {
66 Self::with_tiles(
67 base,
68 cell,
69 u32::from(cell.width) * u32::from(cell.height),
70 64,
71 )
72 }
73
74 #[must_use]
79 pub fn with_tiles(base: u32, cell: CellPixels, tile_side: u32, budget: usize) -> Self {
80 Self {
81 base,
82 tile_side: tile_side.max(1),
83 budget,
84 prev: None,
85 base_shown: None,
86 tile_generations: HashMap::new(),
87 grid: Grid::Plain,
88 cell,
89 stash: None,
90 repaint: false,
91 }
92 }
93
94 #[must_use]
97 pub fn with_stash(mut self, stash: Box<dyn Stash>) -> Self {
98 self.stash = Some(stash);
99 self
100 }
101
102 pub fn set_cell(&mut self, cell: CellPixels) {
105 self.cell = cell;
106 }
107
108 pub fn repaint(&mut self) {
111 self.repaint = true;
112 }
113
114 pub fn free<W: Sink>(&mut self, out: &mut W) -> io::Result<()> {
116 let mut bytes = Vec::new();
117 self.free_tiles(&mut bytes)?;
118 for image in [self.base, self.base + 1] {
119 bytes.write_all(&Delete::Free(KittyImage(image)).encode())?;
120 }
121 self.base_shown = None;
122 self.repaint = true;
123 out.write_all(&bytes)
124 }
125
126 #[must_use]
133 pub fn for_multiplexer(mut self, space: IdSpace, relative_placements: bool) -> Self {
134 self.base = space.image(self.base).0;
135 self.grid = if relative_placements {
136 Grid::Anchored(Anchor::new(space))
137 } else {
138 Grid::Placeholders
139 };
140 self
141 }
142
143 pub fn render_frame<W: Sink>(
145 &mut self,
146 out: &mut W,
147 surface: &Surface,
148 area: CellRect,
149 ) -> io::Result<usize> {
150 let resized = self.prev.as_ref().is_none_or(|prev| {
151 prev.width() != surface.width() || prev.height() != surface.height()
152 });
153 let repaint = std::mem::take(&mut self.repaint);
154 if !matches!(self.grid, Grid::Plain) {
155 if resized && matches!(self.grid, Grid::Placeholders) {
156 self.fit_tiles_to_cells(surface, area);
157 }
158 return self.render_tiles(out, surface, area, resized, repaint);
159 }
160 let fresh_start = resized || repaint;
161 let damage = match self.prev.as_ref() {
162 Some(prev) if !fresh_start => self.damage(prev, surface, area),
163 _ => Vec::new(),
164 };
165 let dense = damage_is_dense(surface, self.tile_side, damage.len());
166 let patching = !fresh_start && !dense && damage.len() <= self.budget;
167
168 let mut bytes = Vec::new();
169 if !patching {
170 self.full_frame(&mut bytes, surface, area)?;
171 } else if damage.is_empty() {
172 self.prev = Some(surface.clone());
173 return Ok(0);
174 } else {
175 self.patch_all(&mut bytes, surface, &damage, area)?;
176 }
177 self.prev = Some(surface.clone());
178 let written = bytes.len();
179 out.write_all(&bytes)?;
180 Ok(written)
181 }
182
183 fn fit_tiles_to_cells(&mut self, surface: &Surface, area: CellRect) {
187 let width = surface.width().checked_div(u32::from(area.columns));
188 let height = surface.height().checked_div(u32::from(area.rows));
189 if let (Some(width), Some(height)) = (width, height)
190 && width > 0
191 && height > 0
192 && width * u32::from(area.columns) == surface.width()
193 && height * u32::from(area.rows) == surface.height()
194 && (!self.tile_side.is_multiple_of(width) || !self.tile_side.is_multiple_of(height))
195 {
196 let whole = least_common_multiple(width, height);
197 self.tile_side = whole * SMALLEST_TILE.div_ceil(whole);
198 }
199 }
200
201 fn patch_all(
203 &mut self,
204 bytes: &mut Vec<u8>,
205 surface: &Surface,
206 damage: &[PixelRect],
207 area: CellRect,
208 ) -> io::Result<()> {
209 bytes.write_all(b"\x1b[?2026h")?;
210 let mut shows = Vec::new();
211 for rect in damage {
212 self.patch(bytes, &mut shows, surface, *rect, area)?;
213 }
214 bytes.append(&mut shows);
215 bytes.write_all(b"\x1b[?2026l")
216 }
217
218 fn render_tiles<W: Sink>(
223 &mut self,
224 out: &mut W,
225 surface: &Surface,
226 area: CellRect,
227 resized: bool,
228 repaint: bool,
229 ) -> io::Result<usize> {
230 let blank = Surface::new(surface.width(), surface.height());
231 let before = self
232 .prev
233 .as_ref()
234 .filter(|_| !resized && !repaint)
235 .unwrap_or(&blank);
236 let damage = self.damage(before, surface, area);
237 let mut bytes = Vec::new();
238 if resized {
239 self.free_tiles(&mut bytes)?;
240 }
241 if !damage.is_empty()
242 && let Grid::Anchored(anchor) = &mut self.grid
243 {
244 anchor.write(&mut bytes)?;
245 }
246 let mut shows = Vec::new();
247 for rect in &damage {
248 self.patch(&mut bytes, &mut shows, surface, *rect, area)?;
249 }
250 bytes.append(&mut shows);
251 self.prev = Some(surface.clone());
252 out.write_all(&bytes)?;
253 Ok(bytes.len())
254 }
255
256 fn free_tiles(&mut self, bytes: &mut Vec<u8>) -> io::Result<()> {
259 for (index, generation) in std::mem::take(&mut self.tile_generations) {
260 bytes.write_all(&Delete::Free(self.tile_image(index, generation)).encode())?;
261 }
262 Ok(())
263 }
264
265 fn full_frame(
270 &mut self,
271 bytes: &mut Vec<u8>,
272 surface: &Surface,
273 area: CellRect,
274 ) -> io::Result<()> {
275 let image = if self.base_shown == Some(KittyImage(self.base)) {
276 KittyImage(self.base + 1)
277 } else {
278 KittyImage(self.base)
279 };
280 bytes.write_all(b"\x1b[?2026h")?;
281 write_whole_image(bytes, image, surface, area, self.stash.as_deref_mut())?;
282 self.free_tiles(bytes)?;
284 if let Some(shown) = self.base_shown.replace(image) {
285 bytes.write_all(&Delete::Free(shown).encode())?;
286 }
287 bytes.write_all(b"\x1b[?2026l")?;
288 Ok(())
289 }
290
291 fn damage(&self, before: &Surface, surface: &Surface, area: CellRect) -> Vec<PixelRect> {
293 diff_rects(before, surface, cell_tiles(surface, area, self.tile_side))
294 }
295
296 fn tile_image(&self, index: u32, second_generation: bool) -> KittyImage {
297 KittyImage(self.base + 2 + index * 2 + u32::from(second_generation))
298 }
299
300 fn patch(
306 &mut self,
307 uploads: &mut Vec<u8>,
308 reveal: &mut Vec<u8>,
309 surface: &Surface,
310 rect: PixelRect,
311 area: CellRect,
312 ) -> io::Result<()> {
313 let (left, right) = column_span(rect, surface, area);
314 let (top, bottom) = row_span(rect, surface, area);
315 let columns = right.saturating_sub(left).max(1);
316 let rows = bottom.saturating_sub(top).max(1);
317 let origin_x = area.x.saturating_add(left);
318 let origin_y = area.y.saturating_add(top);
319 let index = tile_index(origin_x, origin_y);
320 let shown = self.tile_generations.get(&index).copied();
321 let generation = shown.is_some_and(|generation| !generation);
322 let image = self.tile_image(index, generation);
323 let origin = Position {
324 x: origin_x,
325 y: origin_y,
326 };
327 let cells = (columns, rows);
328 if matches!(self.grid, Grid::Placeholders) {
329 upload_placeholder_tile(uploads, image, surface, rect, cells, self.cell)?;
330 show_placeholder_tile(reveal, image, origin, cells)?;
331 } else {
332 let padded = with_apron(rect, surface);
333 transmit_pixels(
334 uploads,
335 image,
336 &tile_pixels(surface, padded),
337 (padded.width, padded.height),
338 self.stash.as_deref_mut(),
339 )?;
340 self.place_tile(reveal, image, rect, padded, origin, cells)?;
341 }
342 if let Some(previous) = shown {
343 reveal.write_all(&Delete::Free(self.tile_image(index, previous)).encode())?;
344 }
345 self.tile_generations.insert(index, generation);
346 Ok(())
347 }
348
349 fn place_tile(
352 &self,
353 bytes: &mut Vec<u8>,
354 image: KittyImage,
355 rect: PixelRect,
356 padded: PixelRect,
357 origin: Position,
358 cells: (u16, u16),
359 ) -> io::Result<()> {
360 let (columns, rows) = cells;
361 let index = tile_index(origin.x, origin.y);
362 let mut place = Place::new(image);
363 place.columns = u32::from(columns);
364 place.rows = u32::from(rows);
365 place.placement = KittyPlacement(2 + index);
366 place.crop = Some(crop_rect(PixelRect {
367 x: rect.x - padded.x,
368 y: rect.y - padded.y,
369 width: rect.width,
370 height: rect.height,
371 }));
372 if let Grid::Anchored(anchor) = &self.grid {
373 place.parent = Some(anchor.offset_to(origin));
374 } else {
375 move_cursor(bytes, origin)?;
376 }
377 bytes.write_all(&place.encode())
378 }
379}
380
381fn tile_index(column: u16, row: u16) -> u32 {
383 u32::from(column) + u32::from(row) * 4096
384}
385
386fn cell_tiles(surface: &Surface, area: CellRect, tile_side: u32) -> Vec<PixelRect> {
390 let columns = u32::from(area.columns).max(1);
391 let rows = u32::from(area.rows).max(1);
392 let across = cells_per_tile(tile_side, surface.width(), columns);
393 let down = cells_per_tile(tile_side, surface.height(), rows);
394 let mut tiles = Vec::new();
395 for row in (0..rows).step_by(down as usize) {
396 let top = edge(row, surface.height(), rows);
397 let bottom = edge((row + down).min(rows), surface.height(), rows);
398 for column in (0..columns).step_by(across as usize) {
399 let left = edge(column, surface.width(), columns);
400 let right = edge((column + across).min(columns), surface.width(), columns);
401 if right > left && bottom > top {
402 tiles.push(PixelRect {
403 x: left,
404 y: top,
405 width: right - left,
406 height: bottom - top,
407 });
408 }
409 }
410 }
411 tiles
412}
413
414fn cells_per_tile(side: u32, pixels: u32, cells: u32) -> u32 {
417 let per_tile = (u64::from(side) * u64::from(cells) * 2 + u64::from(pixels))
418 / (2 * u64::from(pixels.max(1)));
419 u32::try_from(per_tile).unwrap_or(cells).clamp(1, cells)
420}
421
422fn edge(cell: u32, pixels: u32, cells: u32) -> u32 {
425 let at = (u64::from(cell) * u64::from(pixels) * 2 + u64::from(cells))
426 / (2 * u64::from(cells.max(1)));
427 u32::try_from(at).unwrap_or(pixels)
428}
429
430fn cell_at(pixel: u32, pixels: u32, cells: u32) -> u16 {
432 let at = (u64::from(pixel) * u64::from(cells) * 2 + u64::from(pixels))
433 / (2 * u64::from(pixels.max(1)));
434 u16::try_from(at).unwrap_or(u16::MAX)
435}
436
437fn column_span(rect: PixelRect, surface: &Surface, area: CellRect) -> (u16, u16) {
439 let columns = u32::from(area.columns);
440 (
441 cell_at(rect.x, surface.width(), columns),
442 cell_at(rect.x + rect.width, surface.width(), columns),
443 )
444}
445
446fn row_span(rect: PixelRect, surface: &Surface, area: CellRect) -> (u16, u16) {
448 let rows = u32::from(area.rows);
449 (
450 cell_at(rect.y, surface.height(), rows),
451 cell_at(rect.y + rect.height, surface.height(), rows),
452 )
453}
454
455const SMALLEST_TILE: u32 = 64;
458
459fn least_common_multiple(first: u32, second: u32) -> u32 {
460 let (mut a, mut b) = (first, second);
461 while b != 0 {
462 (a, b) = (b, a % b);
463 }
464 first / a.max(1) * second
465}
466
467fn upload_placeholder_tile(
471 bytes: &mut Vec<u8>,
472 image: KittyImage,
473 surface: &Surface,
474 rect: PixelRect,
475 (columns, rows): (u16, u16),
476 cell: CellPixels,
477) -> io::Result<()> {
478 let cells_wide = (u32::from(columns) * u32::from(cell.width)).max(1);
479 let cells_high = u32::from(rows) * u32::from(cell.height);
480 let width = rect.width.max(1);
481 let height = (u64::from(width) * u64::from(cells_high) / u64::from(cells_wide)).max(1);
482 let height = u32::try_from(height).unwrap_or(rect.height);
483 let pixels = resample(
484 &tile_pixels(surface, rect),
485 (rect.width, rect.height),
486 (width, height),
487 );
488 transmit_pixels(bytes, image, &pixels, (width, height), None)
489}
490
491fn show_placeholder_tile(
494 bytes: &mut Vec<u8>,
495 image: KittyImage,
496 origin: Position,
497 (columns, rows): (u16, u16),
498) -> io::Result<()> {
499 let mut place = Place::new(image);
500 place.columns = u32::from(columns);
501 place.rows = u32::from(rows);
502 place.virtual_placement = true;
503 bytes.write_all(&place.encode())?;
504 for row in 0..rows {
505 if let Some(text) = placeholder_row(image, u32::from(row), u32::from(columns)) {
506 move_cursor(
507 bytes,
508 Position {
509 x: origin.x,
510 y: origin.y.saturating_add(row),
511 },
512 )?;
513 bytes.write_all(text.as_bytes())?;
514 }
515 }
516 Ok(())
517}
518
519fn resample(pixels: &[u8], from: (u32, u32), to: (u32, u32)) -> Vec<u8> {
522 if from == to || from.0 == 0 || from.1 == 0 {
523 return pixels.to_vec();
524 }
525 let mut scaled = Vec::with_capacity((to.0 * to.1 * 4) as usize);
526 for y in 0..to.1 {
527 let source_row = (y * from.1 / to.1) * from.0;
528 for x in 0..to.0 {
529 let start = ((source_row + x * from.0 / to.0) * 4) as usize;
530 scaled.extend_from_slice(pixels.get(start..start + 4).unwrap_or(&[0; 4]));
531 }
532 }
533 scaled
534}
535
536fn with_apron(rect: PixelRect, surface: &Surface) -> PixelRect {
541 let left = rect.x.saturating_sub(1);
542 let top = rect.y.saturating_sub(1);
543 let right = (rect.x + rect.width + 1).min(surface.width());
544 let bottom = (rect.y + rect.height + 1).min(surface.height());
545 PixelRect {
546 x: left,
547 y: top,
548 width: right - left,
549 height: bottom - top,
550 }
551}
552
553pub(super) fn tile_pixels(surface: &Surface, rect: PixelRect) -> Vec<u8> {
556 let row_bytes = (surface.width() * 4) as usize;
557 let mut tile = Vec::with_capacity((rect.width * rect.height * 4) as usize);
558 for (row_index, row) in surface.pixels().chunks(row_bytes).enumerate() {
559 if (row_index as u32) < rect.y || (row_index as u32) >= rect.y + rect.height {
560 continue;
561 }
562 let start = (rect.x * 4) as usize;
563 let end = start + (rect.width * 4) as usize;
564 if let Some(slice) = row.get(start..end) {
565 tile.extend_from_slice(slice);
566 }
567 }
568 tile
569}
570
571#[cfg(test)]
572mod tests {
573 use super::PatchedKitty;
574
575 fn plain_surface(width: u32, height: u32) -> Surface {
576 crate::surface::test_support::solid(width, height, Rgba::solid(10, 10, 30))
577 }
578 use katui_proto::probe::CellPixels;
579 use katui_scene::placement::CellRect;
580 use katui_scene::surface::{Rgba, Surface};
581
582 fn cell() -> CellPixels {
584 CellPixels {
585 width: 2,
586 height: 4,
587 }
588 }
589
590 #[test]
591 fn the_first_frame_transmits_the_whole_surface() {
592 let mut presenter = PatchedKitty::new(90, cell());
593 let mut out = Vec::new();
594 presenter
595 .render_frame(
596 &mut out,
597 &plain_surface(64, 64),
598 crate::surface::test_support::area(),
599 )
600 .expect("writing to a Vec cannot fail");
601 let text = String::from_utf8_lossy(&out);
602 assert!(text.contains("a=t"), "the whole image travels");
603 assert!(text.contains("o=z"), "compressed");
604 assert!(text.contains("a=p,i=90"), "and is placed");
605 }
606
607 #[test]
608 fn an_identical_frame_writes_nothing() {
609 let mut presenter = PatchedKitty::new(90, cell());
610 let surface = plain_surface(64, 64);
611 let mut first = Vec::new();
612 presenter
613 .render_frame(&mut first, &surface, crate::surface::test_support::area())
614 .expect("writing to a Vec cannot fail");
615 let mut second = Vec::new();
616 let written = presenter
617 .render_frame(&mut second, &surface, crate::surface::test_support::area())
618 .expect("writing to a Vec cannot fail");
619 assert_eq!(written, 0, "no damage means no bytes");
620 assert!(second.is_empty());
621 }
622
623 #[test]
624 fn tiles_of_a_scaled_surface_cover_whole_cells_edge_to_edge() {
625 let surface = plain_surface(50, 37);
626 let area = CellRect::full(16, 9);
627 let tiles = super::cell_tiles(&surface, area, 8);
628 let covered: u32 = tiles.iter().map(|tile| tile.width * tile.height).sum();
629 assert_eq!(covered, 50 * 37, "the tiles cover the surface once");
630 let mut next_column = 0;
631 for tile in tiles.iter().filter(|tile| tile.y == 0) {
632 let (left, right) = super::column_span(*tile, &surface, area);
633 assert_eq!(left, next_column, "each tile starts where the last ended");
634 assert!(right > left, "every tile covers a cell");
635 next_column = right;
636 }
637 assert_eq!(next_column, 16, "the last tile ends at the last column");
638 }
639
640 #[test]
641 fn a_small_change_retransmits_only_its_tile() {
642 let mut presenter = PatchedKitty::new(90, cell());
643 let mut surface = plain_surface(64, 64);
644 let mut warmup = Vec::new();
645 presenter
646 .render_frame(&mut warmup, &surface, crate::surface::test_support::area())
647 .expect("writing to a Vec cannot fail");
648 surface.set(
649 katui_scene::surface::PixelPos::new(40, 40),
650 Rgba::solid(255, 255, 255),
651 );
652 let mut out = Vec::new();
653 presenter
654 .render_frame(&mut out, &surface, crate::surface::test_support::area())
655 .expect("writing to a Vec cannot fail");
656 let text = String::from_utf8_lossy(&out);
657 assert!(
658 !text.contains("s=64"),
659 "the whole surface does not travel again"
660 );
661 assert!(
662 text.contains("s=10,v=10"),
663 "the damaged 8x8 tile travels with a pixel of apron on each side"
664 );
665 assert!(
666 text.contains("x=1,y=1,w=8,h=8"),
667 "the placement crops the apron away"
668 );
669 assert!(
670 text.contains("a=p,i=82032") && text.contains("c=2,r=2"),
671 "the tile covers its 2x2 cells at the surface's scale"
672 );
673 }
674
675 #[test]
676 fn a_whole_frame_frees_the_tiles_that_would_cover_it() {
677 let mut presenter = PatchedKitty::new(90, cell());
678 let mut surface = plain_surface(64, 64);
679 let mut wire = Vec::new();
680 presenter
681 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
682 .expect("writing to a Vec cannot fail");
683 assert!(
684 !String::from_utf8_lossy(&wire).contains("d=R"),
685 "no tiles exist yet"
686 );
687 surface.set(
688 katui_scene::surface::PixelPos::new(40, 40),
689 Rgba::solid(255, 255, 255),
690 );
691 presenter
692 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
693 .expect("writing to a Vec cannot fail");
694 wire.clear();
695 for y in (0..64).step_by(8) {
696 for x in (0..64).step_by(8) {
697 surface.set(
698 katui_scene::surface::PixelPos::new(x, y),
699 Rgba::solid(1, 2, 3),
700 );
701 }
702 }
703 presenter
704 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
705 .expect("writing to a Vec cannot fail");
706 let text = String::from_utf8_lossy(&wire);
707 let placed = text.find("a=p,i=").expect("the new base is placed");
708 let freed = text.find("d=I").expect("the stale tiles are freed");
709 assert!(
710 !text.contains("d=R"),
711 "Ghostty frees every image on a range delete"
712 );
713 assert!(placed < freed, "the new base appears before the tiles go");
714 }
715
716 fn space() -> katui_proto::kitty::IdSpace {
717 katui_proto::kitty::IdSpace::of_process(4242)
718 }
719
720 const VIRTUAL: &str = ",U=1";
721
722 #[test]
723 fn a_multiplexer_grid_shows_every_tile_through_placeholders_and_never_a_whole_image() {
724 let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
725 let surface = plain_surface(64, 64);
726 let mut wire = Vec::new();
727 presenter
728 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
729 .expect("writing to a Vec cannot fail");
730 let text = String::from_utf8_lossy(&wire);
731 assert!(!text.contains("s=64"), "no whole image travels");
732 assert_eq!(
733 text.matches(VIRTUAL).count(),
734 64,
735 "every tile has a virtual placement"
736 );
737 assert_eq!(
738 text.matches('\u{10eeee}').count(),
739 16 * 16,
740 "placeholder text covers every cell once"
741 );
742 assert!(!text.contains(",P="), "no tile needs a parent placement");
743 }
744
745 #[test]
746 fn a_multiplexer_frame_uploads_every_tile_before_showing_any() {
747 let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
748 let mut wire = Vec::new();
749 presenter
750 .render_frame(
751 &mut wire,
752 &plain_surface(64, 64),
753 crate::surface::test_support::area(),
754 )
755 .expect("writing to a Vec cannot fail");
756 let text = String::from_utf8_lossy(&wire);
757 let last_upload = text.rfind("a=t,").expect("tiles travel");
758 let first_show = text.find(VIRTUAL).expect("tiles show");
759 assert!(last_upload < first_show, "the frame appears at once");
760 }
761
762 #[test]
763 fn a_repainted_grid_resends_every_tile() {
764 let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
765 let surface = plain_surface(64, 64);
766 let mut wire = Vec::new();
767 presenter
768 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
769 .expect("writing to a Vec cannot fail");
770 presenter.repaint();
771 wire.clear();
772 presenter
773 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
774 .expect("writing to a Vec cannot fail");
775 let text = String::from_utf8_lossy(&wire);
776 assert_eq!(text.matches(VIRTUAL).count(), 64, "every tile returns");
777 assert_eq!(text.matches("a=t,").count(), 64, "only the tiles travel");
778 assert!(!text.contains("d=R"), "old tiles stay until replaced");
779 wire.clear();
780 presenter
781 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
782 .expect("writing to a Vec cannot fail");
783 assert!(wire.is_empty(), "one repaint request repaints once");
784 }
785
786 #[test]
787 fn a_multiplexer_grid_frees_the_old_tiles_on_a_resize() {
788 let mut presenter = PatchedKitty::new(90, cell()).for_multiplexer(space(), false);
789 let mut wire = Vec::new();
790 presenter
791 .render_frame(
792 &mut wire,
793 &plain_surface(64, 64),
794 crate::surface::test_support::area(),
795 )
796 .expect("writing to a Vec cannot fail");
797 wire.clear();
798 presenter
799 .render_frame(
800 &mut wire,
801 &plain_surface(32, 32),
802 crate::surface::test_support::area(),
803 )
804 .expect("writing to a Vec cannot fail");
805 let text = String::from_utf8_lossy(&wire);
806 let freed = text.find("d=I").expect("the old grid is freed");
807 let placed = text.find("a=p,").expect("the new grid is placed");
808 assert!(freed < placed);
809 }
810
811 #[test]
812 fn a_base_retransmit_never_reuses_the_visible_id() {
813 let mut presenter = PatchedKitty::with_tiles(90, cell(), 8, 0);
814 let mut surface = plain_surface(16, 16);
815 let mut first = Vec::new();
816 presenter
817 .render_frame(&mut first, &surface, crate::surface::test_support::area())
818 .expect("writing to a Vec cannot fail");
819 surface.set(
820 katui_scene::surface::PixelPos::new(1, 1),
821 Rgba::solid(9, 9, 9),
822 );
823 let mut second = Vec::new();
824 presenter
825 .render_frame(&mut second, &surface, crate::surface::test_support::area())
826 .expect("writing to a Vec cannot fail");
827 let text = String::from_utf8_lossy(&second);
828 let placed = text.find("a=p,i=91").expect("the alternate id is placed");
829 let freed = text.find("d=I,i=90").expect("the old base is freed");
830 assert!(placed < freed, "something is always on screen");
831 }
832
833 #[test]
834 fn a_tile_alternates_ids_and_frees_the_previous_one_after_placing() {
835 let mut presenter = PatchedKitty::new(90, cell());
836 let mut surface = plain_surface(64, 64);
837 let mut wire = Vec::new();
838 presenter
839 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
840 .expect("writing to a Vec cannot fail");
841 for shade in [10, 20] {
842 surface.set(
843 katui_scene::surface::PixelPos::new(40, 40),
844 Rgba::solid(shade, shade, shade),
845 );
846 wire.clear();
847 presenter
848 .render_frame(&mut wire, &surface, crate::surface::test_support::area())
849 .expect("writing to a Vec cannot fail");
850 }
851 let text = String::from_utf8_lossy(&wire);
852 let placed = text.find("a=p,i=82033").expect("the second generation");
853 let freed = text.find("d=I,i=82032").expect("the first is freed");
854 assert!(placed < freed, "the old tile stays until the new one shows");
855 }
856
857 #[test]
858 fn a_whole_surface_change_falls_back_to_one_retransmit() {
859 let mut presenter = PatchedKitty::new(90, cell());
862 let mut surface = plain_surface(128, 128);
863 let big_area = CellRect {
864 x: 0,
865 y: 0,
866 columns: 32,
867 rows: 32,
868 };
869 let mut warmup = Vec::new();
870 presenter
871 .render_frame(&mut warmup, &surface, big_area)
872 .expect("writing to a Vec cannot fail");
873 for y in 0..128u32 {
874 for x in 0..128u32 {
875 surface.set(
876 katui_scene::surface::PixelPos::new(x, y),
877 Rgba::solid(200, 40, 40),
878 );
879 }
880 }
881 let mut out = Vec::new();
882 presenter
883 .render_frame(&mut out, &surface, big_area)
884 .expect("writing to a Vec cannot fail");
885 let text = String::from_utf8_lossy(&out);
886 assert!(
887 text.contains("a=t"),
888 "everything dirty means one full frame"
889 );
890 }
891
892 #[test]
893 fn a_size_change_falls_back_to_one_retransmit() {
894 let mut presenter = PatchedKitty::new(90, cell());
895 let mut out = Vec::new();
896 presenter
897 .render_frame(
898 &mut out,
899 &plain_surface(64, 64),
900 crate::surface::test_support::area(),
901 )
902 .expect("writing to a Vec cannot fail");
903 let mut resized = Vec::new();
904 presenter
905 .render_frame(
906 &mut resized,
907 &plain_surface(32, 32),
908 crate::surface::test_support::area(),
909 )
910 .expect("writing to a Vec cannot fail");
911 let text = String::from_utf8_lossy(&resized);
912 assert!(text.contains("a=t"), "a resize retransmits whole");
913 }
914}