Skip to main content

retroglyph_core/terminal/
retain.rs

1//! Per-layer redraw control: [`retain_layer`](Terminal::retain_layer) and
2//! [`drop_layer`](Terminal::drop_layer).
3//!
4//! Both are one-shot opt-ins that defer part of [`present`](Terminal::present)'s work by a
5//! frame: `retain_layer` re-syncs a layer from `previous` instead of requiring a redraw, and
6//! `drop_layer` defers deallocating a layer until its erase has actually reached the backend.
7//! See each method's own doc for the full contract.
8
9use super::{LayerOp, Terminal};
10use crate::backend::Backend;
11
12impl<B: Backend> Terminal<B> {
13    /// Marks `layer` so the next [`present`](Self::present) treats it as unchanged instead of
14    /// requiring the app to have redrawn it: `present` copies `layer`'s last-presented content
15    /// back into `current` before diffing, so the diff (and thus the backend) sees no change on
16    /// it, whatever the app did or didn't draw into it this frame.
17    ///
18    /// Call this before [`draw`](Self::draw)/[`present`](Self::present) on a frame where a
19    /// layer's content is known not to have changed (e.g. the camera didn't move since the last
20    /// frame, so a cached map layer is still correct) and skip drawing it that frame. This is
21    /// the actual point of the method: [`present`](Self::present)'s diff already keeps the
22    /// *backend* from re-receiving unchanged cells, but the *app* still has to regenerate them
23    /// every frame to produce a buffer worth diffing. Marking a layer retained lets the app skip
24    /// that regeneration too, at the cost of a per-cell copy handled internally (a flat, verbatim
25    /// replace, far cheaper than most real content generation).
26    ///
27    /// This is a one-shot opt-in, not a sticky mode: it only affects the very next `present`, so
28    /// a caller that wants a layer retained for several frames in a row must call this again
29    /// before each of them. [`resize`](Self::resize) also clears any pending retention.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use retroglyph_core::backend::Headless;
35    /// use retroglyph_core::surface::Layer;
36    /// use retroglyph_core::terminal::Terminal;
37    ///
38    /// let mut term = Terminal::new(Headless::new(10, 5));
39    /// let camera_moved = false;
40    ///
41    /// if camera_moved {
42    ///     term.draw(|s| s.on_tier(Layer::World).print((0, 0), "map", Default::default()))
43    ///         .unwrap();
44    /// } else {
45    ///     // The camera didn't move this frame: skip regenerating the map layer.
46    ///     term.retain_layer(Layer::World);
47    ///     term.draw(|s| s.on_tier(Layer::Hud).print((0, 1), "HP: 10", Default::default()))
48    ///         .unwrap();
49    /// }
50    /// ```
51    pub fn retain_layer(&mut self, layer: impl Into<u8>) {
52        self.set_pending_layer_op(layer.into(), LayerOp::Retain);
53    }
54
55    /// Marks `layer` to be deallocated, forgetting it was ever drawn to.
56    ///
57    /// [`Grid::max_layer`](crate::grid::Grid::max_layer) only grows on write, so a terminal that ever draws to a layer above 0,
58    /// even for a single frame, stays on [`present`](Self::present)'s flatten path for the rest of
59    /// the process, whether or not that layer is still in use (retroglyph#1028). This is the
60    /// explicit escape hatch: call it once a layer's content is truly done (a one-off overlay
61    /// dismissed, a transient effect finished), and once every layer above 0 has been dropped,
62    /// `present` falls back onto its single-layer fast path.
63    ///
64    /// `layer`'s content is cleared immediately (so this frame's diff still tells the backend to
65    /// erase whatever it last showed there, exactly as if the app had simply stopped drawing to
66    /// it), but the underlying buffer is only freed, and `max_layer` only allowed to fall, once
67    /// the next [`present`](Self::present) has sent that erase and no longer needs the layer for
68    /// its diff. Deallocating any earlier would make the layer invisible to `present`'s diff
69    /// (which only walks `current`'s own allocated layers), silently dropping the erase instead of
70    /// sending it.
71    ///
72    /// This is a one-shot request, not a sticky mode: unlike [`retain_layer`](Self::retain_layer),
73    /// which defers one frame's redraw, this defers the actual deallocation, so drawing to `layer`
74    /// again before the next `present` (undoing the drop) cancels it instead of losing that draw:
75    /// the layer stays allocated and behaves like any other write. `retain_layer` and `drop_layer`
76    /// share one pending op per layer, last call wins, so a call here also overwrites any pending
77    /// [`retain_layer`](Self::retain_layer) call for the same `layer`: retaining content that no
78    /// longer exists would resurrect it on the next present regardless of whether the drop itself
79    /// goes through.
80    ///
81    /// # Panics
82    ///
83    /// Panics if `layer` is 0: layer 0 is always allocated and can never be dropped.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use retroglyph_core::backend::Headless;
89    /// use retroglyph_core::surface::Layer;
90    /// use retroglyph_core::terminal::Terminal;
91    ///
92    /// let mut term = Terminal::new(Headless::new(10, 5));
93    /// term.draw(|s| s.on_tier(Layer::Hud).print((0, 0), "Paused", Default::default()))
94    ///     .unwrap();
95    ///
96    /// term.drop_layer(Layer::Hud);
97    /// term.present().unwrap(); // Sends the erase, then frees the layer.
98    /// assert_eq!(term.grid().max_layer(), 0);
99    /// ```
100    pub fn drop_layer(&mut self, layer: impl Into<u8>) {
101        let id = layer.into();
102        assert_ne!(id, 0, "layer 0 is always allocated and cannot be dropped");
103        self.current.clear(id);
104        self.set_pending_layer_op(id, LayerOp::Drop);
105    }
106
107    /// Queues `op` as the one-shot [`LayerOp`] pending on `layer`, overwriting whatever op (if
108    /// any) was already pending on it: the two writers of `pending_layer_ops`,
109    /// [`retain_layer`](Self::retain_layer) and [`drop_layer`](Self::drop_layer), share this so
110    /// the last call for a given layer within a frame always wins, and `Retain`/`Drop` can never
111    /// both end up pending on the same layer at once.
112    fn set_pending_layer_op(&mut self, layer: u8, op: LayerOp) {
113        let idx = usize::from(layer);
114        if self.pending_layer_ops.len() <= idx {
115            self.pending_layer_ops.resize(idx + 1, LayerOp::None);
116        }
117        self.pending_layer_ops[idx] = op;
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::backend::{Compositing, Cursor, DrawCell, Headless, Input, Output};
125    use crate::color::Style;
126    use crate::event::Event;
127    use crate::grid::{Pos, Size};
128    use alloc::vec::Vec;
129    use core::time::Duration;
130
131    /// A `Compositing::PixelLayered` cell-recording backend, used to prove `retain_layer`'s
132    /// pre-diff copy from `previous` still applies on the branch of `present` that bypasses the
133    /// fast path and flatten buffers entirely. See `present`'s own test module for the fuller
134    /// version of this fixture (dispatch-mode coverage); this one only needs the diff branch.
135    struct CompositingBackend {
136        size: Size,
137        last_draw_cells: Vec<(u8, Pos, char)>,
138    }
139
140    impl CompositingBackend {
141        fn new(width: u16, height: u16) -> Self {
142            Self {
143                size: Size::new(width, height),
144                last_draw_cells: Vec::new(),
145            }
146        }
147    }
148
149    impl Output for CompositingBackend {
150        type Error = core::convert::Infallible;
151
152        fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
153        where
154            I: Iterator<Item = DrawCell<'a>>,
155        {
156            self.last_draw_cells = content
157                .map(|cell| (cell.layer, cell.pos, cell.tile.glyph()))
158                .collect();
159            Ok(())
160        }
161
162        fn flush(&mut self) -> Result<(), Self::Error> {
163            Ok(())
164        }
165
166        fn size(&self) -> Size {
167            self.size
168        }
169
170        fn clear(&mut self) -> Result<(), Self::Error> {
171            Ok(())
172        }
173
174        fn compositing(&self) -> Compositing {
175            Compositing::PixelLayered {
176                needs_full_frame: false,
177            }
178        }
179    }
180
181    impl Input for CompositingBackend {
182        fn poll_event(&mut self, _timeout: Duration) -> Option<Event> {
183            None
184        }
185    }
186
187    impl Cursor for CompositingBackend {}
188
189    // --- retain_layer ---
190
191    #[test]
192    fn test_retain_layer_skips_redraw_and_keeps_backend_content() {
193        use crate::surface::Layer;
194
195        let mut term = Terminal::new(Headless::new(3, 1));
196        term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
197            .expect("draw failed");
198
199        // Retain `World`, then draw a frame that only touches `Hud`.
200        term.retain_layer(Layer::World);
201        term.draw(|s| s.on_tier(Layer::Hud).put((1, 0), 'H', Style::default()))
202            .expect("draw failed");
203
204        // `World` was never redrawn this frame, but the backend still shows it composited under
205        // the new `Hud` cell: `present` re-synced it from `previous` before diffing.
206        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'W');
207        assert_eq!(term.backend().grid()[Pos::new(1, 0)].glyph(), 'H');
208    }
209
210    #[test]
211    fn test_retain_layer_survives_repeated_retention_without_desync() {
212        use crate::surface::Layer;
213
214        // Retaining a layer for several frames in a row (never redrawing it) must not desync
215        // `current`/`previous`: each present re-syncs the retained layer from `previous`, so the
216        // backend keeps showing it correctly across any number of consecutive retains.
217        let mut term = Terminal::new(Headless::new(3, 1));
218        term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
219            .expect("draw failed");
220
221        for _ in 0..3 {
222            term.retain_layer(Layer::World);
223            term.draw(|_| {}).expect("draw failed");
224            assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'W');
225        }
226    }
227
228    #[test]
229    fn test_retain_layer_is_one_shot() {
230        use crate::surface::Layer;
231
232        let mut term = Terminal::new(Headless::new(3, 1));
233        term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
234            .expect("draw failed");
235
236        term.retain_layer(Layer::World);
237        term.draw(|_| {}).expect("draw failed"); // Retained: the backend keeps showing 'W'.
238        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'W');
239
240        // Retention was one-shot: the next present, with `World` still undrawn, clears it from
241        // the backend like any ordinary immediate-mode frame.
242        term.draw(|_| {}).expect("draw failed");
243        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
244    }
245
246    #[test]
247    fn test_retain_layer_replaces_a_cell_the_app_draws_at_an_empty_previous_cell() {
248        use crate::surface::Layer;
249
250        // retroglyph#956: `present` must discard whatever the app drew into a retained layer
251        // this frame, even at a cell where `previous` had nothing (so a naive transparent-skip
252        // copy would let the app's write leak through).
253        let mut term = Terminal::new(Headless::new(4, 1));
254        term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
255            .expect("draw failed");
256
257        term.retain_layer(Layer::World);
258        term.draw(|s| s.on_tier(Layer::World).put((2, 0), 'X', Style::default()))
259            .expect("draw failed");
260
261        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'W');
262        assert_eq!(term.backend().grid()[Pos::new(2, 0)].glyph(), ' ');
263    }
264
265    #[test]
266    fn test_retain_layer_restores_a_cell_the_app_erases_with_an_explicit_space() {
267        use crate::surface::Layer;
268
269        // retroglyph#956: an explicit-space write on a retained layer is still a draw the app
270        // made this frame, so it must be discarded like any other write on that layer, not
271        // treated as an opaque erase that survives the retention.
272        let mut term = Terminal::new(Headless::new(4, 1));
273        term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
274            .expect("draw failed");
275
276        term.retain_layer(Layer::World);
277        term.draw(|s| s.on_tier(Layer::World).put((0, 0), ' ', Style::default()))
278            .expect("draw failed");
279
280        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'W');
281    }
282
283    #[test]
284    fn test_retain_layer_preserves_multi_cell_span_flags() {
285        use crate::surface::Layer;
286        use crate::tile::TileFlags;
287
288        // retroglyph#955: `retain_layer` used to re-sync via `Grid::blit`, whose clipping-copy
289        // contract intentionally strips `SPAN_ANCHOR`/`SPAN_COVERED` and degrades a span to its
290        // text fallback. That's wrong for a retained layer, which is copied whole at the same
291        // geometry and must be indistinguishable from what was presented last frame.
292        let mut term = Terminal::new(Headless::new(4, 2));
293        term.draw(|s| {
294            s.on_tier(Layer::World)
295                .put_span((0, 0), &["Tr", "__"], Style::default())
296                .unwrap();
297        })
298        .expect("draw failed");
299
300        let anchor_flags = term.backend().grid()[Pos::new(0, 0)].flags();
301        let covered_flags = term.backend().grid()[Pos::new(1, 0)].flags();
302        assert!(anchor_flags.contains(TileFlags::SPAN_ANCHOR));
303        assert!(covered_flags.contains(TileFlags::SPAN_COVERED));
304
305        term.retain_layer(Layer::World);
306        term.draw(|_| {}).expect("draw failed");
307
308        // The span survived the retained present untouched: same glyphs, same span flags.
309        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'T');
310        assert_eq!(term.backend().grid()[Pos::new(1, 0)].glyph(), 'r');
311        assert_eq!(term.backend().grid()[Pos::new(0, 0)].flags(), anchor_flags);
312        assert_eq!(term.backend().grid()[Pos::new(1, 0)].flags(), covered_flags);
313    }
314
315    #[test]
316    fn test_retain_layer_accepts_raw_u8_and_layer() {
317        use crate::surface::Layer;
318
319        // `retain_layer` takes `impl Into<u8>`, so a raw layer id and the `Layer` enum both work.
320        let mut term = Terminal::new(Headless::new(1, 1));
321        term.draw(|s| s.put((0, 0), 'A', Style::default()))
322            .expect("draw failed");
323
324        term.retain_layer(0u8);
325        term.draw(|_| {}).expect("draw failed");
326        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'A');
327
328        term.retain_layer(Layer::World);
329        term.draw(|_| {}).expect("draw failed");
330        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'A');
331    }
332
333    #[test]
334    fn test_retain_layer_skips_redraw_on_a_compositing_backend() {
335        use crate::surface::Layer;
336
337        // `Compositing::PixelLayered` takes `present`'s first branch entirely; `retain_layer`'s
338        // pre-diff copy from `previous` (`Grid::copy_layer_from`) runs before that branch, so it
339        // must still apply here: the retained layer's cell should be re-synced (and so absent
340        // from the diff, since it now matches `previous`) rather than diffed as a real change.
341        let mut term = Terminal::new(CompositingBackend::new(3, 1));
342        term.draw(|s| s.on_tier(Layer::World).put((0, 0), 'W', Style::default()))
343            .expect("draw failed");
344
345        term.retain_layer(Layer::World);
346        term.draw(|s| s.on_tier(Layer::Hud).put((1, 0), 'H', Style::default()))
347            .expect("draw failed");
348
349        let cells = &term.backend().last_draw_cells;
350        assert!(
351            !cells
352                .iter()
353                .any(|&(layer, pos, _)| layer == 0 && pos == Pos::new(0, 0)),
354            "retained layer's unchanged cell must not be re-sent as a diff: {cells:?}"
355        );
356        assert!(
357            cells.contains(&(1, Pos::new(1, 0), 'H')),
358            "the newly drawn Hud cell must still be sent: {cells:?}"
359        );
360    }
361
362    #[test]
363    fn test_retain_layer_never_drawn_is_a_no_op() {
364        // `Grid::copy_layer_from`'s `None` arm, no-op branch: retaining a non-zero layer id
365        // that has never been drawn to on either buffer leaves it unallocated on both sides.
366        // Must not panic or grow the layer table for a layer id nobody ever wrote to.
367        let mut term = Terminal::new(Headless::new(3, 1));
368        term.retain_layer(5u8);
369        term.draw(|_| {}).expect("draw failed");
370        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
371    }
372
373    #[test]
374    fn test_retain_layer_deallocates_when_previous_lacks_it() {
375        // `Grid::copy_layer_from`'s `None` arm, deallocating branch: `current` can carry a
376        // non-zero layer allocated (but emptied by immediate mode) from an older frame while
377        // `previous` never allocated it at all, if that layer went undrawn (and unretained) for
378        // a frame in between. Retaining it then must clear it from `current`, not leave stale
379        // allocation state behind. Layer 0 can't exercise this (always allocated on both
380        // sides), so this writes directly to a non-zero raw layer id via `on_layer`.
381        let mut term = Terminal::new(Headless::new(3, 1));
382        term.draw(|s| s.on_layer(5).put((0, 0), 'W', Style::default()))
383            .expect("draw failed");
384        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'W');
385
386        // Layer 5 goes undrawn and unretained: ordinary immediate-mode clearing puts `current`'s
387        // (still allocated) layer 5 buffer back to empty, and this frame's diff sends that.
388        term.draw(|s| s.on_layer(1).put((1, 0), 'H', Style::default()))
389            .expect("draw failed");
390        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
391
392        // Retaining layer 5 now must not resurrect stale content or panic, even though
393        // `previous` (this frame's source) never allocated layer 5 at all.
394        term.retain_layer(5u8);
395        term.draw(|_| {}).expect("draw failed");
396        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
397    }
398
399    // --- drop_layer ---
400
401    #[test]
402    #[should_panic(expected = "layer 0 is always allocated")]
403    fn test_drop_layer_zero_panics() {
404        let mut term = Terminal::new(Headless::new(3, 1));
405        term.drop_layer(0u8);
406    }
407
408    #[test]
409    fn test_drop_layer_restores_the_single_layer_fast_path() {
410        use crate::surface::Layer;
411
412        // retroglyph#1028: a layer stays allocated once written, permanently moving `present`
413        // onto the flatten path. `drop_layer` is the explicit escape hatch back to the
414        // single-layer fast path once every layer above 0 is gone.
415        let mut term = Terminal::new(Headless::new(3, 1));
416        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'H', Style::default()))
417            .expect("draw failed");
418        // `current` was just swapped to the other (never-written) buffer, so `previous` is the
419        // one still carrying the allocation right after this draw.
420        assert_eq!(term.current.max_layer(), 0);
421        assert_ne!(term.previous.max_layer(), 0);
422
423        // The deallocation is deferred to the next `present`: calling `drop_layer` alone must
424        // not yet change either buffer's `max_layer`.
425        term.drop_layer(Layer::Hud);
426        assert_ne!(term.previous.max_layer(), 0);
427
428        term.draw(|_| {}).expect("draw failed");
429        assert_eq!(term.current.max_layer(), 0);
430        assert_eq!(term.previous.max_layer(), 0);
431    }
432
433    #[test]
434    fn test_drop_layer_erases_content_from_the_backend_on_the_next_present() {
435        use crate::surface::Layer;
436
437        let mut term = Terminal::new(Headless::new(3, 1));
438        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'H', Style::default()))
439            .expect("draw failed");
440        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'H');
441
442        term.drop_layer(Layer::Hud);
443        term.draw(|_| {}).expect("draw failed");
444
445        // The layer's content is gone once the drop has gone through a present, same as if the
446        // app had simply stopped drawing to it.
447        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
448    }
449
450    #[test]
451    fn test_drop_layer_is_cancelled_by_a_redraw_before_the_next_present() {
452        use crate::surface::Layer;
453
454        // A write to `layer` after `drop_layer` but before the deferred deallocation runs is a
455        // live redraw the app wants kept, not stale content left over from before the drop, so
456        // it must not be silently discarded.
457        let mut term = Terminal::new(Headless::new(3, 1));
458        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'H', Style::default()))
459            .expect("draw failed");
460
461        term.drop_layer(Layer::Hud);
462        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'J', Style::default()))
463            .expect("draw failed");
464
465        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'J');
466        assert_ne!(term.current.max_layer(), 0);
467    }
468
469    #[test]
470    fn test_drop_layer_clears_pending_retention_for_that_layer() {
471        use crate::surface::Layer;
472
473        // Retaining content that no longer exists would resurrect it on the next present, so
474        // `drop_layer` must clear any pending `retain_layer` call for the same id.
475        let mut term = Terminal::new(Headless::new(3, 1));
476        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'H', Style::default()))
477            .expect("draw failed");
478
479        term.retain_layer(Layer::Hud);
480        term.drop_layer(Layer::Hud);
481        term.draw(|_| {}).expect("draw failed");
482
483        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
484    }
485
486    #[test]
487    fn test_drop_then_retain_is_last_call_wins() {
488        use crate::surface::Layer;
489
490        // Reverse call order from `test_drop_layer_clears_pending_retention_for_that_layer`:
491        // `retain_layer` has no cross-clear of its own, so a pending drop is only ever
492        // overwritten by whichever call for the same layer comes last within the frame. Pins the
493        // resulting "retain wins, drop cancelled" behavior (retroglyph#1370) so a later change to
494        // the one-shot-op representation can't silently flip it.
495        let mut term = Terminal::new(Headless::new(3, 1));
496        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'H', Style::default()))
497            .expect("draw failed");
498
499        term.drop_layer(Layer::Hud);
500        term.retain_layer(Layer::Hud);
501        term.draw(|_| {}).expect("draw failed");
502
503        // The drop was cancelled: `Hud`'s last-presented content is still shown, and the layer
504        // is still allocated rather than deallocated.
505        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'H');
506        assert_ne!(term.current.max_layer(), 0);
507    }
508
509    #[test]
510    fn test_drop_layer_never_drawn_is_a_no_op() {
511        let mut term = Terminal::new(Headless::new(3, 1));
512        term.drop_layer(5u8);
513        term.draw(|_| {}).expect("draw failed");
514        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), ' ');
515    }
516
517    #[test]
518    fn test_drop_layer_reallocates_on_the_next_write_after_it_takes_effect() {
519        use crate::surface::Layer;
520
521        // The only way back once a drop has actually gone through is drawing to the layer
522        // again, which reallocates it from scratch.
523        let mut term = Terminal::new(Headless::new(3, 1));
524        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'H', Style::default()))
525            .expect("draw failed");
526        term.drop_layer(Layer::Hud);
527        term.draw(|_| {}).expect("draw failed"); // Lets the drop take effect.
528        assert_eq!(term.current.max_layer(), 0);
529        assert_eq!(term.previous.max_layer(), 0);
530
531        term.draw(|s| s.on_tier(Layer::Hud).put((0, 0), 'J', Style::default()))
532            .expect("draw failed");
533        assert_eq!(term.backend().grid()[Pos::new(0, 0)].glyph(), 'J');
534        assert_ne!(term.previous.max_layer(), 0);
535    }
536}