Skip to main content

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}