Skip to main content

theframework/
thewinitapp.rs

1use std::sync::Arc;
2
3#[cfg(feature = "ui")]
4use rfd::MessageDialog;
5
6use crate::thecontext::TheCursorIcon;
7use crate::thewinitbackend::TheWinitBackend;
8
9#[cfg(target_os = "macos")]
10use winit::platform::macos::WindowExtMacOS;
11
12use crate::prelude::*;
13use web_time::{Duration, Instant};
14use winit::{
15    application::ApplicationHandler,
16    dpi::{LogicalSize, PhysicalPosition, PhysicalSize},
17    event::{
18        DeviceEvent, DeviceId, ElementState, MouseButton, MouseScrollDelta, StartCause, Touch,
19        TouchPhase, WindowEvent,
20    },
21    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
22    keyboard::{Key, KeyCode, ModifiersState, NamedKey},
23    window::{Icon, Window, WindowAttributes, WindowId},
24};
25
26// Platform-aware accelerator modifiers (AltGr-safe)
27#[inline]
28fn is_accel_mods(m: &ModifiersState) -> bool {
29    #[cfg(target_os = "macos")]
30    {
31        // macOS commonly uses ⌘ and ⌥ for accelerators
32        m.super_key() || m.alt_key() || m.control_key()
33    }
34    #[cfg(not(target_os = "macos"))]
35    {
36        // On Windows/Linux, avoid treating Alt as an accelerator by default (AltGr = Ctrl+Alt)
37        m.super_key() || m.control_key()
38    }
39}
40
41// US-ANSI physical keycode to ascii (letters, digits, punctuation, space)
42fn accel_physical_to_ascii(code: KeyCode, shift: bool) -> Option<char> {
43    let (base, shifted) = match code {
44        // Letters
45        KeyCode::KeyA => ('a', 'A'),
46        KeyCode::KeyB => ('b', 'B'),
47        KeyCode::KeyC => ('c', 'C'),
48        KeyCode::KeyD => ('d', 'D'),
49        KeyCode::KeyE => ('e', 'E'),
50        KeyCode::KeyF => ('f', 'F'),
51        KeyCode::KeyG => ('g', 'G'),
52        KeyCode::KeyH => ('h', 'H'),
53        KeyCode::KeyI => ('i', 'I'),
54        KeyCode::KeyJ => ('j', 'J'),
55        KeyCode::KeyK => ('k', 'K'),
56        KeyCode::KeyL => ('l', 'L'),
57        KeyCode::KeyM => ('m', 'M'),
58        KeyCode::KeyN => ('n', 'N'),
59        KeyCode::KeyO => ('o', 'O'),
60        KeyCode::KeyP => ('p', 'P'),
61        KeyCode::KeyQ => ('q', 'Q'),
62        KeyCode::KeyR => ('r', 'R'),
63        KeyCode::KeyS => ('s', 'S'),
64        KeyCode::KeyT => ('t', 'T'),
65        KeyCode::KeyU => ('u', 'U'),
66        KeyCode::KeyV => ('v', 'V'),
67        KeyCode::KeyW => ('w', 'W'),
68        KeyCode::KeyX => ('x', 'X'),
69        KeyCode::KeyY => ('y', 'Y'),
70        KeyCode::KeyZ => ('z', 'Z'),
71        // Digits (US)
72        KeyCode::Digit0 => ('0', ')'),
73        KeyCode::Digit1 => ('1', '!'),
74        KeyCode::Digit2 => ('2', '@'),
75        KeyCode::Digit3 => ('3', '#'),
76        KeyCode::Digit4 => ('4', '$'),
77        KeyCode::Digit5 => ('5', '%'),
78        KeyCode::Digit6 => ('6', '^'),
79        KeyCode::Digit7 => ('7', '&'),
80        KeyCode::Digit8 => ('8', '*'),
81        KeyCode::Digit9 => ('9', '('),
82        // Punctuation (US ANSI)
83        KeyCode::Minus => ('-', '_'),
84        KeyCode::Equal => ('=', '+'),
85        KeyCode::BracketLeft => ('[', '{'),
86        KeyCode::BracketRight => (']', '}'),
87        KeyCode::Semicolon => (';', ':'),
88        KeyCode::Quote => ('\'', '"'),
89        KeyCode::Comma => (',', '<'),
90        KeyCode::Period => ('.', '>'),
91        KeyCode::Slash => ('/', '?'),
92        KeyCode::Backquote => ('`', '~'),
93        KeyCode::Backslash => ('\\', '|'),
94        KeyCode::IntlBackslash => ('\\', '|'),
95        // Space
96        KeyCode::Space => (' ', ' '),
97        _ => return None,
98    };
99    Some(if shift { shifted } else { base })
100}
101
102fn translate_coord_to_local(x: f32, y: f32, scale_factor: f32) -> (f32, f32) {
103    (x / scale_factor, y / scale_factor)
104}
105
106struct TheWinitContext {
107    window: Arc<Window>,
108    ctx: TheContext,
109    ui_frame: Vec<u8>,
110    backend: TheWinitBackend,
111}
112
113impl TheWinitContext {
114    fn from_window(window: Arc<Window>) -> Self {
115        #[cfg(not(target_os = "macos"))]
116        let scale_factor = 1.0;
117        // Make sure to set the initial scale factor on macOS
118        #[cfg(target_os = "macos")]
119        let scale_factor = window.scale_factor() as f32;
120
121        let size = window.inner_size();
122
123        // WASM-specific fix: On WASM with Retina displays, inner_size() returns physical size
124        // We need to divide by scale factor to get logical size
125        #[cfg(target_arch = "wasm32")]
126        let (width, height) = {
127            let wasm_scale = window.scale_factor() as f32;
128            (
129                (size.width as f32 / wasm_scale) as usize,
130                (size.height as f32 / wasm_scale) as usize,
131            )
132        };
133        #[cfg(not(target_arch = "wasm32"))]
134        let (width, height) = (size.width as usize, size.height as usize);
135
136        // println!("=== from_window DEBUG ===");
137        // println!(
138        //     "Window scale_factor (from winit): {}",
139        //     window.scale_factor()
140        // );
141        // println!("Using scale_factor: {}", scale_factor);
142        // println!("Physical size (inner_size): {}x{}", size.width, size.height);
143        // println!("Context size: {}x{}", width, height);
144        // println!("ui_frame size: {} bytes", width * height * 4);
145
146        let ctx = TheContext::new(width, height, scale_factor);
147
148        let ui_frame = vec![0; (width * height * 4) as usize];
149
150        let backend = TheWinitBackend::new(window.clone(), width, height, scale_factor);
151
152        TheWinitContext {
153            window,
154            ctx,
155            ui_frame,
156            backend,
157        }
158    }
159}
160
161struct TheWinitApp {
162    args: Option<Vec<String>>,
163    ctx: Option<TheWinitContext>,
164    app: Box<dyn TheTrait>,
165
166    mods: ModifiersState,
167    target_frame_time: Duration,
168    next_frame_time: Instant,
169    last_cursor_pos: Option<(f32, f32)>,
170    left_mouse_down: bool,
171    has_changes: bool,
172
173    #[cfg(feature = "ui")]
174    ui: TheUI,
175}
176
177impl TheWinitApp {
178    fn new(args: Option<Vec<String>>, app: Box<dyn TheTrait>) -> Self {
179        let fps = app.target_fps();
180
181        TheWinitApp {
182            args,
183            ctx: None,
184            app,
185            mods: ModifiersState::empty(),
186            target_frame_time: Duration::from_secs_f64(1.0 / fps),
187            next_frame_time: Instant::now(),
188            last_cursor_pos: None,
189            left_mouse_down: false,
190            has_changes: false,
191            #[cfg(feature = "ui")]
192            ui: TheUI::new(),
193        }
194    }
195
196    fn create_window(&mut self, event_loop: &ActiveEventLoop) -> Option<Arc<Window>> {
197        let window_title = self.app.window_title();
198        let mut icon: Option<Icon> = None;
199        if let Some(window_icon) = self.app.window_icon() {
200            icon = Icon::from_rgba(window_icon.0, window_icon.1, window_icon.2).ok();
201        }
202
203        let (width, height) = self.app.default_window_size();
204        let size = LogicalSize::new(width as f64, height as f64);
205        let (min_width, min_height) = self.app.min_window_size();
206        let min_size = LogicalSize::new(min_width as f64, min_height as f64);
207
208        let window_attributes = WindowAttributes::default()
209            .with_title(window_title)
210            .with_inner_size(size)
211            .with_min_inner_size(min_size)
212            .with_window_icon(icon); //TODO on Windows
213
214        let window_attributes = if let Some((x, y)) = self.app.default_window_position() {
215            window_attributes.with_position(PhysicalPosition::new(x, y))
216        } else {
217            window_attributes
218        };
219
220        #[cfg(target_arch = "wasm32")]
221        let window_attributes = {
222            use winit::platform::web::WindowAttributesExtWebSys;
223
224            window_attributes.with_append(true)
225        };
226
227        let window = event_loop.create_window(window_attributes).unwrap();
228
229        Some(Arc::new(window))
230    }
231
232    fn init_context(&mut self, window: Arc<Window>) -> TheWinitContext {
233        let mut ctx = TheWinitContext::from_window(window);
234
235        #[cfg(feature = "ui")]
236        {
237            self.ui.init(&mut ctx.ctx);
238
239            self.ui.canvas.root = true;
240            self.ui.canvas.set_dim(
241                TheDim::new(0, 0, ctx.ctx.width as i32, ctx.ctx.height as i32),
242                &mut ctx.ctx,
243            );
244
245            self.app.init_ui(&mut self.ui, &mut ctx.ctx);
246            self.ui
247                .canvas
248                .layout(ctx.ctx.width as i32, ctx.ctx.height as i32, &mut ctx.ctx);
249        }
250
251        #[cfg(feature = "i18n")]
252        ctx.ctx.load_system_fonts(self.app.fonts_to_load());
253
254        self.app.init(&mut ctx.ctx);
255
256        // If available set the command line arguments to the trait.
257        if let Some(args) = self.args.take() {
258            self.app.set_cmd_line_args(args, &mut ctx.ctx);
259        }
260
261        ctx
262    }
263
264    fn render(&mut self) {
265        let Some(ctx) = &mut self.ctx else {
266            return;
267        };
268
269        if ctx.ctx.width == 0 || ctx.ctx.height == 0 {
270            return;
271        }
272
273        #[cfg(feature = "ui")]
274        self.app.pre_ui(&mut ctx.ctx);
275
276        #[cfg(feature = "ui")]
277        self.ui.draw(&mut ctx.ui_frame, &mut ctx.ctx);
278
279        // We always call this for apps who use the "ui" feature
280        // but do not use the UI API
281        self.app.draw(&mut ctx.ui_frame, &mut ctx.ctx);
282
283        ctx.backend.present(
284            &ctx.window,
285            &ctx.ui_frame,
286            ctx.ctx.width,
287            ctx.ctx.height,
288            ctx.ctx.scale_factor,
289        );
290
291        #[cfg(feature = "ui")]
292        self.app.post_ui(&mut ctx.ctx);
293    }
294
295    fn process_updates(&mut self) -> bool {
296        let Some(ctx) = &mut self.ctx else {
297            return false;
298        };
299
300        let mut wants_redraw = false;
301
302        let current_has_changes = self.app.has_changes();
303        if current_has_changes != self.has_changes {
304            self.has_changes = current_has_changes;
305            #[cfg(target_os = "macos")]
306            ctx.window.set_document_edited(current_has_changes);
307        }
308
309        #[cfg(feature = "ui")]
310        if self.ui.update(&mut ctx.ctx) {
311            wants_redraw = true;
312        }
313
314        #[cfg(feature = "ui")]
315        if self.app.update_ui(&mut self.ui, &mut ctx.ctx) {
316            wants_redraw = true;
317        }
318
319        if self.app.update(&mut ctx.ctx) {
320            wants_redraw = true;
321        }
322
323        wants_redraw
324    }
325
326    fn resize(&mut self, size: PhysicalSize<u32>) {
327        let Some(ctx) = &mut self.ctx else {
328            return;
329        };
330
331        if size.width != 0 && size.height != 0 {
332            // println!("=== resize DEBUG ===");
333            // println!("New physical size: {}x{}", size.width, size.height);
334
335            let scale_factor = ctx.window.scale_factor() as f32;
336
337            // On non-macOS, if DPI scale is fractional, render at physical resolution with scale_factor forced to 1.0
338            #[cfg(all(not(target_os = "macos"), not(target_arch = "wasm32")))]
339            let (effective_scale, width, height) = if scale_factor.fract() != 0.0 {
340                (1.0_f32, size.width, size.height)
341            } else {
342                (
343                    scale_factor,
344                    (size.width as f32 / scale_factor).round() as u32,
345                    (size.height as f32 / scale_factor).round() as u32,
346                )
347            };
348            // macOS and WASM: keep logical sizing based on scale_factor
349            #[cfg(any(target_os = "macos", target_arch = "wasm32"))]
350            let (effective_scale, width, height) = (
351                scale_factor,
352                (size.width as f32 / scale_factor).round() as u32,
353                (size.height as f32 / scale_factor).round() as u32,
354            );
355
356            ctx.ctx.scale_factor = effective_scale;
357
358            ctx.backend
359                .resize(&ctx.window, size, width, height, scale_factor);
360
361            // println!("Window scale_factor: {}", scale_factor);
362            // println!("New logical size: {}x{}", width, height);
363            // println!("New ui_frame size: {} bytes", width * height * 4);
364
365            ctx.ctx.width = width as usize;
366            ctx.ctx.height = height as usize;
367
368            ctx.ui_frame.resize((width * height * 4) as usize, 0);
369            // println!("===================\n");
370
371            #[cfg(feature = "ui")]
372            self.ui
373                .canvas
374                .set_dim(TheDim::new(0, 0, width as i32, height as i32), &mut ctx.ctx);
375            #[cfg(feature = "ui")]
376            ctx.ctx.ui.send(TheEvent::Resize);
377
378            self.app.window_resized(width as usize, height as usize);
379
380            ctx.window.request_redraw();
381        }
382    }
383}
384
385impl ApplicationHandler for TheWinitApp {
386    fn new_events(&mut self, _: &ActiveEventLoop, _: StartCause) {}
387
388    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
389        if self.ctx.is_none() {
390            if let Some(window) = self.create_window(event_loop) {
391                self.ctx = Some(self.init_context(window));
392                // Set initial cursor to default and ensure it's visible
393                if let Some(ctx) = &mut self.ctx {
394                    ctx.ctx.set_cursor_icon(TheCursorIcon::Default);
395                    ctx.window.set_cursor_visible(true);
396                }
397            }
398        }
399    }
400
401    fn window_event(
402        &mut self,
403        event_loop: &ActiveEventLoop,
404        _window_id: WindowId,
405        event: WindowEvent,
406    ) {
407        match event {
408            WindowEvent::RedrawRequested => {
409                self.render();
410            }
411            WindowEvent::CloseRequested => {
412                if !self.app.closing() {
413                    #[cfg(feature = "ui")]
414                    {
415                        if self.app.has_changes() {
416                            let result = MessageDialog::new()
417                                .set_title("Unsaved Changes")
418                                .set_description(
419                                    "You have unsaved changes. Are you sure you want to quit?",
420                                )
421                                .set_buttons(rfd::MessageButtons::YesNo)
422                                .show();
423
424                            if result == rfd::MessageDialogResult::Yes {
425                                event_loop.exit();
426                            }
427                        } else {
428                            event_loop.exit();
429                        }
430                    }
431                    #[cfg(not(feature = "ui"))]
432                    {
433                        event_loop.exit();
434                    }
435                }
436            }
437            WindowEvent::Resized(size) => {
438                self.resize(size);
439            }
440            WindowEvent::Moved(position) => {
441                self.app.window_moved(position.x, position.y);
442            }
443            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
444                if let Some(ctx) = &mut self.ctx {
445                    ctx.ctx.scale_factor = scale_factor as f32;
446                    let size = ctx.window.inner_size();
447                    self.resize(size);
448                }
449            }
450            event => {
451                let user_driven_event = matches!(
452                    &event,
453                    WindowEvent::KeyboardInput { .. }
454                        | WindowEvent::ModifiersChanged(_)
455                        | WindowEvent::CursorMoved { .. }
456                        | WindowEvent::Touch(_)
457                        | WindowEvent::MouseInput { .. }
458                        | WindowEvent::MouseWheel { .. }
459                        | WindowEvent::DroppedFile(_)
460                );
461                if user_driven_event {
462                    // Wake the frame scheduler immediately on input even in low-FPS idle mode.
463                    self.next_frame_time = Instant::now();
464                }
465
466                let Some(ctx) = &mut self.ctx else {
467                    return;
468                };
469
470                match event {
471                    WindowEvent::KeyboardInput {
472                        event: key_event, ..
473                    } => {
474                        if key_event.state == ElementState::Pressed {
475                            let key = match &key_event.logical_key {
476                                Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Backspace) => {
477                                    Some(TheKeyCode::Delete)
478                                }
479                                Key::Named(NamedKey::Home) => Some(TheKeyCode::Home),
480                                Key::Named(NamedKey::End) => Some(TheKeyCode::End),
481                                Key::Named(NamedKey::ArrowUp) => Some(TheKeyCode::Up),
482                                Key::Named(NamedKey::ArrowRight) => Some(TheKeyCode::Right),
483                                Key::Named(NamedKey::ArrowDown) => Some(TheKeyCode::Down),
484                                Key::Named(NamedKey::ArrowLeft) => Some(TheKeyCode::Left),
485                                Key::Named(NamedKey::Space) => Some(TheKeyCode::Space),
486                                Key::Named(NamedKey::Tab) => Some(TheKeyCode::Tab),
487                                Key::Named(NamedKey::Enter) => Some(TheKeyCode::Return),
488                                Key::Named(NamedKey::Escape) => Some(TheKeyCode::Escape),
489                                Key::Character(str) => {
490                                    // Accelerator: use physical key with modifiers (ignore composed text like "å")
491                                    if is_accel_mods(&self.mods) {
492                                        if let winit::keyboard::PhysicalKey::Code(code) =
493                                            key_event.physical_key
494                                        {
495                                            if let Some(ch) =
496                                                accel_physical_to_ascii(code, self.mods.shift_key())
497                                            {
498                                                #[cfg(feature = "ui")]
499                                                if self.ui.key_down(Some(ch), None, &mut ctx.ctx) {
500                                                    ctx.window.request_redraw();
501                                                }
502                                                if self.app.key_down(Some(ch), None, &mut ctx.ctx) {
503                                                    ctx.window.request_redraw();
504                                                }
505                                                return;
506                                            }
507                                        }
508                                    }
509                                    if str.is_ascii() {
510                                        for ch in str.chars() {
511                                            #[cfg(feature = "ui")]
512                                            if self.ui.key_down(Some(ch), None, &mut ctx.ctx) {
513                                                ctx.window.request_redraw();
514                                            }
515                                            if self.app.key_down(Some(ch), None, &mut ctx.ctx) {
516                                                ctx.window.request_redraw();
517                                            }
518                                        }
519                                    }
520                                    None
521                                }
522                                _ => None,
523                            };
524                            if key.is_some() {
525                                #[cfg(feature = "ui")]
526                                if self.ui.key_down(None, key.clone(), &mut ctx.ctx) {
527                                    ctx.window.request_redraw();
528                                }
529                                if self.app.key_down(None, key, &mut ctx.ctx) {
530                                    ctx.window.request_redraw();
531                                }
532                            }
533
534                            // Update cursor icon after keyboard events (focus may have changed)
535                            if ctx.ctx.cursor_changed() {
536                                let cursor_icon = match ctx.ctx.cursor_icon() {
537                                    TheCursorIcon::Default => winit::window::CursorIcon::Default,
538                                    TheCursorIcon::Crosshair => {
539                                        winit::window::CursorIcon::Crosshair
540                                    }
541                                    TheCursorIcon::Hand => winit::window::CursorIcon::Pointer,
542                                    TheCursorIcon::Arrow => winit::window::CursorIcon::Default,
543                                    TheCursorIcon::Text => winit::window::CursorIcon::Text,
544                                    TheCursorIcon::Wait => winit::window::CursorIcon::Wait,
545                                    TheCursorIcon::Help => winit::window::CursorIcon::Help,
546                                    TheCursorIcon::Progress => winit::window::CursorIcon::Progress,
547                                    TheCursorIcon::NotAllowed => {
548                                        winit::window::CursorIcon::NotAllowed
549                                    }
550                                    TheCursorIcon::ContextMenu => {
551                                        winit::window::CursorIcon::ContextMenu
552                                    }
553                                    TheCursorIcon::Cell => winit::window::CursorIcon::Cell,
554                                    TheCursorIcon::VerticalText => {
555                                        winit::window::CursorIcon::VerticalText
556                                    }
557                                    TheCursorIcon::Alias => winit::window::CursorIcon::Alias,
558                                    TheCursorIcon::Copy => winit::window::CursorIcon::Copy,
559                                    TheCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
560                                    TheCursorIcon::Grab => winit::window::CursorIcon::Grab,
561                                    TheCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
562                                    TheCursorIcon::AllScroll => {
563                                        winit::window::CursorIcon::AllScroll
564                                    }
565                                    TheCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
566                                    TheCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
567                                    TheCursorIcon::EResize => winit::window::CursorIcon::EResize,
568                                    TheCursorIcon::NResize => winit::window::CursorIcon::NResize,
569                                    TheCursorIcon::NEResize => winit::window::CursorIcon::NeResize,
570                                    TheCursorIcon::NWResize => winit::window::CursorIcon::NwResize,
571                                    TheCursorIcon::SResize => winit::window::CursorIcon::SResize,
572                                    TheCursorIcon::SEResize => winit::window::CursorIcon::SeResize,
573                                    TheCursorIcon::SWResize => winit::window::CursorIcon::SwResize,
574                                    TheCursorIcon::WResize => winit::window::CursorIcon::WResize,
575                                    TheCursorIcon::EWResize => winit::window::CursorIcon::EwResize,
576                                    TheCursorIcon::NSResize => winit::window::CursorIcon::NsResize,
577                                    TheCursorIcon::NESWResize => {
578                                        winit::window::CursorIcon::NeswResize
579                                    }
580                                    TheCursorIcon::NWSEResize => {
581                                        winit::window::CursorIcon::NwseResize
582                                    }
583                                    TheCursorIcon::ColResize => {
584                                        winit::window::CursorIcon::ColResize
585                                    }
586                                    TheCursorIcon::RowResize => {
587                                        winit::window::CursorIcon::RowResize
588                                    }
589                                };
590                                ctx.window.set_cursor(cursor_icon);
591                                ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
592                                ctx.ctx.reset_cursor_changed();
593                            }
594                            if ctx.ctx.cursor_visible_changed() {
595                                ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
596                                ctx.ctx.reset_cursor_visible_changed();
597                            }
598                        }
599                        if key_event.state == ElementState::Released {
600                            let key = match &key_event.logical_key {
601                                Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Backspace) => {
602                                    Some(TheKeyCode::Delete)
603                                }
604                                Key::Named(NamedKey::Home) => Some(TheKeyCode::Home),
605                                Key::Named(NamedKey::End) => Some(TheKeyCode::End),
606                                Key::Named(NamedKey::ArrowUp) => Some(TheKeyCode::Up),
607                                Key::Named(NamedKey::ArrowRight) => Some(TheKeyCode::Right),
608                                Key::Named(NamedKey::ArrowDown) => Some(TheKeyCode::Down),
609                                Key::Named(NamedKey::ArrowLeft) => Some(TheKeyCode::Left),
610                                Key::Named(NamedKey::Space) => Some(TheKeyCode::Space),
611                                Key::Named(NamedKey::Tab) => Some(TheKeyCode::Tab),
612                                Key::Named(NamedKey::Enter) => Some(TheKeyCode::Return),
613                                Key::Named(NamedKey::Escape) => Some(TheKeyCode::Escape),
614                                Key::Character(str) => {
615                                    // Accelerator release: use physical key with modifiers (ignore composed text)
616                                    if is_accel_mods(&self.mods) {
617                                        if let winit::keyboard::PhysicalKey::Code(code) =
618                                            key_event.physical_key
619                                        {
620                                            if let Some(ch) =
621                                                accel_physical_to_ascii(code, self.mods.shift_key())
622                                            {
623                                                #[cfg(feature = "ui")]
624                                                if self.ui.key_up(Some(ch), None, &mut ctx.ctx) {
625                                                    ctx.window.request_redraw();
626                                                }
627                                                if self.app.key_up(Some(ch), None, &mut ctx.ctx) {
628                                                    ctx.window.request_redraw();
629                                                }
630                                                return;
631                                            }
632                                        }
633                                    }
634                                    if str.is_ascii() {
635                                        for ch in str.chars() {
636                                            #[cfg(feature = "ui")]
637                                            if self.ui.key_up(Some(ch), None, &mut ctx.ctx) {
638                                                ctx.window.request_redraw();
639                                            }
640                                            if self.app.key_up(Some(ch), None, &mut ctx.ctx) {
641                                                ctx.window.request_redraw();
642                                            }
643                                        }
644                                    }
645                                    None
646                                }
647                                _ => None,
648                            };
649                            if key.is_some() {
650                                #[cfg(feature = "ui")]
651                                if self.ui.key_up(None, key.clone(), &mut ctx.ctx) {
652                                    ctx.window.request_redraw();
653                                }
654                                if self.app.key_up(None, key, &mut ctx.ctx) {
655                                    ctx.window.request_redraw();
656                                }
657                            }
658
659                            // Update cursor icon after keyboard events (focus may have changed)
660                            if ctx.ctx.cursor_changed() {
661                                let cursor_icon = match ctx.ctx.cursor_icon() {
662                                    TheCursorIcon::Default => winit::window::CursorIcon::Default,
663                                    TheCursorIcon::Crosshair => {
664                                        winit::window::CursorIcon::Crosshair
665                                    }
666                                    TheCursorIcon::Hand => winit::window::CursorIcon::Pointer,
667                                    TheCursorIcon::Arrow => winit::window::CursorIcon::Default,
668                                    TheCursorIcon::Text => winit::window::CursorIcon::Text,
669                                    TheCursorIcon::Wait => winit::window::CursorIcon::Wait,
670                                    TheCursorIcon::Help => winit::window::CursorIcon::Help,
671                                    TheCursorIcon::Progress => winit::window::CursorIcon::Progress,
672                                    TheCursorIcon::NotAllowed => {
673                                        winit::window::CursorIcon::NotAllowed
674                                    }
675                                    TheCursorIcon::ContextMenu => {
676                                        winit::window::CursorIcon::ContextMenu
677                                    }
678                                    TheCursorIcon::Cell => winit::window::CursorIcon::Cell,
679                                    TheCursorIcon::VerticalText => {
680                                        winit::window::CursorIcon::VerticalText
681                                    }
682                                    TheCursorIcon::Alias => winit::window::CursorIcon::Alias,
683                                    TheCursorIcon::Copy => winit::window::CursorIcon::Copy,
684                                    TheCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
685                                    TheCursorIcon::Grab => winit::window::CursorIcon::Grab,
686                                    TheCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
687                                    TheCursorIcon::AllScroll => {
688                                        winit::window::CursorIcon::AllScroll
689                                    }
690                                    TheCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
691                                    TheCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
692                                    TheCursorIcon::EResize => winit::window::CursorIcon::EResize,
693                                    TheCursorIcon::NResize => winit::window::CursorIcon::NResize,
694                                    TheCursorIcon::NEResize => winit::window::CursorIcon::NeResize,
695                                    TheCursorIcon::NWResize => winit::window::CursorIcon::NwResize,
696                                    TheCursorIcon::SResize => winit::window::CursorIcon::SResize,
697                                    TheCursorIcon::SEResize => winit::window::CursorIcon::SeResize,
698                                    TheCursorIcon::SWResize => winit::window::CursorIcon::SwResize,
699                                    TheCursorIcon::WResize => winit::window::CursorIcon::WResize,
700                                    TheCursorIcon::EWResize => winit::window::CursorIcon::EwResize,
701                                    TheCursorIcon::NSResize => winit::window::CursorIcon::NsResize,
702                                    TheCursorIcon::NESWResize => {
703                                        winit::window::CursorIcon::NeswResize
704                                    }
705                                    TheCursorIcon::NWSEResize => {
706                                        winit::window::CursorIcon::NwseResize
707                                    }
708                                    TheCursorIcon::ColResize => {
709                                        winit::window::CursorIcon::ColResize
710                                    }
711                                    TheCursorIcon::RowResize => {
712                                        winit::window::CursorIcon::RowResize
713                                    }
714                                };
715                                ctx.window.set_cursor(cursor_icon);
716                                ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
717                                ctx.ctx.reset_cursor_changed();
718                            }
719                            if ctx.ctx.cursor_visible_changed() {
720                                ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
721                                ctx.ctx.reset_cursor_visible_changed();
722                            }
723                        }
724                    }
725                    WindowEvent::ModifiersChanged(modifiers) => {
726                        let state = modifiers.state();
727                        // keep a copy of current modifiers for accelerator checks
728                        self.mods = state;
729
730                        #[cfg(feature = "ui")]
731                        if self.ui.modifier_changed(
732                            state.shift_key(),
733                            state.control_key(),
734                            state.alt_key(),
735                            state.super_key(),
736                            &mut ctx.ctx,
737                        ) {
738                            ctx.window.request_redraw();
739                        }
740                        if self.app.modifier_changed(
741                            state.shift_key(),
742                            state.control_key(),
743                            state.alt_key(),
744                            state.super_key(),
745                        ) {
746                            ctx.window.request_redraw();
747                        }
748                    }
749                    WindowEvent::CursorMoved { position, .. } => {
750                        let (x, y) = translate_coord_to_local(
751                            position.x as f32,
752                            position.y as f32,
753                            ctx.ctx.scale_factor,
754                        );
755
756                        self.last_cursor_pos = Some((x, y));
757
758                        let mut redraw = false;
759                        if self.left_mouse_down {
760                            #[cfg(feature = "ui")]
761                            if self.ui.touch_dragged(x, y, &mut ctx.ctx) {
762                                redraw = true;
763                            }
764
765                            if self.app.touch_dragged(x, y, &mut ctx.ctx) {
766                                redraw = true;
767                            }
768                        } else {
769                            #[cfg(feature = "ui")]
770                            if self.ui.hover(x, y, &mut ctx.ctx) {
771                                redraw = true;
772                            }
773
774                            if self.app.hover(x, y, &mut ctx.ctx) {
775                                redraw = true;
776                            }
777                        }
778
779                        // Update cursor icon immediately after hover detection
780                        if ctx.ctx.cursor_changed() {
781                            let cursor_icon = match ctx.ctx.cursor_icon() {
782                                TheCursorIcon::Default => winit::window::CursorIcon::Default,
783                                TheCursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
784                                TheCursorIcon::Hand => winit::window::CursorIcon::Pointer,
785                                TheCursorIcon::Arrow => winit::window::CursorIcon::Default,
786                                TheCursorIcon::Text => winit::window::CursorIcon::Text,
787                                TheCursorIcon::Wait => winit::window::CursorIcon::Wait,
788                                TheCursorIcon::Help => winit::window::CursorIcon::Help,
789                                TheCursorIcon::Progress => winit::window::CursorIcon::Progress,
790                                TheCursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
791                                TheCursorIcon::ContextMenu => {
792                                    winit::window::CursorIcon::ContextMenu
793                                }
794                                TheCursorIcon::Cell => winit::window::CursorIcon::Cell,
795                                TheCursorIcon::VerticalText => {
796                                    winit::window::CursorIcon::VerticalText
797                                }
798                                TheCursorIcon::Alias => winit::window::CursorIcon::Alias,
799                                TheCursorIcon::Copy => winit::window::CursorIcon::Copy,
800                                TheCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
801                                TheCursorIcon::Grab => winit::window::CursorIcon::Grab,
802                                TheCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
803                                TheCursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
804                                TheCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
805                                TheCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
806                                TheCursorIcon::EResize => winit::window::CursorIcon::EResize,
807                                TheCursorIcon::NResize => winit::window::CursorIcon::NResize,
808                                TheCursorIcon::NEResize => winit::window::CursorIcon::NeResize,
809                                TheCursorIcon::NWResize => winit::window::CursorIcon::NwResize,
810                                TheCursorIcon::SResize => winit::window::CursorIcon::SResize,
811                                TheCursorIcon::SEResize => winit::window::CursorIcon::SeResize,
812                                TheCursorIcon::SWResize => winit::window::CursorIcon::SwResize,
813                                TheCursorIcon::WResize => winit::window::CursorIcon::WResize,
814                                TheCursorIcon::EWResize => winit::window::CursorIcon::EwResize,
815                                TheCursorIcon::NSResize => winit::window::CursorIcon::NsResize,
816                                TheCursorIcon::NESWResize => winit::window::CursorIcon::NeswResize,
817                                TheCursorIcon::NWSEResize => winit::window::CursorIcon::NwseResize,
818                                TheCursorIcon::ColResize => winit::window::CursorIcon::ColResize,
819                                TheCursorIcon::RowResize => winit::window::CursorIcon::RowResize,
820                            };
821                            ctx.window.set_cursor(cursor_icon);
822                            ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
823                            ctx.ctx.reset_cursor_changed();
824                        }
825                        if ctx.ctx.cursor_visible_changed() {
826                            ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
827                            ctx.ctx.reset_cursor_visible_changed();
828                        }
829
830                        if redraw {
831                            ctx.window.request_redraw();
832                        }
833                    }
834                    WindowEvent::Touch(Touch {
835                        phase, location, ..
836                    }) => {
837                        let (x, y) = translate_coord_to_local(
838                            location.x as f32,
839                            location.y as f32,
840                            ctx.ctx.scale_factor,
841                        );
842
843                        match phase {
844                            TouchPhase::Started => {
845                                let mut redraw = false;
846                                #[cfg(feature = "ui")]
847                                {
848                                    if self.ui.touch_down(x as f32, y as f32, &mut ctx.ctx) {
849                                        redraw = true;
850                                    }
851                                }
852                                if self.app.touch_down(x as f32, y as f32, &mut ctx.ctx) {
853                                    redraw = true;
854                                }
855
856                                if redraw {
857                                    ctx.window.request_redraw();
858                                }
859                            }
860                            TouchPhase::Moved => {
861                                let mut redraw = false;
862                                #[cfg(feature = "ui")]
863                                {
864                                    if self.ui.touch_dragged(x as f32, y as f32, &mut ctx.ctx) {
865                                        redraw = true;
866                                    }
867                                }
868                                if self.app.touch_dragged(x as f32, y as f32, &mut ctx.ctx) {
869                                    redraw = true;
870                                }
871                                if redraw {
872                                    ctx.window.request_redraw();
873                                }
874                            }
875                            TouchPhase::Ended | TouchPhase::Cancelled => {
876                                let mut redraw = false;
877                                #[cfg(feature = "ui")]
878                                {
879                                    if self.ui.touch_up(x as f32, y as f32, &mut ctx.ctx) {
880                                        redraw = true;
881                                    }
882                                }
883                                if self.app.touch_up(x as f32, y as f32, &mut ctx.ctx) {
884                                    redraw = true;
885                                }
886
887                                if redraw {
888                                    ctx.window.request_redraw();
889                                }
890                            }
891                        }
892                    }
893                    WindowEvent::MouseInput { state, button, .. } => {
894                        if let Some((x, y)) = self.last_cursor_pos {
895                            let mut redraw = false;
896                            match (button, state) {
897                                (MouseButton::Left, ElementState::Pressed) => {
898                                    self.left_mouse_down = true;
899
900                                    #[cfg(feature = "ui")]
901                                    if self.ui.touch_down(x, y, &mut ctx.ctx) {
902                                        redraw = true;
903                                    }
904
905                                    if self.app.touch_down(x, y, &mut ctx.ctx) {
906                                        redraw = true;
907                                    }
908                                }
909                                (MouseButton::Left, ElementState::Released) => {
910                                    self.left_mouse_down = false;
911
912                                    #[cfg(feature = "ui")]
913                                    if self.ui.touch_up(x, y, &mut ctx.ctx) {
914                                        redraw = true;
915                                    }
916
917                                    if self.app.touch_up(x, y, &mut ctx.ctx) {
918                                        redraw = true;
919                                    }
920                                }
921                                (MouseButton::Right, ElementState::Pressed) => {
922                                    #[cfg(feature = "ui")]
923                                    if self.ui.context(x, y, &mut ctx.ctx) {
924                                        redraw = true;
925                                    }
926
927                                    if self.app.touch_down(x, y, &mut ctx.ctx) {
928                                        redraw = true;
929                                    }
930                                }
931                                (MouseButton::Right, ElementState::Released) => {
932                                    #[cfg(feature = "ui")]
933                                    if self.ui.touch_up(x, y, &mut ctx.ctx) {
934                                        redraw = true;
935                                    }
936
937                                    if self.app.touch_up(x, y, &mut ctx.ctx) {
938                                        redraw = true;
939                                    }
940                                }
941                                _ => {}
942                            }
943
944                            // Update cursor icon after mouse input events that may change focus
945                            if ctx.ctx.cursor_changed() {
946                                let cursor_icon = match ctx.ctx.cursor_icon() {
947                                    TheCursorIcon::Default => winit::window::CursorIcon::Default,
948                                    TheCursorIcon::Crosshair => {
949                                        winit::window::CursorIcon::Crosshair
950                                    }
951                                    TheCursorIcon::Hand => winit::window::CursorIcon::Pointer,
952                                    TheCursorIcon::Arrow => winit::window::CursorIcon::Default,
953                                    TheCursorIcon::Text => winit::window::CursorIcon::Text,
954                                    TheCursorIcon::Wait => winit::window::CursorIcon::Wait,
955                                    TheCursorIcon::Help => winit::window::CursorIcon::Help,
956                                    TheCursorIcon::Progress => winit::window::CursorIcon::Progress,
957                                    TheCursorIcon::NotAllowed => {
958                                        winit::window::CursorIcon::NotAllowed
959                                    }
960                                    TheCursorIcon::ContextMenu => {
961                                        winit::window::CursorIcon::ContextMenu
962                                    }
963                                    TheCursorIcon::Cell => winit::window::CursorIcon::Cell,
964                                    TheCursorIcon::VerticalText => {
965                                        winit::window::CursorIcon::VerticalText
966                                    }
967                                    TheCursorIcon::Alias => winit::window::CursorIcon::Alias,
968                                    TheCursorIcon::Copy => winit::window::CursorIcon::Copy,
969                                    TheCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
970                                    TheCursorIcon::Grab => winit::window::CursorIcon::Grab,
971                                    TheCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
972                                    TheCursorIcon::AllScroll => {
973                                        winit::window::CursorIcon::AllScroll
974                                    }
975                                    TheCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
976                                    TheCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
977                                    TheCursorIcon::EResize => winit::window::CursorIcon::EResize,
978                                    TheCursorIcon::NResize => winit::window::CursorIcon::NResize,
979                                    TheCursorIcon::NEResize => winit::window::CursorIcon::NeResize,
980                                    TheCursorIcon::NWResize => winit::window::CursorIcon::NwResize,
981                                    TheCursorIcon::SResize => winit::window::CursorIcon::SResize,
982                                    TheCursorIcon::SEResize => winit::window::CursorIcon::SeResize,
983                                    TheCursorIcon::SWResize => winit::window::CursorIcon::SwResize,
984                                    TheCursorIcon::WResize => winit::window::CursorIcon::WResize,
985                                    TheCursorIcon::EWResize => winit::window::CursorIcon::EwResize,
986                                    TheCursorIcon::NSResize => winit::window::CursorIcon::NsResize,
987                                    TheCursorIcon::NESWResize => {
988                                        winit::window::CursorIcon::NeswResize
989                                    }
990                                    TheCursorIcon::NWSEResize => {
991                                        winit::window::CursorIcon::NwseResize
992                                    }
993                                    TheCursorIcon::ColResize => {
994                                        winit::window::CursorIcon::ColResize
995                                    }
996                                    TheCursorIcon::RowResize => {
997                                        winit::window::CursorIcon::RowResize
998                                    }
999                                };
1000                                ctx.window.set_cursor(cursor_icon);
1001                                ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
1002                                ctx.ctx.reset_cursor_changed();
1003                            }
1004                            if ctx.ctx.cursor_visible_changed() {
1005                                ctx.window.set_cursor_visible(ctx.ctx.cursor_visible());
1006                                ctx.ctx.reset_cursor_visible_changed();
1007                            }
1008
1009                            if redraw {
1010                                ctx.window.request_redraw();
1011                            }
1012                        }
1013                    }
1014                    WindowEvent::MouseWheel { delta, .. } => {
1015                        let (x, y) = match delta {
1016                            MouseScrollDelta::LineDelta(x, y) => {
1017                                const LINE_HEIGHT_PX: f32 = 20.0;
1018                                (x as f32 * LINE_HEIGHT_PX, y as f32 * LINE_HEIGHT_PX)
1019                            }
1020                            MouseScrollDelta::PixelDelta(delta) => (delta.x as f32, delta.y as f32),
1021                        };
1022
1023                        let mut redraw = false;
1024                        #[cfg(feature = "ui")]
1025                        if self.ui.mouse_wheel((x as i32, y as i32), &mut ctx.ctx) {
1026                            redraw = true;
1027                        }
1028
1029                        if self.app.mouse_wheel((x as isize, y as isize), &mut ctx.ctx) {
1030                            redraw = true;
1031                        }
1032
1033                        if redraw {
1034                            ctx.window.request_redraw();
1035                        }
1036                    }
1037                    WindowEvent::DroppedFile(path) => {
1038                        self.app.dropped_file(path.to_string_lossy().into_owned());
1039                        ctx.window.request_redraw();
1040                    }
1041                    _ => {}
1042                }
1043            }
1044        }
1045    }
1046
1047    fn device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, _: DeviceEvent) {}
1048
1049    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1050        let now = Instant::now();
1051        let fps = self.app.target_fps().max(1.0);
1052        let target_frame_time = Duration::from_secs_f64(1.0 / fps);
1053        if target_frame_time != self.target_frame_time {
1054            self.target_frame_time = target_frame_time;
1055            self.next_frame_time = now + self.target_frame_time;
1056        }
1057
1058        let timer_due = now >= self.next_frame_time;
1059        #[cfg(target_arch = "wasm32")]
1060        let should_redraw = true;
1061        #[cfg(not(target_arch = "wasm32"))]
1062        let should_redraw = timer_due;
1063
1064        #[cfg(target_arch = "wasm32")]
1065        {
1066            if let Some(ctx) = &self.ctx {
1067                ctx.window.request_redraw();
1068            }
1069        }
1070
1071        #[cfg(not(target_arch = "wasm32"))]
1072        {
1073            if should_redraw {
1074                if let Some(ctx) = &self.ctx {
1075                    ctx.window.request_redraw();
1076                }
1077            }
1078        }
1079
1080        if timer_due {
1081            while self.next_frame_time <= now {
1082                self.next_frame_time += self.target_frame_time;
1083            }
1084        }
1085
1086        #[cfg(target_arch = "wasm32")]
1087        {
1088            event_loop.set_control_flow(ControlFlow::Poll);
1089        }
1090
1091        #[cfg(not(target_arch = "wasm32"))]
1092        {
1093            let now_after_schedule = Instant::now();
1094            let wake_ahead = Duration::from_millis(2);
1095            if self.next_frame_time > now_after_schedule + wake_ahead {
1096                event_loop
1097                    .set_control_flow(ControlFlow::WaitUntil(self.next_frame_time - wake_ahead));
1098            } else {
1099                // Short pre-frame poll burst improves frame deadline accuracy without full-time polling.
1100                event_loop.set_control_flow(ControlFlow::Poll);
1101            }
1102        }
1103
1104        if !should_redraw {
1105            return;
1106        }
1107
1108        let Some(window) = self.ctx.as_ref().map(|ctx| ctx.window.clone()) else {
1109            return;
1110        };
1111
1112        if self.process_updates() {
1113            window.request_redraw();
1114        }
1115    }
1116}
1117
1118pub fn run_winit_app(args: Option<Vec<String>>, app: Box<dyn TheTrait>) {
1119    #[cfg(target_arch = "wasm32")]
1120    console_error_panic_hook::set_once();
1121
1122    let mut winit_app = TheWinitApp::new(args, app);
1123
1124    let event_loop = EventLoop::new().unwrap();
1125    event_loop.run_app(&mut winit_app).unwrap();
1126}