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}