retroglyph_window/winit/windowed.rs
1//! [`Windowed`]: pairs a [`PresenterBuilder`] with the window title `Launch` needs.
2
3use core::fmt;
4
5use retroglyph_core::app::{App, Launch, RunOptions};
6use retroglyph_core::terminal::Terminal;
7
8use crate::backend::WindowBackend;
9use crate::presenter_builder::PresenterBuilder;
10use crate::winit::EventLoopError;
11use crate::winit::run::{WindowConfig, run_app_on};
12
13/// Pairs a windowed backend's [`PresenterBuilder`] with the window title [`Launch`] needs to
14/// open one.
15///
16/// `PresenterBuilder` (retroglyph#1192) describes renderer configuration only (grid, scale,
17/// font, tileset): a window title isn't part of that surface, and adding one would mean
18/// `retroglyph-software`'s headless pixel-test path (which never opens a window) carries a field
19/// it has no use for. This wrapper is the seam that adds it back for the one call site that
20/// does need it, [`Launch::launch`], without touching `PresenterBuilder` itself.
21///
22/// Generic over `B: PresenterBuilder` rather than duplicated per backend crate: `Launch` is
23/// implemented here, once, for any `Windowed<B>`, so `retroglyph-software`, `retroglyph-gl`, and
24/// `retroglyph-wgpu` need no impl (and no dependency on this crate's `winit` feature beyond what
25/// they already carry) of their own to be launchable this way.
26///
27/// # Examples
28///
29/// ```no_run
30/// use retroglyph_core::app::{App, Flow, Frame, Launch, RunOptions};
31/// use retroglyph_core::backend::Backend;
32/// use retroglyph_core::terminal::Terminal;
33/// use retroglyph_window::PresenterBuilder;
34/// use retroglyph_window::winit::Windowed;
35///
36/// struct MyGame;
37/// impl<B: Backend> App<B> for MyGame {
38/// fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
39/// Flow::Exit
40/// }
41/// }
42///
43/// # fn launch<B: PresenterBuilder + 'static>(builder: B) -> Result<(), Box<dyn std::error::Error>> {
44/// // Opens a real window, so this example is `no_run`.
45/// Windowed::new(builder, "demo").launch(MyGame, RunOptions::animated(60))?;
46/// # Ok(())
47/// # }
48/// ```
49#[derive(Debug, Clone)]
50pub struct Windowed<B> {
51 builder: B,
52 title: String,
53}
54
55impl<B: PresenterBuilder> Windowed<B> {
56 /// Pairs `builder` with `title`, the window's title bar text.
57 pub fn new(builder: B, title: impl Into<String>) -> Self {
58 Self {
59 builder,
60 title: title.into(),
61 }
62 }
63}
64
65/// The error [`Windowed`]'s [`Launch`] impl can fail with.
66///
67/// Spans the two independent ways launching a windowed backend can fail: the presenter builder's
68/// own [`PresenterBuilder::Error`] (a bad grid/font/tileset configuration), and
69/// [`EventLoopError`] (winit's event loop failing to start or to run). Kept as a small enum
70/// naming both rather than degraded to a `String`: a caller matching on it can still tell which
71/// half failed, and `?` off of either [`PresenterBuilder::build_presenter`] or [`run_app_on`]
72/// converts automatically.
73#[derive(Debug)]
74#[non_exhaustive]
75pub enum WindowedLaunchError<E> {
76 /// [`PresenterBuilder::build_presenter`] failed to build the presenter.
77 Build(E),
78 /// The winit event loop failed to start or failed while running.
79 EventLoop(EventLoopError),
80}
81
82impl<E: fmt::Display> fmt::Display for WindowedLaunchError<E> {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Build(err) => write!(f, "failed to build the presenter: {err}"),
86 Self::EventLoop(err) => write!(f, "windowed event loop failed: {err}"),
87 }
88 }
89}
90
91impl<E: std::error::Error + 'static> std::error::Error for WindowedLaunchError<E> {
92 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
93 match self {
94 Self::Build(err) => Some(err),
95 Self::EventLoop(err) => Some(err),
96 }
97 }
98}
99
100impl<B> Launch for Windowed<B>
101where
102 B: PresenterBuilder,
103{
104 type Backend = WindowBackend<B::Presenter>;
105 type Error = WindowedLaunchError<B::Error>;
106
107 /// Builds `self`'s presenter, opens a window sized to fit it (paced by `options`), and
108 /// drives `app` on it until it returns [`Flow::Exit`](retroglyph_core::app::Flow::Exit).
109 ///
110 /// [`RunOptions::idle_wake`](retroglyph_core::app::RunOptions::idle_wake) has no windowed
111 /// meaning and is ignored; see [`WindowConfig::with_run_options`] for why.
112 ///
113 /// # Errors
114 ///
115 /// Returns [`WindowedLaunchError::Build`] if the presenter builder's configuration is
116 /// invalid, or [`WindowedLaunchError::EventLoop`] if the event loop cannot be created or
117 /// fails while running.
118 fn launch<A>(self, app: A, options: RunOptions) -> Result<(), Self::Error>
119 where
120 A: App<Self::Backend> + 'static,
121 {
122 let presenter = self
123 .builder
124 .build_presenter()
125 .map_err(WindowedLaunchError::Build)?;
126 let config =
127 WindowConfig::fit(&presenter, self.title, None, true).with_run_options(options);
128 let terminal = Terminal::new(WindowBackend::new(presenter));
129 run_app_on(config, terminal, app).map_err(WindowedLaunchError::EventLoop)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn build_error_displays_and_sources_the_inner_error() {
139 let err: WindowedLaunchError<std::io::Error> =
140 WindowedLaunchError::Build(std::io::Error::other("bad grid"));
141 assert!(err.to_string().contains("bad grid"));
142 assert!(std::error::Error::source(&err).is_some());
143 }
144
145 #[test]
146 fn event_loop_error_displays_and_sources_the_inner_error() {
147 // `RecreationAttempt` is the one `EventLoopError` variant with no private fields, so it's
148 // the only one constructible outside `winit` itself.
149 let err: WindowedLaunchError<std::io::Error> =
150 WindowedLaunchError::EventLoop(EventLoopError::RecreationAttempt);
151 assert!(err.to_string().contains("windowed event loop failed"));
152 assert!(std::error::Error::source(&err).is_some());
153 }
154}