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 && let Err(err) = clipboard_ctx.set_contents(text)
421 {
422 error!("Copy/Cut error: {}", err);
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 window.set_mouse_cursor(cursor_icon);
442 }
443
444 #[cfg(feature = "keyboard_focus_workaround")]
447 {
448 if !full_output.platform_output.events.is_empty()
449 || full_output.platform_output.ime.is_some()
450 {
451 window.focus();
452 }
453 }
454 }
455
456 #[allow(unused_variables)]
457 fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus {
458 let mut return_status = EventStatus::Captured;
459
460 if matches!(
463 event,
464 Event::Mouse(baseview::MouseEvent::ButtonPressed { .. })
465 ) && !window.has_focus()
466 {
467 window.focus();
468 }
469
470 match &event {
471 baseview::Event::Mouse(event) => match event {
472 baseview::MouseEvent::CursorMoved {
473 position,
474 modifiers,
475 } => {
476 self.update_modifiers(modifiers);
477
478 let pos = pos2(position.x as f32, position.y as f32);
479 self.pointer_pos_in_points = Some(pos);
480 self.egui_input.events.push(egui::Event::PointerMoved(pos));
481 }
482 baseview::MouseEvent::ButtonPressed { button, modifiers } => {
483 self.update_modifiers(modifiers);
484
485 if let Some(pos) = self.pointer_pos_in_points
486 && let Some(button) = crate::translate::translate_mouse_button(*button)
487 {
488 self.egui_input.events.push(egui::Event::PointerButton {
489 pos,
490 button,
491 pressed: true,
492 modifiers: self.egui_input.modifiers,
493 });
494 }
495 }
496 baseview::MouseEvent::ButtonReleased { button, modifiers } => {
497 self.update_modifiers(modifiers);
498
499 if let Some(pos) = self.pointer_pos_in_points
500 && let Some(button) = crate::translate::translate_mouse_button(*button)
501 {
502 self.egui_input.events.push(egui::Event::PointerButton {
503 pos,
504 button,
505 pressed: false,
506 modifiers: self.egui_input.modifiers,
507 });
508 }
509 }
510 baseview::MouseEvent::WheelScrolled {
511 delta: scroll_delta,
512 modifiers,
513 } => {
514 self.update_modifiers(modifiers);
515
516 #[allow(unused_mut)]
517 let (unit, mut delta) = match scroll_delta {
518 baseview::ScrollDelta::Lines { x, y } => {
519 (egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
520 }
521
522 baseview::ScrollDelta::Pixels { x, y } => (
523 egui::MouseWheelUnit::Point,
524 egui::vec2(*x, *y) * self.points_per_pixel,
525 ),
526 };
527
528 if cfg!(target_os = "macos") {
529 delta.x *= -1.0;
534 }
535
536 self.egui_input.events.push(egui::Event::MouseWheel {
537 unit,
538 delta,
539 modifiers: self.egui_input.modifiers,
540 phase: egui::TouchPhase::Move,
541 });
542 }
543 baseview::MouseEvent::CursorLeft => {
544 self.pointer_pos_in_points = None;
545 self.egui_input.events.push(egui::Event::PointerGone);
546 }
547 _ => {}
548 },
549 baseview::Event::Keyboard(event) => {
550 use keyboard_types::Code;
551
552 let pressed = event.state == keyboard_types::KeyState::Down;
553
554 match event.code {
555 Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed,
556 Code::ControlLeft | Code::ControlRight => {
557 self.egui_input.modifiers.ctrl = pressed;
558
559 #[cfg(not(target_os = "macos"))]
560 {
561 self.egui_input.modifiers.command = pressed;
562 }
563 }
564 Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed,
565 Code::MetaLeft | Code::MetaRight => {
566 #[cfg(target_os = "macos")]
567 {
568 self.egui_input.modifiers.mac_cmd = pressed;
569 self.egui_input.modifiers.command = pressed;
570 }
571 }
573 _ => (),
574 }
575
576 if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
577 self.egui_input.events.push(egui::Event::Key {
578 key,
579 physical_key: None,
580 pressed,
581 repeat: event.repeat,
582 modifiers: self.egui_input.modifiers,
583 });
584 }
585
586 if pressed {
587 if is_cut_command(self.egui_input.modifiers, event.code) {
592 self.egui_input.events.push(egui::Event::Cut);
593 } else if is_copy_command(self.egui_input.modifiers, event.code) {
594 self.egui_input.events.push(egui::Event::Copy);
595 } else if is_paste_command(self.egui_input.modifiers, event.code) {
596 if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
597 match clipboard_ctx.get_contents() {
598 Ok(contents) => {
599 self.egui_input.events.push(egui::Event::Text(contents))
600 }
601 Err(err) => {
602 error!("Paste error: {}", err);
603 }
604 }
605 }
606 } else if let keyboard_types::Key::Character(written) = &event.key
607 && !self.egui_input.modifiers.ctrl
608 && !self.egui_input.modifiers.command
609 {
610 self.egui_input
611 .events
612 .push(egui::Event::Text(written.clone()));
613 }
614 }
615
616 match &self.key_capture {
617 KeyCapture::CaptureAll => {}
618 KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
619 KeyCapture::CaptureKeys(keys) => {
620 if !keys.contains(&event.key) {
621 return_status = EventStatus::Ignored
622 }
623 }
624 KeyCapture::IgnoreKeys(keys) => {
625 if keys.contains(&event.key) {
626 return_status = EventStatus::Ignored
627 }
628 }
629 }
630 }
631 baseview::Event::Window(event) => match event {
632 baseview::WindowEvent::Resized(window_info) => {
633 self.pixels_per_point = match self.scale_policy {
634 WindowScalePolicy::ScaleFactor(scale) => scale,
635 WindowScalePolicy::SystemScaleFactor => window_info.scale(),
636 } as f32;
637 self.points_per_pixel = self.pixels_per_point.recip();
638
639 self.physical_size = window_info.physical_size();
640
641 let screen_rect =
642 calculate_screen_rect(self.physical_size, self.points_per_pixel);
643
644 self.egui_input.screen_rect = Some(screen_rect);
645
646 let viewport_info = self
647 .egui_input
648 .viewports
649 .get_mut(&self.viewport_id)
650 .unwrap();
651 viewport_info.native_pixels_per_point = Some(self.pixels_per_point);
652 viewport_info.inner_rect = Some(screen_rect);
653
654 self.repaint_after = Some(Instant::now());
656 }
657 baseview::WindowEvent::Focused => {
658 self.egui_input
659 .events
660 .push(egui::Event::WindowFocused(true));
661 self.egui_input
662 .viewports
663 .get_mut(&self.viewport_id)
664 .unwrap()
665 .focused = Some(true);
666 }
667 baseview::WindowEvent::Unfocused => {
668 self.egui_input
669 .events
670 .push(egui::Event::WindowFocused(false));
671 self.egui_input
672 .viewports
673 .get_mut(&self.viewport_id)
674 .unwrap()
675 .focused = Some(false);
676 }
677 baseview::WindowEvent::WillClose => {}
678 },
679 }
680
681 match &event {
684 baseview::Event::Keyboard(_) => {
685 if return_status == EventStatus::Captured
686 && !self.egui_ctx.egui_wants_keyboard_input()
687 {
688 EventStatus::Ignored
689 } else {
690 return_status
691 }
692 }
693 baseview::Event::Mouse(_) => {
694 if self.egui_ctx.egui_is_using_pointer() || self.egui_ctx.egui_wants_pointer_input()
695 {
696 EventStatus::Captured
697 } else {
698 EventStatus::Ignored
699 }
700 }
701 baseview::Event::Window(_) => EventStatus::Captured,
702 }
703 }
704}
705
706fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
707 (modifiers.command && keycode == keyboard_types::Code::KeyX)
708 || (cfg!(target_os = "windows")
709 && modifiers.shift
710 && keycode == keyboard_types::Code::Delete)
711}
712
713fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
714 (modifiers.command && keycode == keyboard_types::Code::KeyC)
715 || (cfg!(target_os = "windows")
716 && modifiers.ctrl
717 && keycode == keyboard_types::Code::Insert)
718}
719
720fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
721 (modifiers.command && keycode == keyboard_types::Code::KeyV)
722 || (cfg!(target_os = "windows")
723 && modifiers.shift
724 && keycode == keyboard_types::Code::Insert)
725}
726
727fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect {
729 let logical_size = (
730 physical_size.width as f32 * points_per_pixel,
731 physical_size.height as f32 * points_per_pixel,
732 );
733 Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1))
734}