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 mut key_capture = KeyCapture::default();
199 let mut queue = Queue::new(
200 &mut bg_color,
201 &mut close_requested,
202 &mut physical_size,
203 &mut key_capture,
204 );
205 (build)(&egui_ctx, &mut queue, &mut state);
206
207 let clipboard_ctx = match copypasta::ClipboardContext::new() {
208 Ok(clipboard_ctx) => Some(clipboard_ctx),
209 Err(e) => {
210 error!("Failed to initialize clipboard: {}", e);
211 None
212 }
213 };
214
215 let start_time = Instant::now();
216
217 Self {
218 user_state: Some(state),
219 user_update: update,
220
221 egui_ctx,
222 viewport_id,
223 start_time,
224 egui_input,
225 pointer_pos_in_points: None,
226 current_cursor_icon: baseview::MouseCursor::Default,
227
228 renderer,
229
230 clipboard_ctx,
231
232 physical_size,
233 pixels_per_point,
234 points_per_pixel,
235 scale_policy: open_settings.scale_policy,
236 bg_color,
237 close_requested,
238 repaint_after: Some(start_time),
239 key_capture,
240 }
241 }
242
243 pub fn open_parented<P, B>(
253 parent: &P,
254 #[allow(unused_mut)] mut settings: WindowOpenOptions,
255 graphics_config: GraphicsConfig,
256 state: State,
257 build: B,
258 update: U,
259 ) -> WindowHandle
260 where
261 P: HasRawWindowHandle,
262 B: FnMut(&egui::Context, &mut Queue, &mut State),
263 B: 'static + Send,
264 {
265 #[cfg(feature = "opengl")]
266 if settings.gl_config.is_none() {
267 settings.gl_config = Some(Default::default());
268 }
269
270 let open_settings = OpenSettings::new(&settings);
271
272 Window::open_parented(
273 parent,
274 settings,
275 move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
276 EguiWindow::new(window, open_settings, graphics_config, build, update, state)
277 },
278 )
279 }
280
281 pub fn open_blocking<B>(
290 #[allow(unused_mut)] mut settings: WindowOpenOptions,
291 graphics_config: GraphicsConfig,
292 state: State,
293 build: B,
294 update: U,
295 ) where
296 B: FnMut(&egui::Context, &mut Queue, &mut State),
297 B: 'static + Send,
298 {
299 #[cfg(feature = "opengl")]
300 if settings.gl_config.is_none() {
301 settings.gl_config = Some(Default::default());
302 }
303
304 let open_settings = OpenSettings::new(&settings);
305
306 Window::open_blocking(
307 settings,
308 move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
309 EguiWindow::new(window, open_settings, graphics_config, build, update, state)
310 },
311 )
312 }
313
314 fn update_modifiers(&mut self, modifiers: &Modifiers) {
316 self.egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
317 self.egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
318 self.egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
319 }
320}
321
322impl<State, U> WindowHandler for EguiWindow<State, U>
323where
324 State: 'static + Send,
325 U: FnMut(&mut egui::Ui, &mut Queue, &mut State),
326 U: 'static + Send,
327{
328 fn on_frame(&mut self, window: &mut Window) {
329 let Some(state) = &mut self.user_state else {
330 return;
331 };
332
333 self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
334 self.egui_input.screen_rect = Some(calculate_screen_rect(
335 self.physical_size,
336 self.points_per_pixel,
337 ));
338
339 let mut queue = Queue::new(
341 &mut self.bg_color,
342 &mut self.close_requested,
343 &mut self.physical_size,
344 &mut self.key_capture,
345 );
346
347 let mut full_output = self.egui_ctx.run_ui(self.egui_input.take(), |ui| {
348 (self.user_update)(ui, &mut queue, state)
349 });
350
351 if self.close_requested {
352 window.close();
353 }
354
355 let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
359 window.close();
361 return;
362 };
363
364 for command in viewport_output.commands.iter() {
365 match command {
366 ViewportCommand::Close => {
367 window.close();
368 }
369 ViewportCommand::InnerSize(size) => window.resize(baseview::Size {
370 width: size.x.max(1.0) as f64,
371 height: size.y.max(1.0) as f64,
372 }),
373 _ => {}
374 }
375 }
376
377 let now = Instant::now();
378 let do_repaint_now = if let Some(t) = self.repaint_after {
379 now >= t || viewport_output.repaint_delay.is_zero()
380 } else {
381 viewport_output.repaint_delay.is_zero()
382 };
383
384 if do_repaint_now {
385 self.renderer.render(
386 window,
387 self.bg_color,
388 self.physical_size,
389 self.pixels_per_point,
390 &mut self.egui_ctx,
391 &mut full_output,
392 );
393
394 self.repaint_after = None;
395 } else if let Some(repaint_after) = now.checked_add(viewport_output.repaint_delay) {
396 self.repaint_after = Some(repaint_after);
398 }
399
400 for command in full_output.platform_output.commands {
401 match command {
402 egui::OutputCommand::CopyText(text) => {
403 if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
404 if let Err(err) = clipboard_ctx.set_contents(text) {
405 error!("Copy/Cut error: {}", err);
406 }
407 }
408 }
409 egui::OutputCommand::CopyImage(_) => {
410 warn!("Copying images is not supported in egui_baseview.");
411 }
412 egui::OutputCommand::OpenUrl(open_url) => {
413 if let Err(err) = open::that_detached(&open_url.url) {
414 error!("Open error: {}", err);
415 }
416 }
417 }
418 }
419
420 let cursor_icon =
421 crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
422 if self.current_cursor_icon != cursor_icon {
423 self.current_cursor_icon = cursor_icon;
424
425 #[cfg(not(target_os = "macos"))]
427 window.set_mouse_cursor(cursor_icon);
428 }
429
430 #[cfg(feature = "keyboard_focus_workaround")]
433 {
434 #[cfg(any(target_os = "windows", target_os = "macos"))]
435 {
436 if !full_output.platform_output.events.is_empty()
437 || full_output.platform_output.ime.is_some()
438 {
439 window.focus();
440 }
441 }
442 }
443 }
444
445 #[allow(unused_variables)]
446 fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus {
447 let mut return_status = EventStatus::Captured;
448
449 #[cfg(not(target_os = "linux"))]
452 if matches!(
453 event,
454 Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
455 ) && !window.has_focus()
456 {
457 window.focus();
458 }
459
460 match &event {
461 baseview::Event::Mouse(event) => match event {
462 baseview::MouseEvent::CursorMoved {
463 position,
464 modifiers,
465 } => {
466 self.update_modifiers(modifiers);
467
468 let pos = pos2(position.x as f32, position.y as f32);
469 self.pointer_pos_in_points = Some(pos);
470 self.egui_input.events.push(egui::Event::PointerMoved(pos));
471 }
472 baseview::MouseEvent::ButtonPressed { button, modifiers } => {
473 self.update_modifiers(modifiers);
474
475 if let Some(pos) = self.pointer_pos_in_points {
476 if let Some(button) = crate::translate::translate_mouse_button(*button) {
477 self.egui_input.events.push(egui::Event::PointerButton {
478 pos,
479 button,
480 pressed: true,
481 modifiers: self.egui_input.modifiers,
482 });
483 }
484 }
485 }
486 baseview::MouseEvent::ButtonReleased { button, modifiers } => {
487 self.update_modifiers(modifiers);
488
489 if let Some(pos) = self.pointer_pos_in_points {
490 if let Some(button) = crate::translate::translate_mouse_button(*button) {
491 self.egui_input.events.push(egui::Event::PointerButton {
492 pos,
493 button,
494 pressed: false,
495 modifiers: self.egui_input.modifiers,
496 });
497 }
498 }
499 }
500 baseview::MouseEvent::WheelScrolled {
501 delta: scroll_delta,
502 modifiers,
503 } => {
504 self.update_modifiers(modifiers);
505
506 #[allow(unused_mut)]
507 let (unit, mut delta) = match scroll_delta {
508 baseview::ScrollDelta::Lines { x, y } => {
509 (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
510 }
511
512 baseview::ScrollDelta::Pixels { x, y } => (
513 egui::MouseWheelUnit::Point,
514 egui::vec2(*x, *y) * self.points_per_pixel,
515 ),
516 };
517
518 if cfg!(target_os = "macos") {
519 delta.x *= -1.0;
524 }
525
526 self.egui_input.events.push(egui::Event::MouseWheel {
527 unit,
528 delta,
529 modifiers: self.egui_input.modifiers,
530 phase: egui::TouchPhase::Move,
531 });
532 }
533 baseview::MouseEvent::CursorLeft => {
534 self.pointer_pos_in_points = None;
535 self.egui_input.events.push(egui::Event::PointerGone);
536 }
537 _ => {}
538 },
539 baseview::Event::Keyboard(event) => {
540 use keyboard_types::Code;
541
542 let pressed = event.state == keyboard_types::KeyState::Down;
543
544 match event.code {
545 Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed,
546 Code::ControlLeft | Code::ControlRight => {
547 self.egui_input.modifiers.ctrl = pressed;
548
549 #[cfg(not(target_os = "macos"))]
550 {
551 self.egui_input.modifiers.command = pressed;
552 }
553 }
554 Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed,
555 Code::MetaLeft | Code::MetaRight => {
556 #[cfg(target_os = "macos")]
557 {
558 self.egui_input.modifiers.mac_cmd = pressed;
559 self.egui_input.modifiers.command = pressed;
560 }
561 }
563 _ => (),
564 }
565
566 if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
567 self.egui_input.events.push(egui::Event::Key {
568 key,
569 physical_key: None,
570 pressed,
571 repeat: event.repeat,
572 modifiers: self.egui_input.modifiers,
573 });
574 }
575
576 if pressed {
577 if is_cut_command(self.egui_input.modifiers, event.code) {
582 self.egui_input.events.push(egui::Event::Cut);
583 } else if is_copy_command(self.egui_input.modifiers, event.code) {
584 self.egui_input.events.push(egui::Event::Copy);
585 } else if is_paste_command(self.egui_input.modifiers, event.code) {
586 if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
587 match clipboard_ctx.get_contents() {
588 Ok(contents) => {
589 self.egui_input.events.push(egui::Event::Text(contents))
590 }
591 Err(err) => {
592 error!("Paste error: {}", err);
593 }
594 }
595 }
596 } else if let keyboard_types::Key::Character(written) = &event.key {
597 if !self.egui_input.modifiers.ctrl && !self.egui_input.modifiers.command {
598 self.egui_input
599 .events
600 .push(egui::Event::Text(written.clone()));
601 }
602 }
603 }
604
605 match &self.key_capture {
606 KeyCapture::CaptureAll => {}
607 KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
608 KeyCapture::CaptureKeys(keys) => {
609 if !keys.contains(&event.key) {
610 return_status = EventStatus::Ignored
611 }
612 }
613 KeyCapture::IgnoreKeys(keys) => {
614 if keys.contains(&event.key) {
615 return_status = EventStatus::Ignored
616 }
617 }
618 }
619 }
620 baseview::Event::Window(event) => match event {
621 baseview::WindowEvent::Resized(window_info) => {
622 self.pixels_per_point = match self.scale_policy {
623 WindowScalePolicy::ScaleFactor(scale) => scale,
624 WindowScalePolicy::SystemScaleFactor => window_info.scale(),
625 } as f32;
626 self.points_per_pixel = self.pixels_per_point.recip();
627
628 self.physical_size = window_info.physical_size();
629
630 let screen_rect =
631 calculate_screen_rect(self.physical_size, self.points_per_pixel);
632
633 self.egui_input.screen_rect = Some(screen_rect);
634
635 let viewport_info = self
636 .egui_input
637 .viewports
638 .get_mut(&self.viewport_id)
639 .unwrap();
640 viewport_info.native_pixels_per_point = Some(self.pixels_per_point);
641 viewport_info.inner_rect = Some(screen_rect);
642
643 self.repaint_after = Some(Instant::now());
645 }
646 baseview::WindowEvent::Focused => {
647 self.egui_input
648 .events
649 .push(egui::Event::WindowFocused(true));
650 self.egui_input
651 .viewports
652 .get_mut(&self.viewport_id)
653 .unwrap()
654 .focused = Some(true);
655 }
656 baseview::WindowEvent::Unfocused => {
657 self.egui_input
658 .events
659 .push(egui::Event::WindowFocused(false));
660 self.egui_input
661 .viewports
662 .get_mut(&self.viewport_id)
663 .unwrap()
664 .focused = Some(false);
665 }
666 baseview::WindowEvent::WillClose => {}
667 },
668 }
669
670 match &event {
673 baseview::Event::Keyboard(_) => {
674 if return_status == EventStatus::Captured
675 && !self.egui_ctx.egui_wants_keyboard_input()
676 {
677 EventStatus::Ignored
678 } else {
679 return_status
680 }
681 }
682 baseview::Event::Mouse(_) => {
683 if self.egui_ctx.egui_is_using_pointer() || self.egui_ctx.egui_wants_pointer_input()
684 {
685 EventStatus::Captured
686 } else {
687 EventStatus::Ignored
688 }
689 }
690 baseview::Event::Window(_) => EventStatus::Captured,
691 }
692 }
693}
694
695fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
696 (modifiers.command && keycode == keyboard_types::Code::KeyX)
697 || (cfg!(target_os = "windows")
698 && modifiers.shift
699 && keycode == keyboard_types::Code::Delete)
700}
701
702fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
703 (modifiers.command && keycode == keyboard_types::Code::KeyC)
704 || (cfg!(target_os = "windows")
705 && modifiers.ctrl
706 && keycode == keyboard_types::Code::Insert)
707}
708
709fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
710 (modifiers.command && keycode == keyboard_types::Code::KeyV)
711 || (cfg!(target_os = "windows")
712 && modifiers.shift
713 && keycode == keyboard_types::Code::Insert)
714}
715
716fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect {
718 let logical_size = (
719 physical_size.width as f32 * points_per_pixel,
720 physical_size.height as f32 * points_per_pixel,
721 );
722 Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1))
723}