Skip to main content

egui_winit/
lib.rs

1//! [`egui`] bindings for [`winit`](https://github.com/rust-windowing/winit).
2//!
3//! The library translates winit events to egui, handled copy/paste,
4//! updates the cursor, open links clicked in egui, etc.
5//!
6//! ## Feature flags
7#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
8//!
9
10#![expect(clippy::manual_range_contains)]
11
12#[cfg(target_os = "windows")]
13use std::collections::HashSet;
14
15#[cfg(feature = "accesskit")]
16pub use accesskit_winit;
17pub use egui;
18#[cfg(feature = "accesskit")]
19use egui::accesskit;
20use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo};
21pub use winit;
22
23pub mod clipboard;
24mod dropped_file;
25mod safe_area;
26mod window_settings;
27
28pub use window_settings::WindowSettings;
29
30use raw_window_handle::HasDisplayHandle;
31
32use dropped_file::NativeFile;
33
34use winit::{
35    dpi::{PhysicalPosition, PhysicalSize},
36    event::ElementState,
37    event_loop::ActiveEventLoop,
38    window::{CursorGrabMode, CustomCursor, Window, WindowButtons, WindowLevel},
39};
40
41pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
42    let size = if cfg!(target_os = "ios") {
43        // `outer_size` Includes the area behind the "dynamic island".
44        // It is up to the eframe user to make sure the dynamic island doesn't cover anything important.
45        // That will be easier once https://github.com/rust-windowing/winit/pull/3890 lands
46        window.outer_size()
47    } else {
48        window.inner_size()
49    };
50    egui::vec2(size.width as f32, size.height as f32)
51}
52
53/// Calculate the `pixels_per_point` for a given window, given the current egui zoom factor
54pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 {
55    let native_pixels_per_point = window.scale_factor() as f32;
56    let egui_zoom_factor = egui_ctx.zoom_factor();
57    egui_zoom_factor * native_pixels_per_point
58}
59
60// ----------------------------------------------------------------------------
61
62#[must_use]
63#[derive(Clone, Copy, Debug, Default)]
64pub struct EventResponse {
65    /// If true, egui consumed this event, i.e. wants exclusive use of this event
66    /// (e.g. a mouse click on an egui window, or entering text into a text field).
67    ///
68    /// For instance, if you use egui for a game, you should only
69    /// pass on the events to your game when [`Self::consumed`] is `false`.
70    ///
71    /// Note that egui uses `tab` to move focus between elements, so this will always be `true` for tabs.
72    pub consumed: bool,
73
74    /// Do we need an egui refresh because of this event?
75    pub repaint: bool,
76}
77
78// ----------------------------------------------------------------------------
79
80/// Handles the integration between egui and a winit Window.
81///
82/// Instantiate one of these per viewport/window.
83pub struct State {
84    /// Shared clone.
85    egui_ctx: egui::Context,
86
87    viewport_id: ViewportId,
88    start_time: web_time::Instant,
89    egui_input: egui::RawInput,
90
91    /// The current modifier state.
92    ///
93    /// We keep a copy so we can stamp
94    /// it onto per-event `modifiers` fields and emit [`egui::Event::ModifiersChanged`].
95    modifiers: egui::Modifiers,
96
97    pointer_pos_in_points: Option<egui::Pos2>,
98    any_pointer_button_down: bool,
99    current_cursor_icon: Option<egui::CursorIcon>,
100
101    /// Cached `CustomCursor` for the last RGBA bitmap pushed through
102    /// `PlatformOutput::cursor_image`. We dedupe by `Arc::as_ptr` so the
103    /// integration only re-uploads the bitmap to the OS when the app
104    /// switches sprite, not every frame the cursor moves. `usize` is the
105    /// raw pointer of the source `Arc<[u8]>` — opaque, only used as a
106    /// cache key.
107    current_custom_cursor: Option<(usize, CustomCursor)>,
108
109    clipboard: clipboard::Clipboard,
110
111    /// If `true`, mouse inputs will be treated as touches.
112    /// Useful for debugging touch support in egui.
113    ///
114    /// Creates duplicate touches, if real touch inputs are coming.
115    simulate_touch_screen: bool,
116
117    /// Is Some(…) when a touch is being translated to a pointer.
118    ///
119    /// Only one touch will be interpreted as pointer at any time.
120    pointer_touch_id: Option<u64>,
121
122    #[cfg(feature = "accesskit")]
123    pub accesskit: Option<accesskit_winit::Adapter>,
124
125    allow_ime: bool,
126    ime_rect_px: Option<egui::Rect>,
127    old_ime_purpose: egui::IMEPurpose,
128
129    /// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
130    /// release events that should be filtered out. See comments in that method
131    /// for details.
132    #[cfg(target_os = "windows")]
133    pressed_processed_physical_keys: HashSet<winit::keyboard::PhysicalKey>,
134}
135
136impl State {
137    /// Construct a new instance
138    pub fn new(
139        egui_ctx: egui::Context,
140        viewport_id: ViewportId,
141        display_target: &dyn HasDisplayHandle,
142        native_pixels_per_point: Option<f32>,
143        theme: Option<winit::window::Theme>,
144        max_texture_side: Option<usize>,
145    ) -> Self {
146        profiling::function_scope!();
147
148        let egui_input = egui::RawInput {
149            focused: false, // winit will tell us when we have focus
150            ..Default::default()
151        };
152
153        let mut slf = Self {
154            viewport_id,
155            start_time: web_time::Instant::now()
156                .checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
157                .unwrap_or_else(web_time::Instant::now),
158            egui_ctx,
159            egui_input,
160            modifiers: egui::Modifiers::default(),
161            pointer_pos_in_points: None,
162            any_pointer_button_down: false,
163            current_cursor_icon: None,
164            current_custom_cursor: None,
165
166            clipboard: clipboard::Clipboard::new(
167                display_target.display_handle().ok().map(|h| h.as_raw()),
168            ),
169
170            simulate_touch_screen: false,
171            pointer_touch_id: None,
172
173            #[cfg(feature = "accesskit")]
174            accesskit: None,
175
176            allow_ime: false,
177            ime_rect_px: None,
178            old_ime_purpose: egui::IMEPurpose::Normal,
179            #[cfg(target_os = "windows")]
180            pressed_processed_physical_keys: HashSet::new(),
181        };
182
183        slf.egui_input
184            .viewports
185            .entry(ViewportId::ROOT)
186            .or_default()
187            .native_pixels_per_point = native_pixels_per_point;
188        slf.egui_input.system_theme = theme.map(to_egui_theme);
189
190        if let Some(max_texture_side) = max_texture_side {
191            slf.set_max_texture_side(max_texture_side);
192        }
193        slf
194    }
195
196    #[cfg(feature = "accesskit")]
197    pub fn init_accesskit<T: From<accesskit_winit::Event> + Send>(
198        &mut self,
199        event_loop: &ActiveEventLoop,
200        window: &Window,
201        event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
202    ) {
203        profiling::function_scope!();
204
205        self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy(
206            event_loop,
207            window,
208            event_loop_proxy,
209        ));
210    }
211
212    /// Call this once a graphics context has been created to update the maximum texture dimensions
213    /// that egui will use.
214    pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
215        self.egui_input.max_texture_side = Some(max_texture_side);
216    }
217
218    /// Fetches text from the clipboard and returns it.
219    pub fn clipboard_text(&mut self) -> Option<String> {
220        self.clipboard.get()
221    }
222
223    /// Places the text onto the clipboard.
224    pub fn set_clipboard_text(&mut self, text: String) {
225        self.clipboard.set_text(text);
226    }
227
228    /// Returns [`false`] or the last value that [`Window::set_ime_allowed()`] was called with, used for debouncing.
229    pub fn allow_ime(&self) -> bool {
230        self.allow_ime
231    }
232
233    /// Set the last value that [`Window::set_ime_allowed()`] was called with.
234    pub fn set_allow_ime(&mut self, allow: bool) {
235        self.allow_ime = allow;
236    }
237
238    #[inline]
239    pub fn egui_ctx(&self) -> &egui::Context {
240        &self.egui_ctx
241    }
242
243    /// The current input state.
244    /// This is changed by [`Self::on_window_event`] and cleared by [`Self::take_egui_input`].
245    #[inline]
246    pub fn egui_input(&self) -> &egui::RawInput {
247        &self.egui_input
248    }
249
250    /// The current input state.
251    /// This is changed by [`Self::on_window_event`] and cleared by [`Self::take_egui_input`].
252    #[inline]
253    pub fn egui_input_mut(&mut self) -> &mut egui::RawInput {
254        &mut self.egui_input
255    }
256
257    /// Prepare for a new frame by extracting the accumulated input,
258    ///
259    /// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect).
260    ///
261    /// You need to set [`egui::RawInput::viewports`] yourself though.
262    /// Use [`update_viewport_info`] to update the info for each
263    /// viewport.
264    pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput {
265        profiling::function_scope!();
266
267        self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
268
269        // On Windows, a minimized window will have 0 width and height.
270        // See: https://github.com/rust-windowing/winit/issues/208
271        // This solves an issue where egui window positions would be changed when minimizing on Windows.
272        let screen_size_in_pixels = screen_size_in_pixels(window);
273        let screen_size_in_points =
274            screen_size_in_pixels / pixels_per_point(&self.egui_ctx, window);
275
276        self.egui_input.screen_rect = (screen_size_in_points.x > 0.0
277            && screen_size_in_points.y > 0.0)
278            .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points));
279
280        // Tell egui which viewport is now active:
281        self.egui_input.viewport_id = self.viewport_id;
282
283        self.egui_input
284            .viewports
285            .entry(self.viewport_id)
286            .or_default()
287            .native_pixels_per_point = Some(window.scale_factor() as f32);
288
289        self.egui_input.take()
290    }
291
292    /// Call this when there is a new event.
293    ///
294    /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
295    pub fn on_window_event(
296        &mut self,
297        window: &Window,
298        event: &winit::event::WindowEvent,
299    ) -> EventResponse {
300        profiling::function_scope!(short_window_event_description(event));
301
302        #[cfg(feature = "accesskit")]
303        if let Some(accesskit) = self.accesskit.as_mut() {
304            accesskit.process_event(window, event);
305        }
306
307        use winit::event::WindowEvent;
308
309        #[cfg(target_os = "ios")]
310        match &event {
311            WindowEvent::Resized(_)
312            | WindowEvent::ScaleFactorChanged { .. }
313            | WindowEvent::Focused(true)
314            | WindowEvent::Occluded(false) => {
315                // Once winit v0.31 has been released this can be reworked to get the safe area from
316                // `Window::safe_area`, and updated from a new event which is being discussed in
317                // https://github.com/rust-windowing/winit/issues/3911.
318                self.egui_input_mut().safe_area_insets = Some(safe_area::get_safe_area_insets());
319            }
320            _ => {}
321        }
322
323        match event {
324            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
325                let native_pixels_per_point = *scale_factor as f32;
326
327                self.egui_input
328                    .viewports
329                    .entry(self.viewport_id)
330                    .or_default()
331                    .native_pixels_per_point = Some(native_pixels_per_point);
332
333                EventResponse {
334                    repaint: true,
335                    consumed: false,
336                }
337            }
338            WindowEvent::MouseInput { state, button, .. } => {
339                self.on_mouse_button_input(*state, *button);
340                EventResponse {
341                    repaint: true,
342                    consumed: self.egui_ctx.egui_wants_pointer_input(),
343                }
344            }
345            WindowEvent::MouseWheel { delta, phase, .. } => {
346                self.on_mouse_wheel(window, *delta, *phase);
347                EventResponse {
348                    repaint: true,
349                    consumed: self.egui_ctx.egui_wants_pointer_input(),
350                }
351            }
352            WindowEvent::CursorMoved { position, .. } => {
353                self.on_cursor_moved(window, *position);
354                EventResponse {
355                    repaint: true,
356                    consumed: self.egui_ctx.egui_is_using_pointer(),
357                }
358            }
359            WindowEvent::CursorLeft { .. } => {
360                self.pointer_pos_in_points = None;
361                self.egui_input.events.push(egui::Event::PointerGone);
362                EventResponse {
363                    repaint: true,
364                    consumed: false,
365                }
366            }
367            // WindowEvent::TouchpadPressure {device_id, pressure, stage, ..  } => {} // TODO(emilk)
368            WindowEvent::Touch(touch) => {
369                self.on_touch(window, touch);
370                let consumed = match touch.phase {
371                    winit::event::TouchPhase::Started
372                    | winit::event::TouchPhase::Ended
373                    | winit::event::TouchPhase::Cancelled => {
374                        self.egui_ctx.egui_wants_pointer_input()
375                    }
376                    winit::event::TouchPhase::Moved => self.egui_ctx.egui_is_using_pointer(),
377                };
378                EventResponse {
379                    repaint: true,
380                    consumed,
381                }
382            }
383
384            WindowEvent::Ime(ime) => {
385                self.on_ime(ime);
386
387                EventResponse {
388                    repaint: true,
389                    consumed: self.egui_ctx.egui_wants_keyboard_input(),
390                }
391            }
392            WindowEvent::KeyboardInput {
393                event,
394                is_synthetic,
395                ..
396            } => {
397                if *is_synthetic && event.state == ElementState::Pressed {
398                    // Winit generates fake "synthetic" KeyboardInput events when the focus
399                    // is changed to the window, or away from it. Synthetic key presses
400                    // represent no real key presses and should be ignored.
401                    // See https://github.com/rust-windowing/winit/issues/3543
402                    EventResponse {
403                        repaint: true,
404                        consumed: false,
405                    }
406                } else {
407                    let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input();
408
409                    if let Some(response) =
410                        self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input)
411                    {
412                        response
413                    } else {
414                        self.on_keyboard_input(event);
415
416                        // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
417                        let consumed = egui_wants_keyboard_input
418                            || event.logical_key
419                                == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
420                        EventResponse {
421                            repaint: true,
422                            consumed,
423                        }
424                    }
425                }
426            }
427            WindowEvent::Focused(focused) => {
428                let focused = if cfg!(target_os = "macos") {
429                    // TODO(emilk): remove this work-around once we update winit
430                    // https://github.com/rust-windowing/winit/issues/4371
431                    // https://github.com/emilk/egui/issues/7588
432                    window.has_focus()
433                } else {
434                    *focused
435                };
436
437                self.egui_input.focused = focused;
438                if !focused {
439                    // Avoid sticky modifiers when focus is lost (egui clears its own copy too).
440                    self.modifiers = egui::Modifiers::default();
441                }
442                self.egui_input
443                    .events
444                    .push(egui::Event::WindowFocused(focused));
445                EventResponse {
446                    repaint: true,
447                    consumed: false,
448                }
449            }
450            WindowEvent::ThemeChanged(winit_theme) => {
451                self.egui_input.system_theme = Some(to_egui_theme(*winit_theme));
452                EventResponse {
453                    repaint: true,
454                    consumed: false,
455                }
456            }
457            WindowEvent::HoveredFile(path) => {
458                self.egui_input.hovered_files.push(egui::HoveredFile {
459                    path: Some(path.clone()),
460                    ..Default::default()
461                });
462                EventResponse {
463                    repaint: true,
464                    consumed: false,
465                }
466            }
467            WindowEvent::HoveredFileCancelled => {
468                self.egui_input.hovered_files.clear();
469                EventResponse {
470                    repaint: true,
471                    consumed: false,
472                }
473            }
474            WindowEvent::DroppedFile(path) => {
475                self.egui_input.hovered_files.clear();
476                self.egui_input
477                    .dropped_files
478                    .push(std::sync::Arc::new(NativeFile::from(path.clone())));
479                EventResponse {
480                    repaint: true,
481                    consumed: false,
482                }
483            }
484            WindowEvent::ModifiersChanged(state) => {
485                let state = state.state();
486
487                let alt = state.alt_key();
488                let ctrl = state.control_key();
489                let shift = state.shift_key();
490                let super_ = state.super_key();
491
492                self.modifiers.alt = alt;
493                self.modifiers.ctrl = ctrl;
494                self.modifiers.shift = shift;
495                self.modifiers.mac_cmd = cfg!(target_os = "macos") && super_;
496                self.modifiers.command = if cfg!(target_os = "macos") {
497                    super_
498                } else {
499                    ctrl
500                };
501
502                self.egui_input
503                    .events
504                    .push(egui::Event::ModifiersChanged(self.modifiers));
505
506                EventResponse {
507                    repaint: true,
508                    consumed: false,
509                }
510            }
511
512            // Things that may require repaint:
513            WindowEvent::RedrawRequested
514            | WindowEvent::CursorEntered { .. }
515            | WindowEvent::Destroyed
516            | WindowEvent::Occluded(_)
517            | WindowEvent::Resized(_)
518            | WindowEvent::Moved(_)
519            | WindowEvent::TouchpadPressure { .. }
520            | WindowEvent::CloseRequested => EventResponse {
521                repaint: true,
522                consumed: false,
523            },
524
525            // Things we completely ignore:
526            WindowEvent::ActivationTokenDone { .. }
527            | WindowEvent::AxisMotion { .. }
528            | WindowEvent::DoubleTapGesture { .. } => EventResponse {
529                repaint: false,
530                consumed: false,
531            },
532
533            WindowEvent::PinchGesture { delta, .. } => {
534                // Positive delta values indicate magnification (zooming in).
535                // Negative delta values indicate shrinking (zooming out).
536                let zoom_factor = (*delta as f32).exp();
537                self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
538                EventResponse {
539                    repaint: true,
540                    consumed: self.egui_ctx.egui_wants_pointer_input(),
541                }
542            }
543
544            WindowEvent::RotationGesture { delta, .. } => {
545                // Positive delta values indicate counterclockwise rotation
546                // Negative delta values indicate clockwise rotation
547                // This is opposite of egui's sign convention for angles
548                self.egui_input
549                    .events
550                    .push(egui::Event::Rotate(-delta.to_radians()));
551                EventResponse {
552                    repaint: true,
553                    consumed: self.egui_ctx.egui_wants_pointer_input(),
554                }
555            }
556
557            WindowEvent::PanGesture { delta, phase, .. } => {
558                let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
559
560                self.egui_input.events.push(egui::Event::MouseWheel {
561                    unit: egui::MouseWheelUnit::Point,
562                    delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
563                    phase: to_egui_touch_phase(*phase),
564                    modifiers: self.modifiers,
565                });
566                EventResponse {
567                    repaint: true,
568                    consumed: self.egui_ctx.egui_wants_pointer_input(),
569                }
570            }
571        }
572    }
573
574    #[cfg(not(target_os = "windows"))]
575    #[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)]
576    #[inline(always)]
577    fn try_on_ime_processed_keyboard_input(
578        &mut self,
579        _event: &winit::event::KeyEvent,
580        _egui_wants_keyboard_input: bool,
581    ) -> Option<EventResponse> {
582        // `KeyboardInput` events processed by the IME are not emitted by
583        // `winit` on non-Windows platforms, so we don't need to do anything
584        // here.
585
586        None
587    }
588
589    #[cfg(target_os = "windows")]
590    #[inline(always)]
591    fn try_on_ime_processed_keyboard_input(
592        &mut self,
593        event: &winit::event::KeyEvent,
594        egui_wants_keyboard_input: bool,
595    ) -> Option<EventResponse> {
596        if !self.allow_ime {
597            None
598        } else if event.logical_key == winit::keyboard::NamedKey::Process {
599            // On Windows, the current version of `winit` (0.30.12) has a bug
600            // where `KeyboardInput` events processed by the IME are still
601            // emitted. [^1]
602            //
603            // As a workaround, we detect these events by checking whether their
604            // `logical_key` is `winit::keyboard::NamedKey::Process`, and filter
605            // them out to keep behavior consistent with other platforms.
606            //
607            // `winit::keyboard::NamedKey::Process` is not documented in
608            // `winit`. Reading through its source code, we find that it is
609            // mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated
610            // note, Web is the only other platform that also uses it [^3].)
611            // According to Microsoft, “the IME sets the virtual key value
612            // to `VK_PROCESSKEY` after processing a key input message” [^4].
613            // See also [^5].
614            // (I can't find a documentation page dedicated to this value.)
615            //
616            // TODO(umajho): Remove this workaround once the `winit` bug is fixed
617            // and we've updated to a version that includes the fix. NOTE: Don't
618            // forget to also remove the `pressed_processed_physical_keys` field
619            // and its related code.
620            //
621            // [^1]: https://github.com/rust-windowing/winit/issues/4508
622            // [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946
623            // [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
624            // [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks
625            // [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages
626
627            self.pressed_processed_physical_keys
628                .insert(event.physical_key);
629
630            Some(EventResponse {
631                repaint: false,
632                consumed: egui_wants_keyboard_input,
633            })
634        } else if event.state == ElementState::Released
635            && self
636                .pressed_processed_physical_keys
637                .remove(&event.physical_key)
638        {
639            // Unlike key-presses, we can not tell whether a key-release event
640            // is processed by the IME or not by looking at its `logical_key`,
641            // because their `logical_key` is the original value (e.g.
642            // `winit::keyboard::Key::Character(…)`) rather than
643            // `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`.
644            // (See the screencast for Windows in [^1].)
645            // So we track the physical keys of processed key-presses and
646            // filter out the corresponding key-releases.
647            //
648            // [^1]: https://github.com/rust-windowing/winit/issues/4508
649
650            Some(EventResponse {
651                repaint: false,
652                consumed: egui_wants_keyboard_input,
653            })
654        } else {
655            None
656        }
657    }
658
659    /// ## NOTE
660    ///
661    /// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
662    /// So no need to check `is_mac_cmd`.
663    ///
664    /// ### How events are emitted by [`winit`] across different setups in various situations
665    ///
666    /// This is done by uncommenting the code block at the top of this method
667    /// and checking console outputs.
668    ///
669    /// winit version: 0.30.12.
670    ///
671    /// #### Setups
672    ///
673    /// - `a-macos15-apple_shuangpin`: macOS 15.7.3 `aarch64`, IME: builtin Chinese Shuangpin - Simplified. (Demo app shows: renderer: `wgpu`, backend: `Metal`.)
674    /// - `b-debian13_gnome48_wayland-fcitx5_shuangpin`: Debian 13 `aarch64`, Gnome 48, Wayland, IME: Fcitx5 with fcitx5-chinese-addons's Shuangpin. (Demo app shows: renderer: `wgpu`, backend: `Gl`.)
675    /// - `c-windows11-ms_pinyin`: Windows11 23H2 `x86_64`, IME: builtin Microsoft Pinyin. (Demo app shows: renderer: `wgpu`, backend: `Vulkan` & `Dx12`, others: `Dx12` & `Gl`.)
676    ///
677    /// #### Situation: pressed space to select the first candidate "测试"
678    ///
679    /// | Setup                                       | Events in Order                                                                                                                  |
680    /// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
681    /// | a-macos15-apple_shuangpin                   | `Preedit("", None)` -> `Commit("测试")`                                                                                          |
682    /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", None)` -> `Commit("测试")` -> `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
683    /// | c-windows11-ms_pinyin                       | `Preedit("测试", Some(…))` -> `Preedit("", None)` -> `Commit("测试")` -> `Disabled`                                              |
684    ///
685    /// #### Situation: pressed backspace to delete the last character in the composition
686    ///
687    /// | Setup                                       | Events in Order                                                                       |
688    /// | a-macos15-apple_shuangpin                   | `Preedit("", None)`                                                                   |
689    /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
690    /// | c-windows11-ms_pinyin                       | `Preedit("", Some(0, 0))` -> `Preedit("", None)` -> `Commit("")` -> `Disabled`        |
691    ///
692    /// #### Situation: clicked somewhere else while there is an active composition with the pre-edit text "ce"
693    ///
694    /// | Setup                                       | Events in Order                                                                                   |
695    /// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
696    /// | a-macos15-apple_shuangpin                   | nothing emitted                                                                                   |
697    /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` (duplicate) -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
698    /// | c-windows11-ms_pinyin                       | nothing emitted                                                                                   |
699    fn on_ime(&mut self, ime: &winit::event::Ime) {
700        // // code for inspecting ime events emitted by winit:
701        // {
702        //     static LAST_IME: std::sync::Mutex<Option<winit::event::Ime>> =
703        //         std::sync::Mutex::new(None);
704        //     static IS_LAST_DUPLICATE: std::sync::atomic::AtomicBool =
705        //         std::sync::atomic::AtomicBool::new(false);
706        //     let mut last_ime_guard = LAST_IME.lock().unwrap();
707        //     if { last_ime_guard.as_ref().cloned() }.as_ref() != Some(ime) {
708        //         println!("IME={ime:?}");
709        //         *last_ime_guard = Some(ime.clone());
710        //         IS_LAST_DUPLICATE.store(false, std::sync::atomic::Ordering::Relaxed);
711        //     } else if !IS_LAST_DUPLICATE.load(std::sync::atomic::Ordering::Relaxed) {
712        //         println!("IME=(duplicate)");
713        //         IS_LAST_DUPLICATE.store(true, std::sync::atomic::Ordering::Relaxed);
714        //     }
715        // }
716
717        match ime {
718            // [`winit::event::Ime::Enabled`] means different things in X11 and
719            // Wayland, but it doesn't matter to us.
720            // See <https://github.com/rust-windowing/winit/issues/2498>
721            winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
722            winit::event::Ime::Preedit(text, active_range_bytes) => {
723                let active_range_chars = match *active_range_bytes {
724                    Some((start_bytes, end_bytes)) => {
725                        if let (Some(start_chars), Some(middle_chars)) = (
726                            text.get(..start_bytes).map(|s| s.chars().count()),
727                            text.get(start_bytes..end_bytes).map(|s| s.chars().count()),
728                        ) {
729                            if cfg!(target_os = "windows") && start_chars == 0 && middle_chars == 0
730                            {
731                                // Workaround for a bug on Windows where `winit`
732                                // incorrectly reports the cursor position at
733                                // the start of the preedit text during
734                                // composition with the builtin Korean IME.
735                                // See: https://github.com/emilk/egui/pull/8083#issuecomment-4206742668
736                                // TODO(umajho): Remove this workaround once the
737                                // `winit` bug is fixed and we've updated to a
738                                // version that includes the fix.
739                                None
740                            } else {
741                                Some(start_chars..start_chars + middle_chars)
742                            }
743                        } else {
744                            log::warn!("ignoring {ime:?}'s range because it is invalid");
745                            None
746                        }
747                    }
748                    None => None,
749                };
750
751                self.egui_input
752                    .events
753                    .push(egui::Event::Ime(egui::ImeEvent::Preedit {
754                        text: text.clone(),
755                        active_range_chars,
756                    }));
757            }
758            winit::event::Ime::Commit(text) => {
759                self.egui_input
760                    .events
761                    .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
762            }
763        }
764    }
765
766    /// Returns `true` if the event was sent to egui.
767    pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
768        if !self.is_pointer_in_window() && !self.any_pointer_button_down {
769            return false;
770        }
771
772        self.egui_input.events.push(egui::Event::MouseMoved(Vec2 {
773            x: delta.0 as f32,
774            y: delta.1 as f32,
775        }));
776        true
777    }
778
779    /// Returns `true` when the pointer is currently inside the window.
780    pub fn is_pointer_in_window(&self) -> bool {
781        self.pointer_pos_in_points.is_some()
782    }
783
784    /// Returns `true` if any pointer button is currently held down.
785    pub fn is_any_pointer_button_down(&self) -> bool {
786        self.any_pointer_button_down
787    }
788
789    /// Call this when there is a new [`accesskit::ActionRequest`].
790    ///
791    /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
792    #[cfg(feature = "accesskit")]
793    pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
794        self.egui_input
795            .events
796            .push(egui::Event::AccessKitActionRequest(request));
797    }
798
799    fn on_mouse_button_input(
800        &mut self,
801        state: winit::event::ElementState,
802        button: winit::event::MouseButton,
803    ) {
804        if let Some(pos) = self.pointer_pos_in_points
805            && let Some(button) = translate_mouse_button(button)
806        {
807            let pressed = state == winit::event::ElementState::Pressed;
808
809            self.egui_input.events.push(egui::Event::PointerButton {
810                pos,
811                button,
812                pressed,
813                modifiers: self.modifiers,
814            });
815
816            if self.simulate_touch_screen {
817                if pressed {
818                    self.any_pointer_button_down = true;
819
820                    self.egui_input.events.push(egui::Event::Touch {
821                        device_id: egui::TouchDeviceId(0),
822                        id: egui::TouchId(0),
823                        phase: egui::TouchPhase::Start,
824                        pos,
825                        force: None,
826                    });
827                } else {
828                    self.any_pointer_button_down = false;
829
830                    self.egui_input.events.push(egui::Event::PointerGone);
831
832                    self.egui_input.events.push(egui::Event::Touch {
833                        device_id: egui::TouchDeviceId(0),
834                        id: egui::TouchId(0),
835                        phase: egui::TouchPhase::End,
836                        pos,
837                        force: None,
838                    });
839                }
840            }
841        }
842    }
843
844    fn on_cursor_moved(
845        &mut self,
846        window: &Window,
847        pos_in_pixels: winit::dpi::PhysicalPosition<f64>,
848    ) {
849        let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
850
851        let pos_in_points = egui::pos2(
852            pos_in_pixels.x as f32 / pixels_per_point,
853            pos_in_pixels.y as f32 / pixels_per_point,
854        );
855        self.pointer_pos_in_points = Some(pos_in_points);
856
857        if self.simulate_touch_screen {
858            if self.any_pointer_button_down {
859                self.egui_input
860                    .events
861                    .push(egui::Event::PointerMoved(pos_in_points));
862
863                self.egui_input.events.push(egui::Event::Touch {
864                    device_id: egui::TouchDeviceId(0),
865                    id: egui::TouchId(0),
866                    phase: egui::TouchPhase::Move,
867                    pos: pos_in_points,
868                    force: None,
869                });
870            }
871        } else {
872            self.egui_input
873                .events
874                .push(egui::Event::PointerMoved(pos_in_points));
875        }
876    }
877
878    fn on_touch(&mut self, window: &Window, touch: &winit::event::Touch) {
879        let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
880
881        // Emit touch event
882        self.egui_input.events.push(egui::Event::Touch {
883            device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
884            id: egui::TouchId::from(touch.id),
885            phase: to_egui_touch_phase(touch.phase),
886            pos: egui::pos2(
887                touch.location.x as f32 / pixels_per_point,
888                touch.location.y as f32 / pixels_per_point,
889            ),
890            force: match touch.force {
891                Some(winit::event::Force::Normalized(force)) => Some(force as f32),
892                Some(winit::event::Force::Calibrated {
893                    force,
894                    max_possible_force,
895                    ..
896                }) => Some((force / max_possible_force) as f32),
897                None => None,
898            },
899        });
900        // If we're not yet translating a touch or we're translating this very
901        // touch …
902        if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap_or_default() == touch.id
903        {
904            // … emit PointerButton resp. PointerMoved events to emulate mouse
905            match touch.phase {
906                winit::event::TouchPhase::Started => {
907                    self.pointer_touch_id = Some(touch.id);
908                    // First move the pointer to the right location
909                    self.on_cursor_moved(window, touch.location);
910                    self.on_mouse_button_input(
911                        winit::event::ElementState::Pressed,
912                        winit::event::MouseButton::Left,
913                    );
914                }
915                winit::event::TouchPhase::Moved => {
916                    self.on_cursor_moved(window, touch.location);
917                }
918                winit::event::TouchPhase::Ended => {
919                    self.pointer_touch_id = None;
920                    self.on_mouse_button_input(
921                        winit::event::ElementState::Released,
922                        winit::event::MouseButton::Left,
923                    );
924                    // The pointer should vanish completely to not get any
925                    // hover effects
926                    self.pointer_pos_in_points = None;
927                    self.egui_input.events.push(egui::Event::PointerGone);
928                }
929                winit::event::TouchPhase::Cancelled => {
930                    self.pointer_touch_id = None;
931                    self.pointer_pos_in_points = None;
932                    self.egui_input.events.push(egui::Event::PointerGone);
933                }
934            }
935        }
936    }
937
938    fn on_mouse_wheel(
939        &mut self,
940        window: &Window,
941        delta: winit::event::MouseScrollDelta,
942        phase: winit::event::TouchPhase,
943    ) {
944        let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
945
946        {
947            let (unit, delta) = match delta {
948                winit::event::MouseScrollDelta::LineDelta(x, y) => {
949                    (egui::MouseWheelUnit::Line, egui::vec2(x, y))
950                }
951                winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition {
952                    x,
953                    y,
954                }) => (
955                    egui::MouseWheelUnit::Point,
956                    egui::vec2(x as f32, y as f32) / pixels_per_point,
957                ),
958            };
959            let phase = to_egui_touch_phase(phase);
960            let modifiers = self.modifiers;
961            self.egui_input.events.push(egui::Event::MouseWheel {
962                unit,
963                delta,
964                phase,
965                modifiers,
966            });
967        }
968    }
969
970    fn on_keyboard_input(&mut self, event: &winit::event::KeyEvent) {
971        let winit::event::KeyEvent {
972            // Represents the position of a key independent of the currently active layout.
973            //
974            // It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode).
975            // The most prevalent use case for this is games. For example the default keys for the player
976            // to move around might be the W, A, S, and D keys on a US layout. The position of these keys
977            // is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY"
978            // layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.)
979            physical_key,
980
981            // Represents the results of a keymap, i.e. what character a certain key press represents.
982            // When telling users "Press Ctrl-F to find", this is where we should
983            // look for the "F" key, because they may have a dvorak layout on
984            // a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position.
985            logical_key: winit_logical_key,
986
987            text,
988
989            state,
990
991            location: _, // e.g. is it on the numpad?
992            repeat: _,   // egui will figure this out for us
993            ..
994        } = event;
995
996        let pressed = *state == winit::event::ElementState::Pressed;
997
998        let physical_key = if let winit::keyboard::PhysicalKey::Code(keycode) = *physical_key {
999            key_from_key_code(keycode)
1000        } else {
1001            None
1002        };
1003
1004        let logical_key = key_from_winit_key(winit_logical_key);
1005
1006        // Helpful logging to enable when adding new key support
1007        log::trace!(
1008            "logical {:?} -> {:?},  physical {:?} -> {:?}",
1009            event.logical_key,
1010            logical_key,
1011            event.physical_key,
1012            physical_key
1013        );
1014
1015        // "Logical OR physical key" is a fallback mechanism for keyboard layouts without Latin characters: it lets them
1016        // emit events as if the corresponding keys from the Latin layout were pressed. In this case, clipboard shortcuts
1017        // are mapped to the physical keys that normally contain C, X, V, etc.
1018        // See also: https://github.com/emilk/egui/issues/3653
1019        if let Some(active_key) = logical_key.or(physical_key) {
1020            if pressed {
1021                if is_cut_command(self.modifiers, active_key) {
1022                    self.egui_input.events.push(egui::Event::Cut);
1023                    return;
1024                } else if is_copy_command(self.modifiers, active_key) {
1025                    self.egui_input.events.push(egui::Event::Copy);
1026                    return;
1027                } else if is_paste_command(self.modifiers, active_key) {
1028                    if let Some(contents) = self.clipboard.get() {
1029                        let contents = contents.replace("\r\n", "\n");
1030                        if !contents.is_empty() {
1031                            self.egui_input.events.push(egui::Event::Paste(contents));
1032                        }
1033                    }
1034                    return;
1035                }
1036            }
1037
1038            self.egui_input.events.push(egui::Event::Key {
1039                key: active_key,
1040                physical_key,
1041                pressed,
1042                repeat: false, // egui will fill this in for us!
1043                modifiers: self.modifiers,
1044            });
1045        }
1046
1047        if let Some(text) = text
1048            .as_ref()
1049            .map(|t| t.as_str())
1050            .or_else(|| winit_logical_key.to_text())
1051        {
1052            // Make sure there is text, and that it is not control characters
1053            // (e.g. delete is sent as "\u{f728}" on macOS).
1054            if !text.is_empty() && text.chars().all(is_printable_char) {
1055                // On some platforms we get here when the user presses Cmd-C (copy), ctrl-W, etc.
1056                // We need to ignore these characters that are side-effects of commands.
1057                // Also make sure the key is pressed (not released). On Linux, text might
1058                // contain some data even when the key is released.
1059                let is_cmd =
1060                    self.modifiers.ctrl || self.modifiers.command || self.modifiers.mac_cmd;
1061                if pressed && !is_cmd {
1062                    self.egui_input
1063                        .events
1064                        .push(egui::Event::Text(text.to_owned()));
1065                }
1066            }
1067        }
1068    }
1069
1070    /// Call with the output given by `egui`.
1071    ///
1072    /// This will, if needed:
1073    /// * update the cursor
1074    /// * copy text to the clipboard
1075    /// * open any clicked urls
1076    /// * update the IME
1077    /// *
1078    pub fn handle_platform_output(
1079        &mut self,
1080        window: &Window,
1081        platform_output: egui::PlatformOutput,
1082    ) {
1083        self.handle_platform_output_inner(window, None, platform_output);
1084    }
1085
1086    /// Same as [`Self::handle_platform_output`] but threads the
1087    /// `ActiveEventLoop` so we can register a `winit::CustomCursor` from
1088    /// `PlatformOutput::cursor_image`. Integration paths that don't have
1089    /// access to the event loop (e.g. immediate viewports) should call
1090    /// [`Self::handle_platform_output`] instead — any custom cursor
1091    /// request is silently dropped there and the standard `cursor_icon`
1092    /// path still runs.
1093    pub fn handle_platform_output_with_event_loop(
1094        &mut self,
1095        window: &Window,
1096        event_loop: &ActiveEventLoop,
1097        platform_output: egui::PlatformOutput,
1098    ) {
1099        self.handle_platform_output_inner(window, Some(event_loop), platform_output);
1100    }
1101
1102    fn handle_platform_output_inner(
1103        &mut self,
1104        window: &Window,
1105        event_loop: Option<&ActiveEventLoop>,
1106        platform_output: egui::PlatformOutput,
1107    ) {
1108        profiling::function_scope!();
1109
1110        let egui::PlatformOutput {
1111            commands,
1112            cursor_icon,
1113            cursor_image,
1114            events: _,                    // handled elsewhere
1115            mutable_text_under_cursor: _, // only used in eframe web
1116            ime,
1117            accesskit_update,
1118            num_completed_passes: _,    // `egui::Context::run` handles this
1119            request_discard_reasons: _, // `egui::Context::run` handles this
1120        } = platform_output;
1121
1122        for command in commands {
1123            match command {
1124                egui::OutputCommand::CopyText(text) => {
1125                    self.clipboard.set_text(text);
1126                }
1127                egui::OutputCommand::CopyImage(image) => {
1128                    self.clipboard.set_image(&image);
1129                }
1130                egui::OutputCommand::OpenUrl(open_url) => {
1131                    open_url_in_browser(&open_url.url);
1132                }
1133            }
1134        }
1135
1136        self.apply_cursor(window, event_loop, cursor_icon, cursor_image.as_ref());
1137
1138        let allow_ime = ime.is_some();
1139        let is_toggling_ime = self.allow_ime != allow_ime;
1140        if is_toggling_ime {
1141            self.allow_ime = allow_ime;
1142            #[cfg(target_os = "windows")]
1143            if !self.allow_ime {
1144                // Defensively clear the set to avoid unexpected behavior.
1145                //
1146                // We don't do the same in `ime_event_disable` because the key
1147                // release events for IME confirmation keys arrive after
1148                // `winit::event::Ime::Disabled`.
1149                self.pressed_processed_physical_keys.clear();
1150            }
1151
1152            profiling::scope!("set_ime_allowed");
1153            window.set_ime_allowed(allow_ime);
1154        }
1155
1156        if let Some(ime) = ime {
1157            if !is_toggling_ime && ime.should_interrupt_composition {
1158                // TODO(umajho): use a more proper way to interrupt composition
1159                // if `winit` provides one in the future.
1160
1161                window.set_ime_allowed(false);
1162                window.set_ime_allowed(true);
1163            }
1164
1165            if ime.purpose != self.old_ime_purpose {
1166                self.old_ime_purpose = ime.purpose;
1167                window.set_ime_purpose(to_winit_ime_purpose(ime.purpose));
1168            }
1169
1170            let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
1171            let ime_rect_px = pixels_per_point * ime.rect;
1172            if self.ime_rect_px != Some(ime_rect_px)
1173                || self.egui_ctx.input(|i| !i.events.is_empty())
1174            {
1175                self.ime_rect_px = Some(ime_rect_px);
1176                profiling::scope!("set_ime_cursor_area");
1177                window.set_ime_cursor_area(
1178                    winit::dpi::PhysicalPosition {
1179                        x: ime_rect_px.min.x,
1180                        y: ime_rect_px.min.y,
1181                    },
1182                    winit::dpi::PhysicalSize {
1183                        width: ime_rect_px.width(),
1184                        height: ime_rect_px.height(),
1185                    },
1186                );
1187            }
1188        } else {
1189            self.ime_rect_px = None;
1190        }
1191
1192        #[cfg(feature = "accesskit")]
1193        if let Some(accesskit) = self.accesskit.as_mut()
1194            && let Some(update) = accesskit_update
1195        {
1196            profiling::scope!("accesskit");
1197            accesskit.update_if_active(|| update);
1198        }
1199
1200        #[cfg(not(feature = "accesskit"))]
1201        let _ = accesskit_update;
1202    }
1203
1204    /// Apply either a bitmap cursor (preferred when both `cursor_image`
1205    /// and `event_loop` are `Some`) or the standard `cursor_icon` to the
1206    /// window. Mirrors the no-flicker dedupe the old `set_cursor_icon`
1207    /// did, on the appropriate cache key for whichever path is active.
1208    fn apply_cursor(
1209        &mut self,
1210        window: &Window,
1211        event_loop: Option<&ActiveEventLoop>,
1212        cursor_icon: egui::CursorIcon,
1213        cursor_image: Option<&egui::CustomCursorImage>,
1214    ) {
1215        let is_pointer_in_window = self.pointer_pos_in_points.is_some();
1216        if !is_pointer_in_window {
1217            // Drop both caches so the cursor gets re-applied (and the
1218            // bitmap re-checked for staleness) once the pointer comes
1219            // back. Same contract the old `set_cursor_icon` followed.
1220            self.current_cursor_icon = None;
1221            self.current_custom_cursor = None;
1222            return;
1223        }
1224
1225        // Bitmap cursor wins over CursorIcon when both are present and we
1226        // have an event loop to register it with. Otherwise the bitmap is
1227        // dropped and we fall through to the icon path — this is the
1228        // documented fallback for integrations that didn't opt in.
1229        if let (Some(image), Some(event_loop)) = (cursor_image, event_loop) {
1230            let key = std::sync::Arc::as_ptr(&image.rgba).cast::<u8>() as usize;
1231            let cached = self
1232                .current_custom_cursor
1233                .as_ref()
1234                .filter(|(k, _)| *k == key)
1235                .map(|(_, c)| c.clone());
1236
1237            let custom = match cached {
1238                Some(c) => c,
1239                None => match winit::window::CustomCursor::from_rgba(
1240                    image.rgba.to_vec(),
1241                    image.size[0],
1242                    image.size[1],
1243                    image.hotspot[0],
1244                    image.hotspot[1],
1245                ) {
1246                    Ok(source) => {
1247                        let c = event_loop.create_custom_cursor(source);
1248                        self.current_custom_cursor = Some((key, c.clone()));
1249                        c
1250                    }
1251                    Err(err) => {
1252                        log::warn!(
1253                            "egui-winit: invalid cursor bitmap, falling back to cursor_icon: {err:?}"
1254                        );
1255                        self.current_custom_cursor = None;
1256                        self.set_cursor_icon_inner(window, cursor_icon);
1257                        return;
1258                    }
1259                },
1260            };
1261
1262            window.set_cursor_visible(true);
1263            window.set_cursor(custom);
1264            // Resync `current_cursor_icon` so the next icon-only path
1265            // notices a real change rather than dedupe-skipping it.
1266            self.current_cursor_icon = None;
1267            return;
1268        }
1269
1270        self.current_custom_cursor = None;
1271        self.set_cursor_icon_inner(window, cursor_icon);
1272    }
1273
1274    /// Icon-only path, factored out so `apply_cursor` can fall back to it
1275    /// when the bitmap path bails. Preserves the original dedupe.
1276    fn set_cursor_icon_inner(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
1277        if self.current_cursor_icon == Some(cursor_icon) {
1278            // Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
1279            // On other platforms: just early-out to save CPU.
1280            return;
1281        }
1282
1283        self.current_cursor_icon = Some(cursor_icon);
1284
1285        if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
1286            window.set_cursor_visible(true);
1287            window.set_cursor(winit_cursor_icon);
1288        } else {
1289            window.set_cursor_visible(false);
1290        }
1291    }
1292}
1293
1294fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
1295    match phase {
1296        winit::event::TouchPhase::Started => egui::TouchPhase::Start,
1297        winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
1298        winit::event::TouchPhase::Ended => egui::TouchPhase::End,
1299        winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
1300    }
1301}
1302
1303fn to_egui_theme(theme: winit::window::Theme) -> Theme {
1304    match theme {
1305        winit::window::Theme::Dark => Theme::Dark,
1306        winit::window::Theme::Light => Theme::Light,
1307    }
1308}
1309
1310pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option<Rect> {
1311    let inner_pos_px = window.inner_position().ok()?;
1312    let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32);
1313
1314    let inner_size_px = window.inner_size();
1315    let inner_size_px = egui::vec2(inner_size_px.width as f32, inner_size_px.height as f32);
1316
1317    let inner_rect_px = egui::Rect::from_min_size(inner_pos_px, inner_size_px);
1318
1319    Some(inner_rect_px / pixels_per_point)
1320}
1321
1322pub fn outer_rect_in_points(window: &Window, pixels_per_point: f32) -> Option<Rect> {
1323    let outer_pos_px = window.outer_position().ok()?;
1324    let outer_pos_px = egui::pos2(outer_pos_px.x as f32, outer_pos_px.y as f32);
1325
1326    let outer_size_px = window.outer_size();
1327    let outer_size_px = egui::vec2(outer_size_px.width as f32, outer_size_px.height as f32);
1328
1329    let outer_rect_px = egui::Rect::from_min_size(outer_pos_px, outer_size_px);
1330
1331    Some(outer_rect_px / pixels_per_point)
1332}
1333
1334/// Update the given viewport info with the current state of the window.
1335///
1336/// Call before [`State::take_egui_input`].
1337///
1338/// If this is called right after window creation, `is_init` should be `true`, otherwise `false`.
1339pub fn update_viewport_info(
1340    viewport_info: &mut ViewportInfo,
1341    egui_ctx: &egui::Context,
1342    window: &Window,
1343    is_init: bool,
1344) {
1345    profiling::function_scope!();
1346    let pixels_per_point = pixels_per_point(egui_ctx, window);
1347
1348    let has_a_position = match window.is_minimized() {
1349        Some(true) => false,
1350        Some(false) | None => true,
1351    };
1352
1353    let inner_rect = if has_a_position {
1354        inner_rect_in_points(window, pixels_per_point)
1355    } else {
1356        None
1357    };
1358
1359    let outer_rect = if has_a_position {
1360        outer_rect_in_points(window, pixels_per_point)
1361    } else {
1362        None
1363    };
1364
1365    let monitor_size = {
1366        profiling::scope!("monitor_size");
1367        if let Some(monitor) = window.current_monitor() {
1368            let size = monitor.size().to_logical::<f32>(pixels_per_point.into());
1369            Some(egui::vec2(size.width, size.height))
1370        } else {
1371            None
1372        }
1373    };
1374
1375    viewport_info.title = Some(window.title());
1376    viewport_info.native_pixels_per_point = Some(window.scale_factor() as f32);
1377
1378    viewport_info.monitor_size = monitor_size;
1379    viewport_info.inner_rect = inner_rect;
1380    viewport_info.outer_rect = outer_rect;
1381
1382    if is_init || !cfg!(target_os = "macos") {
1383        // Asking for minimized/maximized state at runtime leads to a deadlock on Mac when running
1384        // `cargo run -p custom_window_frame`.
1385        // See https://github.com/emilk/egui/issues/3494
1386        viewport_info.maximized = Some(window.is_maximized());
1387        viewport_info.minimized = Some(window.is_minimized().unwrap_or(false));
1388    }
1389
1390    viewport_info.fullscreen = Some(window.fullscreen().is_some());
1391    viewport_info.focused = Some(window.has_focus());
1392}
1393
1394fn open_url_in_browser(_url: &str) {
1395    #[cfg(feature = "webbrowser")]
1396    if let Err(err) = webbrowser::open(_url) {
1397        log::warn!("Failed to open url: {err}");
1398    }
1399
1400    #[cfg(not(feature = "webbrowser"))]
1401    {
1402        log::warn!("Cannot open url - feature \"links\" not enabled.");
1403    }
1404}
1405
1406/// Winit sends special keys (backspace, delete, F1, …) as characters.
1407/// Ignore those.
1408/// We also ignore '\r', '\n', '\t'.
1409/// Newlines are handled by the `Key::Enter` event.
1410fn is_printable_char(chr: char) -> bool {
1411    let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}'
1412        || '\u{f0000}' <= chr && chr <= '\u{ffffd}'
1413        || '\u{100000}' <= chr && chr <= '\u{10fffd}';
1414
1415    !is_in_private_use_area && !chr.is_ascii_control()
1416}
1417
1418fn is_cut_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool {
1419    keycode == egui::Key::Cut
1420        || (modifiers.command && keycode == egui::Key::X)
1421        || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Delete)
1422}
1423
1424fn is_copy_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool {
1425    keycode == egui::Key::Copy
1426        || (modifiers.command && keycode == egui::Key::C)
1427        || (cfg!(target_os = "windows") && modifiers.ctrl && keycode == egui::Key::Insert)
1428}
1429
1430fn is_paste_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool {
1431    keycode == egui::Key::Paste
1432        || (modifiers.command && keycode == egui::Key::V)
1433        || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Insert)
1434}
1435
1436fn translate_mouse_button(button: winit::event::MouseButton) -> Option<egui::PointerButton> {
1437    match button {
1438        winit::event::MouseButton::Left => Some(egui::PointerButton::Primary),
1439        winit::event::MouseButton::Right => Some(egui::PointerButton::Secondary),
1440        winit::event::MouseButton::Middle => Some(egui::PointerButton::Middle),
1441        winit::event::MouseButton::Back => Some(egui::PointerButton::Extra1),
1442        winit::event::MouseButton::Forward => Some(egui::PointerButton::Extra2),
1443        winit::event::MouseButton::Other(_) => None,
1444    }
1445}
1446
1447fn key_from_winit_key(key: &winit::keyboard::Key) -> Option<egui::Key> {
1448    match key {
1449        winit::keyboard::Key::Named(named_key) => key_from_named_key(*named_key),
1450        winit::keyboard::Key::Character(str) => egui::Key::from_name(str.as_str()),
1451        winit::keyboard::Key::Unidentified(_) | winit::keyboard::Key::Dead(_) => None,
1452    }
1453}
1454
1455fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option<egui::Key> {
1456    use egui::Key;
1457    use winit::keyboard::NamedKey;
1458
1459    Some(match named_key {
1460        NamedKey::Enter => Key::Enter,
1461        NamedKey::Tab => Key::Tab,
1462        NamedKey::ArrowDown => Key::ArrowDown,
1463        NamedKey::ArrowLeft => Key::ArrowLeft,
1464        NamedKey::ArrowRight => Key::ArrowRight,
1465        NamedKey::ArrowUp => Key::ArrowUp,
1466        NamedKey::End => Key::End,
1467        NamedKey::Home => Key::Home,
1468        NamedKey::PageDown => Key::PageDown,
1469        NamedKey::PageUp => Key::PageUp,
1470        NamedKey::Backspace => Key::Backspace,
1471        NamedKey::Delete => Key::Delete,
1472        NamedKey::Insert => Key::Insert,
1473        NamedKey::Escape => Key::Escape,
1474        NamedKey::Cut => Key::Cut,
1475        NamedKey::Copy => Key::Copy,
1476        NamedKey::Paste => Key::Paste,
1477
1478        NamedKey::Space => Key::Space,
1479
1480        NamedKey::F1 => Key::F1,
1481        NamedKey::F2 => Key::F2,
1482        NamedKey::F3 => Key::F3,
1483        NamedKey::F4 => Key::F4,
1484        NamedKey::F5 => Key::F5,
1485        NamedKey::F6 => Key::F6,
1486        NamedKey::F7 => Key::F7,
1487        NamedKey::F8 => Key::F8,
1488        NamedKey::F9 => Key::F9,
1489        NamedKey::F10 => Key::F10,
1490        NamedKey::F11 => Key::F11,
1491        NamedKey::F12 => Key::F12,
1492        NamedKey::F13 => Key::F13,
1493        NamedKey::F14 => Key::F14,
1494        NamedKey::F15 => Key::F15,
1495        NamedKey::F16 => Key::F16,
1496        NamedKey::F17 => Key::F17,
1497        NamedKey::F18 => Key::F18,
1498        NamedKey::F19 => Key::F19,
1499        NamedKey::F20 => Key::F20,
1500        NamedKey::F21 => Key::F21,
1501        NamedKey::F22 => Key::F22,
1502        NamedKey::F23 => Key::F23,
1503        NamedKey::F24 => Key::F24,
1504        NamedKey::F25 => Key::F25,
1505        NamedKey::F26 => Key::F26,
1506        NamedKey::F27 => Key::F27,
1507        NamedKey::F28 => Key::F28,
1508        NamedKey::F29 => Key::F29,
1509        NamedKey::F30 => Key::F30,
1510        NamedKey::F31 => Key::F31,
1511        NamedKey::F32 => Key::F32,
1512        NamedKey::F33 => Key::F33,
1513        NamedKey::F34 => Key::F34,
1514        NamedKey::F35 => Key::F35,
1515
1516        NamedKey::BrowserBack => Key::BrowserBack,
1517        _ => {
1518            log::trace!("Unknown key: {named_key:?}");
1519            return None;
1520        }
1521    })
1522}
1523
1524fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option<egui::Key> {
1525    use egui::Key;
1526    use winit::keyboard::KeyCode;
1527
1528    Some(match key {
1529        KeyCode::ArrowDown => Key::ArrowDown,
1530        KeyCode::ArrowLeft => Key::ArrowLeft,
1531        KeyCode::ArrowRight => Key::ArrowRight,
1532        KeyCode::ArrowUp => Key::ArrowUp,
1533
1534        KeyCode::Escape => Key::Escape,
1535        KeyCode::Tab => Key::Tab,
1536        KeyCode::Backspace => Key::Backspace,
1537        KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter,
1538
1539        KeyCode::Insert => Key::Insert,
1540        KeyCode::Delete => Key::Delete,
1541        KeyCode::Home => Key::Home,
1542        KeyCode::End => Key::End,
1543        KeyCode::PageUp => Key::PageUp,
1544        KeyCode::PageDown => Key::PageDown,
1545
1546        // Punctuation
1547        KeyCode::Space => Key::Space,
1548        KeyCode::Comma => Key::Comma,
1549        KeyCode::Period => Key::Period,
1550        // KeyCode::Colon => Key::Colon, // NOTE: there is no physical colon key on an american keyboard
1551        KeyCode::Semicolon => Key::Semicolon,
1552        KeyCode::Backslash => Key::Backslash,
1553        KeyCode::Slash | KeyCode::NumpadDivide => Key::Slash,
1554        KeyCode::BracketLeft => Key::OpenBracket,
1555        KeyCode::BracketRight => Key::CloseBracket,
1556        KeyCode::Backquote => Key::Backtick,
1557        KeyCode::Quote => Key::Quote,
1558
1559        KeyCode::Cut => Key::Cut,
1560        KeyCode::Copy => Key::Copy,
1561        KeyCode::Paste => Key::Paste,
1562        KeyCode::Minus | KeyCode::NumpadSubtract => Key::Minus,
1563        KeyCode::NumpadAdd => Key::Plus,
1564        KeyCode::Equal => Key::Equals,
1565
1566        KeyCode::Digit0 | KeyCode::Numpad0 => Key::Num0,
1567        KeyCode::Digit1 | KeyCode::Numpad1 => Key::Num1,
1568        KeyCode::Digit2 | KeyCode::Numpad2 => Key::Num2,
1569        KeyCode::Digit3 | KeyCode::Numpad3 => Key::Num3,
1570        KeyCode::Digit4 | KeyCode::Numpad4 => Key::Num4,
1571        KeyCode::Digit5 | KeyCode::Numpad5 => Key::Num5,
1572        KeyCode::Digit6 | KeyCode::Numpad6 => Key::Num6,
1573        KeyCode::Digit7 | KeyCode::Numpad7 => Key::Num7,
1574        KeyCode::Digit8 | KeyCode::Numpad8 => Key::Num8,
1575        KeyCode::Digit9 | KeyCode::Numpad9 => Key::Num9,
1576
1577        KeyCode::KeyA => Key::A,
1578        KeyCode::KeyB => Key::B,
1579        KeyCode::KeyC => Key::C,
1580        KeyCode::KeyD => Key::D,
1581        KeyCode::KeyE => Key::E,
1582        KeyCode::KeyF => Key::F,
1583        KeyCode::KeyG => Key::G,
1584        KeyCode::KeyH => Key::H,
1585        KeyCode::KeyI => Key::I,
1586        KeyCode::KeyJ => Key::J,
1587        KeyCode::KeyK => Key::K,
1588        KeyCode::KeyL => Key::L,
1589        KeyCode::KeyM => Key::M,
1590        KeyCode::KeyN => Key::N,
1591        KeyCode::KeyO => Key::O,
1592        KeyCode::KeyP => Key::P,
1593        KeyCode::KeyQ => Key::Q,
1594        KeyCode::KeyR => Key::R,
1595        KeyCode::KeyS => Key::S,
1596        KeyCode::KeyT => Key::T,
1597        KeyCode::KeyU => Key::U,
1598        KeyCode::KeyV => Key::V,
1599        KeyCode::KeyW => Key::W,
1600        KeyCode::KeyX => Key::X,
1601        KeyCode::KeyY => Key::Y,
1602        KeyCode::KeyZ => Key::Z,
1603
1604        KeyCode::F1 => Key::F1,
1605        KeyCode::F2 => Key::F2,
1606        KeyCode::F3 => Key::F3,
1607        KeyCode::F4 => Key::F4,
1608        KeyCode::F5 => Key::F5,
1609        KeyCode::F6 => Key::F6,
1610        KeyCode::F7 => Key::F7,
1611        KeyCode::F8 => Key::F8,
1612        KeyCode::F9 => Key::F9,
1613        KeyCode::F10 => Key::F10,
1614        KeyCode::F11 => Key::F11,
1615        KeyCode::F12 => Key::F12,
1616        KeyCode::F13 => Key::F13,
1617        KeyCode::F14 => Key::F14,
1618        KeyCode::F15 => Key::F15,
1619        KeyCode::F16 => Key::F16,
1620        KeyCode::F17 => Key::F17,
1621        KeyCode::F18 => Key::F18,
1622        KeyCode::F19 => Key::F19,
1623        KeyCode::F20 => Key::F20,
1624        KeyCode::F21 => Key::F21,
1625        KeyCode::F22 => Key::F22,
1626        KeyCode::F23 => Key::F23,
1627        KeyCode::F24 => Key::F24,
1628        KeyCode::F25 => Key::F25,
1629        KeyCode::F26 => Key::F26,
1630        KeyCode::F27 => Key::F27,
1631        KeyCode::F28 => Key::F28,
1632        KeyCode::F29 => Key::F29,
1633        KeyCode::F30 => Key::F30,
1634        KeyCode::F31 => Key::F31,
1635        KeyCode::F32 => Key::F32,
1636        KeyCode::F33 => Key::F33,
1637        KeyCode::F34 => Key::F34,
1638        KeyCode::F35 => Key::F35,
1639
1640        // Modifier keys — egui now surfaces them as distinct physical
1641        // variants so games / capture UIs can bind them independently.
1642        // The collapsed `Modifiers.shift/ctrl/alt/command` booleans still
1643        // track just the "any side is pressed" state for shortcut matching.
1644        KeyCode::ShiftLeft => Key::ShiftLeft,
1645        KeyCode::ShiftRight => Key::ShiftRight,
1646        KeyCode::ControlLeft => Key::ControlLeft,
1647        KeyCode::ControlRight => Key::ControlRight,
1648        KeyCode::AltLeft => Key::AltLeft,
1649        KeyCode::AltRight => Key::AltRight,
1650        KeyCode::SuperLeft => Key::SuperLeft,
1651        KeyCode::SuperRight => Key::SuperRight,
1652
1653        // ISO 102nd key — `<>|` on French AZERTY, `\|` on UK QWERTY.
1654        KeyCode::IntlBackslash => Key::IntlBackslash,
1655
1656        _ => {
1657            return None;
1658        }
1659    })
1660}
1661
1662fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::CursorIcon> {
1663    match cursor_icon {
1664        egui::CursorIcon::None => None,
1665
1666        egui::CursorIcon::Alias => Some(winit::window::CursorIcon::Alias),
1667        egui::CursorIcon::AllScroll => Some(winit::window::CursorIcon::AllScroll),
1668        egui::CursorIcon::Cell => Some(winit::window::CursorIcon::Cell),
1669        egui::CursorIcon::ContextMenu => Some(winit::window::CursorIcon::ContextMenu),
1670        egui::CursorIcon::Copy => Some(winit::window::CursorIcon::Copy),
1671        egui::CursorIcon::Crosshair => Some(winit::window::CursorIcon::Crosshair),
1672        egui::CursorIcon::Default => Some(winit::window::CursorIcon::Default),
1673        egui::CursorIcon::Grab => Some(winit::window::CursorIcon::Grab),
1674        egui::CursorIcon::Grabbing => Some(winit::window::CursorIcon::Grabbing),
1675        egui::CursorIcon::Help => Some(winit::window::CursorIcon::Help),
1676        egui::CursorIcon::Move => Some(winit::window::CursorIcon::Move),
1677        egui::CursorIcon::NoDrop => Some(winit::window::CursorIcon::NoDrop),
1678        egui::CursorIcon::NotAllowed => Some(winit::window::CursorIcon::NotAllowed),
1679        egui::CursorIcon::PointingHand => Some(winit::window::CursorIcon::Pointer),
1680        egui::CursorIcon::Progress => Some(winit::window::CursorIcon::Progress),
1681
1682        egui::CursorIcon::ResizeHorizontal => Some(winit::window::CursorIcon::EwResize),
1683        egui::CursorIcon::ResizeNeSw => Some(winit::window::CursorIcon::NeswResize),
1684        egui::CursorIcon::ResizeNwSe => Some(winit::window::CursorIcon::NwseResize),
1685        egui::CursorIcon::ResizeVertical => Some(winit::window::CursorIcon::NsResize),
1686
1687        egui::CursorIcon::ResizeEast => Some(winit::window::CursorIcon::EResize),
1688        egui::CursorIcon::ResizeSouthEast => Some(winit::window::CursorIcon::SeResize),
1689        egui::CursorIcon::ResizeSouth => Some(winit::window::CursorIcon::SResize),
1690        egui::CursorIcon::ResizeSouthWest => Some(winit::window::CursorIcon::SwResize),
1691        egui::CursorIcon::ResizeWest => Some(winit::window::CursorIcon::WResize),
1692        egui::CursorIcon::ResizeNorthWest => Some(winit::window::CursorIcon::NwResize),
1693        egui::CursorIcon::ResizeNorth => Some(winit::window::CursorIcon::NResize),
1694        egui::CursorIcon::ResizeNorthEast => Some(winit::window::CursorIcon::NeResize),
1695        egui::CursorIcon::ResizeColumn => Some(winit::window::CursorIcon::ColResize),
1696        egui::CursorIcon::ResizeRow => Some(winit::window::CursorIcon::RowResize),
1697
1698        egui::CursorIcon::Text => Some(winit::window::CursorIcon::Text),
1699        egui::CursorIcon::VerticalText => Some(winit::window::CursorIcon::VerticalText),
1700        egui::CursorIcon::Wait => Some(winit::window::CursorIcon::Wait),
1701        egui::CursorIcon::ZoomIn => Some(winit::window::CursorIcon::ZoomIn),
1702        egui::CursorIcon::ZoomOut => Some(winit::window::CursorIcon::ZoomOut),
1703    }
1704}
1705
1706// Helpers for egui Viewports
1707// ---------------------------------------------------------------------------
1708#[derive(PartialEq, Eq, Hash, Debug)]
1709pub enum ActionRequested {
1710    Screenshot(egui::UserData),
1711    Cut,
1712    Copy,
1713    Paste,
1714}
1715
1716pub fn process_viewport_commands(
1717    egui_ctx: &egui::Context,
1718    info: &mut ViewportInfo,
1719    commands: impl IntoIterator<Item = ViewportCommand>,
1720    window: &Window,
1721    actions_requested: &mut Vec<ActionRequested>,
1722) {
1723    for command in commands {
1724        process_viewport_command(egui_ctx, window, command, info, actions_requested);
1725    }
1726}
1727
1728fn process_viewport_command(
1729    egui_ctx: &egui::Context,
1730    window: &Window,
1731    command: ViewportCommand,
1732    info: &mut ViewportInfo,
1733    actions_requested: &mut Vec<ActionRequested>,
1734) {
1735    profiling::function_scope!(&format!("{command:?}"));
1736
1737    use winit::window::ResizeDirection;
1738
1739    log::trace!("Processing ViewportCommand::{command:?}");
1740
1741    let pixels_per_point = pixels_per_point(egui_ctx, window);
1742
1743    match command {
1744        ViewportCommand::Close => {
1745            info.events.push(egui::ViewportEvent::Close);
1746        }
1747        ViewportCommand::CancelClose => {
1748            // Need to be handled elsewhere
1749        }
1750        ViewportCommand::StartDrag => {
1751            // If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
1752            if window.has_focus()
1753                && let Err(err) = window.drag_window()
1754            {
1755                log::warn!("{command:?}: {err}");
1756            }
1757        }
1758        ViewportCommand::InnerSize(size) => {
1759            let width_px = pixels_per_point * size.x.max(1.0);
1760            let height_px = pixels_per_point * size.y.max(1.0);
1761            let requested_size = PhysicalSize::new(width_px, height_px);
1762            if let Some(_returned_inner_size) = window.request_inner_size(requested_size) {
1763                // On platforms where the size is entirely controlled by the user the
1764                // applied size will be returned immediately, resize event in such case
1765                // may not be generated.
1766                // e.g. Linux
1767
1768                // On platforms where resizing is disallowed by the windowing system, the current
1769                // inner size is returned immediately, and the user one is ignored.
1770                // e.g. Android, iOS, …
1771
1772                // However, comparing the results is prone to numerical errors
1773                // because the linux backend converts physical to logical and back again.
1774                // So let's just assume it worked:
1775
1776                info.inner_rect = inner_rect_in_points(window, pixels_per_point);
1777                info.outer_rect = outer_rect_in_points(window, pixels_per_point);
1778            } else {
1779                // e.g. macOS, Windows
1780                // The request went to the display system,
1781                // and the actual size will be delivered later with the [`WindowEvent::Resized`].
1782            }
1783        }
1784        ViewportCommand::BeginResize(direction) => {
1785            if let Err(err) = window.drag_resize_window(match direction {
1786                egui::viewport::ResizeDirection::North => ResizeDirection::North,
1787                egui::viewport::ResizeDirection::South => ResizeDirection::South,
1788                egui::viewport::ResizeDirection::East => ResizeDirection::East,
1789                egui::viewport::ResizeDirection::West => ResizeDirection::West,
1790                egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast,
1791                egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast,
1792                egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest,
1793                egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest,
1794            }) {
1795                log::warn!("{command:?}: {err}");
1796            }
1797        }
1798        ViewportCommand::Title(title) => {
1799            window.set_title(&title);
1800        }
1801        ViewportCommand::Transparent(v) => window.set_transparent(v),
1802        ViewportCommand::Visible(v) => window.set_visible(v),
1803        ViewportCommand::OuterPosition(pos) => {
1804            window.set_outer_position(PhysicalPosition::new(
1805                pixels_per_point * pos.x,
1806                pixels_per_point * pos.y,
1807            ));
1808        }
1809        ViewportCommand::MinInnerSize(s) => {
1810            window.set_min_inner_size((s.is_finite() && s != Vec2::ZERO).then_some(
1811                PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y),
1812            ));
1813        }
1814        ViewportCommand::MaxInnerSize(s) => {
1815            window.set_max_inner_size((s.is_finite() && s != Vec2::INFINITY).then_some(
1816                PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y),
1817            ));
1818        }
1819        ViewportCommand::ResizeIncrements(s) => {
1820            window.set_resize_increments(
1821                s.map(|s| PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y)),
1822            );
1823        }
1824        ViewportCommand::Resizable(v) => window.set_resizable(v),
1825        ViewportCommand::EnableButtons {
1826            close,
1827            minimized,
1828            maximize,
1829        } => window.set_enabled_buttons(
1830            if close {
1831                WindowButtons::CLOSE
1832            } else {
1833                WindowButtons::empty()
1834            } | if minimized {
1835                WindowButtons::MINIMIZE
1836            } else {
1837                WindowButtons::empty()
1838            } | if maximize {
1839                WindowButtons::MAXIMIZE
1840            } else {
1841                WindowButtons::empty()
1842            },
1843        ),
1844        ViewportCommand::Minimized(v) => {
1845            window.set_minimized(v);
1846            info.minimized = Some(v);
1847        }
1848        ViewportCommand::Maximized(v) => {
1849            window.set_maximized(v);
1850            info.maximized = Some(v);
1851        }
1852        ViewportCommand::Fullscreen(v) => {
1853            window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
1854        }
1855        ViewportCommand::SetMonitor(idx) => {
1856            if let Some(monitor) = window.available_monitors().nth(idx) {
1857                window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
1858            } else {
1859                log::warn!(
1860                    "ViewportCommand::SetMonitor({idx}): index out of range ({} monitors available)",
1861                    window.available_monitors().count()
1862                );
1863            }
1864        }
1865        ViewportCommand::Decorations(v) => {
1866            window.set_decorations(v);
1867            #[cfg(target_os = "windows")]
1868            {
1869                use winit::platform::windows::WindowExtWindows as _;
1870                window.set_undecorated_shadow(!v);
1871            }
1872        }
1873        ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
1874            egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
1875            egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
1876            egui::viewport::WindowLevel::Normal => WindowLevel::Normal,
1877        }),
1878        ViewportCommand::Icon(icon) => {
1879            let winit_icon = icon.and_then(|icon| to_winit_icon(&icon));
1880            window.set_window_icon(winit_icon);
1881        }
1882        ViewportCommand::IMERect(rect) => {
1883            window.set_ime_cursor_area(
1884                PhysicalPosition::new(pixels_per_point * rect.min.x, pixels_per_point * rect.min.y),
1885                PhysicalSize::new(
1886                    pixels_per_point * rect.size().x,
1887                    pixels_per_point * rect.size().y,
1888                ),
1889            );
1890        }
1891        ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v),
1892        ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(to_winit_ime_purpose(p)),
1893        ViewportCommand::Focus => {
1894            if !window.has_focus() {
1895                window.focus_window();
1896            }
1897        }
1898        ViewportCommand::RequestUserAttention(a) => {
1899            window.request_user_attention(match a {
1900                egui::UserAttentionType::Reset => None,
1901                egui::UserAttentionType::Critical => {
1902                    Some(winit::window::UserAttentionType::Critical)
1903                }
1904                egui::UserAttentionType::Informational => {
1905                    Some(winit::window::UserAttentionType::Informational)
1906                }
1907            });
1908        }
1909        ViewportCommand::SetTheme(t) => window.set_theme(match t {
1910            egui::SystemTheme::Light => Some(winit::window::Theme::Light),
1911            egui::SystemTheme::Dark => Some(winit::window::Theme::Dark),
1912            egui::SystemTheme::SystemDefault => None,
1913        }),
1914        ViewportCommand::ContentProtected(v) => window.set_content_protected(v),
1915        ViewportCommand::CursorPosition(pos) => {
1916            if let Err(err) = window.set_cursor_position(PhysicalPosition::new(
1917                pixels_per_point * pos.x,
1918                pixels_per_point * pos.y,
1919            )) {
1920                log::warn!("{command:?}: {err}");
1921            }
1922        }
1923        ViewportCommand::CursorGrab(o) => {
1924            if let Err(err) = window.set_cursor_grab(match o {
1925                egui::viewport::CursorGrab::None => CursorGrabMode::None,
1926                egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined,
1927                egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked,
1928            }) {
1929                log::warn!("{command:?}: {err}");
1930            }
1931        }
1932        ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v),
1933        ViewportCommand::MousePassthrough(passthrough) => {
1934            if let Err(err) = window.set_cursor_hittest(!passthrough) {
1935                log::warn!("{command:?}: {err}");
1936            }
1937        }
1938        ViewportCommand::Screenshot(user_data) => {
1939            actions_requested.push(ActionRequested::Screenshot(user_data));
1940        }
1941        ViewportCommand::RequestCut => {
1942            actions_requested.push(ActionRequested::Cut);
1943        }
1944        ViewportCommand::RequestCopy => {
1945            actions_requested.push(ActionRequested::Copy);
1946        }
1947        ViewportCommand::RequestPaste => {
1948            actions_requested.push(ActionRequested::Paste);
1949        }
1950    }
1951}
1952
1953fn to_winit_ime_purpose(purpose: egui::IMEPurpose) -> winit::window::ImePurpose {
1954    match purpose {
1955        egui::IMEPurpose::Password => winit::window::ImePurpose::Password,
1956        egui::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
1957        egui::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
1958    }
1959}
1960
1961/// Build and intitlaize a window.
1962///
1963/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`.
1964///
1965/// # Errors
1966/// Possible causes of error include denied permission, incompatible system, and lack of memory.
1967pub fn create_window(
1968    egui_ctx: &egui::Context,
1969    event_loop: &ActiveEventLoop,
1970    viewport_builder: &ViewportBuilder,
1971) -> Result<Window, winit::error::OsError> {
1972    profiling::function_scope!();
1973
1974    let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
1975
1976    // Resolve target monitor index → MonitorHandle, so the window is created
1977    // directly in borderless fullscreen on the requested output. This is the
1978    // only reliable way to target a specific monitor under Wayland, and also
1979    // avoids the Mutter race where OuterPosition is ignored pre-mapping.
1980    if let Some(idx) = viewport_builder.monitor {
1981        if let Some(monitor) = event_loop.available_monitors().nth(idx) {
1982            window_attributes = window_attributes
1983                .with_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
1984        } else {
1985            log::warn!(
1986                "ViewportBuilder::with_monitor({idx}): index out of range ({} monitors available)",
1987                event_loop.available_monitors().count()
1988            );
1989        }
1990    }
1991
1992    let window = event_loop.create_window(window_attributes)?;
1993    apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
1994    Ok(window)
1995}
1996
1997pub fn create_winit_window_attributes(
1998    egui_ctx: &egui::Context,
1999    viewport_builder: ViewportBuilder,
2000) -> winit::window::WindowAttributes {
2001    profiling::function_scope!();
2002
2003    let ViewportBuilder {
2004        title,
2005        position,
2006        inner_size,
2007        min_inner_size,
2008        max_inner_size,
2009        fullscreen,
2010        maximized,
2011        resizable,
2012        transparent,
2013        decorations,
2014        icon,
2015        active,
2016        visible,
2017        close_button,
2018        minimize_button,
2019        maximize_button,
2020        window_level,
2021
2022        // macOS:
2023        fullsize_content_view: _fullsize_content_view,
2024        movable_by_window_background: _movable_by_window_background,
2025        title_shown: _title_shown,
2026        titlebar_buttons_shown: _titlebar_buttons_shown,
2027        titlebar_shown: _titlebar_shown,
2028        has_shadow: _has_shadow,
2029
2030        // Windows:
2031        drag_and_drop: _drag_and_drop,
2032        taskbar: _taskbar,
2033
2034        // wayland:
2035        app_id: _app_id,
2036
2037        // x11
2038        window_type: _window_type,
2039        override_redirect: _override_redirect,
2040
2041        mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
2042        clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs`
2043        monitor: _, // Handled in `create_window` (needs ActiveEventLoop for monitor handle)
2044    } = viewport_builder;
2045
2046    let mut window_attributes = winit::window::WindowAttributes::default()
2047        .with_title(title.unwrap_or_else(|| "egui window".to_owned()))
2048        .with_transparent(transparent.unwrap_or(false))
2049        .with_decorations(decorations.unwrap_or(true))
2050        .with_resizable(resizable.unwrap_or(true))
2051        .with_visible(visible.unwrap_or(true))
2052        .with_maximized(if cfg!(target_os = "ios") {
2053            true
2054        } else {
2055            maximized.unwrap_or(false)
2056        })
2057        .with_window_level(match window_level.unwrap_or_default() {
2058            egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
2059            egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
2060            egui::viewport::WindowLevel::Normal => WindowLevel::Normal,
2061        })
2062        .with_fullscreen(
2063            fullscreen.and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))),
2064        )
2065        .with_enabled_buttons({
2066            let mut buttons = WindowButtons::empty();
2067            if minimize_button.unwrap_or(true) {
2068                buttons |= WindowButtons::MINIMIZE;
2069            }
2070            if maximize_button.unwrap_or(true) {
2071                buttons |= WindowButtons::MAXIMIZE;
2072            }
2073            if close_button.unwrap_or(true) {
2074                buttons |= WindowButtons::CLOSE;
2075            }
2076            buttons
2077        })
2078        .with_active(active.unwrap_or(true));
2079
2080    // Here and below: we create `LogicalSize` / `LogicalPosition` taking
2081    // zoom factor into account. We don't have a good way to get physical size here,
2082    // and trying to do it anyway leads to weird bugs on Wayland, see:
2083    // https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
2084    // https://github.com/rust-windowing/winit/issues/4266
2085    #[expect(
2086        clippy::disallowed_types,
2087        reason = "zoom factor is manually accounted for"
2088    )]
2089    #[cfg(not(target_os = "ios"))]
2090    {
2091        use winit::dpi::{LogicalPosition, LogicalSize};
2092        let zoom_factor = egui_ctx.zoom_factor();
2093
2094        if let Some(size) = inner_size {
2095            window_attributes = window_attributes
2096                .with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
2097        }
2098
2099        if let Some(size) = min_inner_size {
2100            window_attributes = window_attributes
2101                .with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
2102        }
2103
2104        if let Some(size) = max_inner_size {
2105            window_attributes = window_attributes
2106                .with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
2107        }
2108
2109        if let Some(pos) = position {
2110            window_attributes = window_attributes.with_position(LogicalPosition::new(
2111                zoom_factor * pos.x,
2112                zoom_factor * pos.y,
2113            ));
2114        }
2115    }
2116    #[cfg(target_os = "ios")]
2117    {
2118        // Unused:
2119        _ = egui_ctx;
2120        _ = pixels_per_point;
2121        _ = position;
2122        _ = inner_size;
2123        _ = min_inner_size;
2124        _ = max_inner_size;
2125    }
2126
2127    if let Some(icon) = icon {
2128        let winit_icon = to_winit_icon(&icon);
2129        window_attributes = window_attributes.with_window_icon(winit_icon);
2130    }
2131
2132    #[cfg(all(feature = "wayland", target_os = "linux"))]
2133    if let Some(app_id) = _app_id {
2134        use winit::platform::wayland::WindowAttributesExtWayland as _;
2135        window_attributes = window_attributes.with_name(app_id, "");
2136    }
2137
2138    #[cfg(all(feature = "x11", target_os = "linux"))]
2139    {
2140        use winit::platform::x11::WindowAttributesExtX11 as _;
2141        if let Some(window_type) = _window_type {
2142            use winit::platform::x11::WindowType;
2143            window_attributes = window_attributes.with_x11_window_type(vec![match window_type {
2144                egui::X11WindowType::Normal => WindowType::Normal,
2145                egui::X11WindowType::Utility => WindowType::Utility,
2146                egui::X11WindowType::Dock => WindowType::Dock,
2147                egui::X11WindowType::Desktop => WindowType::Desktop,
2148                egui::X11WindowType::Toolbar => WindowType::Toolbar,
2149                egui::X11WindowType::Menu => WindowType::Menu,
2150                egui::X11WindowType::Splash => WindowType::Splash,
2151                egui::X11WindowType::Dialog => WindowType::Dialog,
2152                egui::X11WindowType::DropdownMenu => WindowType::DropdownMenu,
2153                egui::X11WindowType::PopupMenu => WindowType::PopupMenu,
2154                egui::X11WindowType::Tooltip => WindowType::Tooltip,
2155                egui::X11WindowType::Notification => WindowType::Notification,
2156                egui::X11WindowType::Combo => WindowType::Combo,
2157                egui::X11WindowType::Dnd => WindowType::Dnd,
2158            }]);
2159        }
2160        if let Some(override_redirect) = _override_redirect {
2161            window_attributes = window_attributes.with_override_redirect(override_redirect);
2162        }
2163    }
2164
2165    #[cfg(target_os = "windows")]
2166    {
2167        use winit::platform::windows::WindowAttributesExtWindows as _;
2168        if let Some(enable) = _drag_and_drop {
2169            window_attributes = window_attributes.with_drag_and_drop(enable);
2170        }
2171        if let Some(show) = _taskbar {
2172            window_attributes = window_attributes.with_skip_taskbar(!show);
2173        }
2174        window_attributes = window_attributes.with_undecorated_shadow(!decorations.unwrap_or(true));
2175    }
2176
2177    #[cfg(target_os = "macos")]
2178    {
2179        use winit::platform::macos::WindowAttributesExtMacOS as _;
2180        window_attributes = window_attributes
2181            .with_title_hidden(!_title_shown.unwrap_or(true))
2182            .with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
2183            .with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
2184            .with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
2185            .with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
2186            .with_has_shadow(_has_shadow.unwrap_or(true));
2187    }
2188
2189    window_attributes
2190}
2191
2192fn to_winit_icon(icon: &egui::IconData) -> Option<winit::window::Icon> {
2193    if icon.is_empty() {
2194        None
2195    } else {
2196        profiling::function_scope!();
2197        match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) {
2198            Ok(winit_icon) => Some(winit_icon),
2199            Err(err) => {
2200                log::warn!("Invalid IconData: {err}");
2201                None
2202            }
2203        }
2204    }
2205}
2206
2207/// Applies what `create_winit_window_builder` couldn't
2208pub fn apply_viewport_builder_to_window(
2209    egui_ctx: &egui::Context,
2210    window: &Window,
2211    builder: &ViewportBuilder,
2212) {
2213    if let Some(mouse_passthrough) = builder.mouse_passthrough
2214        && let Err(err) = window.set_cursor_hittest(!mouse_passthrough)
2215    {
2216        log::warn!("set_cursor_hittest failed: {err}");
2217    }
2218
2219    {
2220        // In `create_winit_window_builder` we didn't know
2221        // on what monitor the window would appear, so we didn't know
2222        // how to translate egui ui point to native physical pixels.
2223        // Now we do know:
2224
2225        let pixels_per_point = pixels_per_point(egui_ctx, window);
2226
2227        if let Some(size) = builder.inner_size
2228            && window
2229                .request_inner_size(PhysicalSize::new(
2230                    pixels_per_point * size.x,
2231                    pixels_per_point * size.y,
2232                ))
2233                .is_some()
2234        {
2235            log::debug!("Failed to set window size");
2236        }
2237        if let Some(size) = builder.min_inner_size {
2238            window.set_min_inner_size(Some(PhysicalSize::new(
2239                pixels_per_point * size.x,
2240                pixels_per_point * size.y,
2241            )));
2242        }
2243        if let Some(size) = builder.max_inner_size {
2244            window.set_max_inner_size(Some(PhysicalSize::new(
2245                pixels_per_point * size.x,
2246                pixels_per_point * size.y,
2247            )));
2248        }
2249        if let Some(pos) = builder.position {
2250            let pos = PhysicalPosition::new(pixels_per_point * pos.x, pixels_per_point * pos.y);
2251            window.set_outer_position(pos);
2252        }
2253        if let Some(maximized) = builder.maximized {
2254            window.set_maximized(maximized);
2255        }
2256    }
2257}
2258
2259// ---------------------------------------------------------------------------
2260
2261/// Short and fast description of a device event.
2262/// Useful for logging and profiling.
2263pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'static str {
2264    use winit::event::DeviceEvent;
2265
2266    match event {
2267        DeviceEvent::Added => "DeviceEvent::Added",
2268        DeviceEvent::Removed => "DeviceEvent::Removed",
2269        DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
2270        DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
2271        DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
2272        DeviceEvent::Button { .. } => "DeviceEvent::Button",
2273        DeviceEvent::Key { .. } => "DeviceEvent::Key",
2274    }
2275}
2276
2277/// Short and fast description of a window event.
2278/// Useful for logging and profiling.
2279pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'static str {
2280    use winit::event::WindowEvent;
2281
2282    match event {
2283        WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
2284        WindowEvent::Resized { .. } => "WindowEvent::Resized",
2285        WindowEvent::Moved { .. } => "WindowEvent::Moved",
2286        WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
2287        WindowEvent::Destroyed => "WindowEvent::Destroyed",
2288        WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
2289        WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
2290        WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
2291        WindowEvent::Focused { .. } => "WindowEvent::Focused",
2292        WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
2293        WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
2294        WindowEvent::Ime { .. } => "WindowEvent::Ime",
2295        WindowEvent::CursorMoved { .. } => "WindowEvent::CursorMoved",
2296        WindowEvent::CursorEntered { .. } => "WindowEvent::CursorEntered",
2297        WindowEvent::CursorLeft { .. } => "WindowEvent::CursorLeft",
2298        WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
2299        WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
2300        WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
2301        WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
2302        WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
2303        WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
2304        WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",
2305        WindowEvent::AxisMotion { .. } => "WindowEvent::AxisMotion",
2306        WindowEvent::Touch { .. } => "WindowEvent::Touch",
2307        WindowEvent::ScaleFactorChanged { .. } => "WindowEvent::ScaleFactorChanged",
2308        WindowEvent::ThemeChanged { .. } => "WindowEvent::ThemeChanged",
2309        WindowEvent::Occluded { .. } => "WindowEvent::Occluded",
2310        WindowEvent::PanGesture { .. } => "WindowEvent::PanGesture",
2311    }
2312}