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}