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}