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