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