Skip to main content

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}