Skip to main content

egui_baseview/
window.rs

1use std::time::Instant;
2
3use baseview::{
4    Event, EventStatus, PhySize, Size, Window, WindowHandle, WindowHandler, WindowOpenOptions,
5    WindowScalePolicy,
6};
7use copypasta::ClipboardProvider;
8use egui::{Pos2, Rect, Rgba, ViewportCommand, pos2, vec2};
9use keyboard_types::Modifiers;
10use raw_window_handle::HasRawWindowHandle;
11
12use crate::{GraphicsConfig, renderer::Renderer};
13
14#[cfg(feature = "nice-log")]
15use nice_plug_core::{nice_error as error, nice_warn as warn};
16
17#[cfg(all(feature = "tracing", not(feature = "nice-log")))]
18use tracing::{error, warn};
19
20#[derive(Debug, Clone)]
21pub struct EguiWindowSettings {
22    pub title: String,
23
24    /// The logical size of the window
25    ///
26    /// These dimensions will be scaled by the scaling policy specified in `scale`. Mouse
27    /// position will be passed back as logical coordinates.
28    pub logical_size: Size,
29
30    /// The dpi scaling policy
31    pub scale_policy: WindowScalePolicy,
32
33    pub graphics: GraphicsConfig,
34}
35
36impl EguiWindowSettings {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    pub fn with_tile(mut self, title: impl Into<String>) -> Self {
42        self.title = title.into();
43        self
44    }
45
46    pub fn with_logical_size(mut self, size: Size) -> Self {
47        self.logical_size = size;
48        self
49    }
50
51    pub fn with_scale_policy(mut self, scale_policy: WindowScalePolicy) -> Self {
52        self.scale_policy = scale_policy;
53        self
54    }
55
56    pub fn with_graphics_config(mut self, config: GraphicsConfig) -> Self {
57        self.graphics = config;
58        self
59    }
60}
61
62impl Default for EguiWindowSettings {
63    fn default() -> Self {
64        Self {
65            title: String::new(),
66            logical_size: Size {
67                width: 300.0,
68                height: 200.0,
69            },
70            scale_policy: WindowScalePolicy::default(),
71            graphics: GraphicsConfig::default(),
72        }
73    }
74}
75
76pub struct Queue<'a> {
77    bg_color: &'a mut Rgba,
78    close_requested: &'a mut bool,
79    physical_size: &'a mut PhySize,
80    key_capture: &'a mut KeyCapture,
81}
82
83impl<'a> Queue<'a> {
84    pub(crate) fn new(
85        bg_color: &'a mut Rgba,
86        close_requested: &'a mut bool,
87        physical_size: &'a mut PhySize,
88        key_capture: &'a mut KeyCapture,
89    ) -> Self {
90        Self {
91            bg_color,
92            //renderer,
93            //repaint_requested,
94            close_requested,
95            physical_size,
96            key_capture,
97        }
98    }
99
100    /// Set the background color.
101    pub fn bg_color(&mut self, bg_color: Rgba) {
102        *self.bg_color = bg_color;
103    }
104
105    /// Set size of the window.
106    pub fn resize(&mut self, physical_size: PhySize) {
107        *self.physical_size = physical_size;
108    }
109
110    /// Close the window.
111    pub fn close_window(&mut self) {
112        *self.close_requested = true;
113    }
114
115    /// Set how to handle capturing key events from the host.
116    pub fn set_key_capture(&mut self, key_capture: KeyCapture) {
117        *self.key_capture = key_capture;
118    }
119}
120
121/// Describes how to handle capturing key events from the host.
122#[derive(Default, Debug, Clone, PartialEq)]
123pub enum KeyCapture {
124    #[default]
125    /// All keys will be captured from the host.
126    CaptureAll,
127    /// No keys will be captured from the host.
128    IgnoreAll,
129    /// Only the given keys will be captured from the host.
130    CaptureKeys(Vec<keyboard_types::Key>),
131    /// All keys except the given ones will be captured from the host.
132    IgnoreKeys(Vec<keyboard_types::Key>),
133}
134
135/// Handles an egui-baseview application
136pub struct EguiWindow<State, U>
137where
138    State: 'static + Send,
139    U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
140    U: 'static + Send,
141{
142    user_state: Option<State>,
143    user_update: U,
144
145    egui_ctx: egui::Context,
146    viewport_id: egui::ViewportId,
147    start_time: Instant,
148    egui_input: egui::RawInput,
149    pointer_pos_in_points: Option<egui::Pos2>,
150    current_cursor_icon: baseview::MouseCursor,
151
152    renderer: Renderer,
153
154    clipboard_ctx: Option<copypasta::ClipboardContext>,
155
156    physical_size: PhySize,
157    scale_policy: WindowScalePolicy,
158    pixels_per_point: f32,
159    points_per_pixel: f32,
160    bg_color: Rgba,
161    close_requested: bool,
162    repaint_after: Option<Instant>,
163    key_capture: KeyCapture,
164}
165
166impl<State, U> EguiWindow<State, U>
167where
168    State: 'static + Send,
169    U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
170    U: 'static + Send,
171{
172    fn new<B>(
173        window: &mut baseview::Window<'_>,
174        settings: EguiWindowSettings,
175        mut build: B,
176        update: U,
177        mut state: State,
178    ) -> EguiWindow<State, U>
179    where
180        B: FnMut(&egui::Context, &mut Queue, &mut State),
181        B: 'static + Send,
182    {
183        let renderer = Renderer::new(window, settings.graphics).unwrap_or_else(|err| {
184            // TODO: better error log and not panicking, but that's gonna require baseview changes
185            error!("oops! the gpu backend couldn't initialize! \n {err}");
186            panic!("gpu backend failed to initialize: \n {err}")
187        });
188        let egui_ctx = egui::Context::default();
189
190        // Assume scale for now until there is an event with a new one.
191        let pixels_per_point = match settings.scale_policy {
192            WindowScalePolicy::ScaleFactor(scale) => scale,
193            WindowScalePolicy::SystemScaleFactor => 1.0,
194        } as f32;
195        let points_per_pixel = pixels_per_point.recip();
196
197        let screen_rect = Rect::from_min_size(
198            Pos2::new(0f32, 0f32),
199            vec2(
200                settings.logical_size.width as f32,
201                settings.logical_size.height as f32,
202            ),
203        );
204
205        let viewport_info = egui::ViewportInfo {
206            parent: None,
207            title: Some(settings.title),
208            native_pixels_per_point: Some(pixels_per_point),
209            focused: Some(true),
210            inner_rect: Some(screen_rect),
211            ..Default::default()
212        };
213        let viewport_id = egui::ViewportId::default();
214
215        let mut egui_input = egui::RawInput {
216            max_texture_side: Some(renderer.max_texture_side()),
217            screen_rect: Some(screen_rect),
218            ..Default::default()
219        };
220        let _ = egui_input.viewports.insert(viewport_id, viewport_info);
221
222        let mut physical_size = PhySize {
223            width: (settings.logical_size.width * pixels_per_point as f64).round() as u32,
224            height: (settings.logical_size.height * pixels_per_point as f64).round() as u32,
225        };
226
227        let mut bg_color = Rgba::BLACK;
228        let mut close_requested = false;
229        let old_physical_size = physical_size;
230        let mut key_capture = KeyCapture::default();
231        let mut queue = Queue::new(
232            &mut bg_color,
233            &mut close_requested,
234            &mut physical_size,
235            &mut key_capture,
236        );
237        (build)(&egui_ctx, &mut queue, &mut state);
238
239        if physical_size != old_physical_size {
240            window.resize(baseview::Size {
241                width: physical_size.width as f64,
242                height: physical_size.height as f64,
243            });
244        }
245
246        let clipboard_ctx = match copypasta::ClipboardContext::new() {
247            Ok(clipboard_ctx) => Some(clipboard_ctx),
248            Err(e) => {
249                error!("Failed to initialize clipboard: {}", e);
250                None
251            }
252        };
253
254        let start_time = Instant::now();
255
256        Self {
257            user_state: Some(state),
258            user_update: update,
259
260            egui_ctx,
261            viewport_id,
262            start_time,
263            egui_input,
264            pointer_pos_in_points: None,
265            current_cursor_icon: baseview::MouseCursor::Default,
266
267            renderer,
268
269            clipboard_ctx,
270
271            physical_size,
272            pixels_per_point,
273            points_per_pixel,
274            scale_policy: settings.scale_policy,
275            bg_color,
276            close_requested,
277            repaint_after: Some(start_time),
278            key_capture,
279        }
280    }
281
282    /// Open a new child window.
283    ///
284    /// * `parent` - The parent window.
285    /// * `settings` - The settings of the window.
286    /// * `state` - The initial state of your application.
287    /// * `build` - Called once before the first frame. Allows you to do setup code and to
288    ///   call `ctx.set_fonts()`. Optional.
289    /// * `update` - Called before each frame. Here you should update the state of your
290    ///   application and build the UI.
291    pub fn open_parented<P, B>(
292        parent: &P,
293        settings: EguiWindowSettings,
294        state: State,
295        build: B,
296        update: U,
297    ) -> WindowHandle
298    where
299        P: HasRawWindowHandle,
300        B: FnMut(&egui::Context, &mut Queue, &mut State),
301        B: 'static + Send,
302    {
303        Window::open_parented(
304            parent,
305            #[allow(clippy::needless_update)]
306            WindowOpenOptions {
307                title: settings.title.clone(),
308                size: settings.logical_size,
309                scale: settings.scale_policy,
310                #[cfg(feature = "opengl")]
311                gl_config: Some(settings.graphics.gl_config.clone()),
312                ..Default::default()
313            },
314            move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
315                EguiWindow::new(window, settings, build, update, state)
316            },
317        )
318    }
319
320    /// Open a new window that blocks the current thread until the window is destroyed.
321    ///
322    /// * `settings` - The settings of the window.
323    /// * `state` - The initial state of your application.
324    /// * `build` - Called once before the first frame. Allows you to do setup code and to
325    ///   call `ctx.set_fonts()`. Optional.
326    /// * `update` - Called before each frame. Here you should update the state of your
327    ///   application and build the UI.
328    pub fn open_blocking<B>(settings: EguiWindowSettings, state: State, build: B, update: U)
329    where
330        B: FnMut(&egui::Context, &mut Queue, &mut State),
331        B: 'static + Send,
332    {
333        Window::open_blocking(
334            #[allow(clippy::needless_update)]
335            WindowOpenOptions {
336                title: settings.title.clone(),
337                size: settings.logical_size,
338                scale: settings.scale_policy,
339                #[cfg(feature = "opengl")]
340                gl_config: Some(settings.graphics.gl_config.clone()),
341                ..Default::default()
342            },
343            move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
344                EguiWindow::new(window, settings, build, update, state)
345            },
346        )
347    }
348
349    /// Update the pressed key modifiers when a mouse event has sent a new set of modifiers.
350    fn update_modifiers(&mut self, modifiers: &Modifiers) {
351        self.egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
352        self.egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
353        self.egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
354    }
355}
356
357impl<State, U> WindowHandler for EguiWindow<State, U>
358where
359    State: 'static + Send,
360    U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
361    U: 'static + Send,
362{
363    fn on_frame(&mut self, window: &mut Window) {
364        let Some(state) = &mut self.user_state else {
365            return;
366        };
367
368        self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
369        self.egui_input.screen_rect = Some(calculate_screen_rect(
370            self.physical_size,
371            self.points_per_pixel,
372        ));
373
374        //let mut repaint_requested = false;
375        let old_physical_size = self.physical_size;
376        let mut queue = Queue::new(
377            &mut self.bg_color,
378            &mut self.close_requested,
379            &mut self.physical_size,
380            &mut self.key_capture,
381        );
382
383        let mut full_output = self.egui_ctx.run_ui(self.egui_input.take(), |ui| {
384            (self.user_update)(ui, &mut queue, state)
385        });
386
387        if self.close_requested {
388            window.close();
389        }
390
391        // Prevent data from being allocated every frame by storing this
392        // in a member field.
393
394        let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
395            // The main window was closed by egui.
396            window.close();
397            return;
398        };
399
400        for command in viewport_output.commands.iter() {
401            match command {
402                ViewportCommand::Close => {
403                    window.close();
404                }
405                ViewportCommand::InnerSize(size) => window.resize(baseview::Size {
406                    width: size.x.max(1.0) as f64,
407                    height: size.y.max(1.0) as f64,
408                }),
409                _ => {}
410            }
411        }
412
413        if self.physical_size != old_physical_size {
414            window.resize(baseview::Size {
415                width: self.physical_size.width.max(1) as f64,
416                height: self.physical_size.height.max(1) as f64,
417            });
418        }
419
420        let now = Instant::now();
421        let do_repaint_now = if let Some(t) = self.repaint_after {
422            now >= t || viewport_output.repaint_delay.is_zero()
423        } else {
424            viewport_output.repaint_delay.is_zero()
425        };
426
427        if do_repaint_now {
428            self.renderer.render(
429                window,
430                self.bg_color,
431                self.physical_size,
432                self.pixels_per_point,
433                &mut self.egui_ctx,
434                &mut full_output,
435            );
436
437            self.repaint_after = None;
438        } else if let Some(repaint_after) = now.checked_add(viewport_output.repaint_delay) {
439            // Schedule to repaint after the requested time has elapsed.
440            self.repaint_after = Some(repaint_after);
441        }
442
443        for command in full_output.platform_output.commands {
444            match command {
445                egui::OutputCommand::CopyText(text) => {
446                    if let Some(clipboard_ctx) = &mut self.clipboard_ctx
447                        && let Err(err) = clipboard_ctx.set_contents(text)
448                    {
449                        error!("Copy/Cut error: {}", err);
450                    }
451                }
452                egui::OutputCommand::CopyImage(_) => {
453                    warn!("Copying images is not supported in egui_baseview.");
454                }
455                egui::OutputCommand::OpenUrl(open_url) => {
456                    if let Err(err) = open::that_detached(&open_url.url) {
457                        error!("Open error: {}", err);
458                    }
459                }
460            }
461        }
462
463        let cursor_icon =
464            crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
465        if self.current_cursor_icon != cursor_icon {
466            self.current_cursor_icon = cursor_icon;
467
468            window.set_mouse_cursor(cursor_icon);
469        }
470
471        // A temporary workaround for keyboard input not working sometimes.
472        // See https://github.com/BillyDM/egui-baseview/issues/20
473        #[cfg(feature = "keyboard_focus_workaround")]
474        {
475            if !full_output.platform_output.events.is_empty()
476                || full_output.platform_output.ime.is_some()
477            {
478                window.focus();
479            }
480        }
481    }
482
483    #[allow(unused_variables)]
484    fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus {
485        let mut return_status = EventStatus::Captured;
486
487        // Parent/embedded windows do not always gain keyboard focus
488        // Automatically on click. Request focus explicitly before forwarding the event.
489        if matches!(
490            event,
491            Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
492        ) && !window.has_focus()
493        {
494            window.focus();
495        }
496
497        match &event {
498            baseview::Event::Mouse(event) => match event {
499                baseview::MouseEvent::CursorMoved {
500                    position,
501                    modifiers,
502                } => {
503                    self.update_modifiers(modifiers);
504
505                    let pos = pos2(position.x as f32, position.y as f32);
506                    self.pointer_pos_in_points = Some(pos);
507                    self.egui_input.events.push(egui::Event::PointerMoved(pos));
508                }
509                baseview::MouseEvent::ButtonPressed { button, modifiers } => {
510                    self.update_modifiers(modifiers);
511
512                    if let Some(pos) = self.pointer_pos_in_points
513                        && let Some(button) = crate::translate::translate_mouse_button(*button)
514                    {
515                        self.egui_input.events.push(egui::Event::PointerButton {
516                            pos,
517                            button,
518                            pressed: true,
519                            modifiers: self.egui_input.modifiers,
520                        });
521                    }
522                }
523                baseview::MouseEvent::ButtonReleased { button, modifiers } => {
524                    self.update_modifiers(modifiers);
525
526                    if let Some(pos) = self.pointer_pos_in_points
527                        && let Some(button) = crate::translate::translate_mouse_button(*button)
528                    {
529                        self.egui_input.events.push(egui::Event::PointerButton {
530                            pos,
531                            button,
532                            pressed: false,
533                            modifiers: self.egui_input.modifiers,
534                        });
535                    }
536                }
537                baseview::MouseEvent::WheelScrolled {
538                    delta: scroll_delta,
539                    modifiers,
540                } => {
541                    self.update_modifiers(modifiers);
542
543                    #[allow(unused_mut)]
544                    let (unit, mut delta) = match scroll_delta {
545                        baseview::ScrollDelta::Lines { x, y } => {
546                            (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
547                        }
548
549                        baseview::ScrollDelta::Pixels { x, y } => (
550                            egui::MouseWheelUnit::Point,
551                            egui::vec2(*x, *y) * self.points_per_pixel,
552                        ),
553                    };
554
555                    if cfg!(target_os = "macos") {
556                        // This is still buggy in winit despite
557                        // https://github.com/rust-windowing/winit/issues/1695 being closed
558                        //
559                        // TODO: See if this is an issue in baseview as well.
560                        delta.x *= -1.0;
561                    }
562
563                    self.egui_input.events.push(egui::Event::MouseWheel {
564                        unit,
565                        delta,
566                        modifiers: self.egui_input.modifiers,
567                        phase: egui::TouchPhase::Move,
568                    });
569                }
570                baseview::MouseEvent::CursorLeft => {
571                    self.pointer_pos_in_points = None;
572                    self.egui_input.events.push(egui::Event::PointerGone);
573                }
574                _ => {}
575            },
576            baseview::Event::Keyboard(event) => {
577                use keyboard_types::Code;
578
579                let pressed = event.state == keyboard_types::KeyState::Down;
580
581                match event.code {
582                    Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed,
583                    Code::ControlLeft | Code::ControlRight => {
584                        self.egui_input.modifiers.ctrl = pressed;
585
586                        #[cfg(not(target_os = "macos"))]
587                        {
588                            self.egui_input.modifiers.command = pressed;
589                        }
590                    }
591                    Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed,
592                    Code::MetaLeft | Code::MetaRight => {
593                        #[cfg(target_os = "macos")]
594                        {
595                            self.egui_input.modifiers.mac_cmd = pressed;
596                            self.egui_input.modifiers.command = pressed;
597                        }
598                        // prevent `rustfmt` from breaking this
599                    }
600                    _ => (),
601                }
602
603                if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
604                    self.egui_input.events.push(egui::Event::Key {
605                        key,
606                        physical_key: None,
607                        pressed,
608                        repeat: event.repeat,
609                        modifiers: self.egui_input.modifiers,
610                    });
611                }
612
613                if pressed {
614                    // VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
615                    // so we detect these things manually:
616                    //
617                    // TODO: See if this is an issue in baseview as well.
618                    if is_cut_command(self.egui_input.modifiers, event.code) {
619                        self.egui_input.events.push(egui::Event::Cut);
620                    } else if is_copy_command(self.egui_input.modifiers, event.code) {
621                        self.egui_input.events.push(egui::Event::Copy);
622                    } else if is_paste_command(self.egui_input.modifiers, event.code) {
623                        if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
624                            match clipboard_ctx.get_contents() {
625                                Ok(contents) => {
626                                    self.egui_input.events.push(egui::Event::Text(contents))
627                                }
628                                Err(err) => {
629                                    error!("Paste error: {}", err);
630                                }
631                            }
632                        }
633                    } else if let keyboard_types::Key::Character(written) = &event.key
634                        && !self.egui_input.modifiers.ctrl
635                        && !self.egui_input.modifiers.command
636                    {
637                        self.egui_input
638                            .events
639                            .push(egui::Event::Text(written.clone()));
640                    }
641                }
642
643                match &self.key_capture {
644                    KeyCapture::CaptureAll => {}
645                    KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
646                    KeyCapture::CaptureKeys(keys) => {
647                        if !keys.contains(&event.key) {
648                            return_status = EventStatus::Ignored
649                        }
650                    }
651                    KeyCapture::IgnoreKeys(keys) => {
652                        if keys.contains(&event.key) {
653                            return_status = EventStatus::Ignored
654                        }
655                    }
656                }
657            }
658            baseview::Event::Window(event) => match event {
659                baseview::WindowEvent::Resized(window_info) => {
660                    self.pixels_per_point = match self.scale_policy {
661                        WindowScalePolicy::ScaleFactor(scale) => scale,
662                        WindowScalePolicy::SystemScaleFactor => window_info.scale(),
663                    } as f32;
664                    self.points_per_pixel = self.pixels_per_point.recip();
665
666                    self.physical_size = window_info.physical_size();
667
668                    let screen_rect =
669                        calculate_screen_rect(self.physical_size, self.points_per_pixel);
670
671                    self.egui_input.screen_rect = Some(screen_rect);
672
673                    let viewport_info = self
674                        .egui_input
675                        .viewports
676                        .get_mut(&self.viewport_id)
677                        .unwrap();
678                    viewport_info.native_pixels_per_point = Some(self.pixels_per_point);
679                    viewport_info.inner_rect = Some(screen_rect);
680
681                    // Schedule to repaint on the next frame.
682                    self.repaint_after = Some(Instant::now());
683                }
684                baseview::WindowEvent::Focused => {
685                    self.egui_input
686                        .events
687                        .push(egui::Event::WindowFocused(true));
688                    self.egui_input
689                        .viewports
690                        .get_mut(&self.viewport_id)
691                        .unwrap()
692                        .focused = Some(true);
693                }
694                baseview::WindowEvent::Unfocused => {
695                    self.egui_input
696                        .events
697                        .push(egui::Event::WindowFocused(false));
698                    self.egui_input
699                        .viewports
700                        .get_mut(&self.viewport_id)
701                        .unwrap()
702                        .focused = Some(false);
703                }
704                baseview::WindowEvent::WillClose => {}
705            },
706        }
707
708        // For keyboard events, also check if egui actually wants keyboard input
709        // This allows DAW shortcuts (spacebar, etc.) to pass through when no text field is focused
710        match &event {
711            baseview::Event::Keyboard(_) => {
712                if return_status == EventStatus::Captured
713                    && !self.egui_ctx.egui_wants_keyboard_input()
714                {
715                    EventStatus::Ignored
716                } else {
717                    return_status
718                }
719            }
720            baseview::Event::Mouse(_) => {
721                if self.egui_ctx.egui_is_using_pointer() || self.egui_ctx.egui_wants_pointer_input()
722                {
723                    EventStatus::Captured
724                } else {
725                    EventStatus::Ignored
726                }
727            }
728            baseview::Event::Window(_) => EventStatus::Captured,
729        }
730    }
731}
732
733fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
734    (modifiers.command && keycode == keyboard_types::Code::KeyX)
735        || (cfg!(target_os = "windows")
736            && modifiers.shift
737            && keycode == keyboard_types::Code::Delete)
738}
739
740fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
741    (modifiers.command && keycode == keyboard_types::Code::KeyC)
742        || (cfg!(target_os = "windows")
743            && modifiers.ctrl
744            && keycode == keyboard_types::Code::Insert)
745}
746
747fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
748    (modifiers.command && keycode == keyboard_types::Code::KeyV)
749        || (cfg!(target_os = "windows")
750            && modifiers.shift
751            && keycode == keyboard_types::Code::Insert)
752}
753
754/// Calculate screen rectangle in logical size.
755fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect {
756    let logical_size = (
757        physical_size.width as f32 * points_per_pixel,
758        physical_size.height as f32 * points_per_pixel,
759    );
760    Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1))
761}