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}