Skip to main content

egui_baseview/
window.rs

1use std::cell::{Cell, RefCell};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{Arc, Mutex};
4use std::time::Instant;
5
6use baseview::dpi::{LogicalPosition, LogicalSize, Size};
7use baseview::{
8    Event, EventStatus, HandlerError, ParentWindowHandle, Window, WindowContext, WindowHandler,
9    WindowSettings, WindowSize,
10};
11use copypasta::ClipboardProvider;
12use egui::{FullOutput, Pos2, Rect, Rgba, ViewportCommand, pos2, vec2};
13use keyboard_types::Modifiers;
14use raw_window_handle::HasWindowHandle;
15
16use crate::App;
17use crate::{GraphicsConfig, renderer::Renderer};
18
19#[cfg(all(feature = "log", not(feature = "tracing")))]
20use log::{error, warn};
21#[cfg(feature = "tracing")]
22use tracing::{error, warn};
23
24/// A realtime-safe handle to request a update & repaint for an egui app.
25///
26/// This can be used, for example, to notify the GUI that the value of a decibel
27/// meter has changed.
28#[derive(Debug, Clone)]
29pub struct RepaintNotifier {
30    repaint: Arc<AtomicBool>,
31}
32
33impl RepaintNotifier {
34    pub fn new() -> Self {
35        Self {
36            repaint: Arc::new(AtomicBool::new(true)),
37        }
38    }
39
40    pub fn request_repaint(&self) {
41        self.repaint.store(true, Ordering::Relaxed);
42    }
43
44    pub fn request_repaint_with(&self, repaint: bool) {
45        if repaint {
46            self.repaint.store(true, Ordering::Relaxed);
47        }
48    }
49
50    fn repaint_requested(&self) -> bool {
51        self.repaint.swap(false, Ordering::Relaxed)
52    }
53}
54
55impl Default for RepaintNotifier {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61/// Settings used when creating a new window
62#[derive(Debug, Clone)]
63pub struct EguiWindowSettings {
64    /// The window title.
65    pub title: String,
66
67    /// The size of the window, either in physical or logical coordinates.
68    pub size: Size,
69
70    /// The minimum window size. Set to `None` for no minimum size.
71    pub min_size: Option<Size>,
72    /// The maximum window size. Set to `None` for no maximum size.
73    pub max_size: Option<Size>,
74
75    /// Whether the window can be resized.
76    pub resizable: bool,
77
78    /// The amount of zoom (scaling) to apply. This is applied on top of the
79    /// system's native scaling factor.
80    ///
81    /// The zoom factor can also be changed later with
82    /// [`Context::set_zoom_factor()`](egui::Context::set_zoom_factor).
83    pub zoom_factor: f32,
84
85    /// The graphics configuration
86    pub graphics: GraphicsConfig,
87
88    /// If the window is to be embedded in a parent window, the handle to that window.
89    ///
90    /// If `None`, the window will be standalone.
91    pub parent: Option<ParentWindowHandle>,
92
93    /// If the window expects to have a parent when first displayed.
94    ///
95    /// Setting this will delay the actual creation of the window until the parent is set (unless
96    /// the window is shown first).
97    ///
98    /// If the `parent` field is already set, this does nothing and is ignored.
99    pub wait_for_parent: bool,
100
101    /// A fallback scale factor, if Baseview couldn't get one from the platform.
102    ///
103    /// If the platform does already provide an accurate scaling factor, this doesn't do anything.
104    ///
105    /// If the given fallback scale factor is actually useful and different from the current one
106    /// (1.0 by default), this will resize and redraw the window accordingly.
107    ///
108    /// # Platform compatibility notes.
109    ///
110    /// On Win32, this value is used if running on early versions of Windows 10 (or earlier).
111    ///
112    /// On X11, this value is used if no `Xft.dpi`setting is set.
113    ///
114    /// On macOS, this function is always a no-op.
115    pub fallback_scale_factor: Option<f64>,
116
117    /// A realtime-safe handle to request an update & repaint for an egui app.
118    ///
119    /// This can be used, for example, to notify the GUI that the value of a decibel
120    /// meter has changed.
121    pub repaint_notifier: Option<RepaintNotifier>,
122}
123
124impl EguiWindowSettings {
125    #[inline]
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    /// The window title.
131    #[inline]
132    pub fn with_title(mut self, title: impl Into<String>) -> Self {
133        self.title = title.into();
134        self
135    }
136
137    /// The size of the window, either in physical or logical coordinates.
138    #[inline]
139    pub fn with_size(mut self, size: impl Into<Size>) -> Self {
140        self.size = size.into();
141        self
142    }
143
144    /// Sets whether the window can be resized.
145    ///
146    /// Defaults to `true`.
147    #[inline]
148    pub fn with_resizable(mut self, resizable: bool) -> Self {
149        self.resizable = resizable;
150        self
151    }
152
153    /// The minimum window size. Set to `None` for no minimum size.
154    ///
155    /// Defaults to `None`.
156    #[inline]
157    pub fn with_min_size<S: Into<Size>>(mut self, min_size: Option<S>) -> Self {
158        self.min_size = min_size.map(|s| s.into());
159        self
160    }
161
162    /// The maximum window size. Set to `None` for no maximum size.
163    ///
164    /// Defaults to `None`.
165    #[inline]
166    pub fn with_max_size<S: Into<Size>>(mut self, max_size: Option<S>) -> Self {
167        self.max_size = max_size.map(|s| s.into());
168        self
169    }
170
171    /// The amount of zoom (scaling) to apply. This is applied on top of the
172    /// system's native scaling factor.
173    ///
174    /// This is ignored if [`size`](EguiWindowSettings::size) is in physical units.
175    ///
176    /// The zoom factor can also be changed later with
177    /// [`Context::set_zoom_factor()`](egui::Context::set_zoom_factor).
178    #[inline]
179    pub fn with_zoom_factor(mut self, zoom_factor: f32) -> Self {
180        self.zoom_factor = zoom_factor;
181        self
182    }
183
184    /// If the window is to be embedded in a parent window, the handle to that window.
185    ///
186    /// If `None`, the window will be standalone.
187    #[inline]
188    pub fn with_parent<'a, P: HasWindowHandle + 'a>(
189        mut self,
190        parent: impl Into<Option<&'a P>>,
191    ) -> Self {
192        self.parent = parent.into().map(ParentWindowHandle::from_window);
193        self
194    }
195
196    /// Sets [`wait_for_parent`](Self::wait_for_parent) to the given value.
197    pub fn with_wait_for_parent(mut self, wait_for_parent: bool) -> Self {
198        self.wait_for_parent = wait_for_parent;
199        self
200    }
201
202    /// Sets [`wait_for_parent`](Self::wait_for_parent) to `true`.
203    #[inline]
204    pub fn wait_for_parent(mut self) -> Self {
205        self.wait_for_parent = true;
206        self
207    }
208
209    /// Sets [`fallback_scale_factor`](Self::fallback_scale_factor) to the given value.
210    #[inline]
211    pub fn with_fallback_scale_factor(mut self, scale_factor: impl Into<Option<f64>>) -> Self {
212        self.fallback_scale_factor = scale_factor.into();
213        self
214    }
215
216    /// The graphics configuration
217    #[inline]
218    pub fn with_graphics_config(mut self, config: GraphicsConfig) -> Self {
219        self.graphics = config;
220        self
221    }
222
223    /// A clone of the realtime-safe handle to request a update & repaint for an egui app.
224    ///
225    /// This can be used, for example, to notify the GUI that the value of a decibel
226    /// meter has changed.
227    #[inline]
228    pub fn with_repaint_notifier(mut self, repaint_notifier: RepaintNotifier) -> Self {
229        self.repaint_notifier = Some(repaint_notifier);
230        self
231    }
232}
233
234impl Default for EguiWindowSettings {
235    fn default() -> Self {
236        Self {
237            title: String::new(),
238            size: Size::Logical(LogicalSize {
239                width: 300.0,
240                height: 200.0,
241            }),
242            min_size: None,
243            max_size: None,
244            resizable: true,
245            zoom_factor: 1.0,
246            graphics: GraphicsConfig::default(),
247            parent: None,
248            wait_for_parent: false,
249            fallback_scale_factor: None,
250            repaint_notifier: None,
251        }
252    }
253}
254
255/// Represents the surroundings of your app.
256pub struct Frame {
257    renderer: Renderer,
258    window: WindowContext,
259    clear_color: Rgba,
260    key_capture: KeyCapture,
261}
262
263impl Frame {
264    fn new(renderer: Renderer, window: WindowContext) -> Self {
265        Self {
266            renderer,
267            window,
268            clear_color: Rgba::BLACK,
269            key_capture: Default::default(),
270        }
271    }
272
273    /// Set the clear color of the renderer.
274    pub fn set_clear_color(&mut self, clear_color: Rgba) {
275        self.clear_color = clear_color;
276    }
277
278    /// Set how to handle capturing key events from the host.
279    pub fn set_key_capture(&mut self, key_capture: KeyCapture) {
280        self.key_capture = key_capture;
281    }
282
283    /// Access the internal baseview window.
284    pub fn baseview_window(&self) -> &WindowContext {
285        &self.window
286    }
287
288    /// A reference to the underlying
289    /// [glow](https://docs.rs/glow/0.17.0/x86_64-unknown-linux-gnu/glow/index.html)
290    /// (OpenGL) context.
291    ///
292    /// This can be used, for instance, to:
293    ///
294    /// * Render things to offscreen buffers.
295    /// * Read the pixel buffer from the previous frame (glow::Context::read_pixels).
296    /// * Render things behind the egui windows.
297    ///
298    /// Note that all egui painting is deferred to after the call to App::ui
299    /// (egui only collects egui::Shapes and then egui-baseview paints them all in
300    /// one go later on).
301    #[cfg(feature = "opengl")]
302    pub fn gl(&self) -> &std::sync::Arc<egui_glow::glow::Context> {
303        &self.renderer.glow_context
304    }
305}
306
307/// Describes how to handle capturing key events from the host.
308#[derive(Default, Debug, Clone, PartialEq)]
309pub enum KeyCapture {
310    #[default]
311    /// All keys will be captured from the host.
312    CaptureAll,
313    /// No keys will be captured from the host.
314    IgnoreAll,
315    /// Only the given keys will be captured from the host.
316    CaptureKeys(Vec<keyboard_types::Key>),
317    /// All keys except the given ones will be captured from the host.
318    IgnoreKeys(Vec<keyboard_types::Key>),
319}
320
321struct EguiWindowInner<A: App> {
322    user_app: A,
323    egui_ctx: egui::Context,
324    clipboard_ctx: Option<copypasta::ClipboardContext>,
325    frame: Frame,
326}
327
328/// Handles an egui-baseview application
329pub struct EguiWindow<A: App> {
330    inner: RefCell<EguiWindowInner<A>>,
331    egui_input: RefCell<egui::RawInput>,
332    viewport_id: egui::ViewportId,
333    start_time: Instant,
334    system_scale_factor: Cell<f64>,
335    zoom_factor: Cell<f32>,
336    pointer_logical_pos: Cell<Option<egui::Pos2>>,
337    current_cursor_icon: Cell<baseview::MouseCursor>,
338    repaint_after: Arc<Mutex<Option<Instant>>>,
339    repaint_notifier: Option<RepaintNotifier>,
340    modifiers: Cell<egui::Modifiers>,
341
342    // Re-use the allocations from the previous output.
343    full_output: RefCell<FullOutput>,
344
345    pub window: WindowContext,
346}
347
348impl<A: App> EguiWindow<A> {
349    fn new(
350        window: WindowContext,
351        title: String,
352        graphics_config: GraphicsConfig,
353        mut user_app: A,
354        zoom_factor: f32,
355        repaint_notifier: Option<RepaintNotifier>,
356    ) -> Result<EguiWindow<A>, HandlerError> {
357        let renderer = Renderer::new(window.clone(), graphics_config)?;
358        let egui_ctx = egui::Context::default();
359
360        egui_ctx.set_zoom_factor(zoom_factor);
361
362        let repaint_after = Arc::new(Mutex::new(Some(Instant::now())));
363        let repaint_after_2 = Arc::clone(&repaint_after);
364        egui_ctx.set_request_repaint_callback(move |request_repaint_info| {
365            let repaint_instant = Instant::now() + request_repaint_info.delay;
366
367            let mut repaint_after = repaint_after_2.lock().unwrap();
368            let repaint_after = &mut *repaint_after;
369
370            if let Some(repaint_after) = repaint_after.as_mut() {
371                if repaint_instant < *repaint_after {
372                    *repaint_after = repaint_instant;
373                }
374            } else {
375                *repaint_after = Some(repaint_instant);
376            }
377        });
378
379        let size = window.size();
380
381        let system_scale_factor = size.scale_factor;
382        let total_scale_factor = system_scale_factor * zoom_factor as f64;
383
384        let logical_size: LogicalSize<f64> = size.physical.to_logical(total_scale_factor);
385
386        let screen_rect = Rect::from_min_size(
387            Pos2::new(0f32, 0f32),
388            vec2(logical_size.width as f32, logical_size.height as f32),
389        );
390
391        let viewport_info = egui::ViewportInfo {
392            parent: None,
393            title: Some(title),
394            native_pixels_per_point: Some(system_scale_factor as f32),
395            focused: Some(true),
396            inner_rect: Some(screen_rect),
397            ..Default::default()
398        };
399        let viewport_id = egui::ViewportId::default();
400
401        let mut egui_input = egui::RawInput {
402            max_texture_side: Some(renderer.max_texture_side()),
403            screen_rect: Some(screen_rect),
404            ..Default::default()
405        };
406        let _ = egui_input.viewports.insert(viewport_id, viewport_info);
407
408        let mut frame = Frame::new(renderer, window.clone());
409
410        user_app.build(egui_ctx.clone(), &mut frame)?;
411
412        let clipboard_ctx = match copypasta::ClipboardContext::new() {
413            Ok(clipboard_ctx) => Some(clipboard_ctx),
414            Err(e) => {
415                #[cfg(any(feature = "tracing", feature = "log"))]
416                error!("Failed to initialize clipboard: {}", e);
417
418                #[cfg(not(any(feature = "tracing", feature = "log")))]
419                let _ = e;
420
421                None
422            }
423        };
424
425        let start_time = Instant::now();
426
427        Ok(Self {
428            inner: RefCell::new(EguiWindowInner {
429                user_app,
430                egui_ctx,
431                clipboard_ctx,
432                frame,
433            }),
434            viewport_id,
435            start_time,
436            egui_input: egui_input.into(),
437            pointer_logical_pos: None.into(),
438            current_cursor_icon: baseview::MouseCursor::Default.into(),
439            system_scale_factor: system_scale_factor.into(),
440            zoom_factor: zoom_factor.into(),
441            repaint_after,
442            window,
443            repaint_notifier,
444            modifiers: Cell::new(egui::Modifiers::default()),
445            full_output: RefCell::new(FullOutput::default()),
446        })
447    }
448
449    /// Open a new window.
450    ///
451    /// * `settings` - The settings of the window.
452    /// * `app` - The application to run.
453    pub fn create(settings: EguiWindowSettings, app: A) -> Result<Window, baseview::Error> {
454        Self::create_with_host(settings, app, None)
455    }
456
457    /// Open a new window.
458    ///
459    /// * `settings` - The settings of the window.
460    /// * `app` - The application to run.
461    /// * `host` - The baseview ['Host'](baseview::host::Host) callbacks.
462    pub fn create_with_host(
463        settings: EguiWindowSettings,
464        app: A,
465        host: Option<baseview::host::Host>,
466    ) -> Result<Window, baseview::Error> {
467        let size = match settings.size {
468            Size::Logical(size) => Size::Logical(LogicalSize {
469                width: size.width as f64 * settings.zoom_factor as f64,
470                height: size.height as f64 * settings.zoom_factor as f64,
471            }),
472            Size::Physical(size) => Size::Physical(size),
473        };
474
475        let mut options = WindowSettings::new()
476            .with_title(settings.title.clone())
477            .with_size(size)
478            .with_min_size::<Size>(settings.min_size)
479            .with_max_size::<Size>(settings.max_size)
480            .with_resizable(settings.resizable)
481            .with_wait_for_parent(settings.wait_for_parent)
482            .with_fallback_scale_factor(settings.fallback_scale_factor);
483
484        options.parent = settings.parent;
485
486        #[cfg(feature = "opengl")]
487        let options = { options.with_gl_config(Some(settings.graphics.gl_config.clone())) };
488
489        Window::create_with_host(
490            options,
491            move |window| {
492                EguiWindow::new(
493                    window,
494                    settings.title,
495                    settings.graphics,
496                    app,
497                    settings.zoom_factor,
498                    settings.repaint_notifier,
499                )
500            },
501            host,
502        )
503    }
504}
505
506/// Update the pressed key modifiers when a mouse event has sent a new set of modifiers.
507fn update_modifiers(
508    old_modifiers: &Cell<egui::Modifiers>,
509    new_modifiers: &Modifiers,
510    egui_input: &mut egui::RawInput,
511) {
512    let (new_mac_cmd, new_command) = if cfg!(target_os = "macos") {
513        let m = new_modifiers.meta();
514        (m, m)
515    } else {
516        (false, new_modifiers.ctrl())
517    };
518
519    let new_modifiers = egui::Modifiers {
520        alt: new_modifiers.alt(),
521        ctrl: new_modifiers.ctrl(),
522        shift: new_modifiers.shift(),
523        mac_cmd: new_mac_cmd,
524        command: new_command,
525    };
526
527    if old_modifiers.get() != new_modifiers {
528        old_modifiers.set(new_modifiers);
529        egui_input
530            .events
531            .push(egui::Event::ModifiersChanged(new_modifiers));
532    }
533}
534
535impl<A: App> WindowHandler for EguiWindow<A> {
536    fn on_frame(&self) -> Result<(), HandlerError> {
537        let mut do_repaint_now = if let Some(repaint_notifier) = &self.repaint_notifier {
538            repaint_notifier.repaint_requested()
539        } else {
540            false
541        };
542
543        {
544            let mut repaint_after = self.repaint_after.lock().unwrap();
545            let repaint_after = &mut *repaint_after;
546
547            if let Some(instant) = &repaint_after
548                && Instant::now() >= *instant
549            {
550                do_repaint_now = true;
551                *repaint_after = None;
552            }
553        }
554
555        if !do_repaint_now {
556            return Ok(());
557        }
558
559        let (egui_input, logical_size) = {
560            let mut egui_input = self.egui_input.borrow_mut();
561            egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
562
563            let zoom_factor = self.zoom_factor.get();
564
565            let size = self.window.size();
566            let logical_size: LogicalSize<f32> = size
567                .physical
568                .to_logical(size.scale_factor * zoom_factor as f64);
569
570            let screen_rect = Rect::from_min_size(
571                Pos2::new(0f32, 0f32),
572                vec2(logical_size.width, logical_size.height),
573            );
574
575            egui_input.screen_rect = Some(screen_rect);
576            (egui_input.take(), logical_size)
577        };
578
579        // Re-use the allocations from the previous output.
580        let mut full_output = self.full_output.borrow_mut();
581
582        *full_output = {
583            let mut inner = self.inner.borrow_mut();
584            let EguiWindowInner {
585                user_app,
586                egui_ctx,
587                clipboard_ctx: _,
588                frame,
589            } = &mut *inner;
590
591            egui_ctx.run_ui(egui_input, |ui| user_app.ui(ui, frame))
592        };
593
594        let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
595            // The main window was closed by egui.
596            self.window.request_close();
597            return Ok(());
598        };
599
600        let mut new_size = None;
601        let new_zoom = { self.inner.borrow().egui_ctx.zoom_factor() };
602
603        if self.zoom_factor.get() != new_zoom {
604            self.zoom_factor.set(new_zoom);
605
606            let mut inner = self.inner.borrow_mut();
607
608            inner.egui_ctx.set_zoom_factor(new_zoom);
609            inner.user_app.zoom_factor_changed(new_zoom);
610
611            new_size = Some(Size::Logical(LogicalSize::new(
612                (logical_size.width * new_zoom) as f64,
613                (logical_size.height * new_zoom) as f64,
614            )));
615        }
616
617        for command in viewport_output.commands.iter() {
618            match command {
619                ViewportCommand::Close => {
620                    self.window.request_close();
621                }
622                ViewportCommand::InnerSize(size) => {
623                    new_size = Some(Size::Logical(LogicalSize::new(
624                        (size.x * new_zoom) as f64,
625                        (size.y * new_zoom) as f64,
626                    )));
627                }
628                ViewportCommand::Focus => {
629                    self.window.focus()?;
630                }
631                _ => {}
632            }
633        }
634
635        if let Some(new_size) = new_size {
636            if let Err(e) = self.window.resize(new_size) {
637                error!("Failed to resize window: {}", e);
638            }
639        }
640
641        {
642            let mut inner = self.inner.borrow_mut();
643            let EguiWindowInner {
644                user_app: _,
645                egui_ctx,
646                clipboard_ctx,
647                frame,
648            } = &mut *inner;
649
650            let size = self.window.size();
651            frame.renderer.render(
652                &self.window,
653                frame.clear_color,
654                size.physical,
655                size.scale_factor as f32 * egui_ctx.zoom_factor(),
656                egui_ctx,
657                &mut full_output,
658            );
659
660            for command in full_output.platform_output.commands.drain(..) {
661                match command {
662                    egui::OutputCommand::CopyText(text) => {
663                        if let Some(clipboard_ctx) = clipboard_ctx.as_mut()
664                            && let Err(err) = clipboard_ctx.set_contents(text)
665                        {
666                            #[cfg(any(feature = "tracing", feature = "log"))]
667                            error!("Copy/Cut error: {}", err);
668
669                            #[cfg(not(any(feature = "tracing", feature = "log")))]
670                            let _ = err;
671                        }
672                    }
673                    egui::OutputCommand::CopyImage(_) => {
674                        #[cfg(any(feature = "tracing", feature = "log"))]
675                        warn!("Copying images is not supported in egui_baseview.");
676                    }
677                    egui::OutputCommand::OpenUrl(open_url) => {
678                        if let Err(err) = open::that_detached(&open_url.url) {
679                            #[cfg(any(feature = "tracing", feature = "log"))]
680                            error!("Open error: {}", err);
681
682                            #[cfg(not(any(feature = "tracing", feature = "log")))]
683                            let _ = err;
684                        }
685                    }
686                }
687            }
688        }
689
690        let cursor_icon =
691            crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
692        if self.current_cursor_icon.get() != cursor_icon {
693            self.current_cursor_icon.set(cursor_icon);
694
695            self.window.set_mouse_cursor(cursor_icon)?;
696        }
697
698        // A temporary workaround for keyboard input not working sometimes.
699        // See https://codeberg.org/RustAudio/egui-baseview/issues/20
700        #[cfg(feature = "keyboard_focus_workaround")]
701        {
702            if !full_output.platform_output.events.is_empty()
703                || full_output.platform_output.ime.is_some()
704            {
705                window.focus();
706            }
707        }
708
709        Ok(())
710    }
711
712    fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> {
713        let zoom_factor = self.inner.borrow().egui_ctx.zoom_factor();
714
715        let total_scale_factor = new_size.scale_factor * zoom_factor as f64;
716        let logical_size: LogicalSize<f64> = new_size.physical.to_logical(total_scale_factor);
717
718        let screen_rect = Rect::from_min_size(
719            Pos2::new(0f32, 0f32),
720            vec2(logical_size.width as f32, logical_size.height as f32),
721        );
722
723        let mut egui_input = self.egui_input.borrow_mut();
724
725        egui_input.screen_rect = Some(screen_rect);
726
727        let viewport_info = egui_input.viewports.get_mut(&self.viewport_id).unwrap();
728        viewport_info.native_pixels_per_point = Some(new_size.scale_factor as f32);
729        viewport_info.inner_rect = Some(screen_rect);
730
731        self.system_scale_factor.set(new_size.scale_factor);
732
733        let mut inner = self.inner.borrow_mut();
734
735        inner.egui_ctx.request_repaint();
736
737        inner.user_app.resized(WindowSize {
738            physical: new_size.physical,
739            logical: logical_size,
740            scale_factor: total_scale_factor,
741        });
742
743        Ok(())
744    }
745
746    fn on_event(&self, event: Event) -> EventStatus {
747        let mut return_status = EventStatus::Captured;
748
749        // Parent/embedded windows do not always gain keyboard focus
750        // Automatically on click. Request focus explicitly before forwarding the event.
751        //
752        // TODO: Check if this is still necessary.
753        if matches!(
754            event,
755            baseview::Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
756        ) && !self.window.has_focus()
757        {
758            self.window.focus().unwrap();
759        }
760
761        let mut egui_input = self.egui_input.borrow_mut();
762
763        let mut do_repaint = true;
764
765        match &event {
766            baseview::Event::Mouse(event) => match event {
767                baseview::MouseEvent::CursorMoved {
768                    position,
769                    modifiers,
770                } => {
771                    update_modifiers(&self.modifiers, modifiers, &mut egui_input);
772
773                    let logical_pos: LogicalPosition<f32> = position.to_logical(
774                        self.system_scale_factor.get()
775                            * self.inner.borrow().egui_ctx.zoom_factor() as f64,
776                    );
777                    let pos = pos2(logical_pos.x, logical_pos.y);
778
779                    self.pointer_logical_pos.set(Some(pos));
780                    egui_input.events.push(egui::Event::PointerMoved(pos));
781                }
782                baseview::MouseEvent::ButtonPressed { button, modifiers } => {
783                    update_modifiers(&self.modifiers, modifiers, &mut egui_input);
784
785                    if let Some(pos) = self.pointer_logical_pos.get()
786                        && let Some(button) = crate::translate::translate_mouse_button(*button)
787                    {
788                        egui_input.events.push(egui::Event::PointerButton {
789                            pos,
790                            button,
791                            pressed: true,
792                            modifiers: self.modifiers.get(),
793                        });
794                    }
795                }
796                baseview::MouseEvent::ButtonReleased { button, modifiers } => {
797                    update_modifiers(&self.modifiers, modifiers, &mut egui_input);
798
799                    if let Some(pos) = self.pointer_logical_pos.get()
800                        && let Some(button) = crate::translate::translate_mouse_button(*button)
801                    {
802                        egui_input.events.push(egui::Event::PointerButton {
803                            pos,
804                            button,
805                            pressed: false,
806                            modifiers: self.modifiers.get(),
807                        });
808                    }
809                }
810                baseview::MouseEvent::WheelScrolled {
811                    delta: scroll_delta,
812                    modifiers,
813                } => {
814                    update_modifiers(&self.modifiers, modifiers, &mut egui_input);
815
816                    #[allow(unused_mut)]
817                    let (unit, mut delta) = match scroll_delta {
818                        baseview::ScrollDelta::Lines { x, y } => {
819                            (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
820                        }
821
822                        baseview::ScrollDelta::Pixels { x, y } => (
823                            egui::MouseWheelUnit::Point,
824                            egui::vec2(*x, *y) * self.window.scale_factor() as f32,
825                        ),
826                    };
827
828                    if cfg!(target_os = "macos") {
829                        // This is still buggy in winit despite
830                        // https://github.com/rust-windowing/winit/issues/1695 being closed
831                        //
832                        // TODO: See if this is an issue in baseview as well.
833                        delta.x *= -1.0;
834                    }
835
836                    egui_input.events.push(egui::Event::MouseWheel {
837                        unit,
838                        delta,
839                        modifiers: self.modifiers.get(),
840                        phase: egui::TouchPhase::Move,
841                    });
842                }
843                baseview::MouseEvent::CursorLeft => {
844                    self.pointer_logical_pos.set(None);
845                    egui_input.events.push(egui::Event::PointerGone);
846                }
847                _ => do_repaint = false,
848            },
849            baseview::Event::Keyboard(event) => {
850                update_modifiers(&self.modifiers, &event.modifiers, &mut egui_input);
851
852                let pressed = event.state == keyboard_types::KeyState::Down;
853
854                let modifiers = self.modifiers.get();
855
856                if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
857                    egui_input.events.push(egui::Event::Key {
858                        key,
859                        physical_key: None,
860                        pressed,
861                        repeat: event.repeat,
862                        modifiers,
863                    });
864                }
865
866                if pressed {
867                    // VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
868                    // so we detect these things manually:
869                    //
870                    // TODO: See if this is an issue in baseview as well.
871                    if is_cut_command(modifiers, event.code) {
872                        egui_input.events.push(egui::Event::Cut);
873                    } else if is_copy_command(modifiers, event.code) {
874                        egui_input.events.push(egui::Event::Copy);
875                    } else if is_paste_command(modifiers, event.code) {
876                        if let Some(clipboard_ctx) = self.inner.borrow_mut().clipboard_ctx.as_mut()
877                        {
878                            match clipboard_ctx.get_contents() {
879                                Ok(contents) => egui_input.events.push(egui::Event::Text(contents)),
880                                Err(err) => {
881                                    #[cfg(any(feature = "tracing", feature = "log"))]
882                                    error!("Paste error: {}", err);
883
884                                    #[cfg(not(any(feature = "tracing", feature = "log")))]
885                                    let _ = err;
886                                }
887                            }
888                        }
889                    } else if let keyboard_types::Key::Character(written) = &event.key
890                        && !modifiers.ctrl
891                        && !modifiers.command
892                    {
893                        egui_input.events.push(egui::Event::Text(written.clone()));
894                    }
895                }
896
897                match &self.inner.borrow().frame.key_capture {
898                    KeyCapture::CaptureAll => {}
899                    KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
900                    KeyCapture::CaptureKeys(keys) => {
901                        if !keys.contains(&event.key) {
902                            return_status = EventStatus::Ignored
903                        }
904                    }
905                    KeyCapture::IgnoreKeys(keys) => {
906                        if keys.contains(&event.key) {
907                            return_status = EventStatus::Ignored
908                        }
909                    }
910                }
911            }
912            baseview::Event::Window(event) => match event {
913                baseview::WindowEvent::Focused => {
914                    egui_input.events.push(egui::Event::WindowFocused(true));
915                    egui_input
916                        .viewports
917                        .get_mut(&self.viewport_id)
918                        .unwrap()
919                        .focused = Some(true);
920
921                    self.inner.borrow().egui_ctx.request_repaint();
922                }
923                baseview::WindowEvent::Unfocused => {
924                    egui_input.events.push(egui::Event::WindowFocused(false));
925                    egui_input
926                        .viewports
927                        .get_mut(&self.viewport_id)
928                        .unwrap()
929                        .focused = Some(false);
930                }
931                baseview::WindowEvent::WillClose => {}
932                _ => {}
933            },
934            _ => do_repaint = false,
935        }
936
937        if do_repaint {
938            self.inner.borrow().egui_ctx.request_repaint();
939        }
940
941        // For keyboard events, also check if egui actually wants keyboard input
942        // This allows DAW shortcuts (spacebar, etc.) to pass through when no text field is focused
943        match &event {
944            baseview::Event::Keyboard(_) => {
945                let egui_ctx = &self.inner.borrow().egui_ctx;
946                if return_status == EventStatus::Captured && !egui_ctx.egui_wants_keyboard_input() {
947                    EventStatus::Ignored
948                } else {
949                    return_status
950                }
951            }
952            baseview::Event::Mouse(_) => {
953                let egui_ctx = &self.inner.borrow().egui_ctx;
954                if egui_ctx.egui_is_using_pointer() || egui_ctx.egui_wants_pointer_input() {
955                    EventStatus::Captured
956                } else {
957                    EventStatus::Ignored
958                }
959            }
960            baseview::Event::Window(_) => EventStatus::Captured,
961            _ => EventStatus::Ignored,
962        }
963    }
964}
965
966fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
967    (modifiers.command && keycode == keyboard_types::Code::KeyX)
968        || (cfg!(target_os = "windows")
969            && modifiers.shift
970            && keycode == keyboard_types::Code::Delete)
971}
972
973fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
974    (modifiers.command && keycode == keyboard_types::Code::KeyC)
975        || (cfg!(target_os = "windows")
976            && modifiers.ctrl
977            && keycode == keyboard_types::Code::Insert)
978}
979
980fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
981    (modifiers.command && keycode == keyboard_types::Code::KeyV)
982        || (cfg!(target_os = "windows")
983            && modifiers.shift
984            && keycode == keyboard_types::Code::Insert)
985}