1use std::time::Instant;
2
3use baseview::{
4 Event, EventStatus, PhySize, Size, Window, WindowHandle, WindowHandler, WindowOpenOptions,
5 WindowScalePolicy,
6};
7use copypasta::ClipboardProvider;
8use egui::{Pos2, Rect, Rgba, ViewportCommand, pos2, vec2};
9use keyboard_types::Modifiers;
10use raw_window_handle::HasRawWindowHandle;
11
12use crate::{GraphicsConfig, renderer::Renderer};
13
14#[cfg(feature = "nice-log")]
15use nice_plug_core::{nice_error as error, nice_warn as warn};
16
17#[cfg(all(feature = "tracing", not(feature = "nice-log")))]
18use tracing::{error, warn};
19
20#[derive(Debug, Clone)]
21pub struct EguiWindowSettings {
22 pub title: String,
23
24 pub logical_size: Size,
29
30 pub scale_policy: WindowScalePolicy,
32
33 pub graphics: GraphicsConfig,
34}
35
36impl EguiWindowSettings {
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub fn with_tile(mut self, title: impl Into<String>) -> Self {
42 self.title = title.into();
43 self
44 }
45
46 pub fn with_logical_size(mut self, size: Size) -> Self {
47 self.logical_size = size;
48 self
49 }
50
51 pub fn with_scale_policy(mut self, scale_policy: WindowScalePolicy) -> Self {
52 self.scale_policy = scale_policy;
53 self
54 }
55
56 pub fn with_graphics_config(mut self, config: GraphicsConfig) -> Self {
57 self.graphics = config;
58 self
59 }
60}
61
62impl Default for EguiWindowSettings {
63 fn default() -> Self {
64 Self {
65 title: String::new(),
66 logical_size: Size {
67 width: 300.0,
68 height: 200.0,
69 },
70 scale_policy: WindowScalePolicy::default(),
71 graphics: GraphicsConfig::default(),
72 }
73 }
74}
75
76pub struct Queue<'a> {
77 bg_color: &'a mut Rgba,
78 close_requested: &'a mut bool,
79 physical_size: &'a mut PhySize,
80 key_capture: &'a mut KeyCapture,
81}
82
83impl<'a> Queue<'a> {
84 pub(crate) fn new(
85 bg_color: &'a mut Rgba,
86 close_requested: &'a mut bool,
87 physical_size: &'a mut PhySize,
88 key_capture: &'a mut KeyCapture,
89 ) -> Self {
90 Self {
91 bg_color,
92 close_requested,
95 physical_size,
96 key_capture,
97 }
98 }
99
100 pub fn bg_color(&mut self, bg_color: Rgba) {
102 *self.bg_color = bg_color;
103 }
104
105 pub fn resize(&mut self, physical_size: PhySize) {
107 *self.physical_size = physical_size;
108 }
109
110 pub fn close_window(&mut self) {
112 *self.close_requested = true;
113 }
114
115 pub fn set_key_capture(&mut self, key_capture: KeyCapture) {
117 *self.key_capture = key_capture;
118 }
119}
120
121#[derive(Default, Debug, Clone, PartialEq)]
123pub enum KeyCapture {
124 #[default]
125 CaptureAll,
127 IgnoreAll,
129 CaptureKeys(Vec<keyboard_types::Key>),
131 IgnoreKeys(Vec<keyboard_types::Key>),
133}
134
135pub struct EguiWindow<State, U>
137where
138 State: 'static + Send,
139 U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
140 U: 'static + Send,
141{
142 user_state: Option<State>,
143 user_update: U,
144
145 egui_ctx: egui::Context,
146 viewport_id: egui::ViewportId,
147 start_time: Instant,
148 egui_input: egui::RawInput,
149 pointer_pos_in_points: Option<egui::Pos2>,
150 current_cursor_icon: baseview::MouseCursor,
151
152 renderer: Renderer,
153
154 clipboard_ctx: Option<copypasta::ClipboardContext>,
155
156 physical_size: PhySize,
157 scale_policy: WindowScalePolicy,
158 pixels_per_point: f32,
159 points_per_pixel: f32,
160 bg_color: Rgba,
161 close_requested: bool,
162 repaint_after: Option<Instant>,
163 key_capture: KeyCapture,
164}
165
166impl<State, U> EguiWindow<State, U>
167where
168 State: 'static + Send,
169 U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
170 U: 'static + Send,
171{
172 fn new<B>(
173 window: &mut baseview::Window<'_>,
174 settings: EguiWindowSettings,
175 mut build: B,
176 update: U,
177 mut state: State,
178 ) -> EguiWindow<State, U>
179 where
180 B: FnMut(&egui::Context, &mut Queue, &mut State),
181 B: 'static + Send,
182 {
183 let renderer = Renderer::new(window, settings.graphics).unwrap_or_else(|err| {
184 error!("oops! the gpu backend couldn't initialize! \n {err}");
186 panic!("gpu backend failed to initialize: \n {err}")
187 });
188 let egui_ctx = egui::Context::default();
189
190 let pixels_per_point = match settings.scale_policy {
192 WindowScalePolicy::ScaleFactor(scale) => scale,
193 WindowScalePolicy::SystemScaleFactor => 1.0,
194 } as f32;
195 let points_per_pixel = pixels_per_point.recip();
196
197 let screen_rect = Rect::from_min_size(
198 Pos2::new(0f32, 0f32),
199 vec2(
200 settings.logical_size.width as f32,
201 settings.logical_size.height as f32,
202 ),
203 );
204
205 let viewport_info = egui::ViewportInfo {
206 parent: None,
207 title: Some(settings.title),
208 native_pixels_per_point: Some(pixels_per_point),
209 focused: Some(true),
210 inner_rect: Some(screen_rect),
211 ..Default::default()
212 };
213 let viewport_id = egui::ViewportId::default();
214
215 let mut egui_input = egui::RawInput {
216 max_texture_side: Some(renderer.max_texture_side()),
217 screen_rect: Some(screen_rect),
218 ..Default::default()
219 };
220 let _ = egui_input.viewports.insert(viewport_id, viewport_info);
221
222 let mut physical_size = PhySize {
223 width: (settings.logical_size.width * pixels_per_point as f64).round() as u32,
224 height: (settings.logical_size.height * pixels_per_point as f64).round() as u32,
225 };
226
227 let mut bg_color = Rgba::BLACK;
228 let mut close_requested = false;
229 let old_physical_size = physical_size;
230 let mut key_capture = KeyCapture::default();
231 let mut queue = Queue::new(
232 &mut bg_color,
233 &mut close_requested,
234 &mut physical_size,
235 &mut key_capture,
236 );
237 (build)(&egui_ctx, &mut queue, &mut state);
238
239 if physical_size != old_physical_size {
240 window.resize(baseview::Size {
241 width: physical_size.width as f64,
242 height: physical_size.height as f64,
243 });
244 }
245
246 let clipboard_ctx = match copypasta::ClipboardContext::new() {
247 Ok(clipboard_ctx) => Some(clipboard_ctx),
248 Err(e) => {
249 error!("Failed to initialize clipboard: {}", e);
250 None
251 }
252 };
253
254 let start_time = Instant::now();
255
256 Self {
257 user_state: Some(state),
258 user_update: update,
259
260 egui_ctx,
261 viewport_id,
262 start_time,
263 egui_input,
264 pointer_pos_in_points: None,
265 current_cursor_icon: baseview::MouseCursor::Default,
266
267 renderer,
268
269 clipboard_ctx,
270
271 physical_size,
272 pixels_per_point,
273 points_per_pixel,
274 scale_policy: settings.scale_policy,
275 bg_color,
276 close_requested,
277 repaint_after: Some(start_time),
278 key_capture,
279 }
280 }
281
282 pub fn open_parented<P, B>(
292 parent: &P,
293 settings: EguiWindowSettings,
294 state: State,
295 build: B,
296 update: U,
297 ) -> WindowHandle
298 where
299 P: HasRawWindowHandle,
300 B: FnMut(&egui::Context, &mut Queue, &mut State),
301 B: 'static + Send,
302 {
303 Window::open_parented(
304 parent,
305 #[allow(clippy::needless_update)]
306 WindowOpenOptions {
307 title: settings.title.clone(),
308 size: settings.logical_size,
309 scale: settings.scale_policy,
310 #[cfg(feature = "opengl")]
311 gl_config: Some(settings.graphics.gl_config.clone()),
312 ..Default::default()
313 },
314 move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
315 EguiWindow::new(window, settings, build, update, state)
316 },
317 )
318 }
319
320 pub fn open_blocking<B>(settings: EguiWindowSettings, state: State, build: B, update: U)
329 where
330 B: FnMut(&egui::Context, &mut Queue, &mut State),
331 B: 'static + Send,
332 {
333 Window::open_blocking(
334 #[allow(clippy::needless_update)]
335 WindowOpenOptions {
336 title: settings.title.clone(),
337 size: settings.logical_size,
338 scale: settings.scale_policy,
339 #[cfg(feature = "opengl")]
340 gl_config: Some(settings.graphics.gl_config.clone()),
341 ..Default::default()
342 },
343 move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
344 EguiWindow::new(window, settings, build, update, state)
345 },
346 )
347 }
348
349 fn update_modifiers(&mut self, modifiers: &Modifiers) {
351 self.egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
352 self.egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
353 self.egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
354 }
355}
356
357impl<State, U> WindowHandler for EguiWindow<State, U>
358where
359 State: 'static + Send,
360 U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
361 U: 'static + Send,
362{
363 fn on_frame(&mut self, window: &mut Window) {
364 let Some(state) = &mut self.user_state else {
365 return;
366 };
367
368 self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
369 self.egui_input.screen_rect = Some(calculate_screen_rect(
370 self.physical_size,
371 self.points_per_pixel,
372 ));
373
374 let old_physical_size = self.physical_size;
376 let mut queue = Queue::new(
377 &mut self.bg_color,
378 &mut self.close_requested,
379 &mut self.physical_size,
380 &mut self.key_capture,
381 );
382
383 let mut full_output = self.egui_ctx.run_ui(self.egui_input.take(), |ui| {
384 (self.user_update)(ui, &mut queue, state)
385 });
386
387 if self.close_requested {
388 window.close();
389 }
390
391 let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
395 window.close();
397 return;
398 };
399
400 for command in viewport_output.commands.iter() {
401 match command {
402 ViewportCommand::Close => {
403 window.close();
404 }
405 ViewportCommand::InnerSize(size) => window.resize(baseview::Size {
406 width: size.x.max(1.0) as f64,
407 height: size.y.max(1.0) as f64,
408 }),
409 _ => {}
410 }
411 }
412
413 if self.physical_size != old_physical_size {
414 window.resize(baseview::Size {
415 width: self.physical_size.width.max(1) as f64,
416 height: self.physical_size.height.max(1) as f64,
417 });
418 }
419
420 let now = Instant::now();
421 let do_repaint_now = if let Some(t) = self.repaint_after {
422 now >= t || viewport_output.repaint_delay.is_zero()
423 } else {
424 viewport_output.repaint_delay.is_zero()
425 };
426
427 if do_repaint_now {
428 self.renderer.render(
429 window,
430 self.bg_color,
431 self.physical_size,
432 self.pixels_per_point,
433 &mut self.egui_ctx,
434 &mut full_output,
435 );
436
437 self.repaint_after = None;
438 } else if let Some(repaint_after) = now.checked_add(viewport_output.repaint_delay) {
439 self.repaint_after = Some(repaint_after);
441 }
442
443 for command in full_output.platform_output.commands {
444 match command {
445 egui::OutputCommand::CopyText(text) => {
446 if let Some(clipboard_ctx) = &mut self.clipboard_ctx
447 && let Err(err) = clipboard_ctx.set_contents(text)
448 {
449 error!("Copy/Cut error: {}", err);
450 }
451 }
452 egui::OutputCommand::CopyImage(_) => {
453 warn!("Copying images is not supported in egui_baseview.");
454 }
455 egui::OutputCommand::OpenUrl(open_url) => {
456 if let Err(err) = open::that_detached(&open_url.url) {
457 error!("Open error: {}", err);
458 }
459 }
460 }
461 }
462
463 let cursor_icon =
464 crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
465 if self.current_cursor_icon != cursor_icon {
466 self.current_cursor_icon = cursor_icon;
467
468 window.set_mouse_cursor(cursor_icon);
469 }
470
471 #[cfg(feature = "keyboard_focus_workaround")]
474 {
475 if !full_output.platform_output.events.is_empty()
476 || full_output.platform_output.ime.is_some()
477 {
478 window.focus();
479 }
480 }
481 }
482
483 #[allow(unused_variables)]
484 fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus {
485 let mut return_status = EventStatus::Captured;
486
487 if matches!(
490 event,
491 Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
492 ) && !window.has_focus()
493 {
494 window.focus();
495 }
496
497 match &event {
498 baseview::Event::Mouse(event) => match event {
499 baseview::MouseEvent::CursorMoved {
500 position,
501 modifiers,
502 } => {
503 self.update_modifiers(modifiers);
504
505 let pos = pos2(position.x as f32, position.y as f32);
506 self.pointer_pos_in_points = Some(pos);
507 self.egui_input.events.push(egui::Event::PointerMoved(pos));
508 }
509 baseview::MouseEvent::ButtonPressed { button, modifiers } => {
510 self.update_modifiers(modifiers);
511
512 if let Some(pos) = self.pointer_pos_in_points
513 && let Some(button) = crate::translate::translate_mouse_button(*button)
514 {
515 self.egui_input.events.push(egui::Event::PointerButton {
516 pos,
517 button,
518 pressed: true,
519 modifiers: self.egui_input.modifiers,
520 });
521 }
522 }
523 baseview::MouseEvent::ButtonReleased { button, modifiers } => {
524 self.update_modifiers(modifiers);
525
526 if let Some(pos) = self.pointer_pos_in_points
527 && let Some(button) = crate::translate::translate_mouse_button(*button)
528 {
529 self.egui_input.events.push(egui::Event::PointerButton {
530 pos,
531 button,
532 pressed: false,
533 modifiers: self.egui_input.modifiers,
534 });
535 }
536 }
537 baseview::MouseEvent::WheelScrolled {
538 delta: scroll_delta,
539 modifiers,
540 } => {
541 self.update_modifiers(modifiers);
542
543 #[allow(unused_mut)]
544 let (unit, mut delta) = match scroll_delta {
545 baseview::ScrollDelta::Lines { x, y } => {
546 (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
547 }
548
549 baseview::ScrollDelta::Pixels { x, y } => (
550 egui::MouseWheelUnit::Point,
551 egui::vec2(*x, *y) * self.points_per_pixel,
552 ),
553 };
554
555 if cfg!(target_os = "macos") {
556 delta.x *= -1.0;
561 }
562
563 self.egui_input.events.push(egui::Event::MouseWheel {
564 unit,
565 delta,
566 modifiers: self.egui_input.modifiers,
567 phase: egui::TouchPhase::Move,
568 });
569 }
570 baseview::MouseEvent::CursorLeft => {
571 self.pointer_pos_in_points = None;
572 self.egui_input.events.push(egui::Event::PointerGone);
573 }
574 _ => {}
575 },
576 baseview::Event::Keyboard(event) => {
577 use keyboard_types::Code;
578
579 let pressed = event.state == keyboard_types::KeyState::Down;
580
581 match event.code {
582 Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed,
583 Code::ControlLeft | Code::ControlRight => {
584 self.egui_input.modifiers.ctrl = pressed;
585
586 #[cfg(not(target_os = "macos"))]
587 {
588 self.egui_input.modifiers.command = pressed;
589 }
590 }
591 Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed,
592 Code::MetaLeft | Code::MetaRight => {
593 #[cfg(target_os = "macos")]
594 {
595 self.egui_input.modifiers.mac_cmd = pressed;
596 self.egui_input.modifiers.command = pressed;
597 }
598 }
600 _ => (),
601 }
602
603 if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
604 self.egui_input.events.push(egui::Event::Key {
605 key,
606 physical_key: None,
607 pressed,
608 repeat: event.repeat,
609 modifiers: self.egui_input.modifiers,
610 });
611 }
612
613 if pressed {
614 if is_cut_command(self.egui_input.modifiers, event.code) {
619 self.egui_input.events.push(egui::Event::Cut);
620 } else if is_copy_command(self.egui_input.modifiers, event.code) {
621 self.egui_input.events.push(egui::Event::Copy);
622 } else if is_paste_command(self.egui_input.modifiers, event.code) {
623 if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
624 match clipboard_ctx.get_contents() {
625 Ok(contents) => {
626 self.egui_input.events.push(egui::Event::Text(contents))
627 }
628 Err(err) => {
629 error!("Paste error: {}", err);
630 }
631 }
632 }
633 } else if let keyboard_types::Key::Character(written) = &event.key
634 && !self.egui_input.modifiers.ctrl
635 && !self.egui_input.modifiers.command
636 {
637 self.egui_input
638 .events
639 .push(egui::Event::Text(written.clone()));
640 }
641 }
642
643 match &self.key_capture {
644 KeyCapture::CaptureAll => {}
645 KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
646 KeyCapture::CaptureKeys(keys) => {
647 if !keys.contains(&event.key) {
648 return_status = EventStatus::Ignored
649 }
650 }
651 KeyCapture::IgnoreKeys(keys) => {
652 if keys.contains(&event.key) {
653 return_status = EventStatus::Ignored
654 }
655 }
656 }
657 }
658 baseview::Event::Window(event) => match event {
659 baseview::WindowEvent::Resized(window_info) => {
660 self.pixels_per_point = match self.scale_policy {
661 WindowScalePolicy::ScaleFactor(scale) => scale,
662 WindowScalePolicy::SystemScaleFactor => window_info.scale(),
663 } as f32;
664 self.points_per_pixel = self.pixels_per_point.recip();
665
666 self.physical_size = window_info.physical_size();
667
668 let screen_rect =
669 calculate_screen_rect(self.physical_size, self.points_per_pixel);
670
671 self.egui_input.screen_rect = Some(screen_rect);
672
673 let viewport_info = self
674 .egui_input
675 .viewports
676 .get_mut(&self.viewport_id)
677 .unwrap();
678 viewport_info.native_pixels_per_point = Some(self.pixels_per_point);
679 viewport_info.inner_rect = Some(screen_rect);
680
681 self.repaint_after = Some(Instant::now());
683 }
684 baseview::WindowEvent::Focused => {
685 self.egui_input
686 .events
687 .push(egui::Event::WindowFocused(true));
688 self.egui_input
689 .viewports
690 .get_mut(&self.viewport_id)
691 .unwrap()
692 .focused = Some(true);
693 }
694 baseview::WindowEvent::Unfocused => {
695 self.egui_input
696 .events
697 .push(egui::Event::WindowFocused(false));
698 self.egui_input
699 .viewports
700 .get_mut(&self.viewport_id)
701 .unwrap()
702 .focused = Some(false);
703 }
704 baseview::WindowEvent::WillClose => {}
705 },
706 }
707
708 match &event {
711 baseview::Event::Keyboard(_) => {
712 if return_status == EventStatus::Captured
713 && !self.egui_ctx.egui_wants_keyboard_input()
714 {
715 EventStatus::Ignored
716 } else {
717 return_status
718 }
719 }
720 baseview::Event::Mouse(_) => {
721 if self.egui_ctx.egui_is_using_pointer() || self.egui_ctx.egui_wants_pointer_input()
722 {
723 EventStatus::Captured
724 } else {
725 EventStatus::Ignored
726 }
727 }
728 baseview::Event::Window(_) => EventStatus::Captured,
729 }
730 }
731}
732
733fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
734 (modifiers.command && keycode == keyboard_types::Code::KeyX)
735 || (cfg!(target_os = "windows")
736 && modifiers.shift
737 && keycode == keyboard_types::Code::Delete)
738}
739
740fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
741 (modifiers.command && keycode == keyboard_types::Code::KeyC)
742 || (cfg!(target_os = "windows")
743 && modifiers.ctrl
744 && keycode == keyboard_types::Code::Insert)
745}
746
747fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
748 (modifiers.command && keycode == keyboard_types::Code::KeyV)
749 || (cfg!(target_os = "windows")
750 && modifiers.shift
751 && keycode == keyboard_types::Code::Insert)
752}
753
754fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect {
756 let logical_size = (
757 physical_size.width as f32 * points_per_pixel,
758 physical_size.height as f32 * points_per_pixel,
759 );
760 Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1))
761}