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