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