Skip to main content

retroglyph_core/
terminal.rs

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