1use egui::{Key, Modifiers, MouseWheelUnit, PointerButton, Pos2, Rect};
16use sdl2::event::WindowEvent;
17use sdl2::keyboard::Keycode;
18use sdl2::keyboard::Mod;
19use sdl2::keyboard::Scancode;
20use sdl2::mouse::{Cursor, MouseButton, SystemCursor};
21use sdl2::video::Window;
22
23#[must_use]
24#[derive(Clone, Copy, Debug, Default)]
25pub struct EventResponse {
26 pub consumed: bool,
34
35 pub repaint: bool,
37}
38
39pub struct State {
43 egui_ctx: egui::Context,
44 egui_input: egui::RawInput,
45 start_time: std::time::Instant,
46 viewport_id: egui::ViewportId,
47 pointer_pos_in_points: Option<egui::Pos2>,
48 current_cursor: Option<CurrentCursor>,
49 clipboard: sdl2::clipboard::ClipboardUtil,
50 window_size: (u32, u32), }
52
53struct CurrentCursor {
57 icon: egui::CursorIcon,
58 cursor: Option<sdl2::mouse::Cursor>, }
60
61impl State {
62 pub fn new(window: &Window, egui_ctx: egui::Context, viewport_id: egui::ViewportId) -> Self {
63 let screen_rect = new_screen_rect(&egui_ctx, window);
64 let mut egui_input = egui::RawInput {
65 focused: false, screen_rect,
67 ..Default::default()
68 };
69 egui_input
70 .viewports
71 .entry(egui::ViewportId::ROOT)
72 .or_default()
73 .native_pixels_per_point = Some(native_pixels_per_point(window));
74 let clipboard = window.subsystem().clipboard();
75 let window_size = window.size();
76
77 State {
78 egui_ctx,
79 viewport_id,
80 clipboard,
81 start_time: std::time::Instant::now(),
82 egui_input,
83 pointer_pos_in_points: None,
84 current_cursor: None,
85 window_size,
86 }
87 }
88
89 #[inline]
90 pub fn get_window_size(&self) -> (u32, u32) {
91 self.window_size
92 }
93
94 #[inline]
95 pub fn get_pointer_pos_in_points(&self) -> Option<egui::Pos2> {
96 self.pointer_pos_in_points
97 }
98
99 #[inline]
100 pub fn set_theme(&mut self, theme: egui::Theme) {
101 self.egui_input.system_theme.replace(theme);
102 }
103
104 #[inline]
112 pub fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
113 for command in &platform_output.commands {
114 match command {
115 egui::OutputCommand::CopyText(text) => {
116 let result = self.clipboard.set_clipboard_text(text);
117
118 if result.is_err() {
119 log::warn!("Failed to set copied text to clipboard");
120 }
121 }
122 egui::OutputCommand::CopyImage(_color_image) => {
123 log::warn!("CopyImage is not supported")
124 }
125 egui::OutputCommand::OpenUrl(_url) => {
126 #[cfg(feature = "links")]
127 if let Err(err) = webbrowser::open(&_url.url) {
128 log::warn!("Failed to open url: {}", err);
129 }
130
131 #[cfg(not(feature = "links"))]
132 {
133 log::warn!("Cannot open url - feature \"links\" not enabled.");
134 }
135 }
136 }
137 }
138
139 self.set_cursor_icon(platform_output.cursor_icon);
140 }
141
142 #[inline]
148 pub fn take_egui_input(&mut self) -> egui::RawInput {
149 self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
150 self.egui_input.viewport_id = self.viewport_id;
152
153 self.egui_input.take()
154 }
155
156 pub fn on_event(
160 &mut self,
161 window: &sdl2::video::Window,
162 event: &sdl2::event::Event,
163 ) -> EventResponse {
164 use sdl2::event::Event::*;
165 match event {
166 Window { win_event, .. } => self.on_window_event(*win_event, window),
167 MouseButtonDown {
168 mouse_btn, x, y, ..
169 } => self.on_mouse_button_event(window, *mouse_btn, true, *x, *y),
170 MouseButtonUp {
171 mouse_btn, x, y, ..
172 } => self.on_mouse_button_event(window, *mouse_btn, false, *x, *y),
173 MouseMotion { x, y, .. } => {
174 let pos = poiner_pos_in_points(&self.egui_ctx, window, *x as f32, *y as f32);
175 self.pointer_pos_in_points = Some(pos);
176 self.egui_input.events.push(egui::Event::PointerMoved(pos));
177 EventResponse {
178 repaint: true,
179 consumed: self.egui_ctx.is_using_pointer(),
180 }
181 }
182 MouseWheel { x, y, .. } => {
183 let dx = *x as f32;
184 let dy = *y as f32;
185
186 if self.egui_input.modifiers.command {
187 let delta = (dy / 125.0).exp();
189 self.egui_input.events.push(egui::Event::Zoom(delta));
190 } else if self.egui_input.modifiers.shift {
191 self.egui_input.events.push(egui::Event::MouseWheel {
193 unit: MouseWheelUnit::Line,
194 delta: egui::vec2(dx + dy, 0.0),
195 modifiers: self.egui_input.modifiers,
196 });
197 } else {
198 self.egui_input.events.push(egui::Event::MouseWheel {
200 unit: MouseWheelUnit::Line,
201 delta: egui::vec2(dx, dy),
202 modifiers: self.egui_input.modifiers,
203 });
204 }
205 EventResponse {
206 repaint: true,
207 consumed: self.egui_ctx.wants_pointer_input(),
208 }
209 }
210 KeyUp {
211 keycode: Some(kc),
212 scancode: Some(sc),
213 keymod,
214 repeat,
215 ..
216 } => self.on_keyboard_event(*kc, *sc, *keymod, false, *repeat),
217 KeyDown {
218 keycode: Some(kc),
219 scancode: Some(sc),
220 keymod,
221 repeat,
222 ..
223 } => {
224 let resp = self.on_keyboard_event(*kc, *sc, *keymod, true, *repeat);
225
226 if self.egui_input.modifiers.command && *kc == Keycode::C {
227 self.egui_input.events.push(egui::Event::Copy);
228 } else if self.egui_input.modifiers.command && *kc == Keycode::X {
229 self.egui_input.events.push(egui::Event::Cut);
230 } else if self.egui_input.modifiers.command && *kc == Keycode::V {
231 if let Ok(contents) = self.clipboard.clipboard_text() {
232 self.egui_input.events.push(egui::Event::Text(contents));
233 }
234 }
235
236 resp
237 }
238 TextInput { text, .. } => {
239 let mut resp = EventResponse {
240 consumed: true,
241 repaint: false,
242 };
243 if !text.is_empty() {
244 let is_cmd = self.egui_input.modifiers.ctrl
247 || self.egui_input.modifiers.command
248 || self.egui_input.modifiers.mac_cmd;
249
250 if !is_cmd {
251 self.egui_input
252 .events
253 .push(egui::Event::Text(text.to_owned()));
254
255 resp.repaint = true;
256 }
257 }
258
259 resp
260 }
261 DropFile { filename, .. } => {
262 self.egui_input.dropped_files.push(egui::DroppedFile {
263 path: Some(std::path::PathBuf::from(filename)),
264 ..Default::default()
265 });
266 EventResponse {
267 repaint: true,
268 consumed: false,
269 }
270 }
271 FingerDown {
272 touch_id,
273 finger_id,
274 x,
275 y,
276 pressure,
277 ..
278 } => self.on_touch(
279 window,
280 TouchInfo {
281 phase: egui::TouchPhase::Start,
282 touch_id: *touch_id,
283 finger_id: *finger_id,
284 x: *x,
285 y: *y,
286 pressure: *pressure,
287 },
288 ),
289 FingerUp {
290 touch_id,
291 finger_id,
292 x,
293 y,
294 pressure,
295 ..
296 } => self.on_touch(
297 window,
298 TouchInfo {
299 phase: egui::TouchPhase::End,
300 touch_id: *touch_id,
301 finger_id: *finger_id,
302 x: *x,
303 y: *y,
304 pressure: *pressure,
305 },
306 ),
307 FingerMotion {
308 touch_id,
309 finger_id,
310 x,
311 y,
312 pressure,
313 ..
314 } => self.on_touch(
315 window,
316 TouchInfo {
317 phase: egui::TouchPhase::Move,
318 touch_id: *touch_id,
319 finger_id: *finger_id,
320 x: *x,
321 y: *y,
322 pressure: *pressure,
323 },
324 ),
325 _ => EventResponse::default(),
326 }
327 }
328
329 #[inline]
330 fn on_touch(&mut self, window: &Window, info: TouchInfo) -> EventResponse {
331 let consumed = match info.phase {
332 egui::TouchPhase::Start | egui::TouchPhase::End | egui::TouchPhase::Cancel => {
333 self.egui_ctx.wants_pointer_input()
334 }
335 egui::TouchPhase::Move => self.egui_ctx.is_using_pointer(),
336 };
337
338 let pos = poiner_pos_in_points(&self.egui_ctx, window, info.x, info.y);
339 self.pointer_pos_in_points = Some(pos);
340 self.egui_input.events.push(egui::Event::Touch {
341 device_id: egui::TouchDeviceId(info.touch_id as u64),
342 id: egui::TouchId::from(info.finger_id as u64),
343 phase: info.phase,
344 pos,
345 force: Some(info.pressure),
346 });
347
348 EventResponse {
349 repaint: true,
350 consumed,
351 }
352 }
353
354 fn on_window_event(&mut self, event: WindowEvent, window: &Window) -> EventResponse {
355 match event {
356 WindowEvent::Minimized
357 | WindowEvent::Maximized
358 | WindowEvent::Resized(_, _)
359 | WindowEvent::SizeChanged(_, _) => {
360 self.on_size_chage(window);
361 EventResponse {
362 repaint: true,
363 consumed: false,
364 }
365 }
366 WindowEvent::Shown
367 | WindowEvent::Hidden
368 | WindowEvent::Exposed
369 | WindowEvent::Moved(_, _)
370 | WindowEvent::Restored
371 | WindowEvent::Enter
372 | WindowEvent::Close => EventResponse {
373 consumed: false,
374 repaint: true,
375 },
376 WindowEvent::Leave => {
377 self.pointer_pos_in_points = None;
378 self.egui_input.events.push(egui::Event::PointerGone);
379 EventResponse {
380 repaint: true,
381 consumed: false,
382 }
383 }
384 WindowEvent::TakeFocus | WindowEvent::FocusGained => {
385 self.egui_input.focused = true;
386 self.egui_input
387 .events
388 .push(egui::Event::WindowFocused(true));
389 EventResponse {
390 repaint: true,
391 consumed: false,
392 }
393 }
394 WindowEvent::FocusLost => {
395 self.egui_input.focused = false;
396 self.egui_input
397 .events
398 .push(egui::Event::WindowFocused(false));
399 EventResponse {
400 repaint: true,
401 consumed: false,
402 }
403 }
404 WindowEvent::HitTest
405 | WindowEvent::ICCProfChanged
406 | WindowEvent::DisplayChanged(_)
407 | WindowEvent::None => EventResponse::default(),
408 }
409 }
410
411 fn on_mouse_button_event(
412 &mut self,
413 window: &Window,
414 button: MouseButton,
415 pressed: bool,
416 x: i32,
417 y: i32,
418 ) -> EventResponse {
419 let Some(button) = into_egui_button(button) else {
420 return EventResponse::default();
421 };
422
423 let pos = poiner_pos_in_points(&self.egui_ctx, window, x as f32, y as f32);
424 self.pointer_pos_in_points = Some(pos);
425 self.egui_input.events.push(egui::Event::PointerButton {
426 pos,
427 button,
428 pressed,
429 modifiers: self.egui_input.modifiers,
430 });
431 EventResponse {
432 repaint: true,
433 consumed: self.egui_ctx.wants_pointer_input(),
434 }
435 }
436
437 fn on_keyboard_event(
438 &mut self,
439 keycode: Keycode,
440 scancode: Scancode,
441 keymod: Mod,
442 pressed: bool,
443 repeat: bool,
444 ) -> EventResponse {
445 let Some(key) = into_egui_key(keycode) else {
446 return EventResponse::default();
447 };
448
449 self.egui_input.modifiers = into_egui_modifiers(keymod);
450 self.egui_input.events.push(egui::Event::Key {
451 key,
452 physical_key: into_egui_physical_key(scancode),
453 pressed,
454 repeat,
455 modifiers: self.egui_input.modifiers,
456 });
457 let consumed = self.egui_ctx.wants_keyboard_input() || key == Key::Tab;
459 EventResponse {
460 repaint: true,
461 consumed,
462 }
463 }
464
465 #[inline]
466 fn on_size_chage(&mut self, window: &Window) {
467 self.window_size = window.size();
468 self.egui_input.screen_rect = new_screen_rect(&self.egui_ctx, window);
469 self.egui_input
470 .viewports
471 .entry(self.viewport_id)
472 .or_default()
473 .native_pixels_per_point = Some(native_pixels_per_point(window));
474 }
475
476 #[inline]
477 fn set_cursor_icon(&mut self, cursor_icon: egui::CursorIcon) {
478 if let Some(cursor) = &self.current_cursor {
479 if cursor.icon == cursor_icon {
480 return;
481 }
482 }
483
484 if self.pointer_pos_in_points.is_some() {
485 let system_cursor = into_sdl2_cursor(cursor_icon);
486 let mut current_cursor = CurrentCursor {
487 icon: cursor_icon,
488 cursor: None,
489 };
490
491 match Cursor::from_system(system_cursor) {
492 Ok(cursor) => {
493 cursor.set();
494 current_cursor.cursor = Some(cursor);
495 }
496 Err(e) => {
497 log::warn!("Failed to set cursor: {e}")
498 }
499 }
500 self.current_cursor.replace(current_cursor);
501 } else {
502 self.current_cursor = None;
503 }
504 }
505}
506
507#[inline]
508pub fn poiner_pos_in_points(
509 egui_ctx: &egui::Context,
510 window: &Window,
511 x: f32,
512 y: f32,
513) -> egui::Pos2 {
514 let pixels_per_point = pixels_per_point(egui_ctx, window);
515 egui::pos2(x, y) / pixels_per_point
516}
517
518#[inline]
519pub fn into_egui_modifiers(m: Mod) -> Modifiers {
520 let mut mods = Modifiers::NONE;
521
522 if m.intersects(Mod::LCTRLMOD | Mod::RCTRLMOD) {
523 mods.ctrl = true;
524 mods.command = true;
525 }
526
527 if m.intersects(Mod::LSHIFTMOD | Mod::RSHIFTMOD) {
528 mods.shift = true;
529 }
530
531 if m.intersects(Mod::LALTMOD | Mod::RALTMOD) {
532 mods.alt = true;
533 }
534
535 if m.intersects(Mod::LGUIMOD | Mod::RGUIMOD) {
536 mods.mac_cmd = true;
537 mods.command = true;
538 }
539
540 mods
541}
542
543#[inline]
544fn into_sdl2_cursor(cursor_icon: egui::CursorIcon) -> SystemCursor {
545 match cursor_icon {
546 egui::CursorIcon::Crosshair => SystemCursor::Crosshair,
547 egui::CursorIcon::Default => SystemCursor::Arrow,
548 egui::CursorIcon::Grab => SystemCursor::Hand,
549 egui::CursorIcon::Grabbing => SystemCursor::SizeAll,
550 egui::CursorIcon::Move => SystemCursor::SizeAll,
551 egui::CursorIcon::PointingHand => SystemCursor::Hand,
552 egui::CursorIcon::ResizeHorizontal => SystemCursor::SizeWE,
553 egui::CursorIcon::ResizeNeSw => SystemCursor::SizeNESW,
554 egui::CursorIcon::ResizeNwSe => SystemCursor::SizeNWSE,
555 egui::CursorIcon::ResizeVertical => SystemCursor::SizeNS,
556 egui::CursorIcon::Text => SystemCursor::IBeam,
557 egui::CursorIcon::NotAllowed | egui::CursorIcon::NoDrop => SystemCursor::No,
558 egui::CursorIcon::Wait => SystemCursor::Wait,
559 _ => SystemCursor::Arrow,
561 }
562}
563
564#[inline]
565pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
566 let (width, height) = window.drawable_size();
567 egui::vec2(width as f32, height as f32)
568}
569
570#[inline]
571pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 {
572 let native_pixels_per_point = native_pixels_per_point(window);
573 let egui_zoom_factor = egui_ctx.zoom_factor();
574 egui_zoom_factor * native_pixels_per_point
575}
576
577#[inline]
578fn new_screen_rect(egui_ctx: &egui::Context, window: &Window) -> Option<Rect> {
579 let screen_size_in_pixels = screen_size_in_pixels(window);
580 let screen_size_in_points = screen_size_in_pixels / pixels_per_point(egui_ctx, window);
581
582 (screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0)
583 .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points))
584}
585
586#[inline]
587pub fn native_pixels_per_point(window: &Window) -> f32 {
588 let (win_w, win_h) = window.size();
589 let (draw_w, _draw_h) = window.drawable_size();
590
591 if win_w > 0 && win_h > 0 {
592 draw_w as f32 / win_w as f32
593 } else {
594 1.0
595 }
596}
597
598#[inline]
599pub fn into_egui_button(btn: MouseButton) -> Option<PointerButton> {
600 match btn {
601 MouseButton::Left => Some(egui::PointerButton::Primary),
602 MouseButton::Middle => Some(egui::PointerButton::Middle),
603 MouseButton::Right => Some(egui::PointerButton::Secondary),
604 MouseButton::Unknown => None,
605 MouseButton::X1 => Some(egui::PointerButton::Extra1),
606 MouseButton::X2 => Some(egui::PointerButton::Extra2),
607 }
608}
609
610pub fn into_egui_key(key: Keycode) -> Option<Key> {
611 Some(match key {
612 Keycode::Left => Key::ArrowLeft,
613 Keycode::Up => Key::ArrowUp,
614 Keycode::Right => Key::ArrowRight,
615 Keycode::Down => Key::ArrowDown,
616
617 Keycode::Escape => Key::Escape,
618 Keycode::Tab => Key::Tab,
619 Keycode::Backspace => Key::Backspace,
620 Keycode::Space => Key::Space,
621 Keycode::Return => Key::Enter,
622
623 Keycode::Insert => Key::Insert,
624 Keycode::Home => Key::Home,
625 Keycode::Delete => Key::Delete,
626 Keycode::End => Key::End,
627 Keycode::PageDown => Key::PageDown,
628 Keycode::PageUp => Key::PageUp,
629
630 Keycode::Kp0 | Keycode::Num0 => Key::Num0,
631 Keycode::Kp1 | Keycode::Num1 => Key::Num1,
632 Keycode::Kp2 | Keycode::Num2 => Key::Num2,
633 Keycode::Kp3 | Keycode::Num3 => Key::Num3,
634 Keycode::Kp4 | Keycode::Num4 => Key::Num4,
635 Keycode::Kp5 | Keycode::Num5 => Key::Num5,
636 Keycode::Kp6 | Keycode::Num6 => Key::Num6,
637 Keycode::Kp7 | Keycode::Num7 => Key::Num7,
638 Keycode::Kp8 | Keycode::Num8 => Key::Num8,
639 Keycode::Kp9 | Keycode::Num9 => Key::Num9,
640
641 Keycode::A => Key::A,
642 Keycode::B => Key::B,
643 Keycode::C => Key::C,
644 Keycode::D => Key::D,
645 Keycode::E => Key::E,
646 Keycode::F => Key::F,
647 Keycode::G => Key::G,
648 Keycode::H => Key::H,
649 Keycode::I => Key::I,
650 Keycode::J => Key::J,
651 Keycode::K => Key::K,
652 Keycode::L => Key::L,
653 Keycode::M => Key::M,
654 Keycode::N => Key::N,
655 Keycode::O => Key::O,
656 Keycode::P => Key::P,
657 Keycode::Q => Key::Q,
658 Keycode::R => Key::R,
659 Keycode::S => Key::S,
660 Keycode::T => Key::T,
661 Keycode::U => Key::U,
662 Keycode::V => Key::V,
663 Keycode::W => Key::W,
664 Keycode::X => Key::X,
665 Keycode::Y => Key::Y,
666 Keycode::Z => Key::Z,
667
668 Keycode::F1 => Key::F1,
669 Keycode::F2 => Key::F2,
670 Keycode::F3 => Key::F3,
671 Keycode::F4 => Key::F4,
672 Keycode::F5 => Key::F5,
673 Keycode::F6 => Key::F6,
674 Keycode::F7 => Key::F7,
675 Keycode::F8 => Key::F8,
676 Keycode::F9 => Key::F9,
677 Keycode::F10 => Key::F10,
678 Keycode::F11 => Key::F11,
679 Keycode::F12 => Key::F12,
680
681 Keycode::Minus => Key::Minus,
682 Keycode::Equals => Key::Equals,
683 Keycode::Semicolon => Key::Semicolon,
684 Keycode::Comma => Key::Comma,
685 Keycode::Period => Key::Period,
686 Keycode::Slash => Key::Slash,
687 Keycode::Backslash => Key::Backslash,
688
689 _ => {
690 return None;
691 }
692 })
693}
694
695pub fn into_egui_physical_key(scancode: Scancode) -> Option<Key> {
696 match scancode {
697 Scancode::A => Some(Key::A),
698 Scancode::B => Some(Key::B),
699 Scancode::C => Some(Key::C),
700 Scancode::D => Some(Key::D),
701 Scancode::E => Some(Key::E),
702 Scancode::F => Some(Key::F),
703 Scancode::G => Some(Key::G),
704 Scancode::H => Some(Key::H),
705 Scancode::I => Some(Key::I),
706 Scancode::J => Some(Key::J),
707 Scancode::K => Some(Key::K),
708 Scancode::L => Some(Key::L),
709 Scancode::M => Some(Key::M),
710 Scancode::N => Some(Key::N),
711 Scancode::O => Some(Key::O),
712 Scancode::P => Some(Key::P),
713 Scancode::Q => Some(Key::Q),
714 Scancode::R => Some(Key::R),
715 Scancode::S => Some(Key::S),
716 Scancode::T => Some(Key::T),
717 Scancode::U => Some(Key::U),
718 Scancode::V => Some(Key::V),
719 Scancode::W => Some(Key::W),
720 Scancode::X => Some(Key::X),
721 Scancode::Y => Some(Key::Y),
722 Scancode::Z => Some(Key::Z),
723
724 Scancode::Num0 => Some(Key::Num0),
725 Scancode::Num1 => Some(Key::Num1),
726 Scancode::Num2 => Some(Key::Num2),
727 Scancode::Num3 => Some(Key::Num3),
728 Scancode::Num4 => Some(Key::Num4),
729 Scancode::Num5 => Some(Key::Num5),
730 Scancode::Num6 => Some(Key::Num6),
731 Scancode::Num7 => Some(Key::Num7),
732 Scancode::Num8 => Some(Key::Num8),
733 Scancode::Num9 => Some(Key::Num9),
734
735 Scancode::F1 => Some(Key::F1),
736 Scancode::F2 => Some(Key::F2),
737 Scancode::F3 => Some(Key::F3),
738 Scancode::F4 => Some(Key::F4),
739 Scancode::F5 => Some(Key::F5),
740 Scancode::F6 => Some(Key::F6),
741 Scancode::F7 => Some(Key::F7),
742 Scancode::F8 => Some(Key::F8),
743 Scancode::F9 => Some(Key::F9),
744 Scancode::F10 => Some(Key::F10),
745 Scancode::F11 => Some(Key::F11),
746 Scancode::F12 => Some(Key::F12),
747
748 Scancode::Up => Some(Key::ArrowUp),
749 Scancode::Down => Some(Key::ArrowDown),
750 Scancode::Left => Some(Key::ArrowLeft),
751 Scancode::Right => Some(Key::ArrowRight),
752
753 Scancode::Return => Some(Key::Enter),
754 Scancode::Escape => Some(Key::Escape),
755 Scancode::Backspace => Some(Key::Backspace),
756 Scancode::Tab => Some(Key::Tab),
757 Scancode::Space => Some(Key::Space),
758
759 _ => None,
760 }
761}
762
763struct TouchInfo {
764 phase: egui::TouchPhase,
765 touch_id: i64,
766 finger_id: i64,
767 x: f32,
768 y: f32,
769 pressure: f32,
770}