Skip to main content

astrelis_render/
window_manager.rs

1//! WindowManager - Manages multiple windows and eliminates boilerplate
2//!
3//! This module provides a high-level abstraction for managing multiple windows,
4//! automatically handling common events like resizing and providing a clean API
5//! for rendering.
6
7use ahash::{HashMap, HashMapExt};
8use astrelis_core::profiling::profile_function;
9use std::sync::Arc;
10
11use astrelis_winit::{
12    WindowId,
13    app::AppCtx,
14    event::{Event, EventBatch, HandleStatus},
15    window::WindowDescriptor,
16};
17
18use crate::{
19    context::GraphicsContext,
20    window::{RenderWindow, WindowContextDescriptor},
21};
22
23/// Manages multiple renderable windows with automatic event handling.
24///
25/// The WindowManager eliminates the boilerplate of manually managing a
26/// `HashMap<WindowId, RenderWindow>` and handling common events like resizing.
27///
28/// # Example
29///
30/// ```no_run
31/// use astrelis_render::{WindowManager, GraphicsContext, Color};
32/// use astrelis_winit::app::{App, AppCtx};
33/// use astrelis_winit::{WindowId, FrameTime};
34/// use astrelis_winit::event::EventBatch;
35///
36/// struct MyApp {
37///     window_manager: WindowManager,
38/// }
39///
40/// impl App for MyApp {
41///     fn update(&mut self, _ctx: &mut AppCtx, _time: &FrameTime) {}
42///     fn render(&mut self, _ctx: &mut AppCtx, window_id: WindowId, events: &mut EventBatch) {
43///         self.window_manager.render_window(window_id, events, |window, _events| {
44///             // Resize already handled automatically!
45///             let Some(frame) = window.begin_frame() else { return };
46///             {
47///                 let mut pass = frame.render_pass()
48///                     .clear_color(Color::BLACK)
49///                     .build();
50///                 // Your rendering here
51///             }
52///         });
53///     }
54/// }
55/// ```
56pub struct WindowManager {
57    graphics: Arc<GraphicsContext>,
58    windows: HashMap<WindowId, RenderWindow>,
59}
60
61impl WindowManager {
62    /// Creates a new WindowManager with the given graphics context.
63    ///
64    /// # Example
65    ///
66    /// ```no_run
67    /// use astrelis_render::{WindowManager, GraphicsContext};
68    /// use std::sync::Arc;
69    ///
70    /// let graphics = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
71    /// let window_manager = WindowManager::new(graphics);
72    /// ```
73    pub fn new(graphics: Arc<GraphicsContext>) -> Self {
74        Self {
75            graphics,
76            windows: HashMap::new(),
77        }
78    }
79
80    /// Creates a new window and adds it to the manager.
81    ///
82    /// Returns the WindowId of the created window.
83    ///
84    /// # Example
85    ///
86    /// ```no_run
87    /// use astrelis_render::WindowManager;
88    /// use astrelis_winit::window::{WindowDescriptor, WindowBackend};
89    ///
90    /// # fn example(window_manager: &mut WindowManager, ctx: &mut astrelis_winit::app::AppCtx) {
91    /// let window_id = window_manager.create_window(
92    ///     ctx,
93    ///     WindowDescriptor {
94    ///         title: "My Window".to_string(),
95    ///         ..Default::default()
96    ///     },
97    /// );
98    /// # }
99    /// ```
100    pub fn create_window(
101        &mut self,
102        ctx: &mut AppCtx,
103        descriptor: WindowDescriptor,
104    ) -> Result<WindowId, crate::context::GraphicsError> {
105        self.create_window_with_descriptor(ctx, descriptor, WindowContextDescriptor::default())
106    }
107
108    /// Creates a new window with a custom rendering context descriptor.
109    ///
110    /// # Example
111    ///
112    /// ```no_run
113    /// use astrelis_render::{WindowManager, WindowContextDescriptor, wgpu};
114    /// use astrelis_winit::window::{WindowDescriptor, WindowBackend};
115    ///
116    /// # fn example(window_manager: &mut WindowManager, ctx: &mut astrelis_winit::app::AppCtx) {
117    /// let window_id = window_manager.create_window_with_descriptor(
118    ///     ctx,
119    ///     WindowDescriptor::default(),
120    ///     WindowContextDescriptor {
121    ///         present_mode: Some(wgpu::PresentMode::Mailbox),
122    ///         ..Default::default()
123    ///     },
124    /// );
125    /// # }
126    /// ```
127    pub fn create_window_with_descriptor(
128        &mut self,
129        ctx: &mut AppCtx,
130        descriptor: WindowDescriptor,
131        window_descriptor: WindowContextDescriptor,
132    ) -> Result<WindowId, crate::context::GraphicsError> {
133        profile_function!();
134        let window = ctx
135            .create_window(descriptor)
136            .expect("Failed to create window");
137        let id = window.id();
138        let renderable =
139            RenderWindow::new_with_descriptor(window, self.graphics.clone(), window_descriptor)?;
140        self.windows.insert(id, renderable);
141        Ok(id)
142    }
143
144    /// Gets a reference to a window by its ID.
145    ///
146    /// Returns `None` if the window doesn't exist.
147    pub fn get_window(&self, id: WindowId) -> Option<&RenderWindow> {
148        self.windows.get(&id)
149    }
150
151    /// Gets a mutable reference to a window by its ID.
152    ///
153    /// Returns `None` if the window doesn't exist.
154    pub fn get_window_mut(&mut self, id: WindowId) -> Option<&mut RenderWindow> {
155        self.windows.get_mut(&id)
156    }
157
158    /// Removes a window from the manager.
159    ///
160    /// Returns the removed window if it existed.
161    pub fn remove_window(&mut self, id: WindowId) -> Option<RenderWindow> {
162        self.windows.remove(&id)
163    }
164
165    /// Returns the number of windows being managed.
166    pub fn window_count(&self) -> usize {
167        self.windows.len()
168    }
169
170    /// Returns an iterator over all window IDs.
171    pub fn window_ids(&self) -> impl Iterator<Item = WindowId> + '_ {
172        self.windows.keys().copied()
173    }
174
175    /// Renders a window with automatic event handling.
176    ///
177    /// This method:
178    /// 1. Automatically handles common events (resize, etc.)
179    /// 2. Calls your render closure with the window and remaining events
180    /// 3. Returns immediately if the window doesn't exist
181    ///
182    /// # Example
183    ///
184    /// ```no_run
185    /// use astrelis_render::{WindowManager, Color};
186    ///
187    /// # fn example(window_manager: &mut WindowManager, window_id: astrelis_winit::WindowId, events: &mut astrelis_winit::event::EventBatch) {
188    /// window_manager.render_window(window_id, events, |window, events| {
189    ///     // Handle custom events if needed
190    ///     events.dispatch(|_event| {
191    ///         // Your event handling
192    ///         astrelis_winit::event::HandleStatus::ignored()
193    ///     });
194    ///
195    ///     // Render
196    ///     let Some(frame) = window.begin_frame() else { return };
197    ///     {
198    ///         let mut pass = frame.render_pass()
199    ///             .clear_color(Color::BLACK)
200    ///             .build();
201    ///         // Your rendering
202    ///     }
203    /// });
204    /// # }
205    /// ```
206    pub fn render_window<F>(&mut self, id: WindowId, events: &mut EventBatch, mut render_fn: F)
207    where
208        F: FnMut(&mut RenderWindow, &mut EventBatch),
209    {
210        profile_function!();
211        let Some(window) = self.windows.get_mut(&id) else {
212            return;
213        };
214
215        // Handle common events automatically
216        events.dispatch(|event| match event {
217            Event::WindowResized(size) => {
218                window.resized(*size);
219                HandleStatus::consumed()
220            }
221            _ => HandleStatus::ignored(),
222        });
223
224        // Call user's render function with remaining events
225        render_fn(window, events);
226    }
227
228    /// Renders a window with automatic event handling, passing a closure that returns a result.
229    ///
230    /// This is useful when rendering might fail and you want to propagate errors.
231    ///
232    /// # Example
233    ///
234    /// ```no_run
235    /// use astrelis_render::{WindowManager, Color};
236    ///
237    /// # fn example(window_manager: &mut WindowManager, window_id: astrelis_winit::WindowId, events: &mut astrelis_winit::event::EventBatch) -> Result<(), String> {
238    /// window_manager.render_window_result(window_id, events, |window, _events| {
239    ///     let Some(frame) = window.begin_frame() else { return Ok(()) };
240    ///     {
241    ///         let mut pass = frame.render_pass()
242    ///             .clear_color(Color::BLACK)
243    ///             .build();
244    ///         // Rendering that might fail
245    ///     }
246    ///     Ok(())
247    /// })
248    /// # }
249    /// ```
250    pub fn render_window_result<F, E>(
251        &mut self,
252        id: WindowId,
253        events: &mut EventBatch,
254        mut render_fn: F,
255    ) -> Result<(), E>
256    where
257        F: FnMut(&mut RenderWindow, &mut EventBatch) -> Result<(), E>,
258    {
259        let Some(window) = self.windows.get_mut(&id) else {
260            // Window doesn't exist - not an error, just skip
261            return Ok(());
262        };
263
264        // Handle common events automatically
265        events.dispatch(|event| match event {
266            Event::WindowResized(size) => {
267                window.resized(*size);
268                HandleStatus::consumed()
269            }
270            _ => HandleStatus::ignored(),
271        });
272
273        // Call user's render function with remaining events
274        render_fn(window, events)
275    }
276
277    /// Gets the shared graphics context.
278    pub fn graphics(&self) -> &Arc<GraphicsContext> {
279        &self.graphics
280    }
281
282    /// Iterates over all windows with their IDs.
283    pub fn iter(&self) -> impl Iterator<Item = (WindowId, &RenderWindow)> {
284        self.windows.iter().map(|(&id, window)| (id, window))
285    }
286
287    /// Iterates mutably over all windows with their IDs.
288    pub fn iter_mut(&mut self) -> impl Iterator<Item = (WindowId, &mut RenderWindow)> {
289        self.windows.iter_mut().map(|(&id, window)| (id, window))
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_window_manager_creation() {
299        let graphics =
300            GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
301        let manager = WindowManager::new(graphics.clone());
302
303        assert_eq!(manager.window_count(), 0);
304        assert_eq!(
305            manager.graphics().as_ref() as *const _,
306            graphics.as_ref() as *const _
307        );
308    }
309
310    #[test]
311    fn test_window_manager_window_count() {
312        let graphics =
313            GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
314        let manager = WindowManager::new(graphics);
315
316        assert_eq!(manager.window_count(), 0);
317
318        // Note: We can't actually create windows without a running event loop,
319        // so this test just verifies the count starts at 0
320    }
321
322    #[test]
323    fn test_window_manager_window_ids_empty() {
324        let graphics =
325            GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
326        let manager = WindowManager::new(graphics);
327
328        assert_eq!(manager.window_ids().count(), 0);
329    }
330}