Skip to main content

arcane_engine/platform/
window.rs

1use std::cell::RefCell;
2use std::path::PathBuf;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::time::Instant;
6
7use anyhow::Result;
8use winit::application::ApplicationHandler;
9use winit::event::{ElementState, KeyEvent, WindowEvent};
10use winit::event_loop::{ActiveEventLoop, EventLoop};
11use winit::keyboard::{Key, NamedKey};
12use winit::window::{Window, WindowId};
13
14use crate::renderer::Renderer;
15use crate::renderer::camera::CameraBounds;
16
17use super::input::InputState;
18
19/// Shared render state accessible from both the event loop and scripting ops.
20pub struct RenderState {
21    pub renderer: Option<Renderer>,
22    pub input: InputState,
23    pub sprite_commands: Vec<crate::renderer::SpriteCommand>,
24    pub camera_x: f32,
25    pub camera_y: f32,
26    pub camera_zoom: f32,
27    pub camera_bounds: Option<CameraBounds>,
28    pub delta_time: f64,
29}
30
31impl RenderState {
32    pub fn new() -> Self {
33        Self {
34            renderer: None,
35            input: InputState::default(),
36            sprite_commands: Vec::new(),
37            camera_x: 0.0,
38            camera_y: 0.0,
39            camera_zoom: 1.0,
40            camera_bounds: None,
41            delta_time: 0.0,
42        }
43    }
44}
45
46/// Configuration for the dev window.
47pub struct DevConfig {
48    pub entry_file: PathBuf,
49    pub title: String,
50    pub width: u32,
51    pub height: u32,
52}
53
54/// Callback invoked each frame to run the TS step function.
55/// Returns the list of sprite commands to render.
56pub type FrameCallback = Box<dyn FnMut(&mut RenderState) -> Result<()>>;
57
58struct AppState {
59    window: Option<Arc<Window>>,
60    config: DevConfig,
61    render_state: Rc<RefCell<RenderState>>,
62    frame_callback: FrameCallback,
63    last_frame: Instant,
64    /// Display scale factor (e.g. 2.0 on Retina).
65    scale_factor: f64,
66}
67
68impl ApplicationHandler for AppState {
69    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
70        if self.window.is_some() {
71            return;
72        }
73
74        let attrs = Window::default_attributes()
75            .with_title(&self.config.title)
76            .with_inner_size(winit::dpi::LogicalSize::new(
77                self.config.width,
78                self.config.height,
79            ));
80
81        let window = Arc::new(
82            event_loop
83                .create_window(attrs)
84                .expect("Failed to create window"),
85        );
86
87        self.scale_factor = window.scale_factor();
88
89        match Renderer::new(window.clone()) {
90            Ok(renderer) => {
91                self.render_state.borrow_mut().renderer = Some(renderer);
92            }
93            Err(e) => {
94                eprintln!("Failed to initialize renderer: {e}");
95                event_loop.exit();
96                return;
97            }
98        }
99
100        self.window = Some(window);
101        self.last_frame = Instant::now();
102    }
103
104    fn window_event(
105        &mut self,
106        event_loop: &ActiveEventLoop,
107        _window_id: WindowId,
108        event: WindowEvent,
109    ) {
110        match event {
111            WindowEvent::CloseRequested => {
112                event_loop.exit();
113            }
114
115            WindowEvent::Resized(new_size) => {
116                let mut state = self.render_state.borrow_mut();
117                if let Some(ref mut renderer) = state.renderer {
118                    renderer.resize(new_size.width, new_size.height, self.scale_factor as f32);
119                }
120            }
121
122            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
123                self.scale_factor = scale_factor;
124            }
125
126            WindowEvent::KeyboardInput {
127                event:
128                    KeyEvent {
129                        logical_key,
130                        state: key_state,
131                        ..
132                    },
133                ..
134            } => {
135                let key_name = key_to_string(&logical_key);
136                let mut state = self.render_state.borrow_mut();
137                match key_state {
138                    ElementState::Pressed => state.input.key_down(&key_name),
139                    ElementState::Released => state.input.key_up(&key_name),
140                }
141            }
142
143            WindowEvent::CursorMoved { position, .. } => {
144                // Convert from physical pixels to logical pixels
145                let logical_x = position.x as f32 / self.scale_factor as f32;
146                let logical_y = position.y as f32 / self.scale_factor as f32;
147                let mut state = self.render_state.borrow_mut();
148                state.input.mouse_move(logical_x, logical_y);
149            }
150
151            WindowEvent::MouseInput { state: button_state, button, .. } => {
152                let mut state = self.render_state.borrow_mut();
153                let button_id: u8 = match button {
154                    winit::event::MouseButton::Left => 0,
155                    winit::event::MouseButton::Right => 1,
156                    winit::event::MouseButton::Middle => 2,
157                    winit::event::MouseButton::Back => 3,
158                    winit::event::MouseButton::Forward => 4,
159                    winit::event::MouseButton::Other(id) => id.min(255) as u8,
160                };
161                match button_state {
162                    ElementState::Pressed => {
163                        state.input.mouse_buttons.insert(button_id);
164                        // Also add to keys_pressed so isKeyPressed works
165                        let key_name = match button_id {
166                            0 => "MouseLeft",
167                            1 => "MouseRight",
168                            2 => "MouseMiddle",
169                            _ => return,
170                        };
171                        state.input.key_down(key_name);
172                    }
173                    ElementState::Released => {
174                        state.input.mouse_buttons.remove(&button_id);
175                        let key_name = match button_id {
176                            0 => "MouseLeft",
177                            1 => "MouseRight",
178                            2 => "MouseMiddle",
179                            _ => return,
180                        };
181                        state.input.key_up(key_name);
182                    }
183                }
184            }
185
186            WindowEvent::RedrawRequested => {
187                let now = Instant::now();
188                let dt = now.duration_since(self.last_frame).as_secs_f64();
189                self.last_frame = now;
190
191                {
192                    let mut state = self.render_state.borrow_mut();
193                    state.delta_time = dt;
194                }
195
196                // Run the TS frame callback (calls ops that populate sprite_commands)
197                {
198                    let mut state = self.render_state.borrow_mut();
199                    if let Err(e) = (self.frame_callback)(&mut state) {
200                        eprintln!("Frame callback error: {e}");
201                    }
202                }
203
204                // Clear per-frame input AFTER the callback has read it
205                {
206                    let mut state = self.render_state.borrow_mut();
207                    state.input.begin_frame();
208                }
209
210                // Transfer sprite commands and camera to renderer, then render
211                {
212                    let mut state = self.render_state.borrow_mut();
213                    // Extract values before borrowing renderer mutably
214                    let cam_x = state.camera_x;
215                    let cam_y = state.camera_y;
216                    let cam_zoom = state.camera_zoom;
217                    let cam_bounds = state.camera_bounds;
218                    let commands = std::mem::take(&mut state.sprite_commands);
219
220                    if let Some(ref mut renderer) = state.renderer {
221                        renderer.camera.x = cam_x;
222                        renderer.camera.y = cam_y;
223                        renderer.camera.zoom = cam_zoom;
224                        renderer.camera.bounds = cam_bounds;
225                        renderer.camera.clamp_to_bounds();
226                        renderer.frame_commands = commands;
227
228                        if let Err(e) = renderer.render_frame() {
229                            eprintln!("Render error: {e}");
230                        }
231                    }
232                }
233
234                if let Some(ref window) = self.window {
235                    window.request_redraw();
236                }
237            }
238
239            _ => {}
240        }
241    }
242
243    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
244        if let Some(ref window) = self.window {
245            window.request_redraw();
246        }
247    }
248}
249
250/// Convert a winit logical key to a string name for the TS API.
251fn key_to_string(key: &Key) -> String {
252    match key {
253        Key::Named(named) => match named {
254            NamedKey::ArrowUp => "ArrowUp".to_string(),
255            NamedKey::ArrowDown => "ArrowDown".to_string(),
256            NamedKey::ArrowLeft => "ArrowLeft".to_string(),
257            NamedKey::ArrowRight => "ArrowRight".to_string(),
258            NamedKey::Space => "Space".to_string(),
259            NamedKey::Enter => "Enter".to_string(),
260            NamedKey::Escape => "Escape".to_string(),
261            NamedKey::Backspace => "Backspace".to_string(),
262            NamedKey::Tab => "Tab".to_string(),
263            NamedKey::Shift => "Shift".to_string(),
264            NamedKey::Control => "Control".to_string(),
265            NamedKey::Alt => "Alt".to_string(),
266            other => format!("{other:?}"),
267        },
268        Key::Character(c) => c.to_string(),
269        _ => "Unknown".to_string(),
270    }
271}
272
273/// Run the event loop. This blocks until the window is closed.
274pub fn run_event_loop(
275    config: DevConfig,
276    render_state: Rc<RefCell<RenderState>>,
277    frame_callback: FrameCallback,
278) -> Result<()> {
279    let event_loop = EventLoop::new()?;
280
281    let mut app = AppState {
282        window: None,
283        config,
284        render_state,
285        frame_callback,
286        last_frame: Instant::now(),
287        scale_factor: 1.0,
288    };
289
290    event_loop.run_app(&mut app)?;
291    Ok(())
292}