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