Skip to main content

egui_baseview/
window.rs

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