1use 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
38const 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 #[cfg(not(web_platform))]
52 std::thread::spawn(move || {
53 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
73struct Application {
75 custom_cursors: Vec<CustomCursor>,
77 icon: Icon,
79 windows: HashMap<WindowId, WindowState>,
80 #[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 #[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 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 #[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 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 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 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 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 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 self.context = None;
483 }
484}
485
486struct WindowState {
488 ime: bool,
490 #[cfg(not(any(android_platform, ios_platform)))]
494 surface: Surface<DisplayHandle<'static>, Arc<Window>>,
495 window: Arc<Window>,
497 theme: Theme,
499 cursor_position: Option<PhysicalPosition<f64>>,
501 modifiers: ModifiersState,
503 occluded: bool,
505 cursor_grab: CursorGrabMode,
507 zoom: f64,
509 rotated: f32,
511 panned: PhysicalPosition<f32>,
513
514 #[cfg(macos_platform)]
515 option_as_alt: OptionAsAlt,
516
517 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 #[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 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 fn toggle_maximize(&self) {
591 let maximized = self.window.is_maximized();
592 self.window.set_maximized(!maximized);
593 }
594
595 fn toggle_decorations(&self) {
597 let decorated = self.window.is_decorated();
598 self.window.set_decorations(!decorated);
599 }
600
601 fn toggle_resizable(&self) {
603 let resizable = self.window.is_resizable();
604 self.window.set_resizable(!resizable);
605 }
606
607 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 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 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 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 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 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 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 #[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 #[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 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 fn set_theme(&mut self, theme: Theme) {
738 self.theme = theme;
739 self.window.request_redraw();
740 }
741
742 fn show_menu(&self) {
744 if let Some(position) = self.cursor_position {
745 self.window.show_window_menu(position);
746 }
747 }
748
749 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 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 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 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 fn set_occluded(&mut self, occluded: bool) {
810 self.occluded = occluded;
811 if !occluded {
812 self.window.request_redraw();
813 }
814 }
815
816 #[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 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
993const 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 Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize),
1043 Binding::new("M", ModifiersState::ALT, Action::Minimize),
1044 Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow),
1046 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];