window/
window.rs

1//! Simple winit application.
2
3use std::collections::HashMap;
4use std::error::Error;
5use std::fmt::Debug;
6#[cfg(not(any(android_platform, ios_platform)))]
7use std::num::NonZeroU32;
8use std::sync::Arc;
9use std::{fmt, mem};
10
11use ::tracing::{error, info};
12use cursor_icon::CursorIcon;
13#[cfg(not(any(android_platform, ios_platform)))]
14use rwh_06::{DisplayHandle, HasDisplayHandle};
15#[cfg(not(any(android_platform, ios_platform)))]
16use softbuffer::{Context, Surface};
17
18use winit::application::ApplicationHandler;
19use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
20use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, WindowEvent};
21use winit::event_loop::{ActiveEventLoop, EventLoop};
22use winit::keyboard::{Key, ModifiersState};
23use winit::window::{
24    Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, ResizeDirection,
25    Theme, Window, WindowId,
26};
27
28#[cfg(macos_platform)]
29use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS};
30#[cfg(any(x11_platform, wayland_platform))]
31use winit::platform::startup_notify::{
32    self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify,
33};
34#[cfg(x11_platform)]
35use winit::platform::x11::WindowAttributesExtX11;
36
37#[path = "util/tracing.rs"]
38mod tracing;
39
40/// The amount of points to around the window for drag resize direction calculations.
41const BORDER_SIZE: f64 = 20.;
42
43fn main() -> Result<(), Box<dyn Error>> {
44    #[cfg(web_platform)]
45    console_error_panic_hook::set_once();
46
47    tracing::init();
48
49    let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
50    let _event_loop_proxy = event_loop.create_proxy();
51
52    // Wire the user event from another thread.
53    #[cfg(not(web_platform))]
54    std::thread::spawn(move || {
55        // Wake up the `event_loop` once every second and dispatch a custom event
56        // from a different thread.
57        info!("Starting to send user event every second");
58        loop {
59            let _ = _event_loop_proxy.send_event(UserEvent::WakeUp);
60            std::thread::sleep(std::time::Duration::from_secs(1));
61        }
62    });
63
64    let mut state = Application::new(&event_loop);
65
66    event_loop.run_app(&mut state).map_err(Into::into)
67}
68
69#[allow(dead_code)]
70#[derive(Debug, Clone, Copy)]
71enum UserEvent {
72    WakeUp,
73}
74
75/// Application state and event handling.
76struct Application {
77    /// Custom cursors assets.
78    custom_cursors: Vec<CustomCursor>,
79    /// Application icon.
80    icon: Icon,
81    windows: HashMap<WindowId, WindowState>,
82    /// Drawing context.
83    ///
84    /// With OpenGL it could be EGLDisplay.
85    #[cfg(not(any(android_platform, ios_platform)))]
86    context: Option<Context<DisplayHandle<'static>>>,
87}
88
89impl Application {
90    fn new<T>(event_loop: &EventLoop<T>) -> Self {
91        // SAFETY: we drop the context right before the event loop is stopped, thus making it safe.
92        #[cfg(not(any(android_platform, ios_platform)))]
93        let context = Some(
94            Context::new(unsafe {
95                std::mem::transmute::<DisplayHandle<'_>, DisplayHandle<'static>>(
96                    event_loop.display_handle().unwrap(),
97                )
98            })
99            .unwrap(),
100        );
101
102        // You'll have to choose an icon size at your own discretion. On X11, the desired size
103        // varies by WM, and on Windows, you still have to account for screen scaling. Here
104        // we use 32px, since it seems to work well enough in most cases. Be careful about
105        // going too high, or you'll be bitten by the low-quality downscaling built into the
106        // WM.
107        let icon = load_icon(include_bytes!("data/icon.png"));
108
109        info!("Loading cursor assets");
110        let custom_cursors = vec![
111            event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross.png"))),
112            event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross2.png"))),
113            event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/gradient.png"))),
114        ];
115
116        Self {
117            #[cfg(not(any(android_platform, ios_platform)))]
118            context,
119            custom_cursors,
120            icon,
121            windows: Default::default(),
122        }
123    }
124
125    fn create_window(
126        &mut self,
127        event_loop: &ActiveEventLoop,
128        _tab_id: Option<String>,
129    ) -> Result<WindowId, Box<dyn Error>> {
130        // TODO read-out activation token.
131
132        #[allow(unused_mut)]
133        let mut window_attributes = Window::default_attributes()
134            .with_title("Winit window")
135            .with_transparent(true)
136            .with_window_icon(Some(self.icon.clone()));
137
138        #[cfg(any(x11_platform, wayland_platform))]
139        if let Some(token) = event_loop.read_token_from_env() {
140            startup_notify::reset_activation_token_env();
141            info!("Using token {:?} to activate a window", token);
142            window_attributes = window_attributes.with_activation_token(token);
143        }
144
145        #[cfg(x11_platform)]
146        match std::env::var("X11_VISUAL_ID") {
147            Ok(visual_id_str) => {
148                info!("Using X11 visual id {visual_id_str}");
149                let visual_id = visual_id_str.parse()?;
150                window_attributes = window_attributes.with_x11_visual(visual_id);
151            },
152            Err(_) => info!("Set the X11_VISUAL_ID env variable to request specific X11 visual"),
153        }
154
155        #[cfg(x11_platform)]
156        match std::env::var("X11_SCREEN_ID") {
157            Ok(screen_id_str) => {
158                info!("Placing the window on X11 screen {screen_id_str}");
159                let screen_id = screen_id_str.parse()?;
160                window_attributes = window_attributes.with_x11_screen(screen_id);
161            },
162            Err(_) => info!(
163                "Set the X11_SCREEN_ID env variable to place the window on non-default screen"
164            ),
165        }
166
167        #[cfg(macos_platform)]
168        if let Some(tab_id) = _tab_id {
169            window_attributes = window_attributes.with_tabbing_identifier(&tab_id);
170        }
171
172        #[cfg(web_platform)]
173        {
174            use winit::platform::web::WindowAttributesExtWebSys;
175            window_attributes = window_attributes.with_append(true);
176        }
177
178        let window = event_loop.create_window(window_attributes)?;
179
180        #[cfg(ios_platform)]
181        {
182            use winit::platform::ios::WindowExtIOS;
183            window.recognize_doubletap_gesture(true);
184            window.recognize_pinch_gesture(true);
185            window.recognize_rotation_gesture(true);
186            window.recognize_pan_gesture(true, 2, 2);
187        }
188
189        let window_state = WindowState::new(self, window)?;
190        let window_id = window_state.window.id();
191        info!("Created new window with id={window_id:?}");
192        self.windows.insert(window_id, window_state);
193        Ok(window_id)
194    }
195
196    fn handle_action(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, action: Action) {
197        // let cursor_position = self.cursor_position;
198        let window = self.windows.get_mut(&window_id).unwrap();
199        info!("Executing action: {action:?}");
200        match action {
201            Action::CloseWindow => {
202                let _ = self.windows.remove(&window_id);
203            },
204            Action::CreateNewWindow => {
205                #[cfg(any(x11_platform, wayland_platform))]
206                if let Err(err) = window.window.request_activation_token() {
207                    info!("Failed to get activation token: {err}");
208                } else {
209                    return;
210                }
211
212                if let Err(err) = self.create_window(event_loop, None) {
213                    error!("Error creating new window: {err}");
214                }
215            },
216            Action::ToggleResizeIncrements => window.toggle_resize_increments(),
217            Action::ToggleCursorVisibility => window.toggle_cursor_visibility(),
218            Action::ToggleResizable => window.toggle_resizable(),
219            Action::ToggleDecorations => window.toggle_decorations(),
220            Action::ToggleFullscreen => window.toggle_fullscreen(),
221            Action::ToggleMaximize => window.toggle_maximize(),
222            Action::ToggleImeInput => window.toggle_ime(),
223            Action::Minimize => window.minimize(),
224            Action::NextCursor => window.next_cursor(),
225            Action::NextCustomCursor => window.next_custom_cursor(&self.custom_cursors),
226            #[cfg(web_platform)]
227            Action::UrlCustomCursor => window.url_custom_cursor(event_loop),
228            #[cfg(web_platform)]
229            Action::AnimationCustomCursor => {
230                window.animation_custom_cursor(event_loop, &self.custom_cursors)
231            },
232            Action::CycleCursorGrab => window.cycle_cursor_grab(),
233            Action::DragWindow => window.drag_window(),
234            Action::DragResizeWindow => window.drag_resize_window(),
235            Action::ShowWindowMenu => window.show_menu(),
236            Action::PrintHelp => self.print_help(),
237            #[cfg(macos_platform)]
238            Action::CycleOptionAsAlt => window.cycle_option_as_alt(),
239            Action::SetTheme(theme) => {
240                window.window.set_theme(theme);
241                // Get the resulting current theme to draw with
242                let actual_theme = theme.or_else(|| window.window.theme()).unwrap_or(Theme::Dark);
243                window.set_draw_theme(actual_theme);
244            },
245            #[cfg(macos_platform)]
246            Action::CreateNewTab => {
247                let tab_id = window.window.tabbing_identifier();
248                if let Err(err) = self.create_window(event_loop, Some(tab_id)) {
249                    error!("Error creating new window: {err}");
250                }
251            },
252            Action::RequestResize => window.swap_dimensions(),
253        }
254    }
255
256    fn dump_monitors(&self, event_loop: &ActiveEventLoop) {
257        info!("Monitors information");
258        let primary_monitor = event_loop.primary_monitor();
259        for monitor in event_loop.available_monitors() {
260            let intro = if primary_monitor.as_ref() == Some(&monitor) {
261                "Primary monitor"
262            } else {
263                "Monitor"
264            };
265
266            if let Some(name) = monitor.name() {
267                info!("{intro}: {name}");
268            } else {
269                info!("{intro}: [no name]");
270            }
271
272            let PhysicalSize { width, height } = monitor.size();
273            info!(
274                "  Current mode: {width}x{height}{}",
275                if let Some(m_hz) = monitor.refresh_rate_millihertz() {
276                    format!(" @ {}.{} Hz", m_hz / 1000, m_hz % 1000)
277                } else {
278                    String::new()
279                }
280            );
281
282            let PhysicalPosition { x, y } = monitor.position();
283            info!("  Position: {x},{y}");
284
285            info!("  Scale factor: {}", monitor.scale_factor());
286
287            info!("  Available modes (width x height x bit-depth):");
288            for mode in monitor.video_modes() {
289                let PhysicalSize { width, height } = mode.size();
290                let bits = mode.bit_depth();
291                let m_hz = mode.refresh_rate_millihertz();
292                info!("    {width}x{height}x{bits} @ {}.{} Hz", m_hz / 1000, m_hz % 1000);
293            }
294        }
295    }
296
297    /// Process the key binding.
298    fn process_key_binding(key: &str, mods: &ModifiersState) -> Option<Action> {
299        KEY_BINDINGS
300            .iter()
301            .find_map(|binding| binding.is_triggered_by(&key, mods).then_some(binding.action))
302    }
303
304    /// Process mouse binding.
305    fn process_mouse_binding(button: MouseButton, mods: &ModifiersState) -> Option<Action> {
306        MOUSE_BINDINGS
307            .iter()
308            .find_map(|binding| binding.is_triggered_by(&button, mods).then_some(binding.action))
309    }
310
311    fn print_help(&self) {
312        info!("Keyboard bindings:");
313        for binding in KEY_BINDINGS {
314            info!(
315                "{}{:<10} - {} ({})",
316                modifiers_to_string(binding.mods),
317                binding.trigger,
318                binding.action,
319                binding.action.help(),
320            );
321        }
322        info!("Mouse bindings:");
323        for binding in MOUSE_BINDINGS {
324            info!(
325                "{}{:<10} - {} ({})",
326                modifiers_to_string(binding.mods),
327                mouse_button_to_string(binding.trigger),
328                binding.action,
329                binding.action.help(),
330            );
331        }
332    }
333}
334
335impl ApplicationHandler<UserEvent> for Application {
336    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
337        info!("User event: {event:?}");
338    }
339
340    fn window_event(
341        &mut self,
342        event_loop: &ActiveEventLoop,
343        window_id: WindowId,
344        event: WindowEvent,
345    ) {
346        let window = match self.windows.get_mut(&window_id) {
347            Some(window) => window,
348            None => return,
349        };
350
351        match event {
352            WindowEvent::Resized(size) => {
353                window.resize(size);
354            },
355            WindowEvent::Focused(focused) => {
356                if focused {
357                    info!("Window={window_id:?} focused");
358                } else {
359                    info!("Window={window_id:?} unfocused");
360                }
361            },
362            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
363                info!("Window={window_id:?} changed scale to {scale_factor}");
364            },
365            WindowEvent::ThemeChanged(theme) => {
366                info!("Theme changed to {theme:?}");
367                window.set_draw_theme(theme);
368            },
369            WindowEvent::RedrawRequested => {
370                if let Err(err) = window.draw() {
371                    error!("Error drawing window: {err}");
372                }
373            },
374            WindowEvent::Occluded(occluded) => {
375                window.set_occluded(occluded);
376            },
377            WindowEvent::CloseRequested => {
378                info!("Closing Window={window_id:?}");
379                self.windows.remove(&window_id);
380            },
381            WindowEvent::ModifiersChanged(modifiers) => {
382                window.modifiers = modifiers.state();
383                info!("Modifiers changed to {:?}", window.modifiers);
384            },
385            WindowEvent::MouseWheel { delta, .. } => match delta {
386                MouseScrollDelta::LineDelta(x, y) => {
387                    info!("Mouse wheel Line Delta: ({x},{y})");
388                },
389                MouseScrollDelta::PixelDelta(px) => {
390                    info!("Mouse wheel Pixel Delta: ({},{})", px.x, px.y);
391                },
392            },
393            WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => {
394                let mods = window.modifiers;
395
396                // Dispatch actions only on press.
397                if event.state.is_pressed() {
398                    let action = if let Key::Character(ch) = event.logical_key.as_ref() {
399                        Self::process_key_binding(&ch.to_uppercase(), &mods)
400                    } else {
401                        None
402                    };
403
404                    if let Some(action) = action {
405                        self.handle_action(event_loop, window_id, action);
406                    }
407                }
408            },
409            WindowEvent::MouseInput { button, state, .. } => {
410                let mods = window.modifiers;
411                if let Some(action) =
412                    state.is_pressed().then(|| Self::process_mouse_binding(button, &mods)).flatten()
413                {
414                    self.handle_action(event_loop, window_id, action);
415                }
416            },
417            WindowEvent::CursorLeft { .. } => {
418                info!("Cursor left Window={window_id:?}");
419                window.cursor_left();
420            },
421            WindowEvent::CursorMoved { position, .. } => {
422                info!("Moved cursor to {position:?}");
423                window.cursor_moved(position);
424            },
425            WindowEvent::ActivationTokenDone { token: _token, .. } => {
426                #[cfg(any(x11_platform, wayland_platform))]
427                {
428                    startup_notify::set_activation_token_env(_token);
429                    if let Err(err) = self.create_window(event_loop, None) {
430                        error!("Error creating new window: {err}");
431                    }
432                }
433            },
434            WindowEvent::Ime(event) => match event {
435                Ime::Enabled => info!("IME enabled for Window={window_id:?}"),
436                Ime::Preedit(text, caret_pos) => {
437                    info!("Preedit: {}, with caret at {:?}", text, caret_pos);
438                },
439                Ime::Commit(text) => {
440                    info!("Committed: {}", text);
441                },
442                Ime::Disabled => info!("IME disabled for Window={window_id:?}"),
443            },
444            WindowEvent::PinchGesture { delta, .. } => {
445                window.zoom += delta;
446                let zoom = window.zoom;
447                if delta > 0.0 {
448                    info!("Zoomed in {delta:.5} (now: {zoom:.5})");
449                } else {
450                    info!("Zoomed out {delta:.5} (now: {zoom:.5})");
451                }
452            },
453            WindowEvent::RotationGesture { delta, .. } => {
454                window.rotated += delta;
455                let rotated = window.rotated;
456                if delta > 0.0 {
457                    info!("Rotated counterclockwise {delta:.5} (now: {rotated:.5})");
458                } else {
459                    info!("Rotated clockwise {delta:.5} (now: {rotated:.5})");
460                }
461            },
462            WindowEvent::PanGesture { delta, phase, .. } => {
463                window.panned.x += delta.x;
464                window.panned.y += delta.y;
465                info!("Panned ({delta:?})) (now: {:?}), {phase:?}", window.panned);
466            },
467            WindowEvent::DoubleTapGesture { .. } => {
468                info!("Smart zoom");
469            },
470            WindowEvent::TouchpadPressure { .. }
471            | WindowEvent::HoveredFileCancelled
472            | WindowEvent::KeyboardInput { .. }
473            | WindowEvent::CursorEntered { .. }
474            | WindowEvent::AxisMotion { .. }
475            | WindowEvent::DroppedFile(_)
476            | WindowEvent::HoveredFile(_)
477            | WindowEvent::Destroyed
478            | WindowEvent::Touch(_)
479            | WindowEvent::Moved(_) => (),
480        }
481    }
482
483    fn device_event(
484        &mut self,
485        _event_loop: &ActiveEventLoop,
486        device_id: DeviceId,
487        event: DeviceEvent,
488    ) {
489        info!("Device {device_id:?} event: {event:?}");
490    }
491
492    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
493        info!("Resumed the event loop");
494        self.dump_monitors(event_loop);
495
496        // Create initial window.
497        self.create_window(event_loop, None).expect("failed to create initial window");
498
499        self.print_help();
500    }
501
502    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
503        if self.windows.is_empty() {
504            info!("No windows left, exiting...");
505            event_loop.exit();
506        }
507    }
508
509    #[cfg(not(any(android_platform, ios_platform)))]
510    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
511        // We must drop the context here.
512        self.context = None;
513    }
514}
515
516/// State of the window.
517struct WindowState {
518    /// IME input.
519    ime: bool,
520    /// Render surface.
521    ///
522    /// NOTE: This surface must be dropped before the `Window`.
523    #[cfg(not(any(android_platform, ios_platform)))]
524    surface: Surface<DisplayHandle<'static>, Arc<Window>>,
525    /// The actual winit Window.
526    window: Arc<Window>,
527    /// The window theme we're drawing with.
528    theme: Theme,
529    /// Cursor position over the window.
530    cursor_position: Option<PhysicalPosition<f64>>,
531    /// Window modifiers state.
532    modifiers: ModifiersState,
533    /// Occlusion state of the window.
534    occluded: bool,
535    /// Current cursor grab mode.
536    cursor_grab: CursorGrabMode,
537    /// The amount of zoom into window.
538    zoom: f64,
539    /// The amount of rotation of the window.
540    rotated: f32,
541    /// The amount of pan of the window.
542    panned: PhysicalPosition<f32>,
543
544    #[cfg(macos_platform)]
545    option_as_alt: OptionAsAlt,
546
547    // Cursor states.
548    named_idx: usize,
549    custom_idx: usize,
550    cursor_hidden: bool,
551}
552
553impl WindowState {
554    fn new(app: &Application, window: Window) -> Result<Self, Box<dyn Error>> {
555        let window = Arc::new(window);
556
557        // SAFETY: the surface is dropped before the `window` which provided it with handle, thus
558        // it doesn't outlive it.
559        #[cfg(not(any(android_platform, ios_platform)))]
560        let surface = Surface::new(app.context.as_ref().unwrap(), Arc::clone(&window))?;
561
562        let theme = window.theme().unwrap_or(Theme::Dark);
563        info!("Theme: {theme:?}");
564        let named_idx = 0;
565        window.set_cursor(CURSORS[named_idx]);
566
567        // Allow IME out of the box.
568        let ime = true;
569        window.set_ime_allowed(ime);
570
571        let size = window.inner_size();
572        let mut state = Self {
573            #[cfg(macos_platform)]
574            option_as_alt: window.option_as_alt(),
575            custom_idx: app.custom_cursors.len() - 1,
576            cursor_grab: CursorGrabMode::None,
577            named_idx,
578            #[cfg(not(any(android_platform, ios_platform)))]
579            surface,
580            window,
581            theme,
582            ime,
583            cursor_position: Default::default(),
584            cursor_hidden: Default::default(),
585            modifiers: Default::default(),
586            occluded: Default::default(),
587            rotated: Default::default(),
588            panned: Default::default(),
589            zoom: Default::default(),
590        };
591
592        state.resize(size);
593        Ok(state)
594    }
595
596    pub fn toggle_ime(&mut self) {
597        self.ime = !self.ime;
598        self.window.set_ime_allowed(self.ime);
599        if let Some(position) = self.ime.then_some(self.cursor_position).flatten() {
600            self.window.set_ime_cursor_area(position, PhysicalSize::new(20, 20));
601        }
602    }
603
604    pub fn minimize(&mut self) {
605        self.window.set_minimized(true);
606    }
607
608    pub fn cursor_moved(&mut self, position: PhysicalPosition<f64>) {
609        self.cursor_position = Some(position);
610        if self.ime {
611            self.window.set_ime_cursor_area(position, PhysicalSize::new(20, 20));
612        }
613    }
614
615    pub fn cursor_left(&mut self) {
616        self.cursor_position = None;
617    }
618
619    /// Toggle maximized.
620    fn toggle_maximize(&self) {
621        let maximized = self.window.is_maximized();
622        self.window.set_maximized(!maximized);
623    }
624
625    /// Toggle window decorations.
626    fn toggle_decorations(&self) {
627        let decorated = self.window.is_decorated();
628        self.window.set_decorations(!decorated);
629    }
630
631    /// Toggle window resizable state.
632    fn toggle_resizable(&self) {
633        let resizable = self.window.is_resizable();
634        self.window.set_resizable(!resizable);
635    }
636
637    /// Toggle cursor visibility
638    fn toggle_cursor_visibility(&mut self) {
639        self.cursor_hidden = !self.cursor_hidden;
640        self.window.set_cursor_visible(!self.cursor_hidden);
641    }
642
643    /// Toggle resize increments on a window.
644    fn toggle_resize_increments(&mut self) {
645        let new_increments = match self.window.resize_increments() {
646            Some(_) => None,
647            None => Some(LogicalSize::new(25.0, 25.0)),
648        };
649        info!("Had increments: {}", new_increments.is_none());
650        self.window.set_resize_increments(new_increments);
651    }
652
653    /// Toggle fullscreen.
654    fn toggle_fullscreen(&self) {
655        let fullscreen = if self.window.fullscreen().is_some() {
656            None
657        } else {
658            Some(Fullscreen::Borderless(None))
659        };
660
661        self.window.set_fullscreen(fullscreen);
662    }
663
664    /// Cycle through the grab modes ignoring errors.
665    fn cycle_cursor_grab(&mut self) {
666        self.cursor_grab = match self.cursor_grab {
667            CursorGrabMode::None => CursorGrabMode::Confined,
668            CursorGrabMode::Confined => CursorGrabMode::Locked,
669            CursorGrabMode::Locked => CursorGrabMode::None,
670        };
671        info!("Changing cursor grab mode to {:?}", self.cursor_grab);
672        if let Err(err) = self.window.set_cursor_grab(self.cursor_grab) {
673            error!("Error setting cursor grab: {err}");
674        }
675    }
676
677    #[cfg(macos_platform)]
678    fn cycle_option_as_alt(&mut self) {
679        self.option_as_alt = match self.option_as_alt {
680            OptionAsAlt::None => OptionAsAlt::OnlyLeft,
681            OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight,
682            OptionAsAlt::OnlyRight => OptionAsAlt::Both,
683            OptionAsAlt::Both => OptionAsAlt::None,
684        };
685        info!("Setting option as alt {:?}", self.option_as_alt);
686        self.window.set_option_as_alt(self.option_as_alt);
687    }
688
689    /// Swap the window dimensions with `request_inner_size`.
690    fn swap_dimensions(&mut self) {
691        let old_inner_size = self.window.inner_size();
692        let mut inner_size = old_inner_size;
693
694        mem::swap(&mut inner_size.width, &mut inner_size.height);
695        info!("Requesting resize from {old_inner_size:?} to {inner_size:?}");
696
697        if let Some(new_inner_size) = self.window.request_inner_size(inner_size) {
698            if old_inner_size == new_inner_size {
699                info!("Inner size change got ignored");
700            } else {
701                self.resize(new_inner_size);
702            }
703        } else {
704            info!("Request inner size is asynchronous");
705        }
706    }
707
708    /// Pick the next cursor.
709    fn next_cursor(&mut self) {
710        self.named_idx = (self.named_idx + 1) % CURSORS.len();
711        info!("Setting cursor to \"{:?}\"", CURSORS[self.named_idx]);
712        self.window.set_cursor(Cursor::Icon(CURSORS[self.named_idx]));
713    }
714
715    /// Pick the next custom cursor.
716    fn next_custom_cursor(&mut self, custom_cursors: &[CustomCursor]) {
717        self.custom_idx = (self.custom_idx + 1) % custom_cursors.len();
718        let cursor = Cursor::Custom(custom_cursors[self.custom_idx].clone());
719        self.window.set_cursor(cursor);
720    }
721
722    /// Custom cursor from an URL.
723    #[cfg(web_platform)]
724    fn url_custom_cursor(&mut self, event_loop: &ActiveEventLoop) {
725        let cursor = event_loop.create_custom_cursor(url_custom_cursor());
726
727        self.window.set_cursor(cursor);
728    }
729
730    /// Custom cursor from a URL.
731    #[cfg(web_platform)]
732    fn animation_custom_cursor(
733        &mut self,
734        event_loop: &ActiveEventLoop,
735        custom_cursors: &[CustomCursor],
736    ) {
737        use std::time::Duration;
738        use winit::platform::web::CustomCursorExtWebSys;
739
740        let cursors = vec![
741            custom_cursors[0].clone(),
742            custom_cursors[1].clone(),
743            event_loop.create_custom_cursor(url_custom_cursor()),
744        ];
745        let cursor = CustomCursor::from_animation(Duration::from_secs(3), cursors).unwrap();
746        let cursor = event_loop.create_custom_cursor(cursor);
747
748        self.window.set_cursor(cursor);
749    }
750
751    /// Resize the window to the new size.
752    fn resize(&mut self, size: PhysicalSize<u32>) {
753        info!("Resized to {size:?}");
754        #[cfg(not(any(android_platform, ios_platform)))]
755        {
756            let (width, height) = match (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
757            {
758                (Some(width), Some(height)) => (width, height),
759                _ => return,
760            };
761            self.surface.resize(width, height).expect("failed to resize inner buffer");
762        }
763        self.window.request_redraw();
764    }
765
766    /// Change the theme that things are drawn in.
767    fn set_draw_theme(&mut self, theme: Theme) {
768        self.theme = theme;
769        self.window.request_redraw();
770    }
771
772    /// Show window menu.
773    fn show_menu(&self) {
774        if let Some(position) = self.cursor_position {
775            self.window.show_window_menu(position);
776        }
777    }
778
779    /// Drag the window.
780    fn drag_window(&self) {
781        if let Err(err) = self.window.drag_window() {
782            info!("Error starting window drag: {err}");
783        } else {
784            info!("Dragging window Window={:?}", self.window.id());
785        }
786    }
787
788    /// Drag-resize the window.
789    fn drag_resize_window(&self) {
790        let position = match self.cursor_position {
791            Some(position) => position,
792            None => {
793                info!("Drag-resize requires cursor to be inside the window");
794                return;
795            },
796        };
797
798        let win_size = self.window.inner_size();
799        let border_size = BORDER_SIZE * self.window.scale_factor();
800
801        let x_direction = if position.x < border_size {
802            ResizeDirection::West
803        } else if position.x > (win_size.width as f64 - border_size) {
804            ResizeDirection::East
805        } else {
806            // Use arbitrary direction instead of None for simplicity.
807            ResizeDirection::SouthEast
808        };
809
810        let y_direction = if position.y < border_size {
811            ResizeDirection::North
812        } else if position.y > (win_size.height as f64 - border_size) {
813            ResizeDirection::South
814        } else {
815            // Use arbitrary direction instead of None for simplicity.
816            ResizeDirection::SouthEast
817        };
818
819        let direction = match (x_direction, y_direction) {
820            (ResizeDirection::West, ResizeDirection::North) => ResizeDirection::NorthWest,
821            (ResizeDirection::West, ResizeDirection::South) => ResizeDirection::SouthWest,
822            (ResizeDirection::West, _) => ResizeDirection::West,
823            (ResizeDirection::East, ResizeDirection::North) => ResizeDirection::NorthEast,
824            (ResizeDirection::East, ResizeDirection::South) => ResizeDirection::SouthEast,
825            (ResizeDirection::East, _) => ResizeDirection::East,
826            (_, ResizeDirection::South) => ResizeDirection::South,
827            (_, ResizeDirection::North) => ResizeDirection::North,
828            _ => return,
829        };
830
831        if let Err(err) = self.window.drag_resize_window(direction) {
832            info!("Error starting window drag-resize: {err}");
833        } else {
834            info!("Drag-resizing window Window={:?}", self.window.id());
835        }
836    }
837
838    /// Change window occlusion state.
839    fn set_occluded(&mut self, occluded: bool) {
840        self.occluded = occluded;
841        if !occluded {
842            self.window.request_redraw();
843        }
844    }
845
846    /// Draw the window contents.
847    #[cfg(not(any(android_platform, ios_platform)))]
848    fn draw(&mut self) -> Result<(), Box<dyn Error>> {
849        if self.occluded {
850            info!("Skipping drawing occluded window={:?}", self.window.id());
851            return Ok(());
852        }
853
854        const WHITE: u32 = 0xffffffff;
855        const DARK_GRAY: u32 = 0xff181818;
856
857        let color = match self.theme {
858            Theme::Light => WHITE,
859            Theme::Dark => DARK_GRAY,
860        };
861
862        let mut buffer = self.surface.buffer_mut()?;
863        buffer.fill(color);
864        self.window.pre_present_notify();
865        buffer.present()?;
866        Ok(())
867    }
868
869    #[cfg(any(android_platform, ios_platform))]
870    fn draw(&mut self) -> Result<(), Box<dyn Error>> {
871        info!("Drawing but without rendering...");
872        Ok(())
873    }
874}
875
876struct Binding<T: Eq> {
877    trigger: T,
878    mods: ModifiersState,
879    action: Action,
880}
881
882impl<T: Eq> Binding<T> {
883    const fn new(trigger: T, mods: ModifiersState, action: Action) -> Self {
884        Self { trigger, mods, action }
885    }
886
887    fn is_triggered_by(&self, trigger: &T, mods: &ModifiersState) -> bool {
888        &self.trigger == trigger && &self.mods == mods
889    }
890}
891
892#[derive(Debug, Clone, Copy, PartialEq, Eq)]
893enum Action {
894    CloseWindow,
895    ToggleCursorVisibility,
896    CreateNewWindow,
897    ToggleResizeIncrements,
898    ToggleImeInput,
899    ToggleDecorations,
900    ToggleResizable,
901    ToggleFullscreen,
902    ToggleMaximize,
903    Minimize,
904    NextCursor,
905    NextCustomCursor,
906    #[cfg(web_platform)]
907    UrlCustomCursor,
908    #[cfg(web_platform)]
909    AnimationCustomCursor,
910    CycleCursorGrab,
911    PrintHelp,
912    DragWindow,
913    DragResizeWindow,
914    ShowWindowMenu,
915    #[cfg(macos_platform)]
916    CycleOptionAsAlt,
917    SetTheme(Option<Theme>),
918    #[cfg(macos_platform)]
919    CreateNewTab,
920    RequestResize,
921}
922
923impl Action {
924    fn help(&self) -> &'static str {
925        match self {
926            Action::CloseWindow => "Close window",
927            Action::ToggleCursorVisibility => "Hide cursor",
928            Action::CreateNewWindow => "Create new window",
929            Action::ToggleImeInput => "Toggle IME input",
930            Action::ToggleDecorations => "Toggle decorations",
931            Action::ToggleResizable => "Toggle window resizable state",
932            Action::ToggleFullscreen => "Toggle fullscreen",
933            Action::ToggleMaximize => "Maximize",
934            Action::Minimize => "Minimize",
935            Action::ToggleResizeIncrements => "Use resize increments when resizing window",
936            Action::NextCursor => "Advance the cursor to the next value",
937            Action::NextCustomCursor => "Advance custom cursor to the next value",
938            #[cfg(web_platform)]
939            Action::UrlCustomCursor => "Custom cursor from an URL",
940            #[cfg(web_platform)]
941            Action::AnimationCustomCursor => "Custom cursor from an animation",
942            Action::CycleCursorGrab => "Cycle through cursor grab mode",
943            Action::PrintHelp => "Print help",
944            Action::DragWindow => "Start window drag",
945            Action::DragResizeWindow => "Start window drag-resize",
946            Action::ShowWindowMenu => "Show window menu",
947            #[cfg(macos_platform)]
948            Action::CycleOptionAsAlt => "Cycle option as alt mode",
949            Action::SetTheme(None) => "Change to the system theme",
950            Action::SetTheme(Some(Theme::Light)) => "Change to a light theme",
951            Action::SetTheme(Some(Theme::Dark)) => "Change to a dark theme",
952            #[cfg(macos_platform)]
953            Action::CreateNewTab => "Create new tab",
954            Action::RequestResize => "Request a resize",
955        }
956    }
957}
958
959impl fmt::Display for Action {
960    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
961        Debug::fmt(&self, f)
962    }
963}
964
965fn decode_cursor(bytes: &[u8]) -> CustomCursorSource {
966    let img = image::load_from_memory(bytes).unwrap().to_rgba8();
967    let samples = img.into_flat_samples();
968    let (_, w, h) = samples.extents();
969    let (w, h) = (w as u16, h as u16);
970    CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap()
971}
972
973#[cfg(web_platform)]
974fn url_custom_cursor() -> CustomCursorSource {
975    use std::sync::atomic::{AtomicU64, Ordering};
976
977    use winit::platform::web::CustomCursorExtWebSys;
978
979    static URL_COUNTER: AtomicU64 = AtomicU64::new(0);
980
981    CustomCursor::from_url(
982        format!("https://picsum.photos/128?random={}", URL_COUNTER.fetch_add(1, Ordering::Relaxed)),
983        64,
984        64,
985    )
986}
987
988fn load_icon(bytes: &[u8]) -> Icon {
989    let (icon_rgba, icon_width, icon_height) = {
990        let image = image::load_from_memory(bytes).unwrap().into_rgba8();
991        let (width, height) = image.dimensions();
992        let rgba = image.into_raw();
993        (rgba, width, height)
994    };
995    Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
996}
997
998fn modifiers_to_string(mods: ModifiersState) -> String {
999    let mut mods_line = String::new();
1000    // Always add + since it's printed as a part of the bindings.
1001    for (modifier, desc) in [
1002        (ModifiersState::SUPER, "Super+"),
1003        (ModifiersState::ALT, "Alt+"),
1004        (ModifiersState::CONTROL, "Ctrl+"),
1005        (ModifiersState::SHIFT, "Shift+"),
1006    ] {
1007        if !mods.contains(modifier) {
1008            continue;
1009        }
1010
1011        mods_line.push_str(desc);
1012    }
1013    mods_line
1014}
1015
1016fn mouse_button_to_string(button: MouseButton) -> &'static str {
1017    match button {
1018        MouseButton::Left => "LMB",
1019        MouseButton::Right => "RMB",
1020        MouseButton::Middle => "MMB",
1021        MouseButton::Back => "Back",
1022        MouseButton::Forward => "Forward",
1023        MouseButton::Other(_) => "",
1024    }
1025}
1026
1027/// Cursor list to cycle through.
1028const CURSORS: &[CursorIcon] = &[
1029    CursorIcon::Default,
1030    CursorIcon::Crosshair,
1031    CursorIcon::Pointer,
1032    CursorIcon::Move,
1033    CursorIcon::Text,
1034    CursorIcon::Wait,
1035    CursorIcon::Help,
1036    CursorIcon::Progress,
1037    CursorIcon::NotAllowed,
1038    CursorIcon::ContextMenu,
1039    CursorIcon::Cell,
1040    CursorIcon::VerticalText,
1041    CursorIcon::Alias,
1042    CursorIcon::Copy,
1043    CursorIcon::NoDrop,
1044    CursorIcon::Grab,
1045    CursorIcon::Grabbing,
1046    CursorIcon::AllScroll,
1047    CursorIcon::ZoomIn,
1048    CursorIcon::ZoomOut,
1049    CursorIcon::EResize,
1050    CursorIcon::NResize,
1051    CursorIcon::NeResize,
1052    CursorIcon::NwResize,
1053    CursorIcon::SResize,
1054    CursorIcon::SeResize,
1055    CursorIcon::SwResize,
1056    CursorIcon::WResize,
1057    CursorIcon::EwResize,
1058    CursorIcon::NsResize,
1059    CursorIcon::NeswResize,
1060    CursorIcon::NwseResize,
1061    CursorIcon::ColResize,
1062    CursorIcon::RowResize,
1063];
1064
1065const KEY_BINDINGS: &[Binding<&'static str>] = &[
1066    Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow),
1067    Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp),
1068    Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen),
1069    Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations),
1070    Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput),
1071    Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab),
1072    Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements),
1073    Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable),
1074    Binding::new("R", ModifiersState::ALT, Action::RequestResize),
1075    // M.
1076    Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize),
1077    Binding::new("M", ModifiersState::ALT, Action::Minimize),
1078    // N.
1079    Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow),
1080    // C.
1081    Binding::new("C", ModifiersState::CONTROL, Action::NextCursor),
1082    Binding::new("C", ModifiersState::ALT, Action::NextCustomCursor),
1083    #[cfg(web_platform)]
1084    Binding::new(
1085        "C",
1086        ModifiersState::CONTROL.union(ModifiersState::SHIFT),
1087        Action::UrlCustomCursor,
1088    ),
1089    #[cfg(web_platform)]
1090    Binding::new(
1091        "C",
1092        ModifiersState::ALT.union(ModifiersState::SHIFT),
1093        Action::AnimationCustomCursor,
1094    ),
1095    Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility),
1096    // K.
1097    Binding::new("K", ModifiersState::empty(), Action::SetTheme(None)),
1098    Binding::new("K", ModifiersState::SUPER, Action::SetTheme(Some(Theme::Light))),
1099    Binding::new("K", ModifiersState::CONTROL, Action::SetTheme(Some(Theme::Dark))),
1100    #[cfg(macos_platform)]
1101    Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab),
1102    #[cfg(macos_platform)]
1103    Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt),
1104];
1105
1106const MOUSE_BINDINGS: &[Binding<MouseButton>] = &[
1107    Binding::new(MouseButton::Left, ModifiersState::ALT, Action::DragResizeWindow),
1108    Binding::new(MouseButton::Left, ModifiersState::CONTROL, Action::DragWindow),
1109    Binding::new(MouseButton::Right, ModifiersState::CONTROL, Action::ShowWindowMenu),
1110];