Skip to main content

retroglyph_window/
backend.rs

1//! [`WindowBackend`]: the generic [`Backend`](retroglyph_core::backend::Backend) implementation for
2//! windowed presenters.
3
4use crate::presenter::Presenter;
5use retroglyph_core::backend::DrawCell;
6use retroglyph_core::backend::{Cursor, Input, Output};
7use retroglyph_core::event::{Event, coalesces_with};
8use retroglyph_core::grid::Size;
9use std::collections::VecDeque;
10use std::time::Duration;
11
12/// A [`Backend`](retroglyph_core::backend::Backend) built from a [`Presenter`] plus an input event queue.
13///
14/// [`Input`] and [`Output`] are independent facets of `Backend`, which does not fit a window as
15/// one type: some event loop owns input, while a per-renderer surface owns output.
16/// `WindowBackend` reunites the two (implementing `Output` by delegating to `P`, `Input` via
17/// its own event queue, and the no-op default `Cursor`), so [`Terminal`](retroglyph_core::terminal::Terminal)
18/// gets the full `Backend` it needs, while renderer crates implement only [`Presenter`]. See the
19/// crate-level [Architecture](crate#architecture) section for the data-flow diagram.
20///
21/// Because `WindowBackend` owns input, a [`Presenter`] should **not** implement [`Input`] or
22/// [`Cursor`] itself for windowed use: those impls would be dead (the event loop pushes to
23/// *this* queue, not the presenter's) and would silently miss the `Mouse(Moved)` coalescing that
24/// [`push_event`](WindowBackend::push_event) applies. A presenter that also wants a direct
25/// headless `Terminal<Self>` input path (as `retroglyph-software` does for pixel tests) may still
26/// implement `Input` for that path, accepting that a bare queue does not coalesce; a presenter
27/// with no such path (as `retroglyph-gl`) implements only `Presenter`.
28///
29/// With the `winit` feature enabled, `winit::run_windowed` and `winit::run_app` own the event
30/// loop, call `push_event` as winit events are translated, and call [`Presenter::present`] once
31/// per frame; callers never touch `WindowBackend` directly. With `winit` disabled,
32/// `retroglyph-window` exports no event loop at all: a caller driving its own loop (SDL2, tao, a
33/// custom driver) constructs `WindowBackend::new(presenter)` itself, calls `push_event` for each
34/// translated input event, and calls `Terminal::present` (which drives `Presenter::flush`) plus
35/// `presenter_mut().present()` once per frame.
36///
37/// # Examples
38///
39/// ```
40/// use retroglyph_core::backend::{Backend, DrawCell, Input, Output};
41/// use retroglyph_core::event::Event;
42/// use retroglyph_core::grid::{Pos, Size};
43/// use retroglyph_core::terminal::Terminal;
44/// use retroglyph_core::tile::Tile;
45/// use retroglyph_window::{Presenter, WindowBackend, WindowHandle};
46/// use std::sync::Arc;
47/// use std::time::Duration;
48///
49/// struct NullPresenter;
50///
51/// impl Output for NullPresenter {
52///     type Error = core::convert::Infallible;
53///
54///     fn draw<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
55///     where
56///         I: Iterator<Item = DrawCell<'a>>,
57///     {
58///         Ok(())
59///     }
60///
61///     fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
62///     where
63///         I: Iterator<Item = DrawCell<'a>>,
64///     {
65///         Ok(())
66///     }
67///
68///     fn flush(&mut self) -> Result<(), Self::Error> {
69///         Ok(())
70///     }
71///
72///     fn size(&self) -> Size {
73///         Size::new(4, 2)
74///     }
75///
76///     fn clear(&mut self) -> Result<(), Self::Error> {
77///         Ok(())
78///     }
79///
80///     fn resize(&mut self, _size: Size) {}
81/// }
82///
83/// impl Presenter for NullPresenter {
84///     type SurfaceError = core::convert::Infallible;
85///
86///     fn init_surface(&mut self, _window: Arc<dyn WindowHandle>) -> Result<(), Self::SurfaceError> {
87///         Ok(())
88///     }
89///
90///     fn resize_surface(&mut self, _width: u32, _height: u32) {}
91///
92///     fn present(&mut self) -> Result<(), Self::SurfaceError> {
93///         Ok(())
94///     }
95///
96///     fn cell_size(&self) -> (u32, u32) {
97///         (8, 16)
98///     }
99/// }
100///
101/// // A caller driving its own loop (SDL2, tao, a hand-rolled driver) builds
102/// // `WindowBackend` directly, no `winit` feature required.
103/// let backend = WindowBackend::new(NullPresenter);
104/// let mut term = Terminal::new(backend);
105///
106/// // The loop pushes each translated input event onto the queue...
107/// term.backend_mut().push_event(Event::FocusGained);
108///
109/// // ...and the app drains it through the normal `Terminal` polling API,
110/// // which never blocks for `WindowBackend`.
111/// while term.poll(Duration::ZERO).is_some() {}
112///
113/// // Once per frame: `Terminal::present` diffs the grid and drives
114/// // `Presenter::flush`, then the caller drives `Presenter::present` itself
115/// // to push pixels to the window.
116/// term.present().unwrap();
117/// term.backend_mut().presenter_mut().present().unwrap();
118/// ```
119///
120/// [`poll_event`](Input::poll_event) never blocks: frame timing is owned by the event loop, not
121/// by input waits.
122#[derive(Debug)]
123pub struct WindowBackend<P: Presenter> {
124    presenter: P,
125    events: VecDeque<Event>,
126}
127
128impl<P: Presenter> WindowBackend<P> {
129    /// Wrap a presenter, creating an empty event queue.
130    #[must_use]
131    pub const fn new(presenter: P) -> Self {
132        Self {
133            presenter,
134            events: VecDeque::new(),
135        }
136    }
137
138    /// The wrapped presenter.
139    #[must_use]
140    pub const fn presenter(&self) -> &P {
141        &self.presenter
142    }
143
144    /// The wrapped presenter, mutably.
145    pub const fn presenter_mut(&mut self) -> &mut P {
146        &mut self.presenter
147    }
148
149    /// Unwrap into the presenter, discarding queued events.
150    #[must_use]
151    pub fn into_presenter(self) -> P {
152        self.presenter
153    }
154}
155
156impl<P: Presenter> Output for WindowBackend<P> {
157    type Error = P::Error;
158
159    fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
160    where
161        I: Iterator<Item = DrawCell<'a>>,
162    {
163        self.presenter.draw(content)
164    }
165
166    fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
167    where
168        I: Iterator<Item = DrawCell<'a>>,
169    {
170        self.presenter.draw_layers(content)
171    }
172
173    fn flush(&mut self) -> Result<(), Self::Error> {
174        self.presenter.flush()
175    }
176
177    fn size(&self) -> Size {
178        self.presenter.size()
179    }
180
181    fn clear(&mut self) -> Result<(), Self::Error> {
182        self.presenter.clear()
183    }
184
185    fn resize(&mut self, size: Size) {
186        self.presenter.resize(size);
187    }
188
189    fn needs_full_frame(&self) -> bool {
190        self.presenter.needs_full_frame()
191    }
192
193    fn composites_layers(&self) -> bool {
194        self.presenter.composites_layers()
195    }
196}
197
198impl<P: Presenter> Input for WindowBackend<P> {
199    fn poll_event(&mut self, _timeout: Duration) -> Option<Event> {
200        // Non-blocking by design: the caller's event loop drives frame
201        // timing, so there is nothing to sleep on here.
202        self.events.pop_front()
203    }
204
205    fn push_event(&mut self, event: Event) {
206        // Coalesce consecutive `Mouse(Moved)` or same-button `Mouse(Drag)` events: winit can
207        // deliver `CursorMoved`/drag motion at device polling rate (hundreds/sec) though only the
208        // latest position matters once the next frame polls the queue, so replace the queue's
209        // tail in place instead of growing it unbounded (retroglyph#294, retroglyph#768). Every
210        // other event kind (clicks, scrolls, keys, resize, ...) still pushes in O(1) as before;
211        // only a back-to-back `Moved` or same-button `Drag` run collapses. See [`coalesces_with`]
212        // for the shared rule (also used by `retroglyph-terminal-wasm` and `Headless`).
213        if let Some(back) = self.events.back_mut()
214            && coalesces_with(&event, back)
215        {
216            *back = event;
217            return;
218        }
219        self.events.push_back(event);
220    }
221}
222
223// No hardware text cursor in windowed mode (games draw their own): the trait's no-op default
224// bodies are exactly right here.
225impl<P: Presenter> Cursor for WindowBackend<P> {}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::presenter::WindowHandle;
231    use retroglyph_core::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
232    use retroglyph_core::grid::Pos;
233    use std::sync::Arc;
234
235    struct NullPresenter;
236
237    impl Output for NullPresenter {
238        type Error = core::convert::Infallible;
239
240        fn draw<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
241        where
242            I: Iterator<Item = DrawCell<'a>>,
243        {
244            Ok(())
245        }
246
247        fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
248        where
249            I: Iterator<Item = DrawCell<'a>>,
250        {
251            Ok(())
252        }
253
254        fn flush(&mut self) -> Result<(), Self::Error> {
255            Ok(())
256        }
257
258        fn size(&self) -> Size {
259            Size::new(4, 2)
260        }
261
262        fn clear(&mut self) -> Result<(), Self::Error> {
263            Ok(())
264        }
265
266        fn resize(&mut self, _size: Size) {}
267    }
268
269    impl Presenter for NullPresenter {
270        type SurfaceError = core::convert::Infallible;
271
272        fn init_surface(
273            &mut self,
274            _window: Arc<dyn WindowHandle>,
275        ) -> Result<(), Self::SurfaceError> {
276            Ok(())
277        }
278
279        fn resize_surface(&mut self, _width: u32, _height: u32) {}
280
281        fn present(&mut self) -> Result<(), Self::SurfaceError> {
282            Ok(())
283        }
284
285        fn cell_size(&self) -> (u32, u32) {
286            (8, 16)
287        }
288    }
289
290    fn moved(x: u16) -> Event {
291        Event::Mouse(MouseEvent::new(
292            MouseEventKind::Moved,
293            Pos { x, y: 0 },
294            KeyModifiers::NONE,
295        ))
296    }
297
298    /// Regression test for retroglyph#294: a burst of consecutive `Moved` events must coalesce
299    /// down to the single most recent one instead of growing the queue by one entry per event.
300    #[test]
301    fn consecutive_moved_events_coalesce_to_one() {
302        let mut backend = WindowBackend::new(NullPresenter);
303        for x in 0..1_000u16 {
304            backend.push_event(moved(x));
305        }
306        assert_eq!(backend.events.len(), 1);
307        assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(999)));
308        assert_eq!(backend.poll_event(Duration::ZERO), None);
309    }
310
311    /// A non-`Moved` event between two `Moved` bursts must not be swallowed: only *consecutive*
312    /// `Moved` events collapse, so interleaving a click still yields three distinct events.
313    #[test]
314    fn non_moved_event_breaks_coalescing() {
315        let mut backend = WindowBackend::new(NullPresenter);
316        backend.push_event(moved(1));
317        backend.push_event(moved(2));
318        backend.push_event(Event::Mouse(MouseEvent::new(
319            MouseEventKind::Down(MouseButton::Left),
320            Pos { x: 2, y: 0 },
321            KeyModifiers::NONE,
322        )));
323        backend.push_event(moved(3));
324        assert_eq!(backend.events.len(), 3);
325        assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(2)));
326        assert!(matches!(
327            backend.poll_event(Duration::ZERO),
328            Some(Event::Mouse(MouseEvent {
329                kind: MouseEventKind::Down(MouseButton::Left),
330                ..
331            }))
332        ));
333        assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(3)));
334    }
335}