Skip to main content

egui_baseview/
window.rs

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