Skip to main content

retroglyph/backend/
headless.rs

1//! In-memory backend for testing. Stores presented content
2//! and allows injecting synthetic events.
3
4use crate::backend::Backend;
5use crate::event::Event;
6use crate::grid::{Grid, Pos, Size};
7use crate::tile::Tile;
8use alloc::collections::VecDeque;
9use alloc::string::String;
10use core::time::Duration;
11
12/// In-memory backend for testing. Stores presented content
13/// and allows injecting synthetic events.
14pub struct Headless {
15    grid: Grid,
16    cursor_visible: bool,
17    cursor_pos: Pos,
18    event_queue: VecDeque<Event>,
19}
20
21impl Headless {
22    /// Creates a new headless backend of the given dimensions.
23    #[must_use]
24    pub fn new(width: u16, height: u16) -> Self {
25        Self {
26            grid: Grid::new(width, height),
27            cursor_visible: false,
28            cursor_pos: Pos::default(),
29            event_queue: VecDeque::new(),
30        }
31    }
32
33    /// Returns a reference to the grid.
34    #[must_use]
35    pub const fn grid(&self) -> &Grid {
36        &self.grid
37    }
38
39    /// Returns the cursor visibility.
40    #[must_use]
41    pub const fn cursor_visible(&self) -> bool {
42        self.cursor_visible
43    }
44
45    /// Returns the cursor position.
46    #[must_use]
47    pub const fn cursor_position(&self) -> Pos {
48        self.cursor_pos
49    }
50
51    /// Injects a synthetic event into the queue.
52    pub fn push_event(&mut self, event: Event) {
53        self.event_queue.push_back(event);
54    }
55
56    /// Converts the current grid state into a readable string for snapshot testing.
57    ///
58    /// Space cells are rendered as `·` so layout is visible in text diffs.
59    #[must_use]
60    pub fn format_view(&self) -> String {
61        let mut out = String::new();
62        for y in 0..self.grid.height() {
63            for x in 0..self.grid.width() {
64                let cell = self.grid.get(x, y);
65                #[cfg(feature = "egc")]
66                let is_spacer = cell
67                    .flags()
68                    .contains(crate::tile::TileFlags::WIDE_CHAR_SPACER);
69                #[cfg(not(feature = "egc"))]
70                let is_spacer = cell.glyph() == '\0';
71                let c = if is_spacer {
72                    ' '
73                } else if cell.glyph() == ' ' {
74                    '·'
75                } else {
76                    cell.glyph()
77                };
78                out.push(c);
79            }
80            out.push('\n');
81        }
82        out
83    }
84}
85
86impl Backend for Headless {
87    fn draw<'a, I>(&mut self, content: I)
88    where
89        I: Iterator<Item = (Pos, &'a Tile)>,
90    {
91        for (pos, cell) in content {
92            self.grid.checked_put(pos.x, pos.y, cell.clone());
93        }
94    }
95
96    fn resize(&mut self, size: Size) {
97        self.grid.resize(size.width, size.height);
98    }
99
100    fn flush(&mut self) {
101        // Headless backend is already in memory.
102    }
103
104    fn size(&self) -> Size {
105        Size {
106            width: self.grid.width(),
107            height: self.grid.height(),
108        }
109    }
110
111    fn clear(&mut self) {
112        self.grid.clear_all();
113    }
114
115    fn poll_event(&mut self, _timeout: Duration) -> Option<Event> {
116        self.event_queue.pop_front()
117    }
118
119    fn push_event(&mut self, event: Event) {
120        Self::push_event(self, event);
121    }
122
123    fn set_cursor_visible(&mut self, visible: bool) {
124        self.cursor_visible = visible;
125    }
126
127    fn set_cursor_position(&mut self, position: Pos) {
128        self.cursor_pos = position;
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_headless_new() {
138        let backend = Headless::new(80, 25);
139        assert_eq!(backend.grid().width(), 80);
140        assert_eq!(backend.grid().height(), 25);
141    }
142
143    #[test]
144    fn test_headless_events() {
145        let mut backend = Headless::new(10, 10);
146        let event = Event::Close;
147        backend.push_event(event);
148        assert_eq!(backend.poll_event(Duration::ZERO), Some(Event::Close));
149        assert_eq!(backend.poll_event(Duration::ZERO), None);
150    }
151
152    #[test]
153    fn test_format_view_snapshot() {
154        use crate::Terminal;
155        let backend = Headless::new(10, 3);
156        let mut term = Terminal::new(backend);
157        term.put(1, 1, 'H');
158        term.put(2, 1, 'i');
159        term.present();
160        let view = term.backend().format_view();
161        insta::assert_snapshot!(view, @r#"
162        ··········
163        ·Hi·······
164        ··········
165        "#);
166    }
167}