Skip to main content

retroglyph_core/
terminal.rs

1//! Stateful terminal management and double-buffering.
2
3use crate::backend::Backend;
4use crate::color::Color;
5use crate::event::Event;
6use crate::grid::{Grid, Pos, Rect, Size};
7use crate::style::Style;
8use crate::text::Line;
9use crate::tile::Tile;
10use core::time::Duration;
11#[cfg(not(feature = "egc"))]
12use unicode_width::UnicodeWidthChar;
13
14/// A double-buffered terminal generic over a [`Backend`].
15///
16/// Owns the current and previous frame grids and exposes a stateful drawing
17/// API (`put`, `print`, `layer`, ...). Call [`present`](Self::present) once
18/// per frame to diff against the previous frame and send only the changed
19/// cells to the backend.
20pub struct Terminal<B: Backend> {
21    current: Grid,
22    previous: Grid,
23    /// Single-layer scratch buffers used only when the backend does not
24    /// composite layers itself. `present` flattens `current` into
25    /// `flattened_current`, diffs it against `flattened_previous`, and sends the
26    /// result. Unused (but allocated) for compositing backends.
27    flattened_current: Grid,
28    flattened_previous: Grid,
29    backend: B,
30    drawing_style: Style,
31    queued_event: Option<Event>,
32    /// The layer that `put`, `put_styled`, and `put_offset` write to.
33    active_layer: u8,
34    /// `true` when the flatten buffers no longer reflect the last frame sent to
35    /// the backend (because the single-layer fast path bypassed them). The next
36    /// multi-layer present clears `flattened_previous` first so it does a full
37    /// redraw instead of diffing against stale data.
38    flattened_stale: bool,
39}
40
41impl<B: Backend> Terminal<B> {
42    /// Create a terminal with the given backend.
43    /// Grid dimensions are queried from the backend.
44    #[must_use]
45    pub fn new(backend: B) -> Self {
46        let size = backend.size();
47        let current = Grid::new(size.width, size.height);
48        let previous = Grid::new(size.width, size.height);
49        let flattened_current = Grid::new(size.width, size.height);
50        let flattened_previous = Grid::new(size.width, size.height);
51        Self {
52            current,
53            previous,
54            flattened_current,
55            flattened_previous,
56            backend,
57            drawing_style: Style::default(),
58            queued_event: None,
59            active_layer: 0,
60            flattened_stale: false,
61        }
62    }
63
64    /// Sets the active drawing layer (0-255). Returns `&mut Self` for chaining.
65    ///
66    /// All subsequent `put`, `put_styled`, `put_offset`, `print`, and
67    /// `print_styled` calls write to this layer until `layer()` is called again.
68    pub const fn layer(&mut self, layer: u8) -> &mut Self {
69        self.active_layer = layer;
70        self
71    }
72
73    /// Sets the foreground color for the stateful API.
74    pub const fn fg(&mut self, color: Color) -> &mut Self {
75        self.drawing_style.fg = color;
76        self
77    }
78
79    /// Sets the background color for the stateful API.
80    pub const fn bg(&mut self, color: Color) -> &mut Self {
81        self.drawing_style.bg = color;
82        self
83    }
84
85    /// Resets the drawing style to defaults.
86    pub fn reset_style(&mut self) -> &mut Self {
87        self.drawing_style = Style::default();
88        self
89    }
90
91    /// Returns the current drawing style.
92    #[must_use]
93    pub const fn style(&self) -> Style {
94        self.drawing_style
95    }
96
97    /// Returns the current grid dimensions.
98    #[must_use]
99    pub const fn size(&self) -> Size {
100        Size {
101            width: self.current.width(),
102            height: self.current.height(),
103        }
104    }
105
106    /// Returns the full drawing surface as a [`Rect`] at the origin.
107    ///
108    /// Equivalent to `Rect::new(0, 0, width, height)`. Handy for passing the
109    /// whole terminal to layout helpers or region-based drawing.
110    #[must_use]
111    pub const fn area(&self) -> Rect {
112        Rect::new(0, 0, self.current.width(), self.current.height())
113    }
114
115    /// Resize both grids to `width` × `height` cells.
116    ///
117    /// Content within the overlapping region is preserved in the current grid.
118    /// The previous grid is cleared so the next [`present`](Self::present) redraws
119    /// the entire new surface rather than diffing stale data.
120    pub fn resize(&mut self, width: u16, height: u16) {
121        self.current.resize(width, height);
122        self.previous.resize(width, height);
123        self.flattened_current.resize(width, height);
124        self.flattened_previous.resize(width, height);
125        // Clearing previous forces a full redraw next present(), ensuring no
126        // stale cells bleed into the resized layout.
127        self.previous.clear_all();
128        self.flattened_previous.clear_all();
129        self.backend.resize(Size { width, height });
130    }
131
132    /// Place a character at `(x, y)` on the active layer with the current style.
133    ///
134    /// If `ch` is a wide character (e.g. CJK or emoji) that occupies two columns,
135    /// the adjacent cell at `(x + 1, y)` is set to a zero-width continuation
136    /// marker so it is not rendered independently.
137    ///
138    /// Sub-cell offsets are always visual only — use [`put_offset`](Self::put_offset)
139    /// for offset writes.
140    pub fn put(&mut self, x: u16, y: u16, ch: char) {
141        let style = self.drawing_style;
142        #[cfg(feature = "egc")]
143        {
144            let mut buf = [0u8; 4];
145            let s = ch.encode_utf8(&mut buf);
146            self.current
147                .write_grapheme(self.active_layer, x, y, s, style);
148        }
149        #[cfg(not(feature = "egc"))]
150        {
151            let tile = Tile::new(ch, style);
152            self.current.put_tile(self.active_layer, x, y, tile);
153        }
154    }
155
156    /// Place a character at `pos` on the active layer with the current style.
157    ///
158    /// Equivalent to [`put`](Self::put), but takes a [`Pos`] to match the
159    /// `Rect`/`Size`-based drawing APIs elsewhere on `Terminal`.
160    pub fn put_at(&mut self, pos: Pos, ch: char) {
161        self.put(pos.x, pos.y, ch);
162    }
163
164    /// Returns a reference to the current grid.
165    #[must_use]
166    pub const fn grid(&self) -> &Grid {
167        &self.current
168    }
169
170    /// Returns a mutable reference to the current grid.
171    pub const fn grid_mut(&mut self) -> &mut Grid {
172        &mut self.current
173    }
174
175    /// Returns a reference to the backend.
176    #[must_use]
177    pub const fn backend(&self) -> &B {
178        &self.backend
179    }
180
181    /// Returns a mutable reference to the backend.
182    pub const fn backend_mut(&mut self) -> &mut B {
183        &mut self.backend
184    }
185
186    /// Clear the active layer.
187    pub fn clear(&mut self) {
188        self.current.clear(self.active_layer);
189    }
190
191    /// Clear every allocated layer.
192    pub fn clear_all(&mut self) {
193        self.current.clear_all();
194    }
195
196    /// Clear a rectangular region.
197    pub fn clear_region(&mut self, rect: Rect) {
198        for y in rect.top()..rect.bottom() {
199            for x in rect.left()..rect.right() {
200                if let Some(cell) = self.current.checked_get_mut(x, y) {
201                    *cell = Tile::default();
202                }
203            }
204        }
205    }
206
207    /// Place a character on the active layer with an explicit style.
208    pub fn put_styled(&mut self, x: u16, y: u16, ch: char, style: Style) {
209        #[cfg(feature = "egc")]
210        {
211            let mut buf = [0u8; 4];
212            let s = ch.encode_utf8(&mut buf);
213            self.current
214                .write_grapheme(self.active_layer, x, y, s, style);
215        }
216        #[cfg(not(feature = "egc"))]
217        {
218            let tile = Tile::new(ch, style);
219            self.current.put_tile(self.active_layer, x, y, tile);
220        }
221    }
222
223    /// Place a character at `(x, y)` with a sub-cell pixel offset `(dx, dy)`.
224    ///
225    /// Uses the current style and active layer. Sub-cell offsets are visual
226    /// only — they do not affect grid logic or hit-testing. Backends that
227    /// cannot represent pixel offsets (e.g. `CrosstermBackend`) ignore them.
228    pub fn put_offset(&mut self, x: u16, y: u16, dx: i16, dy: i16, ch: char) {
229        let tile = Tile::new(ch, self.drawing_style).with_offset(dx, dy);
230        self.current.put_tile(self.active_layer, x, y, tile);
231    }
232
233    /// Print a string starting at `(x, y)` with the current style.
234    ///
235    /// `\n` advances to the next row at the original `x`. Wide characters
236    /// (CJK, emoji) advance the cursor by 2 columns. Characters that would
237    /// extend beyond the grid width wrap to the next row.
238    pub fn print(&mut self, x: u16, y: u16, text: &str) {
239        let style = self.drawing_style;
240        #[cfg(feature = "egc")]
241        self.print_str_egc(x, y, text, style);
242        #[cfg(not(feature = "egc"))]
243        self.print_str_chars(x, y, text, style);
244    }
245
246    /// Print a [`Line`] of styled spans starting at `(x, y)`.
247    ///
248    /// Each span's style is applied independently. The terminal's current
249    /// drawing style is not modified. Wide characters advance the cursor by
250    /// 2 columns. Rendering stops at the grid boundary.
251    pub fn print_styled(&mut self, x: u16, y: u16, line: &Line) {
252        #[cfg(feature = "egc")]
253        {
254            use unicode_segmentation::UnicodeSegmentation;
255            use unicode_width::UnicodeWidthStr;
256            let mut cur_x = x;
257            for span in &line.spans {
258                for grapheme in span.content.graphemes(true) {
259                    if grapheme == "\n" {
260                        break;
261                    }
262                    #[allow(clippy::cast_possible_truncation)]
263                    let w = grapheme.width() as u16;
264                    if w == 0 {
265                        continue;
266                    }
267                    if cur_x >= self.current.width() {
268                        break;
269                    }
270                    self.current
271                        .write_grapheme(self.active_layer, cur_x, y, grapheme, span.style);
272                    cur_x += w;
273                }
274            }
275        }
276        #[cfg(not(feature = "egc"))]
277        {
278            use unicode_width::UnicodeWidthChar;
279            let mut cur_x = x;
280            for span in &line.spans {
281                for ch in span.content.chars() {
282                    if ch == '\n' {
283                        break;
284                    }
285                    #[allow(clippy::cast_possible_truncation)]
286                    let w = UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
287                    if usize::from(cur_x) >= usize::from(self.current.width()) {
288                        break;
289                    }
290                    let tile = Tile::new(ch, span.style);
291                    self.current.put_tile(self.active_layer, cur_x, y, tile);
292                    cur_x += w;
293                }
294            }
295        }
296    }
297
298    /// Render a [`Line`] of styled text into a bounded rectangle.
299    ///
300    /// Performs greedy word-wrapping at `rect`'s width, then positions the
301    /// resulting lines according to `h_align` and `v_align`. Lines that
302    /// overflow `rect`'s height are silently clipped.
303    ///
304    /// This is a convenience wrapper around [`TextLayout`](crate::layout::TextLayout).
305    ///
306    /// Only available when the `egc` feature is enabled.
307    #[cfg(feature = "egc")]
308    pub fn print_box(
309        &mut self,
310        rect: Rect,
311        line: &Line,
312        h_align: crate::layout::HAlign,
313        v_align: crate::layout::VAlign,
314    ) {
315        crate::layout::TextLayout::new(line)
316            .rect(rect)
317            .h_align(h_align)
318            .v_align(v_align)
319            .render(self);
320    }
321
322    /// Present the current frame.
323    ///
324    /// Computes diff, sends changed cells to the backend, flushes, then swaps buffers.
325    ///
326    /// When the backend requires a full frame (see
327    /// [`crate::Backend::needs_full_frame`]), all cells from every allocated layer are
328    /// sent rather than just the diff, so pixel-based backends can clear and
329    /// redraw to avoid orphaned pixels from sub-cell offsets.
330    ///
331    /// After swap the new current buffer is cleared so the next frame starts
332    /// empty. Callers should not call `clear()` before drawing the next frame.
333    ///
334    /// # Immediate mode
335    ///
336    /// This is an immediate-mode API (the same trade [ratatui] makes): the
337    /// current buffer is wiped after every present, so each frame must redraw
338    /// its entire scene from scratch. Cells are **not** retained between
339    /// frames. The diff only bounds what is sent to the backend (terminal or
340    /// pixel I/O); it does not bound the CPU cost of your redraw.
341    ///
342    /// Turn-based games that render only when state changes should gate their
343    /// calls to `present` on an actual state change rather than presenting on a
344    /// fixed clock and expecting the previous frame's cells to persist.
345    ///
346    /// [ratatui]: https://docs.rs/ratatui
347    ///
348    /// # Errors
349    ///
350    /// Propagates errors from the backend's
351    /// [`draw_layers`](crate::Backend::draw_layers) or
352    /// [`flush`](crate::Backend::flush) operations.
353    pub fn present(&mut self) -> Result<(), <B as Backend>::Error> {
354        if self.backend.composites_layers() {
355            // Pixel/GPU backends composite the raw layered stream themselves.
356            if self.backend.needs_full_frame() {
357                let all = self.current.layers();
358                self.backend.draw_layers(all)?;
359            } else {
360                let diff = self.current.diff(&self.previous);
361                self.backend.draw_layers(diff)?;
362            }
363        } else if self.current.max_layer() == 0 && self.previous.max_layer() == 0 {
364            // Fast path: only layer 0 is in play, so flattening would be an exact
365            // copy of `current`. Diff the real grids directly and skip the
366            // flatten buffers entirely.
367            let diff = self.current.diff(&self.previous);
368            self.backend.draw_layers(diff)?;
369            self.flattened_stale = true;
370        } else {
371            // Cell backends receive a pre-flattened, single-layer diff so layers
372            // 1+ appear everywhere, not just on pixel backends.
373            if self.flattened_stale {
374                // The previous frame used the fast path, so `flattened_previous`
375                // is stale. Clear it to force a full redraw this frame.
376                self.flattened_previous.clear_all();
377                self.flattened_stale = false;
378            }
379            self.current.flatten_into(&mut self.flattened_current);
380            let diff = self.flattened_current.diff(&self.flattened_previous);
381            self.backend.draw_layers(diff)?;
382            core::mem::swap(&mut self.flattened_current, &mut self.flattened_previous);
383        }
384        self.backend.flush()?;
385        core::mem::swap(&mut self.current, &mut self.previous);
386        self.current.clear_all();
387        Ok(())
388    }
389
390    /// Polls for an input event, waiting up to `timeout`.
391    ///
392    /// If an event was previously buffered by [`has_input`](Self::has_input), it is
393    /// returned immediately. Otherwise, the backend is polled for a new event.
394    ///
395    /// [`Event::Resize`] events are automatically applied: both grids are resized
396    /// before the event is returned to the caller, so the game loop can immediately
397    /// redraw at the new size.
398    pub fn poll(&mut self, timeout: Duration) -> Option<Event> {
399        let event = self
400            .queued_event
401            .take()
402            .or_else(|| self.backend.poll_event(timeout))?;
403        if let Event::Resize(w, h) = event {
404            self.resize(w, h);
405        }
406        Some(event)
407    }
408
409    /// Reads an input event, blocking indefinitely until one is available.
410    ///
411    /// Only call this on backends that genuinely block (e.g. crossterm, window). Backends
412    /// that never block (e.g. [`Headless`](crate::backend::Headless), which returns
413    /// immediately regardless of timeout) will panic here once their event queue is
414    /// empty; use [`poll`](Self::poll) or [`drain_events`](Self::drain_events) instead if
415    /// that is a possibility.
416    ///
417    /// # Panics
418    ///
419    /// Panics if the backend's [`poll_event`](crate::Backend::poll_event) returns
420    /// `None` even with an unbounded timeout.
421    pub fn read_blocking(&mut self) -> Event {
422        self.poll(Duration::MAX)
423            .expect("read_blocking() called but no events available")
424    }
425
426    /// Drains all available events without blocking.
427    ///
428    /// Returns an iterator that yields every pending event — the internal queued event
429    /// followed by all events buffered in the backend. The iterator polls the backend
430    /// with zero timeout repeatedly until `None` is returned.
431    ///
432    /// This is needed for frame-based game loops (e.g. software backend + WASM, where
433    /// frames are gated by `requestAnimationFrame`). Multiple keypresses can arrive
434    /// between frames; draining all of them ensures accumulated input doesn't replay in
435    /// slow motion.
436    ///
437    /// Crossterm and headless backends can also use this, but the single-event `poll`
438    /// pattern works for them because their loops aren't frame-capped.
439    pub fn drain_events(&mut self) -> impl Iterator<Item = Event> + use<'_, B> {
440        struct DrainEvents<'a, B: Backend> {
441            terminal: &'a mut Terminal<B>,
442        }
443
444        impl<B: Backend> Iterator for DrainEvents<'_, B> {
445            type Item = Event;
446
447            fn next(&mut self) -> Option<Event> {
448                self.terminal.poll(Duration::ZERO)
449            }
450        }
451
452        impl<B: Backend> core::iter::FusedIterator for DrainEvents<'_, B> {}
453
454        DrainEvents { terminal: self }
455    }
456
457    /// Checks if a pending input event is available without blocking.
458    ///
459    /// If an event is already buffered, returns `true`. Otherwise, polls the backend
460    /// with zero timeout. If the backend returns an event, it is stored in the internal
461    /// buffer and `true` is returned; otherwise, returns `false`.
462    pub fn has_input(&mut self) -> bool {
463        if self.queued_event.is_some() {
464            true
465        } else if let Some(event) = self.backend.poll_event(Duration::ZERO) {
466            self.queued_event = Some(event);
467            true
468        } else {
469            false
470        }
471    }
472
473    /// String printing implementation used when `egc` is enabled.
474    #[cfg(feature = "egc")]
475    fn print_str_egc(&mut self, x: u16, y: u16, text: &str, style: Style) {
476        use unicode_segmentation::UnicodeSegmentation;
477        use unicode_width::UnicodeWidthStr;
478        let layer = self.active_layer;
479        let mut cur_x = x;
480        let mut cur_y = y;
481        for grapheme in text.graphemes(true) {
482            if grapheme == "\n" {
483                cur_x = x;
484                cur_y += 1;
485                continue;
486            }
487            #[allow(clippy::cast_possible_truncation)]
488            let w = grapheme.width() as u16;
489            if w == 0 {
490                continue;
491            }
492            self.current
493                .write_grapheme(layer, cur_x, cur_y, grapheme, style);
494            cur_x += w;
495            if cur_x >= self.current.width() {
496                cur_x = x;
497                cur_y += 1;
498            }
499        }
500    }
501
502    /// String printing implementation used when `egc` is disabled.
503    #[cfg(not(feature = "egc"))]
504    fn print_str_chars(&mut self, x: u16, y: u16, text: &str, style: Style) {
505        let mut cur_x = x;
506        let mut cur_y = y;
507        for c in text.chars() {
508            if c == '\n' {
509                cur_x = x;
510                cur_y += 1;
511            } else {
512                #[allow(clippy::cast_possible_truncation)]
513                let w = UnicodeWidthChar::width(c).unwrap_or(1) as u16;
514                let tile = Tile::new(c, style);
515                self.current.put_tile(self.active_layer, cur_x, cur_y, tile);
516                cur_x += w;
517                if usize::from(cur_x) >= usize::from(self.current.width()) {
518                    cur_x = x;
519                    cur_y += 1;
520                }
521            }
522        }
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use crate::backend::Headless;
530    use crate::tile::Tile;
531
532    #[test]
533    fn test_terminal_grid_mut() {
534        let backend = Headless::new(10, 10);
535        let mut terminal = Terminal::new(backend);
536
537        assert_eq!(terminal.grid().get(0, 0).glyph(), ' ');
538
539        terminal
540            .grid_mut()
541            .put(0, 0, Tile::new('X', Style::default()));
542
543        assert_eq!(terminal.grid().get(0, 0).glyph(), 'X');
544    }
545
546    #[test]
547    fn test_terminal_poll_and_read() {
548        let backend = Headless::new(10, 10);
549        let mut terminal = Terminal::new(backend);
550
551        assert_eq!(terminal.poll(Duration::ZERO), None);
552
553        terminal.backend_mut().push_event(Event::Close);
554        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
555
556        terminal.backend_mut().push_event(Event::Resize(80, 25));
557        assert_eq!(terminal.read_blocking(), Event::Resize(80, 25));
558    }
559
560    #[test]
561    fn test_terminal_has_input() {
562        let backend = Headless::new(10, 10);
563        let mut terminal = Terminal::new(backend);
564
565        assert!(!terminal.has_input());
566
567        terminal.backend_mut().push_event(Event::Close);
568        assert!(terminal.has_input());
569        assert!(terminal.has_input()); // Repeated calls should still be true
570
571        // Read/Poll should retrieve the buffered event
572        assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
573
574        // After taking, it should be false again
575        assert!(!terminal.has_input());
576    }
577
578    #[test]
579    #[should_panic(expected = "read_blocking() called but no events available")]
580    fn test_terminal_read_panic() {
581        let backend = Headless::new(10, 10);
582        let mut terminal = Terminal::new(backend);
583        let _ = terminal.read_blocking();
584    }
585
586    // --- resize ---
587
588    #[test]
589    fn test_present_composites_layers_for_cell_backend() {
590        // A cell backend (Headless) must see layers 1+ composited, not
591        // dropped. Terrain on layer 0, entity on layer 1.
592        let mut term = Terminal::new(Headless::new(3, 1));
593        term.layer(0).put(0, 0, '.');
594        term.layer(0).put(1, 0, '.');
595        term.layer(1).put(1, 0, '@');
596        term.present().expect("present failed");
597        assert_eq!(term.backend().grid().get(0, 0).glyph(), '.');
598        // Layer 1's glyph wins at (1, 0).
599        assert_eq!(term.backend().grid().get(1, 0).glyph(), '@');
600    }
601
602    #[test]
603    fn test_present_explicit_space_on_higher_layer_erases_and_sets_bg() {
604        // An explicit space on a higher layer is opaque: it overwrites the
605        // glyph beneath (erase) and applies its background. This is the
606        // deliberate consequence of the explicit-EMPTY transparency model.
607        let mut term = Terminal::new(Headless::new(2, 1));
608        term.layer(0).put(0, 0, 'x');
609        term.layer(1)
610            .put_styled(0, 0, ' ', Style::new().bg(Color::RED));
611        term.present().expect("present failed");
612        let cell = term.backend().grid().get(0, 0);
613        assert_eq!(cell.glyph(), ' ');
614        assert_eq!(cell.style().background(), Color::RED);
615    }
616
617    #[test]
618    fn test_present_single_layer_fast_path_matches_backend() {
619        // Only layer 0 is ever touched: the fast path must still deliver the
620        // correct cells to a cell backend across multiple frames.
621        let mut term = Terminal::new(Headless::new(3, 1));
622        term.put(0, 0, 'a');
623        term.present().expect("present failed");
624        assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
625
626        // Immediate mode: redraw 'a' and add 'c'. The diff updates the new
627        // cell while 'a' stays put.
628        term.put(0, 0, 'a');
629        term.put(2, 0, 'c');
630        term.present().expect("present failed");
631        assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
632        assert_eq!(term.backend().grid().get(2, 0).glyph(), 'c');
633
634        // A cell that is not redrawn is erased (immediate mode).
635        term.put(0, 0, 'a');
636        term.present().expect("present failed");
637        assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
638        assert_eq!(term.backend().grid().get(2, 0).glyph(), ' ');
639    }
640
641    #[test]
642    fn test_present_transition_single_to_multi_layer() {
643        // Start single-layer (fast path), then introduce layer 1. The frame
644        // that adds the layer must composite correctly despite the fast path
645        // having bypassed the flatten buffers.
646        let mut term = Terminal::new(Headless::new(2, 1));
647        term.layer(0).put(0, 0, '.');
648        term.layer(0).put(1, 0, '.');
649        term.present().expect("present failed");
650
651        term.layer(0).put(0, 0, '.');
652        term.layer(0).put(1, 0, '.');
653        term.layer(1).put(1, 0, '@');
654        term.present().expect("present failed");
655        assert_eq!(term.backend().grid().get(0, 0).glyph(), '.');
656        assert_eq!(term.backend().grid().get(1, 0).glyph(), '@');
657    }
658
659    #[test]
660    fn test_present_untouched_higher_layer_is_transparent() {
661        // A higher layer that was allocated but not written at this cell must
662        // not disturb the lower layer's glyph or background.
663        let mut term = Terminal::new(Headless::new(2, 1));
664        term.layer(0).put(0, 0, 'x');
665        // Allocate layer 1 by writing elsewhere, leaving (0, 0) empty.
666        term.layer(1).put(1, 0, 'y');
667        term.present().expect("present failed");
668        assert_eq!(term.backend().grid().get(0, 0).glyph(), 'x');
669    }
670
671    #[test]
672    fn test_terminal_size() {
673        let term = Terminal::new(Headless::new(40, 20));
674        assert_eq!(
675            term.size(),
676            Size {
677                width: 40,
678                height: 20
679            }
680        );
681    }
682
683    #[test]
684    fn test_terminal_area() {
685        let term = Terminal::new(Headless::new(40, 20));
686        assert_eq!(term.area(), Rect::new(0, 0, 40, 20));
687    }
688
689    #[test]
690    fn test_terminal_resize_changes_dimensions() {
691        let mut term = Terminal::new(Headless::new(10, 10));
692        term.resize(30, 15);
693        assert_eq!(
694            term.size(),
695            Size {
696                width: 30,
697                height: 15
698            }
699        );
700        assert_eq!(term.grid().width(), 30);
701        assert_eq!(term.grid().height(), 15);
702    }
703
704    #[test]
705    fn test_terminal_resize_preserves_current_content() {
706        let mut term = Terminal::new(Headless::new(10, 10));
707        term.put(2, 2, 'X');
708        term.resize(20, 20);
709        assert_eq!(term.grid().get(2, 2).glyph(), 'X');
710        assert_eq!(term.grid().get(15, 15).glyph(), ' ');
711    }
712
713    #[test]
714    fn test_terminal_resize_event_auto_applies() {
715        let mut term = Terminal::new(Headless::new(10, 10));
716        term.backend_mut().push_event(Event::Resize(80, 25));
717        let event = term.poll(Duration::ZERO);
718        assert_eq!(event, Some(Event::Resize(80, 25)));
719        assert_eq!(
720            term.size(),
721            Size {
722                width: 80,
723                height: 25
724            }
725        );
726    }
727
728    #[test]
729    fn test_terminal_resize_new_cells_accessible() {
730        // Resize to a larger area, then draw in the newly created region.
731        let mut term = Terminal::new(Headless::new(3, 3));
732        term.put(0, 0, 'A');
733        term.present();
734
735        term.resize(5, 5);
736
737        // Draw into the expanded region and verify it reaches the backend.
738        term.put(4, 4, 'B');
739        term.present();
740
741        assert_eq!(term.backend().grid().get(4, 4).glyph(), 'B');
742        // (0,0) was not redrawn this frame; backend retains 'A' from before resize.
743        assert_eq!(term.backend().grid().get(0, 0).glyph(), 'A');
744    }
745
746    // --- unicode width ---
747
748    #[test]
749    fn test_put_wide_char_sets_continuation() {
750        let mut term = Terminal::new(Headless::new(10, 3));
751        term.put(0, 0, '\u{4e2d}'); // '中', width 2
752        assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
753        // With egc: spacer uses WIDE_CHAR_SPACER flag, glyph is space.
754        // Without egc: spacer is '\0'.
755        #[cfg(feature = "egc")]
756        {
757            use crate::tile::TileFlags;
758            assert!(
759                term.grid()
760                    .get(1, 0)
761                    .flags()
762                    .contains(TileFlags::WIDE_CHAR_SPACER)
763            );
764            assert_eq!(term.grid().get(1, 0).glyph(), ' ');
765        }
766        #[cfg(not(feature = "egc"))]
767        assert_eq!(term.grid().get(1, 0).glyph(), '\0');
768        assert_eq!(term.grid().get(2, 0).glyph(), ' '); // untouched
769    }
770
771    #[test]
772    fn test_print_advances_by_char_width() {
773        let mut term = Terminal::new(Headless::new(10, 3));
774        term.print(0, 0, "\u{4e2d}x"); // '中' (2) then 'x' at col 2
775        assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
776        #[cfg(feature = "egc")]
777        {
778            use crate::tile::TileFlags;
779            assert!(
780                term.grid()
781                    .get(1, 0)
782                    .flags()
783                    .contains(TileFlags::WIDE_CHAR_SPACER)
784            );
785        }
786        #[cfg(not(feature = "egc"))]
787        assert_eq!(term.grid().get(1, 0).glyph(), '\0');
788        assert_eq!(term.grid().get(2, 0).glyph(), 'x');
789    }
790
791    #[test]
792    fn test_put_at_matches_put() {
793        let mut term = Terminal::new(Headless::new(10, 3));
794        term.put_at(Pos::new(2, 1), 'X');
795        assert_eq!(term.grid().get(2, 1).glyph(), 'X');
796    }
797
798    #[test]
799    fn test_put_wide_char_at_last_column_does_not_overflow() {
800        // Wide char placed at the last column: can't place a spacer.
801        // write_grapheme silently refuses rather than leaving an orphan.
802        let mut term = Terminal::new(Headless::new(4, 1));
803        term.put(3, 0, '\u{4e2d}'); // col 3 is last; need col 4 for spacer
804        assert_eq!(term.grid().get(3, 0).glyph(), ' '); // nothing written
805    }
806
807    // --- styled spans ---
808
809    #[test]
810    fn test_print_styled_basic() {
811        use crate::text::{Line, Span};
812        let mut term = Terminal::new(Headless::new(20, 3));
813        let line = Line::from(vec![
814            Span::raw("HP: "),
815            Span::styled("100", Style::new().fg(Color::GREEN)),
816        ]);
817        term.print_styled(0, 0, &line);
818        assert_eq!(term.grid().get(0, 0).glyph(), 'H');
819        assert_eq!(term.grid().get(3, 0).glyph(), ' ');
820        assert_eq!(term.grid().get(4, 0).glyph(), '1');
821        assert_eq!(term.grid().get(4, 0).style.fg, Color::GREEN);
822        assert_eq!(term.grid().get(6, 0).glyph(), '0');
823    }
824
825    #[test]
826    fn test_print_styled_does_not_modify_drawing_style() {
827        use crate::text::{Line, Span};
828        let mut term = Terminal::new(Headless::new(20, 3));
829        term.fg(Color::RED);
830        let line = Line::from(vec![Span::styled("hi", Style::new().fg(Color::BLUE))]);
831        term.print_styled(0, 0, &line);
832        // Drawing style must be unchanged.
833        assert_eq!(term.style().fg, Color::RED);
834    }
835
836    #[test]
837    fn test_print_styled_wide_chars() {
838        use crate::text::{Line, Span};
839        let mut term = Terminal::new(Headless::new(10, 3));
840        let line = Line::from(vec![Span::raw("\u{4e2d}x")]);
841        term.print_styled(0, 0, &line);
842        assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
843        #[cfg(feature = "egc")]
844        {
845            use crate::tile::TileFlags;
846            assert!(
847                term.grid()
848                    .get(1, 0)
849                    .flags()
850                    .contains(TileFlags::WIDE_CHAR_SPACER)
851            );
852        }
853        #[cfg(not(feature = "egc"))]
854        assert_eq!(term.grid().get(1, 0).glyph(), '\0');
855        assert_eq!(term.grid().get(2, 0).glyph(), 'x');
856    }
857}