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