Skip to main content

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