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