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 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
43const 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 #[cfg(not(web_platform))]
57 std::thread::spawn(move || {
58 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
78struct Application {
80 custom_cursors: Vec<CustomCursor>,
82 icon: Icon,
84 windows: HashMap<WindowId, WindowState>,
85 #[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 #[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 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 #[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 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 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 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 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 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 self.context = None;
514 }
515}
516
517struct WindowState {
519 ime: bool,
521 #[cfg(not(any(android_platform, ios_platform)))]
525 surface: Surface<DisplayHandle<'static>, Arc<Window>>,
526 window: Arc<Window>,
528 theme: Theme,
530 cursor_position: Option<PhysicalPosition<f64>>,
532 modifiers: ModifiersState,
534 occluded: bool,
536 cursor_grab: CursorGrabMode,
538 zoom: f64,
540 rotated: f32,
542 panned: PhysicalPosition<f32>,
544
545 #[cfg(macos_platform)]
546 option_as_alt: OptionAsAlt,
547
548 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 #[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 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 fn toggle_maximize(&self) {
624 let maximized = self.window.is_maximized();
625 self.window.set_maximized(!maximized);
626 }
627
628 fn toggle_decorations(&self) {
630 let decorated = self.window.is_decorated();
631 self.window.set_decorations(!decorated);
632 }
633
634 fn toggle_resizable(&self) {
636 let resizable = self.window.is_resizable();
637 self.window.set_resizable(!resizable);
638 }
639
640 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 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 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 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 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 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 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 #[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 #[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 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 fn set_theme(&mut self, theme: Theme) {
775 self.theme = theme;
776 self.window.request_redraw();
777 }
778
779 fn show_menu(&self) {
781 if let Some(position) = self.cursor_position {
782 self.window.show_window_menu(position);
783 }
784 }
785
786 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 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 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 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 fn set_occluded(&mut self, occluded: bool) {
847 self.occluded = occluded;
848 if !occluded {
849 self.window.request_redraw();
850 }
851 }
852
853 #[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 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
1039const 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 Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize),
1089 Binding::new("M", ModifiersState::ALT, Action::Minimize),
1090 Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow),
1092 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];