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 pub size: Size,
27
28 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
79pub 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 pub fn clear_color(&mut self, clear_color: Rgba) {
95 self.clear_color = Some(clear_color);
96 }
97
98 pub fn set_key_capture(&mut self, key_capture: KeyCapture) {
100 self.key_capture = Some(key_capture);
101 }
102}
103
104#[derive(Default, Debug, Clone, PartialEq)]
106pub enum KeyCapture {
107 #[default]
108 CaptureAll,
110 IgnoreAll,
112 CaptureKeys(Vec<keyboard_types::Key>),
114 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
136pub 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 #[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 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 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 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 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 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 #[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 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 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 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 }
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 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 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
786fn 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}