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