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