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