retroglyph_core/terminal/present.rs
1//! Frame presentation: [`draw`](Terminal::draw), [`present`](Terminal::present), and
2//! [`present_count`](Terminal::present_count).
3//!
4//! `present` is the one piece of `Terminal`'s API that has to reconcile three different backend
5//! shapes (compositing vs. cell, single-layer vs. multi-layer) with the error-recovery contract
6//! documented on it; its own doc comment and the tests below cover that matrix directly.
7
8use super::Terminal;
9use crate::backend::{Backend, Output};
10use crate::grid::Grid;
11use crate::surface::Surface;
12use ixy::HasSize;
13
14impl<B: Backend> Terminal<B> {
15 /// Draws one frame: `f` gets a [`Surface`] scoped to the whole terminal on layer 0, then the
16 /// frame is presented (see [`present`](Self::present)) once `f` returns.
17 ///
18 /// This is the common entry point for drawing: a caller that draws every frame regardless of
19 /// whether anything changed calls this once per frame. A caller that only wants to redraw
20 /// when its own state changed should gate the call to `draw` itself (e.g. `if
21 /// state.changed() { term.draw(|s| render(s, &state))?; }`) rather than rely on `draw`/
22 /// [`present`](Self::present) to no-op.
23 ///
24 /// # Errors
25 ///
26 /// Propagates errors from [`present`](Self::present).
27 pub fn draw(&mut self, f: impl FnOnce(&mut Surface<'_>)) -> Result<(), <B as Output>::Error> {
28 let area = self.area();
29 let mut surface = Surface::new(&mut self.current, area, 0);
30 f(&mut surface);
31 self.present()
32 }
33
34 /// Number of times [`present`](Self::present) has been called so far.
35 ///
36 /// Wraps on overflow; intended for detecting whether `present` was called *at all* between two
37 /// points in time (compare a saved count against the current one), not as a precise total.
38 /// Embedding drivers (e.g. `retroglyph-window`'s windowed drivers) use this to decide whether
39 /// application code already presented during a frame, so they can skip a redundant
40 /// driver-side present.
41 #[must_use]
42 pub const fn present_count(&self) -> u64 {
43 self.present_count
44 }
45
46 /// Present the current frame: computes the diff against the previous frame, sends changed
47 /// cells to the backend, flushes, then swaps buffers. Always presents unconditionally, even
48 /// if nothing was drawn since the last call; most callers want [`draw`](Self::draw) instead
49 /// of calling this directly.
50 ///
51 /// When the backend requires a full frame (see
52 /// [`crate::backend::Output::needs_full_frame`]), all cells from every allocated layer are
53 /// sent rather than just the diff, so pixel-based backends can clear and
54 /// redraw to avoid orphaned pixels from sub-cell offsets.
55 ///
56 /// After a present, the new current buffer is cleared so the next frame starts empty.
57 /// Callers should not draw into a frame and skip presenting it: the next [`draw`](Self::draw)
58 /// call starts from an empty grid regardless.
59 ///
60 /// # Immediate mode
61 ///
62 /// This is an immediate-mode API (the same trade [ratatui] makes): the
63 /// current buffer is wiped after every present, so each frame must redraw
64 /// its entire scene from scratch by default. [`retain_layer`](Self::retain_layer) is the
65 /// escape hatch: it makes one specific layer's last-presented content stand in for a redraw,
66 /// so the app can skip regenerating it. The diff only bounds what is sent to the backend
67 /// (terminal or pixel I/O); it does not bound the CPU cost of your redraw, except for a
68 /// layer marked via `retain_layer`.
69 ///
70 /// [ratatui]: https://docs.rs/ratatui
71 ///
72 /// # Panics
73 ///
74 /// Never panics in practice: `retained_layers` and `dropped_layers` are indexed by u8 layer
75 /// id and grown only up to `idx + 1` for `idx = usize::from(layer_id)` in
76 /// [`retain_layer`](Self::retain_layer)/[`drop_layer`](Self::drop_layer), so their length is
77 /// always at most 256 and every index encountered here fits in u8.
78 ///
79 /// # Errors
80 ///
81 /// Propagates errors from the backend's [`draw_layers`](crate::backend::Output::draw_layers) or
82 /// [`flush`](crate::backend::Output::flush) operations. Either failure returns before the
83 /// current/previous buffers are swapped, so the cells from the failed frame stay marked
84 /// dirty in `previous` and are resent the next time `present` succeeds. `current` is still
85 /// cleared, same as on success, so the caller doesn't need to redraw anything to recover:
86 /// just call `draw`/`present` again, and the next frame starts from an empty grid like any
87 /// other.
88 pub fn present(&mut self) -> Result<(), <B as Output>::Error> {
89 self.present_count = self.present_count.wrapping_add(1);
90 if self.retained_layers.iter().any(|&retained| retained) {
91 // Overwrite each retained layer's (empty, never-drawn-this-frame) content in
92 // `current` with `previous`'s, so the diff below finds no change on it: the backend
93 // gets nothing to redraw, and the copy (a flat per-layer clone) is far cheaper than
94 // whatever the app would have spent regenerating identical content. See
95 // `retain_layer`'s doc for why this has to run before the diff rather than skip the
96 // post-swap clear: `current` and `previous` alternate buffers every present, so
97 // anything short of re-syncing from the authoritative `previous` here would desync
98 // them again after a second consecutive retained frame.
99 //
100 // Uses `copy_layer_from` rather than `blit`: `blit` is a clipping/positioning copy
101 // that degrades multi-cell spans to their text fallback and treats empty tiles as
102 // transparent (an overlay, not a replacement), both wrong here, since a retained
103 // layer is copied whole, at the same geometry, and must be indistinguishable from
104 // what was presented last frame, whatever the app did or didn't draw into it this
105 // frame (retroglyph#955, retroglyph#956).
106 for (id, &retained) in self.retained_layers.iter().enumerate() {
107 if retained {
108 // `retained_layers` is indexed by u8 layer id: `retain_layer` only ever grows
109 // it to `idx + 1` for `idx = usize::from(layer_id)`, so its length is at most
110 // 256 and every index here fits in u8. `expect` makes that a checked invariant
111 // instead of a silently-truncating `as`.
112 let id = u8::try_from(id).expect("layer table is indexed by u8 layer ids");
113 self.current.copy_layer_from(id, &self.previous);
114 }
115 }
116 for retained in &mut self.retained_layers {
117 *retained = false;
118 }
119 }
120 let mut swap_flattened = false;
121 // The fallible part is scoped to this closure so both the success and error paths
122 // below can clear `current` before returning: `current` is presentation-buffer state
123 // for the *next* frame, not part of what makes the resend-on-retry behavior work (that
124 // lives entirely in `previous`/`flattened_previous`, left untouched here), so clearing
125 // it is safe unconditionally and keeps immediate mode's "next `draw` starts empty"
126 // contract true even after a failed present.
127 let result = (|| -> Result<(), <B as Output>::Error> {
128 if self.backend.composites_layers() {
129 // Pixel/GPU backends composite the raw layered stream themselves.
130 if self.backend.needs_full_frame() {
131 let all = self.current.layers();
132 self.backend.draw_layers(all)?;
133 } else {
134 let diff = self.current.diff(&self.previous);
135 self.backend.draw_layers(diff)?;
136 }
137 // Same reasoning as the fast path below: this branch bypasses the flatten buffers
138 // too, so the next present that lands in the flatten branch (e.g. a backend whose
139 // `composites_layers()` flips to `false`) must not diff against a
140 // `flattened_previous` that was never actually the last frame presented.
141 self.flattened_stale = true;
142 } else if self.current.max_layer() == 0 && self.previous.max_layer() == 0 {
143 // Fast path: only layer 0 is in play, so flattening would be an exact
144 // copy of `current`. Diff the real grids directly and skip the
145 // flatten buffers entirely.
146 //
147 // This is sticky-off, not sticky-on: layers are never deallocated on their own
148 // once written (see `Grid`'s layer storage), so `max_layer()` never drops back to
149 // 0 on its own. A terminal that ever draws to layer 1+, even for a single
150 // transient frame, stays on the flatten path in the `else` branch below for the
151 // rest of the process, unless it explicitly calls `drop_layer` on every layer
152 // above 0 (retroglyph#1028).
153 let diff = self.current.diff(&self.previous);
154 self.backend.draw_layers(diff)?;
155 self.flattened_stale = true;
156 } else {
157 // Cell backends receive a pre-flattened, single-layer diff so layers
158 // 1+ appear everywhere, not just on pixel backends.
159 let size = self.current.size();
160 let flattened_current = self
161 .flattened_current
162 .get_or_insert_with(|| Grid::new(size.width(), size.height()));
163 let flattened_previous = self
164 .flattened_previous
165 .get_or_insert_with(|| Grid::new(size.width(), size.height()));
166 if self.flattened_stale {
167 // The previous frame used the fast path, so `flattened_previous`
168 // is stale. Clear it to force a full redraw this frame.
169 flattened_previous.clear_all();
170 self.flattened_stale = false;
171 }
172 self.current.flatten_into(flattened_current);
173 let diff = flattened_current.diff(flattened_previous);
174 self.backend.draw_layers(diff)?;
175 swap_flattened = true;
176 }
177 self.backend.flush()
178 })();
179 if let Err(err) = result {
180 // `current` is cleared even on failure so the next frame still starts from an
181 // empty grid; only the swap below is skipped. `previous`/`flattened_previous`
182 // still hold the last confirmed frame, so the next `present`'s diff against them
183 // resends the cells that never actually reached the backend instead of silently
184 // dropping them.
185 self.current.clear_all();
186 return Err(err);
187 }
188 // Deallocate any layer `drop_layer` marked, now that the diff above (computed while the
189 // layer was still allocated, if only as an already-cleared buffer) has told the backend
190 // to erase whatever it last showed there. Also gated on `flush` succeeding, for the same
191 // reason as the swaps below: on failure, `previous` must keep the layer allocated so a
192 // retried `present` can still resend the erase that never actually reached the backend.
193 if self.dropped_layers.iter().any(|&dropped| dropped) {
194 for (id, &dropped) in self.dropped_layers.iter().enumerate() {
195 if dropped {
196 let id = u8::try_from(id).expect("layer table is indexed by u8 layer ids");
197 // If the app drew to `layer` again after calling `drop_layer` but before
198 // this present, that write is a live redraw the app clearly wants kept, not
199 // stale content: cancel the drop instead of discarding it.
200 if self.current.layer_is_empty(id) {
201 self.current.deallocate_layer(id);
202 self.previous.deallocate_layer(id);
203 }
204 }
205 }
206 for dropped in &mut self.dropped_layers {
207 *dropped = false;
208 }
209 }
210 // Both swaps happen only after `flush` succeeds, for the same reason described above.
211 if swap_flattened {
212 core::mem::swap(&mut self.flattened_current, &mut self.flattened_previous);
213 }
214 core::mem::swap(&mut self.current, &mut self.previous);
215 self.current.clear_all();
216 Ok(())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::backend::{Cursor, DrawCell, Headless, Input};
224 use crate::color::Color;
225 use crate::color::Style;
226 use crate::event::Event;
227 use crate::grid::{Pos, Size};
228 use alloc::vec::Vec;
229 use core::time::Duration;
230
231 /// Wraps [`Headless`] and fails the next [`flush`](Output::flush) or
232 /// [`draw_layers`](Output::draw_layers) call once, then forwards everything (including a
233 /// failed `draw_layers` call's content, which already reached the inner backend) as normal.
234 /// Used to exercise `present`'s documented error-recovery contract: either failure must
235 /// leave the frame's cells marked dirty so they are resent on the next successful `present`.
236 ///
237 /// `composites_layers` is also configurable, so the same helper covers the compositing,
238 /// flatten, and single-layer fast-path branches of `present`.
239 ///
240 /// `std`-only: its `Output::Error` is `std::io::Error`, purely as a convenient stand-in
241 /// error type for this test.
242 #[cfg(feature = "std")]
243 struct FlushOnceFailing {
244 inner: Headless,
245 fail_next_flush: bool,
246 fail_next_draw_layers: bool,
247 composites_layers: bool,
248 /// Number of cells received by the most recent `draw_layers` call, so tests can
249 /// tell whether a frame's diff was actually sent, independent of `Headless`'s
250 /// applied grid (which a real backend might not update until well after `flush`).
251 last_draw_len: usize,
252 }
253
254 #[cfg(feature = "std")]
255 impl FlushOnceFailing {
256 fn new(width: u16, height: u16) -> Self {
257 Self {
258 inner: Headless::new(width, height),
259 fail_next_flush: false,
260 fail_next_draw_layers: false,
261 composites_layers: false,
262 last_draw_len: 0,
263 }
264 }
265 }
266
267 #[cfg(feature = "std")]
268 impl Output for FlushOnceFailing {
269 type Error = std::io::Error;
270
271 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
272 where
273 I: Iterator<Item = DrawCell<'a>>,
274 {
275 if self.fail_next_draw_layers {
276 self.fail_next_draw_layers = false;
277 return Err(std::io::Error::other("simulated draw_layers failure"));
278 }
279 let content: Vec<_> = content.collect();
280 self.last_draw_len = content.len();
281 // Infallible in `Headless`; map its error type to ours to keep the wrapper's
282 // error type consistent across all `Output` methods.
283 self.inner
284 .draw_layers(content.into_iter())
285 .map_err(|e| match e {})
286 }
287
288 fn flush(&mut self) -> Result<(), Self::Error> {
289 if self.fail_next_flush {
290 self.fail_next_flush = false;
291 return Err(std::io::Error::other("simulated flush failure"));
292 }
293 self.inner.flush().map_err(|e| match e {})
294 }
295
296 fn size(&self) -> Size {
297 self.inner.size()
298 }
299
300 fn clear(&mut self) -> Result<(), Self::Error> {
301 self.inner.clear().map_err(|e| match e {})
302 }
303
304 fn composites_layers(&self) -> bool {
305 self.composites_layers
306 }
307 }
308
309 #[cfg(feature = "std")]
310 impl Input for FlushOnceFailing {
311 fn poll_event(&mut self, timeout: Duration) -> Option<Event> {
312 self.inner.poll_event(timeout)
313 }
314 }
315
316 #[cfg(feature = "std")]
317 impl Cursor for FlushOnceFailing {}
318
319 #[test]
320 fn test_draw_composites_layers_for_cell_backend() {
321 // A cell backend (Headless) must see layers 1+ composited, not
322 // dropped. Terrain on layer 0, entity on layer 1.
323 let mut term = Terminal::new(Headless::new(3, 1));
324 term.draw(|s| {
325 s.put((0, 0), '.', Style::default());
326 s.put((1, 0), '.', Style::default());
327 s.on_layer(1).put((1, 0), '@', Style::default());
328 })
329 .expect("draw failed");
330 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), '.');
331 // Layer 1's glyph wins at (1, 0).
332 assert_eq!(term.backend().grid()[Pos::new(1, 0)].glyph(), '@');
333 }
334
335 /// A cell backend with `needs_full_frame() == true` and the default `composites_layers()`.
336 ///
337 /// No real backend in this workspace uses that combination; this pins the interaction
338 /// `Output::draw_layers`'s docs describe (retroglyph#763): a `true` `needs_full_frame` only
339 /// takes effect inside `composites_layers`'s branch of `present`, so this combination gets
340 /// the same diff-only stream as `needs_full_frame() == false` would, not the "all cells,
341 /// every call" this method's own doc otherwise promises unconditionally.
342 struct NeedsFullFrameWithoutCompositing {
343 inner: Headless,
344 last_draw_len: usize,
345 }
346
347 impl Output for NeedsFullFrameWithoutCompositing {
348 type Error = core::convert::Infallible;
349
350 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
351 where
352 I: Iterator<Item = DrawCell<'a>>,
353 {
354 let content: Vec<_> = content.collect();
355 self.last_draw_len = content.len();
356 self.inner.draw_layers(content.into_iter())
357 }
358
359 fn flush(&mut self) -> Result<(), Self::Error> {
360 self.inner.flush()
361 }
362
363 fn size(&self) -> Size {
364 self.inner.size()
365 }
366
367 fn clear(&mut self) -> Result<(), Self::Error> {
368 self.inner.clear()
369 }
370
371 fn needs_full_frame(&self) -> bool {
372 true
373 }
374
375 // `composites_layers` left at its default `false`: exactly the combination the docs on
376 // `Output::draw_layers`/`Output::needs_full_frame` now call out.
377 }
378
379 impl Input for NeedsFullFrameWithoutCompositing {
380 fn poll_event(&mut self, timeout: Duration) -> Option<Event> {
381 self.inner.poll_event(timeout)
382 }
383 }
384
385 impl Cursor for NeedsFullFrameWithoutCompositing {}
386
387 #[test]
388 fn needs_full_frame_without_composites_layers_still_gets_only_the_diff() {
389 let mut term = Terminal::new(NeedsFullFrameWithoutCompositing {
390 inner: Headless::new(3, 1),
391 last_draw_len: 0,
392 });
393 term.draw(|s| {
394 s.put((0, 0), 'a', Style::default());
395 s.put((1, 0), 'b', Style::default());
396 })
397 .expect("draw failed");
398 assert_eq!(
399 term.backend().last_draw_len,
400 2,
401 "first frame: diff and full-frame agree (everything is new)"
402 );
403
404 // Second, identical frame: a backend for which `needs_full_frame` actually took effect
405 // would still receive both cells here. This one, per the documented caveat, gets the
406 // diff instead, which is empty, since nothing changed.
407 term.draw(|s| {
408 s.put((0, 0), 'a', Style::default());
409 s.put((1, 0), 'b', Style::default());
410 })
411 .expect("draw failed");
412 assert_eq!(
413 term.backend().last_draw_len,
414 0,
415 "needs_full_frame() alone (without composites_layers()) does not widen present's \
416 diff-only dispatch; see Output::draw_layers's docs (retroglyph#763)"
417 );
418 }
419
420 /// A `composites_layers() == true` backend, the branch of `present` no real backend in this
421 /// workspace's core tests exercises (`retroglyph-gl`/`retroglyph-software` test their own
422 /// side of the [`Output`] contract, not `present`'s choice between it and a diff).
423 /// `needs_full_frame` is fixed at construction, so one struct covers both dispatch modes.
424 ///
425 /// Unlike [`Headless`], this records the raw `(layer, pos, glyph)` cells it receives instead
426 /// of writing them into a single flat grid: a real compositing backend interprets an
427 /// unwritten (default/blank) cell on a higher layer as transparent, but `Headless::draw_layers`
428 /// writes every cell it's handed literally to one shared grid regardless of layer, so replaying
429 /// a raw multi-layer stream through it (rather than the pre-flattened stream the non-
430 /// compositing path sends) does not reproduce correct compositing.
431 struct CompositingBackend {
432 size: Size,
433 full_frame: bool,
434 last_draw_cells: Vec<(u8, Pos, char)>,
435 }
436
437 impl CompositingBackend {
438 fn new(width: u16, height: u16, full_frame: bool) -> Self {
439 Self {
440 size: Size::new(width, height),
441 full_frame,
442 last_draw_cells: Vec::new(),
443 }
444 }
445 }
446
447 impl Output for CompositingBackend {
448 type Error = core::convert::Infallible;
449
450 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
451 where
452 I: Iterator<Item = DrawCell<'a>>,
453 {
454 self.last_draw_cells = content
455 .map(|cell| (cell.layer, cell.pos, cell.tile.glyph()))
456 .collect();
457 Ok(())
458 }
459
460 fn flush(&mut self) -> Result<(), Self::Error> {
461 Ok(())
462 }
463
464 fn size(&self) -> Size {
465 self.size
466 }
467
468 fn clear(&mut self) -> Result<(), Self::Error> {
469 Ok(())
470 }
471
472 fn composites_layers(&self) -> bool {
473 true
474 }
475
476 fn needs_full_frame(&self) -> bool {
477 self.full_frame
478 }
479 }
480
481 impl Input for CompositingBackend {
482 fn poll_event(&mut self, _timeout: Duration) -> Option<Event> {
483 None
484 }
485 }
486
487 impl Cursor for CompositingBackend {}
488
489 #[test]
490 fn test_composites_layers_diff_dispatches_only_changed_cells() {
491 let mut term = Terminal::new(CompositingBackend::new(3, 1, false));
492 term.draw(|s| {
493 s.put((0, 0), 'a', Style::default());
494 s.put((1, 0), 'b', Style::default());
495 })
496 .expect("draw failed");
497 assert_eq!(
498 term.backend().last_draw_cells.len(),
499 2,
500 "first frame: only the two written cells differ from the pre-allocated blank layer 0"
501 );
502
503 // Second, identical frame: nothing changed, so the compositing branch's diff half sends
504 // nothing, same as the non-compositing diff path.
505 term.draw(|s| {
506 s.put((0, 0), 'a', Style::default());
507 s.put((1, 0), 'b', Style::default());
508 })
509 .expect("draw failed");
510 assert!(
511 term.backend().last_draw_cells.is_empty(),
512 "composites_layers() == true with needs_full_frame() == false still dispatches only \
513 the diff"
514 );
515 }
516
517 #[test]
518 fn test_composites_layers_full_frame_dispatches_every_allocated_cell() {
519 let mut term = Terminal::new(CompositingBackend::new(3, 1, true));
520 term.draw(|s| {
521 s.put((0, 0), 'a', Style::default());
522 s.put((1, 0), 'b', Style::default());
523 })
524 .expect("draw failed");
525 assert_eq!(
526 term.backend().last_draw_cells.len(),
527 3,
528 "first frame: every cell in the sole allocated layer (width 3), not just the two \
529 written ones"
530 );
531
532 // Second, identical frame: unlike the diff branch above, needs_full_frame() actually
533 // takes effect here, so the whole layer is resent rather than an empty diff.
534 term.draw(|s| {
535 s.put((0, 0), 'a', Style::default());
536 s.put((1, 0), 'b', Style::default());
537 })
538 .expect("draw failed");
539 assert_eq!(
540 term.backend().last_draw_cells.len(),
541 3,
542 "composites_layers() == true with needs_full_frame() == true resends every allocated \
543 cell on every present"
544 );
545 }
546
547 /// A cell backend whose `composites_layers()` can be toggled between presents, standing in
548 /// for a backend that degrades from pixel compositing to a cell path at runtime (retroglyph#960).
549 struct TogglingCompositor {
550 inner: Headless,
551 composites: bool,
552 }
553
554 impl Output for TogglingCompositor {
555 type Error = core::convert::Infallible;
556
557 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
558 where
559 I: Iterator<Item = DrawCell<'a>>,
560 {
561 self.inner.draw_layers(content)
562 }
563
564 fn flush(&mut self) -> Result<(), Self::Error> {
565 self.inner.flush()
566 }
567
568 fn size(&self) -> Size {
569 self.inner.size()
570 }
571
572 fn clear(&mut self) -> Result<(), Self::Error> {
573 self.inner.clear()
574 }
575
576 fn composites_layers(&self) -> bool {
577 self.composites
578 }
579 }
580
581 impl Input for TogglingCompositor {
582 fn poll_event(&mut self, timeout: Duration) -> Option<Event> {
583 self.inner.poll_event(timeout)
584 }
585 }
586
587 impl Cursor for TogglingCompositor {}
588
589 #[test]
590 fn present_marks_flatten_buffers_stale_after_a_composites_layers_present() {
591 // A backend that ever answers `true` from `composites_layers()` and later `false` must
592 // not leave `flattened_previous` holding a frame that was never actually the last one
593 // presented. Sequence: flatten branch (establishes stale-looking data) -> composites
594 // branch (bypasses the flatten buffers entirely) -> flatten branch again, where the bug
595 // would incorrectly diff against the first frame's flattened data instead of the second.
596 let mut term = Terminal::new(TogglingCompositor {
597 inner: Headless::new(3, 1),
598 composites: false,
599 });
600
601 // Frame 1: flatten branch. Layer 1 is touched so `max_layer() != 0`, and
602 // `composites_layers()` is `false`, so this flattens and diffs normally.
603 term.draw(|s| {
604 s.put((0, 0), 'a', Style::default());
605 s.on_layer(1).put((1, 0), '#', Style::default());
606 })
607 .expect("draw failed");
608 assert_eq!(term.backend().inner.grid()[Pos::new(0, 0)].glyph(), 'a');
609
610 // Frame 2: composites branch. Bypasses the flatten buffers entirely, so
611 // `flattened_previous` still holds frame 1's flattened content. Layer 1 is redrawn
612 // identically to frame 1 so `Grid::diff` (now that it also reports a layer that stopped
613 // being written, retroglyph#1018) sees no change there and doesn't emit anything for it;
614 // `Headless::draw_layers` writes every cell to one shared grid regardless of layer (see
615 // `CompositingBackend`'s docs above), so a real layer-1 diff would corrupt this frame's
616 // single-grid glyph check, which is unrelated to what this test is verifying.
617 term.backend_mut().composites = true;
618 term.draw(|s| {
619 s.put((0, 0), 'b', Style::default());
620 s.on_layer(1).put((1, 0), '#', Style::default());
621 })
622 .expect("draw failed");
623 assert_eq!(term.backend().inner.grid()[Pos::new(0, 0)].glyph(), 'b');
624
625 // Frame 3: back to the flatten branch. Draws 'a' at (0, 0) again, on layer 0, but with
626 // layer 1 also touched so this lands in the flatten branch rather than the fast path.
627 // 'a' matches what frame 1 left in `flattened_previous`, even though the real last
628 // presented frame (frame 2) showed 'b' there. Without the fix, the stale match makes the
629 // diff skip (0, 0), and the backend keeps showing frame 2's 'b' forever.
630 term.backend_mut().composites = false;
631 term.draw(|s| {
632 s.put((0, 0), 'a', Style::default());
633 s.on_layer(1).put((2, 0), '@', Style::default());
634 })
635 .expect("draw failed");
636 assert_eq!(
637 term.backend().inner.grid()[Pos::new(0, 0)].glyph(),
638 'a',
639 "flattened_previous must be cleared after a composites_layers() present, not diffed \
640 against as if it were the last frame actually shown"
641 );
642
643 // `TogglingCompositor` forwards `clear` and `poll_event` unconditionally, same as every
644 // other method on it, so exercise both here rather than leaving them as dead delegation.
645 term.backend_mut().clear().expect("clear failed");
646 term.backend_mut().inner.push_event(Event::Close);
647 assert_eq!(term.poll(Duration::ZERO), Some(Event::Close));
648 }
649
650 #[test]
651 fn test_draw_explicit_space_on_higher_layer_erases_and_sets_bg() {
652 // An explicit space on a higher layer is opaque: it overwrites the
653 // glyph beneath (erase) and applies its background. This is the
654 // deliberate consequence of the explicit-EMPTY transparency model.
655 let mut term = Terminal::new(Headless::new(2, 1));
656 term.draw(|s| {
657 s.put((0, 0), 'x', Style::default());
658 s.on_layer(1).put((0, 0), ' ', Style::new().bg(Color::RED));
659 })
660 .expect("draw failed");
661 let cell = term.backend().grid()[Pos::new(0, 0)];
662 assert_eq!(cell.glyph(), ' ');
663 assert_eq!(cell.style().background(), Color::RED);
664 }
665
666 #[test]
667 fn test_draw_single_layer_fast_path_matches_backend() {
668 // Only layer 0 is ever touched: the fast path must still deliver the
669 // correct cells to a cell backend across multiple frames.
670 let mut term = Terminal::new(Headless::new(3, 1));
671 term.draw(|s| s.put((0, 0), 'a', Style::default()))
672 .expect("draw failed");
673 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'a');
674
675 // Immediate mode: redraw 'a' and add 'c'.
676 term.draw(|s| {
677 s.put((0, 0), 'a', Style::default());
678 s.put((2, 0), 'c', Style::default());
679 })
680 .expect("draw failed");
681 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'a');
682 assert_eq!(term.backend().grid()[Pos::new(2, 0)].glyph(), 'c');
683
684 // A cell that is not redrawn is erased (immediate mode).
685 term.draw(|s| s.put((0, 0), 'a', Style::default()))
686 .expect("draw failed");
687 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'a');
688 assert_eq!(term.backend().grid()[Pos::new(2, 0)].glyph(), ' ');
689 }
690
691 #[test]
692 fn test_present_transition_single_to_multi_layer() {
693 // Start single-layer (fast path), then introduce layer 1. The frame
694 // that adds the layer must composite correctly despite the fast path
695 // having bypassed the flatten buffers.
696 let mut term = Terminal::new(Headless::new(2, 1));
697 term.draw(|s| {
698 s.put((0, 0), '.', Style::default());
699 s.put((1, 0), '.', Style::default());
700 })
701 .expect("draw failed");
702
703 term.draw(|s| {
704 s.put((0, 0), '.', Style::default());
705 s.put((1, 0), '.', Style::default());
706 s.on_layer(1).put((1, 0), '@', Style::default());
707 })
708 .expect("draw failed");
709 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), '.');
710 assert_eq!(term.backend().grid()[Pos::new(1, 0)].glyph(), '@');
711 }
712
713 #[test]
714 fn test_present_transition_multi_to_single_to_multi_layer() {
715 // The reverse of the transition above: multi-layer (flatten path) drops back to
716 // single-layer (fast path, sets `flattened_stale`), then multi-layer again. The frame
717 // that returns to multi-layer must see `flattened_previous` cleared rather than diffed
718 // against the stale content the fast path bypassed, or the reintroduced layer's cells
719 // would wrongly look unchanged.
720 let mut term = Terminal::new(Headless::new(2, 1));
721 term.draw(|s| {
722 s.put((0, 0), '.', Style::default());
723 s.on_layer(1).put((1, 0), '@', Style::default());
724 })
725 .expect("draw failed");
726
727 // Single-layer frame: fast path, `flattened_stale` set.
728 term.draw(|s| s.put((0, 0), '.', Style::default()))
729 .expect("draw failed");
730 assert_eq!(term.backend().grid()[Pos::new(1, 0)].glyph(), ' ');
731
732 // Back to multi-layer: must composite correctly despite the intervening fast-path frame.
733 term.draw(|s| {
734 s.put((0, 0), '.', Style::default());
735 s.on_layer(1).put((1, 0), '@', Style::default());
736 })
737 .expect("draw failed");
738 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), '.');
739 assert_eq!(term.backend().grid()[Pos::new(1, 0)].glyph(), '@');
740 }
741
742 #[cfg(feature = "std")]
743 #[test]
744 fn present_resends_cells_after_a_failed_flush_on_the_multi_layer_path() {
745 // Two-layer terminal so `present` takes the flatten-buffer path (not the
746 // single-layer fast path, which already handled this correctly).
747 let mut term = Terminal::new(FlushOnceFailing::new(2, 1));
748
749 term.backend_mut().fail_next_flush = true;
750 let result = term.draw(|s| {
751 s.put((0, 0), 'a', Style::default());
752 s.on_layer(1).put((1, 0), 'b', Style::default());
753 });
754 assert!(result.is_err(), "flush was expected to fail this frame");
755 assert_eq!(
756 term.backend().last_draw_len,
757 2,
758 "the failed frame's diff should still have been sent to draw_layers"
759 );
760
761 // Same content, flush succeeds this time. If the flatten buffers had already been
762 // swapped on the failed attempt, this diff would see "no change" and send nothing.
763 term.draw(|s| {
764 s.put((0, 0), 'a', Style::default());
765 s.on_layer(1).put((1, 0), 'b', Style::default());
766 })
767 .expect("draw failed");
768 assert_eq!(
769 term.backend().last_draw_len,
770 2,
771 "both cells must be resent since neither ever reached the screen"
772 );
773 assert_eq!(term.backend().inner.grid()[Pos::new(0, 0)].glyph(), 'a');
774 assert_eq!(term.backend().inner.grid()[Pos::new(1, 0)].glyph(), 'b');
775 }
776
777 #[cfg(feature = "std")]
778 #[test]
779 fn present_resends_cells_after_a_failed_draw_layers_on_the_multi_layer_path() {
780 // `draw_layers` is the other documented early return in `present`: it must leave
781 // `previous`/`flattened_previous` untouched, same as a failed `flush`, so the next
782 // `present` resends everything rather than silently dropping it.
783 let mut term = Terminal::new(FlushOnceFailing::new(2, 1));
784
785 term.backend_mut().fail_next_draw_layers = true;
786 let result = term.draw(|s| {
787 s.put((0, 0), 'a', Style::default());
788 s.on_layer(1).put((1, 0), 'b', Style::default());
789 });
790 assert!(
791 result.is_err(),
792 "draw_layers was expected to fail this frame"
793 );
794 assert_eq!(
795 term.backend().last_draw_len,
796 0,
797 "a failed draw_layers call never recorded any content on the wrapper"
798 );
799
800 // Same content, draw_layers succeeds this time.
801 term.draw(|s| {
802 s.put((0, 0), 'a', Style::default());
803 s.on_layer(1).put((1, 0), 'b', Style::default());
804 })
805 .expect("draw failed");
806 assert_eq!(
807 term.backend().last_draw_len,
808 2,
809 "both cells must be resent since neither ever reached the screen"
810 );
811 assert_eq!(term.backend().inner.grid()[Pos::new(0, 0)].glyph(), 'a');
812 assert_eq!(term.backend().inner.grid()[Pos::new(1, 0)].glyph(), 'b');
813 }
814
815 #[cfg(feature = "std")]
816 #[test]
817 fn present_resends_cells_after_a_failed_flush_on_the_single_layer_fast_path() {
818 // Only layer 0 is ever touched, so `present` takes the fast path that diffs the raw
819 // grids directly, skipping the flatten buffers entirely.
820 let mut term = Terminal::new(FlushOnceFailing::new(2, 1));
821
822 term.backend_mut().fail_next_flush = true;
823 let result = term.draw(|s| s.put((0, 0), 'a', Style::default()));
824 assert!(result.is_err(), "flush was expected to fail this frame");
825 assert_eq!(
826 term.backend().last_draw_len,
827 1,
828 "the failed frame's diff should still have been sent to draw_layers"
829 );
830
831 // Nothing drawn this time: if the fast path's diff had already been swapped forward on
832 // the failed attempt, this frame would see no change and resend nothing.
833 term.draw(|s| s.put((0, 0), 'a', Style::default()))
834 .expect("draw failed");
835 assert_eq!(
836 term.backend().last_draw_len,
837 1,
838 "the cell must be resent since it never reached the screen"
839 );
840 assert_eq!(term.backend().inner.grid()[Pos::new(0, 0)].glyph(), 'a');
841 }
842
843 #[cfg(feature = "std")]
844 #[test]
845 fn present_failure_does_not_leak_the_failed_frames_content_into_the_next_frame() {
846 // Single-layer terminal so `present` takes the fast path, matching the issue's repro.
847 let mut term = Terminal::new(FlushOnceFailing::new(3, 1));
848
849 term.backend_mut().fail_next_flush = true;
850 let result = term.draw(|s| s.put((2, 0), 'X', Style::default()));
851 assert!(result.is_err(), "flush was expected to fail this frame");
852
853 // Next frame redraws different content and never touches (2, 0). `previous` is still
854 // empty (the swap was skipped), so if `current` had also been left holding the failed
855 // frame's 'X' (the bug), the diff below would see (2, 0) as newly changed from empty
856 // to 'X' and needlessly resend it, on top of the one cell this frame actually drew.
857 term.draw(|s| s.put((0, 0), 'A', Style::default()))
858 .expect("draw failed");
859 assert_eq!(
860 term.backend().last_draw_len,
861 1,
862 "only the redrawn cell should be sent; the failed frame's 'X' must not leak back in"
863 );
864 assert_eq!(term.backend().inner.grid()[Pos::new(0, 0)].glyph(), 'A');
865 }
866
867 #[cfg(feature = "std")]
868 #[test]
869 fn present_resends_cells_after_a_failed_flush_on_the_compositing_path() {
870 // `composites_layers() == true` takes `present`'s first branch entirely, bypassing both
871 // the fast path and the flatten buffers; a failed flush there must still leave `previous`
872 // untouched so the raw per-layer diff is resent.
873 let mut term = Terminal::new(FlushOnceFailing::new(2, 1));
874 term.backend_mut().composites_layers = true;
875
876 // Layer 1 is newly allocated this frame, so its diff against an absent previous layer
877 // includes every cell in its width (see `Grid::diff`'s "newly allocated layer" case), not
878 // just the one actually written; layer 0 contributes only its one real change.
879 term.backend_mut().fail_next_flush = true;
880 let result = term.draw(|s| {
881 s.put((0, 0), 'a', Style::default());
882 s.on_layer(1).put((1, 0), 'b', Style::default());
883 });
884 assert!(result.is_err(), "flush was expected to fail this frame");
885 assert_eq!(
886 term.backend().last_draw_len,
887 3,
888 "the failed frame's diff should still have been sent to draw_layers"
889 );
890
891 // Same content, flush succeeds this time. If `previous` had already been swapped forward
892 // on the failed attempt, layer 1 would no longer be "newly allocated" and this diff would
893 // shrink to just the real changes instead of resending the same content.
894 term.draw(|s| {
895 s.put((0, 0), 'a', Style::default());
896 s.on_layer(1).put((1, 0), 'b', Style::default());
897 })
898 .expect("draw failed");
899 assert_eq!(
900 term.backend().last_draw_len,
901 3,
902 "the same diff must be resent since it never reached the screen"
903 );
904 }
905
906 #[test]
907 fn test_present_untouched_higher_layer_is_transparent() {
908 // A higher layer that was allocated but not written at this cell must
909 // not disturb the lower layer's glyph or background.
910 let mut term = Terminal::new(Headless::new(2, 1));
911 term.draw(|s| {
912 s.put((0, 0), 'x', Style::default());
913 // Allocate layer 1 by writing elsewhere, leaving (0, 0) empty.
914 s.on_layer(1).put((1, 0), 'y', Style::default());
915 })
916 .expect("draw failed");
917 assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'x');
918 }
919
920 #[test]
921 fn test_terminal_present_count_advances_once_per_present_call() {
922 let mut term = Terminal::new(Headless::new(2, 1));
923 assert_eq!(term.present_count(), 0);
924
925 term.draw(|_| {}).expect("draw failed"); // `draw` always presents.
926 assert_eq!(term.present_count(), 1);
927
928 term.present().expect("present failed");
929 assert_eq!(term.present_count(), 2);
930
931 term.present().expect("present failed");
932 assert_eq!(term.present_count(), 3);
933 }
934}