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