1use std::collections::HashMap;
4use std::error::Error;
5use std::fmt::Debug;
6#[cfg(not(android_platform))]
7use std::num::NonZeroU32;
8use std::sync::mpsc::{self, Receiver, Sender};
9use std::sync::Arc;
10use std::{fmt, mem};
11
12use ::tracing::{error, info};
13use cursor_icon::CursorIcon;
14#[cfg(not(android_platform))]
15use rwh_06::{DisplayHandle, HasDisplayHandle};
16#[cfg(not(android_platform))]
17use softbuffer::{Context, Surface};
18use winit::application::ApplicationHandler;
19use winit::cursor::{Cursor, CustomCursor, CustomCursorSource};
20use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
21use winit::error::RequestError;
22use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, WindowEvent};
23use winit::event_loop::{ActiveEventLoop, EventLoop};
24use winit::icon::{Icon, RgbaIcon};
25use winit::keyboard::{Key, ModifiersState};
26use winit::monitor::Fullscreen;
27#[cfg(macos_platform)]
28use winit::platform::macos::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS};
29#[cfg(any(x11_platform, wayland_platform))]
30use winit::platform::startup_notify::{self, EventLoopExtStartupNotify, WindowExtStartupNotify};
31#[cfg(wayland_platform)]
32use winit::platform::wayland::{ActiveEventLoopExtWayland, WindowAttributesWayland};
33#[cfg(web_platform)]
34use winit::platform::web::{ActiveEventLoopExtWeb, WindowAttributesWeb};
35#[cfg(x11_platform)]
36use winit::platform::x11::{ActiveEventLoopExtX11, WindowAttributesX11};
37use winit::window::{CursorGrabMode, ResizeDirection, Theme, Window, WindowAttributes, WindowId};
38use winit_core::application::macos::ApplicationHandlerExtMacOS;
39
40#[path = "util/tracing.rs"]
41mod tracing;
42
43#[path = "util/fill.rs"]
44mod fill;
45
46const BORDER_SIZE: f64 = 20.;
48
49fn main() -> Result<(), Box<dyn Error>> {
50 #[cfg(web_platform)]
51 console_error_panic_hook::set_once();
52
53 tracing::init();
54
55 let event_loop = EventLoop::new()?;
56 let (sender, receiver) = mpsc::channel();
57
58 #[cfg(not(web_platform))]
60 {
61 let event_loop_proxy = event_loop.create_proxy();
62 let sender = sender.clone();
63 std::thread::spawn(move || {
64 info!("Starting to send user event every second");
67 loop {
68 let _ = sender.send(Action::Message);
69 event_loop_proxy.wake_up();
70 std::thread::sleep(std::time::Duration::from_secs(1));
71 }
72 });
73 }
74
75 let app = Application::new(&event_loop, receiver, sender);
76 Ok(event_loop.run_app(app)?)
77}
78
79struct Application {
81 receiver: Receiver<Action>,
83 sender: Sender<Action>,
84 custom_cursors: Result<Vec<CustomCursor>, RequestError>,
86 icon: Icon,
88 windows: HashMap<WindowId, WindowState>,
89 #[cfg(not(android_platform))]
93 context: Option<Context<DisplayHandle<'static>>>,
94}
95
96impl Application {
97 fn new(event_loop: &EventLoop, receiver: Receiver<Action>, sender: Sender<Action>) -> Self {
98 #[cfg(not(android_platform))]
100 let context = Some(
101 Context::new(unsafe {
102 std::mem::transmute::<DisplayHandle<'_>, DisplayHandle<'static>>(
103 event_loop.display_handle().unwrap(),
104 )
105 })
106 .unwrap(),
107 );
108
109 let icon = load_icon(include_bytes!("data/icon.png"));
115
116 info!("Loading cursor assets");
117 let custom_cursors = [
118 event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross.png"))),
119 event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross2.png"))),
120 event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/gradient.png"))),
121 ]
122 .into_iter()
123 .collect();
124
125 Self {
126 receiver,
127 sender,
128 #[cfg(not(android_platform))]
129 context,
130 custom_cursors,
131 icon,
132 windows: Default::default(),
133 }
134 }
135
136 fn create_window(
137 &mut self,
138 event_loop: &dyn ActiveEventLoop,
139 _tab_id: Option<String>,
140 ) -> Result<WindowId, Box<dyn Error>> {
141 #[allow(unused_mut)]
144 let mut window_attributes = WindowAttributes::default()
145 .with_title("Winit window")
146 .with_transparent(true)
147 .with_window_icon(Some(self.icon.clone()));
148
149 #[cfg(x11_platform)]
150 if event_loop.is_x11() {
151 window_attributes = window_attributes
152 .with_platform_attributes(Box::new(window_attributes_x11(event_loop)?));
153 }
154
155 #[cfg(wayland_platform)]
156 if event_loop.is_wayland() {
157 window_attributes = window_attributes
158 .with_platform_attributes(Box::new(window_attributes_wayland(event_loop)));
159 }
160
161 #[cfg(macos_platform)]
162 if let Some(tab_id) = _tab_id {
163 let window_attributes_macos =
164 Box::new(WindowAttributesMacOS::default().with_tabbing_identifier(&tab_id));
165 window_attributes = window_attributes.with_platform_attributes(window_attributes_macos);
166 }
167
168 #[cfg(web_platform)]
169 {
170 window_attributes =
171 window_attributes.with_platform_attributes(Box::new(window_attributes_web()));
172 }
173
174 let window = event_loop.create_window(window_attributes)?;
175
176 #[cfg(ios_platform)]
177 {
178 use winit::platform::ios::WindowExtIOS;
179 window.recognize_doubletap_gesture(true);
180 window.recognize_pinch_gesture(true);
181 window.recognize_rotation_gesture(true);
182 window.recognize_pan_gesture(true, 2, 2);
183 }
184
185 let window_state = WindowState::new(self, window)?;
186 let window_id = window_state.window.id();
187 info!("Created new window with id={window_id:?}");
188 self.windows.insert(window_id, window_state);
189 Ok(window_id)
190 }
191
192 fn handle_action_from_proxy(&mut self, _event_loop: &dyn ActiveEventLoop, action: Action) {
193 match action {
194 #[cfg(web_platform)]
195 Action::DumpMonitors => self.dump_monitors(_event_loop),
196 Action::Message => {
197 info!("User wake up");
198 },
199 _ => unreachable!("Tried to execute invalid action without `WindowId`"),
200 }
201 }
202
203 fn handle_action_with_window(
204 &mut self,
205 event_loop: &dyn ActiveEventLoop,
206 window_id: WindowId,
207 action: Action,
208 ) {
209 let window = self.windows.get_mut(&window_id).unwrap();
211 info!("Executing action: {action:?}");
212 match action {
213 Action::CloseWindow => {
214 let _ = self.windows.remove(&window_id);
215 },
216 Action::CreateNewWindow => {
217 #[cfg(any(x11_platform, wayland_platform))]
218 if let Err(err) = window.window.request_activation_token() {
219 info!("Failed to get activation token: {err}");
220 } else {
221 return;
222 }
223
224 if let Err(err) = self.create_window(event_loop, None) {
225 error!("Error creating new window: {err}");
226 }
227 },
228 Action::ToggleResizeIncrements => window.toggle_resize_increments(),
229 Action::ToggleCursorVisibility => window.toggle_cursor_visibility(),
230 Action::ToggleResizable => window.toggle_resizable(),
231 Action::ToggleDecorations => window.toggle_decorations(),
232 Action::ToggleFullscreen => window.toggle_fullscreen(),
233 #[cfg(macos_platform)]
234 Action::ToggleSimpleFullscreen => {
235 window.window.set_simple_fullscreen(!window.window.simple_fullscreen());
236 },
237 Action::ToggleMaximize => window.toggle_maximize(),
238 Action::ToggleImeInput => window.toggle_ime(),
239 Action::Minimize => window.minimize(),
240 Action::NextCursor => window.next_cursor(),
241 Action::NextCustomCursor => {
242 if let Err(err) = self.custom_cursors.as_ref().map(|c| window.next_custom_cursor(c))
243 {
244 error!("Error creating custom cursor: {err}");
245 }
246 },
247 #[cfg(web_platform)]
248 Action::UrlCustomCursor => {
249 if let Err(err) = window.url_custom_cursor(event_loop) {
250 error!("Error creating custom cursor from URL: {err}");
251 }
252 },
253 #[cfg(web_platform)]
254 Action::AnimationCustomCursor => {
255 if let Err(err) = self
256 .custom_cursors
257 .as_ref()
258 .map(|c| window.animation_custom_cursor(event_loop, c))
259 {
260 error!("Error creating animated custom cursor: {err}");
261 }
262 },
263 Action::CycleCursorGrab => window.cycle_cursor_grab(),
264 Action::DragWindow => window.drag_window(),
265 Action::DragResizeWindow => window.drag_resize_window(),
266 Action::ShowWindowMenu => window.show_menu(),
267 Action::PrintHelp => self.print_help(),
268 #[cfg(macos_platform)]
269 Action::CycleOptionAsAlt => window.cycle_option_as_alt(),
270 Action::SetTheme(theme) => {
271 window.window.set_theme(theme);
272 let actual_theme = theme.or_else(|| window.window.theme()).unwrap_or(Theme::Dark);
274 window.set_draw_theme(actual_theme);
275 },
276 #[cfg(macos_platform)]
277 Action::CreateNewTab => {
278 let tab_id = window.window.tabbing_identifier();
279 if let Err(err) = self.create_window(event_loop, Some(tab_id)) {
280 error!("Error creating new window: {err}");
281 }
282 },
283 Action::RequestResize => window.swap_dimensions(),
284 #[cfg(web_platform)]
285 Action::DumpMonitors => {
286 let future = event_loop.request_detailed_monitor_permission();
287 let proxy = event_loop.create_proxy();
288 let sender = self.sender.clone();
289 wasm_bindgen_futures::spawn_local(async move {
290 if let Err(error) = future.await {
291 error!("{error}")
292 }
293
294 let _ = sender.send(Action::DumpMonitors);
295 proxy.wake_up();
296 });
297 },
298 #[cfg(not(web_platform))]
299 Action::DumpMonitors => self.dump_monitors(event_loop),
300 Action::Message => {
301 self.sender.send(Action::Message).unwrap();
302 event_loop.create_proxy().wake_up();
303 },
304 Action::ToggleAnimatedFillColor => {
305 window.animated_fill_color = !window.animated_fill_color;
306 },
307 Action::ToggleContinuousRedraw => {
308 window.continuous_redraw = !window.continuous_redraw;
309 window.window.request_redraw();
310 },
311 }
312 }
313
314 fn dump_monitors(&self, event_loop: &dyn ActiveEventLoop) {
315 info!("Monitors information");
316 let primary_monitor = event_loop.primary_monitor();
317 for monitor in event_loop.available_monitors() {
318 let intro = if primary_monitor.as_ref() == Some(&monitor) {
319 "Primary monitor"
320 } else {
321 "Monitor"
322 };
323
324 if let Some(name) = monitor.name() {
325 info!("{intro}: {name}");
326 } else {
327 info!("{intro}: [no name]");
328 }
329
330 if let Some(current_mode) = monitor.current_video_mode() {
331 let PhysicalSize { width, height } = current_mode.size();
332 let bits =
333 current_mode.bit_depth().map(|bits| format!("x{bits}")).unwrap_or_default();
334 let m_hz = current_mode
335 .refresh_rate_millihertz()
336 .map(|m_hz| format!(" @ {}.{} Hz", m_hz.get() / 1000, m_hz.get() % 1000))
337 .unwrap_or_default();
338 info!(" {width}x{height}{bits}{m_hz}");
339 }
340
341 if let Some(PhysicalPosition { x, y }) = monitor.position() {
342 info!(" Position: {x},{y}");
343 }
344
345 info!(" Scale factor: {}", monitor.scale_factor());
346
347 info!(" Available modes (width x height x bit-depth):");
348 for mode in monitor.video_modes() {
349 let PhysicalSize { width, height } = mode.size();
350 let bits = mode.bit_depth().map(|bits| format!("x{bits}")).unwrap_or_default();
351 let m_hz = mode
352 .refresh_rate_millihertz()
353 .map(|m_hz| format!(" @ {}.{} Hz", m_hz.get() / 1000, m_hz.get() % 1000))
354 .unwrap_or_default();
355 info!(" {width}x{height}{bits}{m_hz}");
356 }
357 }
358 }
359
360 fn process_key_binding(key: &str, mods: &ModifiersState) -> Option<Action> {
362 KEY_BINDINGS
363 .iter()
364 .find_map(|binding| binding.is_triggered_by(&key, mods).then_some(binding.action))
365 }
366
367 fn process_mouse_binding(button: MouseButton, mods: &ModifiersState) -> Option<Action> {
369 MOUSE_BINDINGS
370 .iter()
371 .find_map(|binding| binding.is_triggered_by(&button, mods).then_some(binding.action))
372 }
373
374 fn print_help(&self) {
375 info!("Keyboard bindings:");
376 for binding in KEY_BINDINGS {
377 info!(
378 "{}{:<10} - {} ({})",
379 modifiers_to_string(binding.mods),
380 binding.trigger,
381 binding.action,
382 binding.action.help(),
383 );
384 }
385 info!("Mouse bindings:");
386 for binding in MOUSE_BINDINGS {
387 info!(
388 "{}{:<10} - {} ({})",
389 modifiers_to_string(binding.mods),
390 mouse_button_to_string(binding.trigger),
391 binding.action,
392 binding.action.help(),
393 );
394 }
395 }
396}
397
398impl ApplicationHandler for Application {
399 fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
400 while let Ok(action) = self.receiver.try_recv() {
401 self.handle_action_from_proxy(event_loop, action)
402 }
403 }
404
405 fn window_event(
406 &mut self,
407 event_loop: &dyn ActiveEventLoop,
408 window_id: WindowId,
409 event: WindowEvent,
410 ) {
411 let window = match self.windows.get_mut(&window_id) {
412 Some(window) => window,
413 None => return,
414 };
415
416 match event {
417 WindowEvent::SurfaceResized(size) => {
418 window.resize(size);
419 },
420 WindowEvent::Focused(focused) => {
421 if focused {
422 info!("Window={window_id:?} focused");
423 } else {
424 info!("Window={window_id:?} unfocused");
425 }
426 },
427 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
428 info!("Window={window_id:?} changed scale to {scale_factor}");
429 },
430 WindowEvent::ThemeChanged(theme) => {
431 info!("Theme changed to {theme:?}");
432 window.set_draw_theme(theme);
433 },
434 WindowEvent::RedrawRequested => {
435 if let Err(err) = window.draw() {
436 error!("Error drawing window: {err}");
437 }
438 if window.continuous_redraw {
439 window.window.request_redraw();
440 }
441 },
442 WindowEvent::Occluded(occluded) => {
443 window.set_occluded(occluded);
444 },
445 WindowEvent::CloseRequested => {
446 info!("Closing Window={window_id:?}");
447 self.windows.remove(&window_id);
448 },
449 WindowEvent::ModifiersChanged(modifiers) => {
450 window.modifiers = modifiers.state();
451 info!("Modifiers changed to {:?}", window.modifiers);
452 },
453 WindowEvent::MouseWheel { delta, .. } => match delta {
454 MouseScrollDelta::LineDelta(x, y) => {
455 info!("Mouse wheel Line Delta: ({x},{y})");
456 },
457 MouseScrollDelta::PixelDelta(px) => {
458 info!("Mouse wheel Pixel Delta: ({},{})", px.x, px.y);
459 },
460 },
461 WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => {
462 let mods = window.modifiers;
463
464 if event.state.is_pressed() {
466 let action = if let Key::Character(ch) = event.key_without_modifiers.as_ref() {
467 Self::process_key_binding(&ch.to_uppercase(), &mods)
468 } else {
469 None
470 };
471
472 if let Some(action) = action {
473 self.handle_action_with_window(event_loop, window_id, action);
474 }
475 }
476 },
477 WindowEvent::PointerButton { button, state, .. } => {
478 info!("Pointer button {button:?} {state:?}");
479 let mods = window.modifiers;
480 if let Some(action) = state
481 .is_pressed()
482 .then(|| Self::process_mouse_binding(button.mouse_button(), &mods))
483 .flatten()
484 {
485 self.handle_action_with_window(event_loop, window_id, action);
486 }
487 },
488 WindowEvent::PointerLeft { .. } => {
489 info!("Pointer left Window={window_id:?}");
490 window.cursor_left();
491 },
492 WindowEvent::PointerMoved { position, .. } => {
493 info!("Moved pointer to {position:?}");
494 window.cursor_moved(position);
495 },
496 WindowEvent::ActivationTokenDone { token: _token, .. } => {
497 #[cfg(any(x11_platform, wayland_platform))]
498 {
499 startup_notify::set_activation_token_env(_token);
500 if let Err(err) = self.create_window(event_loop, None) {
501 error!("Error creating new window: {err}");
502 }
503 }
504 },
505 WindowEvent::Ime(event) => match event {
506 Ime::Enabled => info!("IME enabled for Window={window_id:?}"),
507 Ime::Preedit(text, caret_pos) => {
508 info!("Preedit: {}, with caret at {:?}", text, caret_pos);
509 },
510 Ime::Commit(text) => {
511 info!("Committed: {}", text);
512 },
513 Ime::Disabled => info!("IME disabled for Window={window_id:?}"),
514 },
515 WindowEvent::PinchGesture { delta, .. } => {
516 window.zoom += delta;
517 let zoom = window.zoom;
518 if delta > 0.0 {
519 info!("Zoomed in {delta:.5} (now: {zoom:.5})");
520 } else {
521 info!("Zoomed out {delta:.5} (now: {zoom:.5})");
522 }
523 },
524 WindowEvent::RotationGesture { delta, .. } => {
525 window.rotated += delta;
526 let rotated = window.rotated;
527 if delta > 0.0 {
528 info!("Rotated counterclockwise {delta:.5} (now: {rotated:.5})");
529 } else {
530 info!("Rotated clockwise {delta:.5} (now: {rotated:.5})");
531 }
532 },
533 WindowEvent::PanGesture { delta, phase, .. } => {
534 window.panned.x += delta.x;
535 window.panned.y += delta.y;
536 info!("Panned ({delta:?})) (now: {:?}), {phase:?}", window.panned);
537 },
538 WindowEvent::DoubleTapGesture { .. } => {
539 info!("Smart zoom");
540 },
541 WindowEvent::TouchpadPressure { .. }
542 | WindowEvent::DragLeft { .. }
543 | WindowEvent::KeyboardInput { .. }
544 | WindowEvent::PointerEntered { .. }
545 | WindowEvent::DragEntered { .. }
546 | WindowEvent::DragMoved { .. }
547 | WindowEvent::DragDropped { .. }
548 | WindowEvent::Destroyed
549 | WindowEvent::Moved(_) => (),
550 }
551 }
552
553 fn device_event(
554 &mut self,
555 _event_loop: &dyn ActiveEventLoop,
556 device_id: Option<DeviceId>,
557 event: DeviceEvent,
558 ) {
559 info!("Device {device_id:?} event: {event:?}");
560 }
561
562 fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
563 info!("Ready to create surfaces");
564 self.dump_monitors(event_loop);
565
566 self.create_window(event_loop, None).expect("failed to create initial window");
568
569 self.print_help();
570 }
571
572 fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
573 if self.windows.is_empty() {
574 info!("No windows left, exiting...");
575 event_loop.exit();
576 }
577 }
578
579 fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> {
580 Some(self)
581 }
582}
583
584impl Drop for Application {
585 fn drop(&mut self) {
586 info!("Application exited");
587 }
588}
589
590impl ApplicationHandlerExtMacOS for Application {
591 fn standard_key_binding(
592 &mut self,
593 _event_loop: &dyn ActiveEventLoop,
594 window_id: WindowId,
595 action: &str,
596 ) {
597 info!(?window_id, ?action, "macOS standard key binding");
598 }
599}
600
601struct WindowState {
603 ime: bool,
605 #[cfg(not(android_platform))]
609 surface: Surface<DisplayHandle<'static>, Arc<dyn Window>>,
610 window: Arc<dyn Window>,
612 theme: Theme,
614 animated_fill_color: bool,
616 #[cfg(not(android_platform))]
618 start_time: std::time::Instant,
619 continuous_redraw: bool,
621 cursor_position: Option<PhysicalPosition<f64>>,
623 modifiers: ModifiersState,
625 occluded: bool,
627 cursor_grab: CursorGrabMode,
629 zoom: f64,
631 rotated: f32,
633 panned: PhysicalPosition<f32>,
635
636 #[cfg(macos_platform)]
637 option_as_alt: OptionAsAlt,
638
639 named_idx: usize,
641 custom_idx: usize,
642 cursor_hidden: bool,
643}
644
645impl WindowState {
646 fn new(app: &Application, window: Box<dyn Window>) -> Result<Self, Box<dyn Error>> {
647 let window: Arc<dyn Window> = Arc::from(window);
648
649 #[cfg(not(android_platform))]
652 let surface = Surface::new(app.context.as_ref().unwrap(), Arc::clone(&window))?;
653
654 let theme = window.theme().unwrap_or(Theme::Dark);
655 info!("Theme: {theme:?}");
656 let named_idx = 0;
657 window.set_cursor(CURSORS[named_idx].into());
658
659 let ime = true;
661 window.set_ime_allowed(ime);
662
663 let size = window.surface_size();
664 let mut state = Self {
665 #[cfg(macos_platform)]
666 option_as_alt: window.option_as_alt(),
667 custom_idx: app.custom_cursors.as_ref().map(Vec::len).unwrap_or(1) - 1,
668 cursor_grab: CursorGrabMode::None,
669 named_idx,
670 #[cfg(not(android_platform))]
671 surface,
672 window,
673 theme,
674 animated_fill_color: false,
675 continuous_redraw: false,
676 #[cfg(not(android_platform))]
677 start_time: std::time::Instant::now(),
678 ime,
679 cursor_position: Default::default(),
680 cursor_hidden: Default::default(),
681 modifiers: Default::default(),
682 occluded: Default::default(),
683 rotated: Default::default(),
684 panned: Default::default(),
685 zoom: Default::default(),
686 };
687
688 state.resize(size);
689 Ok(state)
690 }
691
692 pub fn toggle_ime(&mut self) {
693 self.ime = !self.ime;
694 self.window.set_ime_allowed(self.ime);
695 if let Some(position) = self.ime.then_some(self.cursor_position).flatten() {
696 self.window.set_ime_cursor_area(position.into(), PhysicalSize::new(20, 20).into());
697 }
698 }
699
700 pub fn minimize(&mut self) {
701 self.window.set_minimized(true);
702 }
703
704 pub fn cursor_moved(&mut self, position: PhysicalPosition<f64>) {
705 self.cursor_position = Some(position);
706 if self.ime {
707 self.window.set_ime_cursor_area(position.into(), PhysicalSize::new(20, 20).into());
708 }
709 }
710
711 pub fn cursor_left(&mut self) {
712 self.cursor_position = None;
713 }
714
715 fn toggle_maximize(&self) {
717 let maximized = self.window.is_maximized();
718 self.window.set_maximized(!maximized);
719 }
720
721 fn toggle_decorations(&self) {
723 let decorated = self.window.is_decorated();
724 self.window.set_decorations(!decorated);
725 }
726
727 fn toggle_resizable(&self) {
729 let resizable = self.window.is_resizable();
730 self.window.set_resizable(!resizable);
731 }
732
733 fn toggle_cursor_visibility(&mut self) {
735 self.cursor_hidden = !self.cursor_hidden;
736 self.window.set_cursor_visible(!self.cursor_hidden);
737 }
738
739 fn toggle_resize_increments(&mut self) {
741 let new_increments = match self.window.surface_resize_increments() {
742 Some(_) => None,
743 None => Some(LogicalSize::new(25.0, 25.0).into()),
744 };
745 info!("Had increments: {}", new_increments.is_none());
746 self.window.set_surface_resize_increments(new_increments);
747 }
748
749 fn toggle_fullscreen(&self) {
751 let fullscreen = if self.window.fullscreen().is_some() {
752 None
753 } else {
754 Some(Fullscreen::Borderless(None))
755 };
756
757 self.window.set_fullscreen(fullscreen);
758 }
759
760 fn cycle_cursor_grab(&mut self) {
762 self.cursor_grab = match self.cursor_grab {
763 CursorGrabMode::None => CursorGrabMode::Confined,
764 CursorGrabMode::Confined => CursorGrabMode::Locked,
765 CursorGrabMode::Locked => CursorGrabMode::None,
766 };
767 info!("Changing cursor grab mode to {:?}", self.cursor_grab);
768 if let Err(err) = self.window.set_cursor_grab(self.cursor_grab) {
769 error!("Error setting cursor grab: {err}");
770 }
771 }
772
773 #[cfg(macos_platform)]
774 fn cycle_option_as_alt(&mut self) {
775 self.option_as_alt = match self.option_as_alt {
776 OptionAsAlt::None => OptionAsAlt::OnlyLeft,
777 OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight,
778 OptionAsAlt::OnlyRight => OptionAsAlt::Both,
779 OptionAsAlt::Both => OptionAsAlt::None,
780 };
781 info!("Setting option as alt {:?}", self.option_as_alt);
782 self.window.set_option_as_alt(self.option_as_alt);
783 }
784
785 fn swap_dimensions(&mut self) {
787 let old_surface_size = self.window.surface_size();
788 let mut surface_size = old_surface_size;
789
790 mem::swap(&mut surface_size.width, &mut surface_size.height);
791 info!("Requesting resize from {old_surface_size:?} to {surface_size:?}");
792
793 if let Some(new_surface_size) = self.window.request_surface_size(surface_size.into()) {
794 if old_surface_size == new_surface_size {
795 info!("Inner size change got ignored");
796 } else {
797 self.resize(new_surface_size);
798 }
799 } else {
800 info!("Requesting surface size is asynchronous");
801 }
802 }
803
804 fn next_cursor(&mut self) {
806 self.named_idx = (self.named_idx + 1) % CURSORS.len();
807 info!("Setting cursor to \"{:?}\"", CURSORS[self.named_idx]);
808 self.window.set_cursor(Cursor::Icon(CURSORS[self.named_idx]));
809 }
810
811 fn next_custom_cursor(&mut self, custom_cursors: &[CustomCursor]) {
813 self.custom_idx = (self.custom_idx + 1) % custom_cursors.len();
814 let cursor = Cursor::Custom(custom_cursors[self.custom_idx].clone());
815 self.window.set_cursor(cursor);
816 }
817
818 #[cfg(web_platform)]
820 fn url_custom_cursor(
821 &mut self,
822 event_loop: &dyn ActiveEventLoop,
823 ) -> Result<(), Box<dyn Error>> {
824 let cursor = event_loop.create_custom_cursor(url_custom_cursor())?;
825
826 self.window.set_cursor(cursor.into());
827
828 Ok(())
829 }
830
831 #[cfg(web_platform)]
833 fn animation_custom_cursor(
834 &mut self,
835 event_loop: &dyn ActiveEventLoop,
836 custom_cursors: &[CustomCursor],
837 ) -> Result<(), Box<dyn Error>> {
838 use std::time::Duration;
839
840 let cursors = vec![
841 custom_cursors[0].clone(),
842 custom_cursors[1].clone(),
843 event_loop.create_custom_cursor(url_custom_cursor())?,
844 ];
845 let cursor = CustomCursorSource::from_animation(Duration::from_secs(3), cursors).unwrap();
846 let cursor = event_loop.create_custom_cursor(cursor)?;
847
848 self.window.set_cursor(cursor.into());
849
850 Ok(())
851 }
852
853 fn resize(&mut self, size: PhysicalSize<u32>) {
855 info!("Surface resized to {size:?}");
856 #[cfg(not(android_platform))]
857 {
858 let (width, height) = match (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
859 {
860 (Some(width), Some(height)) => (width, height),
861 _ => return,
862 };
863 self.surface.resize(width, height).expect("failed to resize inner buffer");
864 }
865 self.window.request_redraw();
866 }
867
868 fn set_draw_theme(&mut self, theme: Theme) {
870 self.theme = theme;
871 self.window.request_redraw();
872 }
873
874 fn show_menu(&self) {
876 if let Some(position) = self.cursor_position {
877 self.window.show_window_menu(position.into());
878 }
879 }
880
881 fn drag_window(&self) {
883 if let Err(err) = self.window.drag_window() {
884 info!("Error starting window drag: {err}");
885 } else {
886 info!("Dragging window Window={:?}", self.window.id());
887 }
888 }
889
890 fn drag_resize_window(&self) {
892 let position = match self.cursor_position {
893 Some(position) => position,
894 None => {
895 info!("Drag-resize requires cursor to be inside the window");
896 return;
897 },
898 };
899
900 let win_size = self.window.surface_size();
901 let border_size = BORDER_SIZE * self.window.scale_factor();
902
903 let x_direction = if position.x < border_size {
904 ResizeDirection::West
905 } else if position.x > (win_size.width as f64 - border_size) {
906 ResizeDirection::East
907 } else {
908 ResizeDirection::SouthEast
910 };
911
912 let y_direction = if position.y < border_size {
913 ResizeDirection::North
914 } else if position.y > (win_size.height as f64 - border_size) {
915 ResizeDirection::South
916 } else {
917 ResizeDirection::SouthEast
919 };
920
921 let direction = match (x_direction, y_direction) {
922 (ResizeDirection::West, ResizeDirection::North) => ResizeDirection::NorthWest,
923 (ResizeDirection::West, ResizeDirection::South) => ResizeDirection::SouthWest,
924 (ResizeDirection::West, _) => ResizeDirection::West,
925 (ResizeDirection::East, ResizeDirection::North) => ResizeDirection::NorthEast,
926 (ResizeDirection::East, ResizeDirection::South) => ResizeDirection::SouthEast,
927 (ResizeDirection::East, _) => ResizeDirection::East,
928 (_, ResizeDirection::South) => ResizeDirection::South,
929 (_, ResizeDirection::North) => ResizeDirection::North,
930 _ => return,
931 };
932
933 if let Err(err) = self.window.drag_resize_window(direction) {
934 info!("Error starting window drag-resize: {err}");
935 } else {
936 info!("Drag-resizing window Window={:?}", self.window.id());
937 }
938 }
939
940 fn set_occluded(&mut self, occluded: bool) {
942 self.occluded = occluded;
943 if !occluded {
944 self.window.request_redraw();
945 }
946 }
947
948 #[cfg(not(android_platform))]
950 fn draw(&mut self) -> Result<(), Box<dyn Error>> {
951 if self.occluded {
952 info!("Skipping drawing occluded window={:?}", self.window.id());
953 return Ok(());
954 }
955
956 if self.animated_fill_color {
957 fill::fill_window_with_animated_color(&*self.window, self.start_time);
958 return Ok(());
959 }
960
961 let mut buffer = self.surface.buffer_mut()?;
962
963 let surface_size = self.window.surface_size();
965 let insets = self.window.safe_area();
966 for y in 0..surface_size.height {
967 for x in 0..surface_size.width {
968 let index = y as usize * surface_size.width as usize + x as usize;
969 if insets.left <= x
970 && x <= (surface_size.width - insets.right)
971 && insets.top <= y
972 && y <= (surface_size.height - insets.bottom)
973 {
974 buffer[index] = match self.theme {
976 Theme::Light => 0xffe8e8e8, Theme::Dark => 0xff525252, };
979 } else {
980 buffer[index] = match self.theme {
982 Theme::Light => 0xffffffff, Theme::Dark => 0xff181818, };
985 }
986 }
987 }
988
989 self.window.pre_present_notify();
991 buffer.present()?;
992
993 Ok(())
994 }
995
996 #[cfg(android_platform)]
997 fn draw(&mut self) -> Result<(), Box<dyn Error>> {
998 info!("Drawing but without rendering...");
999 Ok(())
1000 }
1001}
1002
1003struct Binding<T: Eq> {
1004 trigger: T,
1005 mods: ModifiersState,
1006 action: Action,
1007}
1008
1009impl<T: Eq> Binding<T> {
1010 const fn new(trigger: T, mods: ModifiersState, action: Action) -> Self {
1011 Self { trigger, mods, action }
1012 }
1013
1014 fn is_triggered_by(&self, trigger: &T, mods: &ModifiersState) -> bool {
1015 &self.trigger == trigger && &self.mods == mods
1016 }
1017}
1018
1019#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1020enum Action {
1021 CloseWindow,
1022 ToggleCursorVisibility,
1023 CreateNewWindow,
1024 ToggleResizeIncrements,
1025 ToggleImeInput,
1026 ToggleDecorations,
1027 ToggleResizable,
1028 ToggleFullscreen,
1029 #[cfg(macos_platform)]
1030 ToggleSimpleFullscreen,
1031 ToggleMaximize,
1032 Minimize,
1033 NextCursor,
1034 NextCustomCursor,
1035 #[cfg(web_platform)]
1036 UrlCustomCursor,
1037 #[cfg(web_platform)]
1038 AnimationCustomCursor,
1039 CycleCursorGrab,
1040 PrintHelp,
1041 DragWindow,
1042 DragResizeWindow,
1043 ShowWindowMenu,
1044 #[cfg(macos_platform)]
1045 CycleOptionAsAlt,
1046 SetTheme(Option<Theme>),
1047 #[cfg(macos_platform)]
1048 CreateNewTab,
1049 RequestResize,
1050 DumpMonitors,
1051 Message,
1052 ToggleAnimatedFillColor,
1053 ToggleContinuousRedraw,
1054}
1055
1056impl Action {
1057 fn help(&self) -> &'static str {
1058 match self {
1059 Action::CloseWindow => "Close window",
1060 Action::ToggleCursorVisibility => "Hide cursor",
1061 Action::CreateNewWindow => "Create new window",
1062 Action::ToggleImeInput => "Toggle IME input",
1063 Action::ToggleDecorations => "Toggle decorations",
1064 Action::ToggleResizable => "Toggle window resizable state",
1065 Action::ToggleFullscreen => "Toggle fullscreen",
1066 #[cfg(macos_platform)]
1067 Action::ToggleSimpleFullscreen => "Toggle simple fullscreen",
1068 Action::ToggleMaximize => "Maximize",
1069 Action::Minimize => "Minimize",
1070 Action::ToggleResizeIncrements => "Use resize increments when resizing window",
1071 Action::NextCursor => "Advance the cursor to the next value",
1072 Action::NextCustomCursor => "Advance custom cursor to the next value",
1073 #[cfg(web_platform)]
1074 Action::UrlCustomCursor => "Custom cursor from an URL",
1075 #[cfg(web_platform)]
1076 Action::AnimationCustomCursor => "Custom cursor from an animation",
1077 Action::CycleCursorGrab => "Cycle through cursor grab mode",
1078 Action::PrintHelp => "Print help",
1079 Action::DragWindow => "Start window drag",
1080 Action::DragResizeWindow => "Start window drag-resize",
1081 Action::ShowWindowMenu => "Show window menu",
1082 #[cfg(macos_platform)]
1083 Action::CycleOptionAsAlt => "Cycle option as alt mode",
1084 Action::SetTheme(None) => "Change to the system theme",
1085 Action::SetTheme(Some(Theme::Light)) => "Change to a light theme",
1086 Action::SetTheme(Some(Theme::Dark)) => "Change to a dark theme",
1087 #[cfg(macos_platform)]
1088 Action::CreateNewTab => "Create new tab",
1089 Action::RequestResize => "Request a resize",
1090 #[cfg(not(web_platform))]
1091 Action::DumpMonitors => "Dump monitor information",
1092 #[cfg(web_platform)]
1093 Action::DumpMonitors => {
1094 "Request permission to query detailed monitor information and dump monitor \
1095 information"
1096 },
1097 Action::Message => "Prints a message through a user wake up",
1098 Action::ToggleAnimatedFillColor => "Toggle animated fill color",
1099 Action::ToggleContinuousRedraw => "Toggle continuous redraw",
1100 }
1101 }
1102}
1103
1104impl fmt::Display for Action {
1105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1106 Debug::fmt(&self, f)
1107 }
1108}
1109
1110fn decode_cursor(bytes: &[u8]) -> CustomCursorSource {
1111 let img = image::load_from_memory(bytes).unwrap().to_rgba8();
1112 let samples = img.into_flat_samples();
1113 let (_, w, h) = samples.extents();
1114 let (w, h) = (w as u16, h as u16);
1115 CustomCursorSource::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap()
1116}
1117
1118#[cfg(web_platform)]
1119fn url_custom_cursor() -> CustomCursorSource {
1120 use std::sync::atomic::{AtomicU64, Ordering};
1121
1122 static URL_COUNTER: AtomicU64 = AtomicU64::new(0);
1123
1124 CustomCursorSource::Url {
1125 hotspot_x: 64,
1126 hotspot_y: 64,
1127 url: format!(
1128 "https://picsum.photos/128?random={}",
1129 URL_COUNTER.fetch_add(1, Ordering::Relaxed)
1130 ),
1131 }
1132}
1133
1134fn load_icon(bytes: &[u8]) -> Icon {
1135 let (icon_rgba, icon_width, icon_height) = {
1136 let image = image::load_from_memory(bytes).unwrap().into_rgba8();
1137 let (width, height) = image.dimensions();
1138 let rgba = image.into_raw();
1139 (rgba, width, height)
1140 };
1141 RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into()
1142}
1143
1144fn modifiers_to_string(mods: ModifiersState) -> String {
1145 let mut mods_line = String::new();
1146 for (modifier, desc) in [
1148 (
1149 ModifiersState::META,
1150 if cfg!(target_os = "windows") {
1151 "Win+"
1152 } else if cfg!(target_vendor = "apple") {
1153 "Cmd+"
1154 } else {
1155 "Super+"
1156 },
1157 ),
1158 (ModifiersState::ALT, "Alt+"),
1159 (ModifiersState::CONTROL, "Ctrl+"),
1160 (ModifiersState::SHIFT, "Shift+"),
1161 ] {
1162 if !mods.contains(modifier) {
1163 continue;
1164 }
1165
1166 mods_line.push_str(desc);
1167 }
1168 mods_line
1169}
1170
1171fn mouse_button_to_string(button: MouseButton) -> &'static str {
1172 match button {
1173 MouseButton::Left => "LMB",
1174 MouseButton::Right => "RMB",
1175 MouseButton::Middle => "MMB",
1176 MouseButton::Back => "Back",
1177 MouseButton::Forward => "Forward",
1178 MouseButton::Other(_) => "",
1179 }
1180}
1181
1182#[cfg(web_platform)]
1183fn window_attributes_web() -> WindowAttributesWeb {
1184 WindowAttributesWeb::default().with_append(true)
1185}
1186
1187#[cfg(wayland_platform)]
1188fn window_attributes_wayland(event_loop: &dyn ActiveEventLoop) -> WindowAttributesWayland {
1189 let mut window_attributes = WindowAttributesWayland::default();
1190
1191 if let Some(token) = event_loop.read_token_from_env() {
1192 startup_notify::reset_activation_token_env();
1193 info!("Using token {:?} to activate a window", token);
1194 window_attributes = window_attributes.with_activation_token(token);
1195 }
1196
1197 window_attributes
1198}
1199
1200#[cfg(x11_platform)]
1201fn window_attributes_x11(
1202 event_loop: &dyn ActiveEventLoop,
1203) -> Result<WindowAttributesX11, Box<dyn Error>> {
1204 let mut window_attributes = WindowAttributesX11::default();
1205
1206 #[cfg(any(x11_platform, wayland_platform))]
1207 if let Some(token) = event_loop.read_token_from_env() {
1208 startup_notify::reset_activation_token_env();
1209 info!("Using token {:?} to activate a window", token);
1210 window_attributes = window_attributes.with_activation_token(token);
1211 }
1212
1213 match std::env::var("X11_VISUAL_ID") {
1214 Ok(visual_id_str) => {
1215 info!("Using X11 visual id {visual_id_str}");
1216 let visual_id = visual_id_str.parse()?;
1217 window_attributes = window_attributes.with_x11_visual(visual_id);
1218 },
1219 Err(_) => info!("Set the X11_VISUAL_ID env variable to request specific X11 visual"),
1220 }
1221
1222 match std::env::var("X11_SCREEN_ID") {
1223 Ok(screen_id_str) => {
1224 info!("Placing the window on X11 screen {screen_id_str}");
1225 let screen_id = screen_id_str.parse()?;
1226 window_attributes = window_attributes.with_x11_screen(screen_id);
1227 },
1228 Err(_) => {
1229 info!("Set the X11_SCREEN_ID env variable to place the window on non-default screen")
1230 },
1231 }
1232
1233 Ok(window_attributes)
1234}
1235
1236const CURSORS: &[CursorIcon] = &[
1238 CursorIcon::Default,
1239 CursorIcon::Crosshair,
1240 CursorIcon::Pointer,
1241 CursorIcon::Move,
1242 CursorIcon::Text,
1243 CursorIcon::Wait,
1244 CursorIcon::Help,
1245 CursorIcon::Progress,
1246 CursorIcon::NotAllowed,
1247 CursorIcon::ContextMenu,
1248 CursorIcon::Cell,
1249 CursorIcon::VerticalText,
1250 CursorIcon::Alias,
1251 CursorIcon::Copy,
1252 CursorIcon::NoDrop,
1253 CursorIcon::Grab,
1254 CursorIcon::Grabbing,
1255 CursorIcon::AllScroll,
1256 CursorIcon::ZoomIn,
1257 CursorIcon::ZoomOut,
1258 CursorIcon::EResize,
1259 CursorIcon::NResize,
1260 CursorIcon::NeResize,
1261 CursorIcon::NwResize,
1262 CursorIcon::SResize,
1263 CursorIcon::SeResize,
1264 CursorIcon::SwResize,
1265 CursorIcon::WResize,
1266 CursorIcon::EwResize,
1267 CursorIcon::NsResize,
1268 CursorIcon::NeswResize,
1269 CursorIcon::NwseResize,
1270 CursorIcon::ColResize,
1271 CursorIcon::RowResize,
1272];
1273
1274const KEY_BINDINGS: &[Binding<&'static str>] = &[
1275 Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow),
1276 Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp),
1277 Binding::new("F", ModifiersState::SHIFT, Action::ToggleAnimatedFillColor),
1278 Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen),
1279 #[cfg(macos_platform)]
1280 Binding::new("F", ModifiersState::ALT, Action::ToggleSimpleFullscreen),
1281 Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations),
1282 Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput),
1283 Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab),
1284 Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements),
1285 Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable),
1286 Binding::new("R", ModifiersState::ALT, Action::RequestResize),
1287 Binding::new("R", ModifiersState::SHIFT, Action::ToggleContinuousRedraw),
1288 Binding::new("M", ModifiersState::CONTROL.union(ModifiersState::ALT), Action::DumpMonitors),
1290 Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize),
1291 Binding::new("M", ModifiersState::ALT, Action::Minimize),
1292 Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow),
1294 Binding::new("C", ModifiersState::CONTROL, Action::NextCursor),
1296 Binding::new("C", ModifiersState::ALT, Action::NextCustomCursor),
1297 #[cfg(web_platform)]
1298 Binding::new(
1299 "C",
1300 ModifiersState::CONTROL.union(ModifiersState::SHIFT),
1301 Action::UrlCustomCursor,
1302 ),
1303 #[cfg(web_platform)]
1304 Binding::new(
1305 "C",
1306 ModifiersState::ALT.union(ModifiersState::SHIFT),
1307 Action::AnimationCustomCursor,
1308 ),
1309 Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility),
1310 Binding::new("K", ModifiersState::empty(), Action::SetTheme(None)),
1312 Binding::new("K", ModifiersState::META, Action::SetTheme(Some(Theme::Light))),
1313 Binding::new("K", ModifiersState::CONTROL, Action::SetTheme(Some(Theme::Dark))),
1314 #[cfg(macos_platform)]
1315 Binding::new("T", ModifiersState::META, Action::CreateNewTab),
1316 #[cfg(macos_platform)]
1317 Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt),
1318 Binding::new("S", ModifiersState::CONTROL, Action::Message),
1319];
1320
1321const MOUSE_BINDINGS: &[Binding<MouseButton>] = &[
1322 Binding::new(MouseButton::Left, ModifiersState::ALT, Action::DragResizeWindow),
1323 Binding::new(MouseButton::Left, ModifiersState::CONTROL, Action::DragWindow),
1324 Binding::new(MouseButton::Right, ModifiersState::CONTROL, Action::ShowWindowMenu),
1325];