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(feature = "nice-log")]
17use nice_plug_core::{nice_error as error, nice_warn as warn};
18#[cfg(all(feature = "tracing", not(feature = "nice-log")))]
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 error!("oops! the gpu backend couldn't initialize! \n {err}");
181 panic!("gpu backend failed to initialize: \n {err}")
182 });
183 let egui_ctx = egui::Context::default();
184
185 let size = window.size();
186
187 let screen_rect = logical_screen_rect(size);
188 let scale_factor = size.scale_factor;
189
190 let viewport_info = egui::ViewportInfo {
191 parent: None,
192 title: Some(title),
193 native_pixels_per_point: Some(scale_factor as f32),
194 focused: Some(true),
195 inner_rect: Some(screen_rect),
196 ..Default::default()
197 };
198 let viewport_id = egui::ViewportId::default();
199
200 let mut egui_input = egui::RawInput {
201 max_texture_side: Some(renderer.max_texture_side()),
202 screen_rect: Some(screen_rect),
203 ..Default::default()
204 };
205 let _ = egui_input.viewports.insert(viewport_id, viewport_info);
206
207 let mut commands = ExtraOutputCommands::new();
208
209 (build)(&egui_ctx, &mut commands, &mut state);
210
211 let clipboard_ctx = match copypasta::ClipboardContext::new() {
212 Ok(clipboard_ctx) => Some(clipboard_ctx),
213 Err(e) => {
214 error!("Failed to initialize clipboard: {}", e);
215 None
216 }
217 };
218
219 let start_time = Instant::now();
220
221 Self {
222 inner: RefCell::new(EguiWindowInner {
223 user_state: state,
224 user_update: update,
225 user_output: output,
226 egui_ctx,
227 clipboard_ctx,
228 renderer,
229 clear_color: commands.clear_color.unwrap_or(Rgba::BLACK),
230 key_capture: commands.key_capture.unwrap_or_default(),
231 }),
232 viewport_id,
233 start_time,
234 egui_input: egui_input.into(),
235 pointer_logical_pos: None.into(),
236 current_cursor_icon: baseview::MouseCursor::Default.into(),
237 scale_factor: scale_factor.into(),
238 repaint_after: Some(start_time).into(),
239 window,
240 }
241 }
242
243 pub fn open_parented<P, B>(
256 parent: &P,
257 settings: EguiWindowSettings,
258 state: State,
259 build: B,
260 output: O,
261 update: U,
262 ) -> WindowHandle
263 where
264 P: HasWindowHandle,
265 B: FnMut(&egui::Context, &mut ExtraOutputCommands, &mut State),
266 B: 'static + Send,
267 {
268 let options = WindowOpenOptions::new()
269 .with_title(settings.title.clone())
270 .with_size(settings.size)
271 .with_scale_policy(settings.scale_policy);
272
273 #[cfg(feature = "opengl")]
274 let options = { options.with_gl_config(Some(settings.graphics.gl_config.clone())) };
275
276 Window::open_parented(parent, options, move |window| -> EguiWindow<State, U, O> {
277 EguiWindow::new(
278 window,
279 settings.title,
280 settings.graphics,
281 build,
282 output,
283 update,
284 state,
285 )
286 })
287 }
288
289 pub fn open_blocking<B>(
301 settings: EguiWindowSettings,
302 state: State,
303 build: B,
304 output: O,
305 update: U,
306 ) where
307 B: FnMut(&egui::Context, &mut ExtraOutputCommands, &mut State),
308 B: 'static + Send,
309 {
310 let options = WindowOpenOptions::new()
311 .with_title(settings.title.clone())
312 .with_size(settings.size)
313 .with_scale_policy(settings.scale_policy);
314
315 #[cfg(feature = "opengl")]
316 let options = { options.with_gl_config(Some(settings.graphics.gl_config.clone())) };
317
318 Window::open_blocking(options, move |window| -> EguiWindow<State, U, O> {
319 EguiWindow::new(
320 window,
321 settings.title,
322 settings.graphics,
323 build,
324 output,
325 update,
326 state,
327 )
328 })
329 }
330
331 fn update_modifiers(&self, modifiers: &Modifiers) {
333 let mut egui_input = self.egui_input.borrow_mut();
334 egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
335 egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
336 egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
337 }
338}
339
340impl<State, U, O> WindowHandler for EguiWindow<State, U, O>
341where
342 State: 'static + Send,
343 U: FnMut(&mut egui::Ui, &mut ExtraOutputCommands, &mut State),
344 U: 'static + Send,
345 O: FnMut(&FullOutput, &ViewportOutput, &mut State),
346 O: 'static + Send,
347{
348 fn on_frame(&self) {
349 let egui_input = {
350 let mut egui_input = self.egui_input.borrow_mut();
351 egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
352 egui_input.screen_rect = Some(logical_screen_rect(self.window.size()));
353 egui_input.take()
354 };
355
356 let mut full_output = {
357 let mut extra_commands = ExtraOutputCommands::new();
358
359 let mut inner = self.inner.borrow_mut();
360 let EguiWindowInner {
361 user_state,
362 user_update,
363 user_output,
364 egui_ctx,
365 clipboard_ctx: _,
366 renderer: _,
367 clear_color,
368 key_capture,
369 } = &mut *inner;
370
371 let output = egui_ctx.run_ui(egui_input, |ui| {
372 user_update(ui, &mut extra_commands, user_state)
373 });
374
375 if let Some(c) = extra_commands.clear_color.take() {
376 *clear_color = c;
377 }
378 if let Some(k) = extra_commands.key_capture.take() {
379 *key_capture = k;
380 }
381
382 if let Some(viewport_output) = output.viewport_output.get(&self.viewport_id) {
383 user_output(&output, viewport_output, user_state);
384 }
385
386 output
387 };
388
389 let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
390 self.window.request_close();
392 return;
393 };
394
395 for command in viewport_output.commands.iter() {
396 match command {
397 ViewportCommand::Close => {
398 self.window.request_close();
399 }
400 ViewportCommand::InnerSize(size) => self.window.resize(LogicalSize {
401 width: size.x.max(1.0),
402 height: size.y.max(1.0),
403 }),
404 ViewportCommand::Focus => {
405 self.window.focus();
406 }
407 _ => {}
408 }
409 }
410
411 {
412 let mut inner = self.inner.borrow_mut();
413 let EguiWindowInner {
414 user_state: _,
415 user_update: _,
416 user_output: _,
417 egui_ctx,
418 clipboard_ctx,
419 renderer,
420 clear_color: bg_color,
421 key_capture: _,
422 } = &mut *inner;
423
424 let now = Instant::now();
425 let do_repaint_now = if let Some(t) = self.repaint_after.get() {
426 now >= t || viewport_output.repaint_delay.is_zero()
427 } else {
428 viewport_output.repaint_delay.is_zero()
429 };
430
431 if do_repaint_now {
432 let size = self.window.size();
433 renderer.render(
434 &self.window,
435 *bg_color,
436 size.physical,
437 size.scale_factor as f32,
438 egui_ctx,
439 &mut full_output,
440 );
441
442 self.repaint_after.set(None);
443 } else if let Some(t) = now.checked_add(viewport_output.repaint_delay) {
444 self.repaint_after.set(Some(t));
446 }
447
448 for command in full_output.platform_output.commands {
449 match command {
450 egui::OutputCommand::CopyText(text) => {
451 if let Some(clipboard_ctx) = clipboard_ctx.as_mut()
452 && let Err(err) = clipboard_ctx.set_contents(text)
453 {
454 error!("Copy/Cut error: {}", err);
455 }
456 }
457 egui::OutputCommand::CopyImage(_) => {
458 warn!("Copying images is not supported in egui_baseview.");
459 }
460 egui::OutputCommand::OpenUrl(open_url) => {
461 if let Err(err) = open::that_detached(&open_url.url) {
462 error!("Open error: {}", err);
463 }
464 }
465 }
466 }
467 }
468
469 let cursor_icon =
470 crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
471 if self.current_cursor_icon.get() != cursor_icon {
472 self.current_cursor_icon.set(cursor_icon);
473
474 self.window.set_mouse_cursor(cursor_icon);
475 }
476
477 #[cfg(feature = "keyboard_focus_workaround")]
480 {
481 if !full_output.platform_output.events.is_empty()
482 || full_output.platform_output.ime.is_some()
483 {
484 window.focus();
485 }
486 }
487 }
488
489 fn resized(&self, new_size: WindowSize) {
490 let screen_rect = logical_screen_rect(new_size);
491
492 let mut egui_input = self.egui_input.borrow_mut();
493
494 egui_input.screen_rect = Some(screen_rect);
495
496 let viewport_info = egui_input.viewports.get_mut(&self.viewport_id).unwrap();
497 viewport_info.native_pixels_per_point = Some(new_size.scale_factor as f32);
498 viewport_info.inner_rect = Some(screen_rect);
499
500 self.repaint_after.set(Some(Instant::now()));
502
503 self.scale_factor.set(new_size.scale_factor);
504 }
505
506 fn on_event(&self, event: Event) -> EventStatus {
507 let mut return_status = EventStatus::Captured;
508
509 if matches!(
514 event,
515 baseview::Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
516 ) && !self.window.has_focus()
517 {
518 self.window.focus();
519 }
520
521 match &event {
522 baseview::Event::Mouse(event) => match event {
523 baseview::MouseEvent::CursorMoved {
524 position,
525 modifiers,
526 } => {
527 self.update_modifiers(modifiers);
528
529 let logical_pos: LogicalPosition<f32> =
530 position.to_logical(self.scale_factor.get());
531 let pos = pos2(logical_pos.x, logical_pos.y);
532
533 self.pointer_logical_pos.set(Some(pos));
534 self.egui_input
535 .borrow_mut()
536 .events
537 .push(egui::Event::PointerMoved(pos));
538 }
539 baseview::MouseEvent::ButtonPressed { button, modifiers } => {
540 self.update_modifiers(modifiers);
541
542 if let Some(pos) = self.pointer_logical_pos.get()
543 && let Some(button) = crate::translate::translate_mouse_button(*button)
544 {
545 let mut egui_input = self.egui_input.borrow_mut();
546 let modifiers = egui_input.modifiers;
547 egui_input.events.push(egui::Event::PointerButton {
548 pos,
549 button,
550 pressed: true,
551 modifiers,
552 });
553 }
554 }
555 baseview::MouseEvent::ButtonReleased { button, modifiers } => {
556 self.update_modifiers(modifiers);
557
558 if let Some(pos) = self.pointer_logical_pos.get()
559 && let Some(button) = crate::translate::translate_mouse_button(*button)
560 {
561 let mut egui_input = self.egui_input.borrow_mut();
562 let modifiers = egui_input.modifiers;
563 egui_input.events.push(egui::Event::PointerButton {
564 pos,
565 button,
566 pressed: false,
567 modifiers,
568 });
569 }
570 }
571 baseview::MouseEvent::WheelScrolled {
572 delta: scroll_delta,
573 modifiers,
574 } => {
575 self.update_modifiers(modifiers);
576
577 #[allow(unused_mut)]
578 let (unit, mut delta) = match scroll_delta {
579 baseview::ScrollDelta::Lines { x, y } => {
580 (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
581 }
582
583 baseview::ScrollDelta::Pixels { x, y } => (
584 egui::MouseWheelUnit::Point,
585 egui::vec2(*x, *y) * self.window.scale_factor() as f32,
586 ),
587 };
588
589 if cfg!(target_os = "macos") {
590 delta.x *= -1.0;
595 }
596
597 let mut egui_input = self.egui_input.borrow_mut();
598 let modifiers = egui_input.modifiers;
599 egui_input.events.push(egui::Event::MouseWheel {
600 unit,
601 delta,
602 modifiers,
603 phase: egui::TouchPhase::Move,
604 });
605 }
606 baseview::MouseEvent::CursorLeft => {
607 self.pointer_logical_pos.set(None);
608 self.egui_input
609 .borrow_mut()
610 .events
611 .push(egui::Event::PointerGone);
612 }
613 _ => {}
614 },
615 baseview::Event::Keyboard(event) => {
616 use keyboard_types::Code;
617
618 let pressed = event.state == keyboard_types::KeyState::Down;
619 let mut egui_input = self.egui_input.borrow_mut();
620
621 match event.code {
622 Code::ShiftLeft | Code::ShiftRight => egui_input.modifiers.shift = pressed,
623 Code::ControlLeft | Code::ControlRight => {
624 egui_input.modifiers.ctrl = pressed;
625
626 #[cfg(not(target_os = "macos"))]
627 {
628 egui_input.modifiers.command = pressed;
629 }
630 }
631 Code::AltLeft | Code::AltRight => egui_input.modifiers.alt = pressed,
632 Code::MetaLeft | Code::MetaRight => {
633 #[cfg(target_os = "macos")]
634 {
635 egui_input.modifiers.mac_cmd = pressed;
636 egui_input.modifiers.command = pressed;
637 }
638 }
640 _ => (),
641 }
642
643 if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
644 let modifiers = egui_input.modifiers;
645 egui_input.events.push(egui::Event::Key {
646 key,
647 physical_key: None,
648 pressed,
649 repeat: event.repeat,
650 modifiers,
651 });
652 }
653
654 if pressed {
655 if is_cut_command(egui_input.modifiers, event.code) {
660 egui_input.events.push(egui::Event::Cut);
661 } else if is_copy_command(egui_input.modifiers, event.code) {
662 egui_input.events.push(egui::Event::Copy);
663 } else if is_paste_command(egui_input.modifiers, event.code) {
664 if let Some(clipboard_ctx) = self.inner.borrow_mut().clipboard_ctx.as_mut()
665 {
666 match clipboard_ctx.get_contents() {
667 Ok(contents) => egui_input.events.push(egui::Event::Text(contents)),
668 Err(err) => {
669 error!("Paste error: {}", err);
670 }
671 }
672 }
673 } else if let keyboard_types::Key::Character(written) = &event.key
674 && !egui_input.modifiers.ctrl
675 && !egui_input.modifiers.command
676 {
677 egui_input.events.push(egui::Event::Text(written.clone()));
678 }
679 }
680
681 match &self.inner.borrow().key_capture {
682 KeyCapture::CaptureAll => {}
683 KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
684 KeyCapture::CaptureKeys(keys) => {
685 if !keys.contains(&event.key) {
686 return_status = EventStatus::Ignored
687 }
688 }
689 KeyCapture::IgnoreKeys(keys) => {
690 if keys.contains(&event.key) {
691 return_status = EventStatus::Ignored
692 }
693 }
694 }
695 }
696 baseview::Event::Window(event) => match event {
697 baseview::WindowEvent::Focused => {
698 let mut egui_input = self.egui_input.borrow_mut();
699 egui_input.events.push(egui::Event::WindowFocused(true));
700 egui_input
701 .viewports
702 .get_mut(&self.viewport_id)
703 .unwrap()
704 .focused = Some(true);
705 }
706 baseview::WindowEvent::Unfocused => {
707 let mut egui_input = self.egui_input.borrow_mut();
708 egui_input.events.push(egui::Event::WindowFocused(false));
709 egui_input
710 .viewports
711 .get_mut(&self.viewport_id)
712 .unwrap()
713 .focused = Some(false);
714 }
715 baseview::WindowEvent::WillClose => {}
716 _ => {}
717 },
718 _ => {}
719 }
720
721 match &event {
724 baseview::Event::Keyboard(_) => {
725 let egui_ctx = &self.inner.borrow().egui_ctx;
726 if return_status == EventStatus::Captured && !egui_ctx.egui_wants_keyboard_input() {
727 EventStatus::Ignored
728 } else {
729 return_status
730 }
731 }
732 baseview::Event::Mouse(_) => {
733 let egui_ctx = &self.inner.borrow().egui_ctx;
734 if egui_ctx.egui_is_using_pointer() || egui_ctx.egui_wants_pointer_input() {
735 EventStatus::Captured
736 } else {
737 EventStatus::Ignored
738 }
739 }
740 baseview::Event::Window(_) => EventStatus::Captured,
741 _ => EventStatus::Ignored,
742 }
743 }
744}
745
746fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
747 (modifiers.command && keycode == keyboard_types::Code::KeyX)
748 || (cfg!(target_os = "windows")
749 && modifiers.shift
750 && keycode == keyboard_types::Code::Delete)
751}
752
753fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
754 (modifiers.command && keycode == keyboard_types::Code::KeyC)
755 || (cfg!(target_os = "windows")
756 && modifiers.ctrl
757 && keycode == keyboard_types::Code::Insert)
758}
759
760fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
761 (modifiers.command && keycode == keyboard_types::Code::KeyV)
762 || (cfg!(target_os = "windows")
763 && modifiers.shift
764 && keycode == keyboard_types::Code::Insert)
765}
766
767fn logical_screen_rect(size: WindowSize) -> Rect {
769 Rect::from_min_size(
770 Pos2::new(0f32, 0f32),
771 vec2(size.logical.width as f32, size.logical.height as f32),
772 )
773}