egui_winit/
lib.rs

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