1use super::translate::{
11 physical_pos_from, pixel_to_cell, translate_key, translate_modifiers, translate_mouse_button,
12};
13use crate::backend::WindowBackend;
14use crate::presenter::Presenter;
15use retroglyph_core::Terminal;
16use retroglyph_core::backend::Backend;
17use retroglyph_core::event::{Event, KeyModifiers, MouseEvent, MouseEventKind, PhysicalPos};
18use std::sync::Arc;
19#[cfg(not(target_arch = "wasm32"))]
20use std::time::Duration;
21use winit::application::ApplicationHandler;
22use winit::event::WindowEvent;
23use winit::event_loop::{ActiveEventLoop, EventLoop};
24use winit::window::{Window, WindowId};
25
26pub struct WindowConfig {
32 pub title: String,
34 pub width: u32,
36 pub height: u32,
38 pub target_fps: Option<u32>,
41 pub fill_viewport: bool,
53}
54
55impl WindowConfig {
56 #[must_use]
63 pub fn fit<P: Presenter>(
64 presenter: &P,
65 title: impl Into<String>,
66 target_fps: Option<u32>,
67 ) -> Self {
68 let grid = presenter.size();
69 let (cell_w, cell_h) = presenter.cell_size();
70 Self {
71 title: title.into(),
72 width: u32::from(grid.width) * cell_w,
73 height: u32::from(grid.height) * cell_h,
74 target_fps,
75 fill_viewport: false,
76 }
77 }
78}
79
80pub fn run_windowed<P, F>(
94 config: WindowConfig,
95 presenter: P,
96 app_loop: F,
97) -> Result<(), winit::error::EventLoopError>
98where
99 P: Presenter + 'static,
100 F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
101{
102 let terminal = Terminal::new(WindowBackend::new(presenter));
103 let event_loop = EventLoop::new()?;
104
105 #[cfg(not(target_arch = "wasm32"))]
106 let frame_interval = config
107 .target_fps
108 .map(|fps| Duration::from_secs_f64(1.0 / f64::from(fps)));
109
110 let app = WindowApp {
111 terminal: Some(terminal),
112 app_loop,
113 window: None,
114 title: config.title,
115 init_size: InitWindowSize {
116 width: config.width,
117 height: config.height,
118 },
119 #[cfg(target_arch = "wasm32")]
120 fill_viewport: config.fill_viewport,
121 current_modifiers: KeyModifiers::NONE,
122 cursor_px: (0.0, 0.0),
123 active_touch: None,
124 #[cfg(not(target_arch = "wasm32"))]
125 frame_interval,
126 #[cfg(not(target_arch = "wasm32"))]
127 next_frame: std::time::Instant::now(),
128 };
129
130 #[cfg(not(target_arch = "wasm32"))]
131 {
132 let mut app = app;
133 event_loop.run_app(&mut app)
134 }
135
136 #[cfg(target_arch = "wasm32")]
137 {
138 use winit::platform::web::EventLoopExtWebSys;
139 event_loop.spawn_app(app);
140 Ok(())
141 }
142}
143
144pub fn run_app<P, A>(
164 config: WindowConfig,
165 presenter: P,
166 mut app: A,
167) -> Result<(), winit::error::EventLoopError>
168where
169 P: Presenter + 'static,
170 A: retroglyph_core::App<WindowBackend<P>> + 'static,
171{
172 let mut frame_count = 0u64;
173 let mut last = web_time::Instant::now();
174 run_windowed(config, presenter, move |term| {
175 let now = web_time::Instant::now();
176 let delta = now.duration_since(last);
177 last = now;
178 let frame = retroglyph_core::Frame {
179 delta,
180 frame: frame_count,
181 };
182 frame_count = frame_count.wrapping_add(1);
183 if retroglyph_core::step(term, &mut app, &frame) == retroglyph_core::Flow::Exit {
184 #[cfg(not(target_arch = "wasm32"))]
185 std::process::exit(0);
186 }
187 })
188}
189
190struct InitWindowSize {
192 width: u32,
193 height: u32,
194}
195
196struct WindowApp<P: Presenter, F> {
199 terminal: Option<Terminal<WindowBackend<P>>>,
200 app_loop: F,
201 window: Option<Arc<Window>>,
202 title: String,
203 init_size: InitWindowSize,
204 #[cfg(target_arch = "wasm32")]
207 fill_viewport: bool,
208 current_modifiers: KeyModifiers,
210 cursor_px: (f64, f64),
212 active_touch: Option<u64>,
221 #[cfg(not(target_arch = "wasm32"))]
223 frame_interval: Option<Duration>,
224 #[cfg(not(target_arch = "wasm32"))]
226 next_frame: std::time::Instant,
227}
228
229impl<P: Presenter, F> WindowApp<P, F> {
230 fn create_window_and_surface(&mut self, event_loop: &ActiveEventLoop) -> Option<Arc<Window>> {
234 #[cfg(not(target_arch = "wasm32"))]
260 let physical_size =
261 winit::dpi::PhysicalSize::new(self.init_size.width, self.init_size.height);
262 #[cfg(target_arch = "wasm32")]
263 let physical_size = if self.fill_viewport {
264 web_viewport_layout_physical_size().unwrap_or_else(|| {
265 winit::dpi::PhysicalSize::new(self.init_size.width, self.init_size.height)
266 })
267 } else {
268 winit::dpi::PhysicalSize::new(self.init_size.width, self.init_size.height)
269 };
270 #[cfg(target_arch = "wasm32")]
271 let surface_physical_size = if self.fill_viewport {
272 web_viewport_surface_physical_size().unwrap_or(physical_size)
273 } else {
274 physical_size
275 };
276 #[cfg(not(target_arch = "wasm32"))]
277 let surface_physical_size = physical_size;
278
279 let attrs = Window::default_attributes()
280 .with_title(&self.title)
281 .with_inner_size(physical_size);
282
283 #[cfg(target_family = "wasm")]
284 let attrs = {
285 use winit::platform::web::WindowAttributesExtWebSys;
286 attrs.with_append(true)
287 };
288
289 let window = Arc::new(match event_loop.create_window(attrs) {
290 Ok(w) => w,
291 Err(e) => {
292 log::error!("window creation failed: {e}");
293 event_loop.exit();
294 return None;
295 }
296 });
297
298 if let Some(term) = self.terminal.as_mut() {
299 let handle: Arc<dyn crate::presenter::WindowHandle> = window.clone();
302 if let Err(e) = term.backend_mut().presenter_mut().init_surface(handle) {
303 log::error!("surface init failed: {e}");
304 event_loop.exit();
305 return None;
306 }
307 term.backend_mut()
313 .presenter_mut()
314 .resize_surface(surface_physical_size.width, surface_physical_size.height);
315 }
316
317 #[cfg(target_arch = "wasm32")]
326 if self.fill_viewport {
327 install_viewport_resize_listener(&window);
328 }
329
330 if let Some(theme) = window.theme()
338 && let Some(term) = self.terminal.as_mut()
339 {
340 term.backend_mut().push_event(system_theme_event(theme));
341 }
342
343 Some(window)
344 }
345}
346
347const fn system_theme_event(theme: winit::window::Theme) -> Event {
350 use retroglyph_core::event::SystemTheme;
351 match theme {
352 winit::window::Theme::Light => Event::ThemeChanged(SystemTheme::Light),
353 winit::window::Theme::Dark => Event::ThemeChanged(SystemTheme::Dark),
354 }
355}
356
357#[cfg(target_arch = "wasm32")]
362const MAX_DEVICE_PIXEL_RATIO: f64 = 1.5;
363
364#[cfg(target_arch = "wasm32")]
367fn web_viewport_css_size() -> Option<(f64, f64)> {
368 let window = web_sys::window()?;
369 let width = window.inner_width().ok()?.as_f64()?;
370 let height = window.inner_height().ok()?.as_f64()?;
371 Some((width, height))
372}
373
374#[cfg(target_arch = "wasm32")]
385#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
386fn web_viewport_layout_physical_size() -> Option<winit::dpi::PhysicalSize<u32>> {
387 let (width, height) = web_viewport_css_size()?;
388 let dpr = web_sys::window()?.device_pixel_ratio();
389 Some(winit::dpi::PhysicalSize::new(
390 (width * dpr).round() as u32,
391 (height * dpr).round() as u32,
392 ))
393}
394
395#[cfg(any(target_arch = "wasm32", test))]
410fn dpr_pointer_scale(real_dpr: f64, capped_dpr: f64) -> f64 {
411 (capped_dpr / real_dpr).min(1.0)
412}
413
414#[cfg(target_arch = "wasm32")]
417fn wasm_pointer_scale() -> f64 {
418 web_sys::window().map_or(1.0, |w| {
419 dpr_pointer_scale(w.device_pixel_ratio(), MAX_DEVICE_PIXEL_RATIO)
420 })
421}
422
423#[cfg(target_arch = "wasm32")]
433#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
434fn web_viewport_surface_physical_size() -> Option<winit::dpi::PhysicalSize<u32>> {
435 let (width, height) = web_viewport_css_size()?;
436 let dpr = web_sys::window()?
437 .device_pixel_ratio()
438 .min(MAX_DEVICE_PIXEL_RATIO);
439 Some(winit::dpi::PhysicalSize::new(
440 (width * dpr).round() as u32,
441 (height * dpr).round() as u32,
442 ))
443}
444
445#[cfg(target_arch = "wasm32")]
449fn install_viewport_resize_listener(window: &Arc<Window>) {
450 use wasm_bindgen::JsCast;
451 use wasm_bindgen::prelude::Closure;
452
453 let Some(web_window) = web_sys::window() else {
454 return;
455 };
456 let window = window.clone();
457 let closure = Closure::<dyn FnMut()>::new(move || {
458 if let Some(size) = web_viewport_layout_physical_size() {
462 let _ = window.request_inner_size(size);
463 }
464 });
465 if web_window
466 .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref())
467 .is_ok()
468 {
469 closure.forget();
473 }
474}
475
476impl<P, F> ApplicationHandler for WindowApp<P, F>
477where
478 P: Presenter,
479 F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
480{
481 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
482 if let Some(window) = self.create_window_and_surface(event_loop) {
483 self.window = Some(window);
484 }
485 }
486
487 fn window_event(
488 &mut self,
489 _event_loop: &ActiveEventLoop,
490 _window_id: WindowId,
491 event: WindowEvent,
492 ) {
493 self.handle_window_event(event);
494 }
495
496 fn about_to_wait(
497 &mut self,
498 #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] event_loop: &ActiveEventLoop,
499 ) {
500 #[cfg(not(target_arch = "wasm32"))]
501 if let Some(interval) = self.frame_interval {
502 let now = std::time::Instant::now();
504 if self.next_frame > now {
505 event_loop
506 .set_control_flow(winit::event_loop::ControlFlow::WaitUntil(self.next_frame));
507 return;
508 }
509 self.next_frame = (self.next_frame + interval).max(now);
512 }
513 if let Some(window) = &self.window {
514 window.request_redraw();
515 }
516 }
517}
518
519impl<P, F> WindowApp<P, F>
520where
521 P: Presenter,
522 F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
523{
524 fn handle_window_event(&mut self, event: WindowEvent) {
530 match event {
531 WindowEvent::CloseRequested => {
532 if let Some(term) = self.terminal.as_mut() {
536 term.backend_mut().push_event(Event::Close);
537 }
538 }
539 WindowEvent::Resized(size) => self.on_resized(size),
540 WindowEvent::CursorMoved { position, .. } => self.on_cursor_moved(position),
541 WindowEvent::MouseInput { state, button, .. } => self.on_mouse_input(state, button),
542 WindowEvent::MouseWheel { delta, .. } => self.on_mouse_wheel(delta),
543 WindowEvent::Touch(touch) => self.on_touch(touch),
544 WindowEvent::ModifiersChanged(mods) => {
545 self.current_modifiers = translate_modifiers(mods.state());
546 }
547 WindowEvent::ThemeChanged(theme) => {
548 if let Some(term) = self.terminal.as_mut() {
549 term.backend_mut().push_event(system_theme_event(theme));
550 }
551 }
552 WindowEvent::KeyboardInput { event, .. } => {
553 if let Some(term) = self.terminal.as_mut()
554 && let Some(e) = translate_key(event, self.current_modifiers)
555 {
556 term.backend_mut().push_event(e);
557 }
558 }
559
560 WindowEvent::RedrawRequested => {
561 let Some(term) = self.terminal.as_mut() else {
562 return;
563 };
564 (self.app_loop)(term);
565 if let Err(e) = term.backend_mut().presenter_mut().present() {
566 log::error!("frame present failed: {e}");
567 }
568 }
569
570 _ => {}
571 }
572 }
573
574 fn on_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
575 let Some(term) = self.terminal.as_mut() else {
576 return;
577 };
578 #[cfg(target_arch = "wasm32")]
586 let size = if self.fill_viewport {
587 web_viewport_surface_physical_size().unwrap_or(size)
588 } else {
589 size
590 };
591 let (cell_w, cell_h) = term.backend().presenter().cell_size();
592 let cols = size.width / cell_w;
593 let rows = size.height / cell_h;
594 term.backend_mut()
595 .presenter_mut()
596 .resize_surface(cols * cell_w, rows * cell_h);
597 #[allow(clippy::cast_possible_truncation)]
598 term.backend_mut()
599 .push_event(Event::Resize(cols.max(1) as u16, rows.max(1) as u16));
600 }
601
602 fn on_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
603 #[cfg(target_arch = "wasm32")]
610 let scale = wasm_pointer_scale();
611 #[cfg(not(target_arch = "wasm32"))]
612 let scale = 1.0;
613 let (x, y) = (position.x * scale, position.y * scale);
614 self.cursor_px = (x, y);
615 let px = physical_pos_from(x, y);
616 let Some(term) = self.terminal.as_mut() else {
617 return;
618 };
619 let (cell_w, cell_h) = term.backend().presenter().cell_size();
620 let pos = pixel_to_cell(x, y, cell_w, cell_h);
621 term.backend_mut().push_event(Event::Mouse(MouseEvent {
622 kind: MouseEventKind::Moved,
623 position: pos,
624 pixel_position: Some(px),
625 modifiers: self.current_modifiers,
626 }));
627 }
628
629 fn on_mouse_input(
630 &mut self,
631 state: winit::event::ElementState,
632 button: winit::event::MouseButton,
633 ) {
634 let Some(btn) = translate_mouse_button(button) else {
635 return;
636 };
637 let px = self.cursor_physical_pos();
638 let Some(term) = self.terminal.as_mut() else {
639 return;
640 };
641 let (cell_w, cell_h) = term.backend().presenter().cell_size();
642 let pos = pixel_to_cell(self.cursor_px.0, self.cursor_px.1, cell_w, cell_h);
643 let kind = if state.is_pressed() {
644 MouseEventKind::Down(btn)
645 } else {
646 MouseEventKind::Up(btn)
647 };
648 term.backend_mut().push_event(Event::Mouse(MouseEvent {
649 kind,
650 position: pos,
651 pixel_position: Some(px),
652 modifiers: self.current_modifiers,
653 }));
654 }
655
656 fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
657 let px = self.cursor_physical_pos();
658 let Some(term) = self.terminal.as_mut() else {
659 return;
660 };
661 let (cell_w, cell_h) = term.backend().presenter().cell_size();
662 let pos = pixel_to_cell(self.cursor_px.0, self.cursor_px.1, cell_w, cell_h);
663 let scroll_y = match delta {
664 winit::event::MouseScrollDelta::LineDelta(_, y) => f64::from(y),
665 winit::event::MouseScrollDelta::PixelDelta(p) => p.y,
666 };
667 let kind = if scroll_y > 0.0 {
668 MouseEventKind::ScrollUp
669 } else {
670 MouseEventKind::ScrollDown
671 };
672 term.backend_mut().push_event(Event::Mouse(MouseEvent {
673 kind,
674 position: pos,
675 pixel_position: Some(px),
676 modifiers: self.current_modifiers,
677 }));
678 }
679
680 fn on_touch(&mut self, touch: winit::event::Touch) {
689 use winit::event::TouchPhase;
690
691 match touch.phase {
692 TouchPhase::Started => {
693 if self.active_touch.is_some() {
694 return; }
696 self.active_touch = Some(touch.id);
697 self.on_cursor_moved(touch.location);
698 self.on_mouse_input(
699 winit::event::ElementState::Pressed,
700 winit::event::MouseButton::Left,
701 );
702 }
703 TouchPhase::Moved => {
704 if self.active_touch == Some(touch.id) {
705 self.on_cursor_moved(touch.location);
706 }
707 }
708 TouchPhase::Ended | TouchPhase::Cancelled => {
709 if self.active_touch != Some(touch.id) {
710 return;
711 }
712 self.active_touch = None;
713 self.on_cursor_moved(touch.location);
714 self.on_mouse_input(
715 winit::event::ElementState::Released,
716 winit::event::MouseButton::Left,
717 );
718 }
719 }
720 }
721
722 const fn cursor_physical_pos(&self) -> PhysicalPos {
724 physical_pos_from(self.cursor_px.0, self.cursor_px.1)
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731 use retroglyph_core::event::{MouseButton, MouseEvent, MouseEventKind};
732 use retroglyph_core::grid::{Pos, Size};
733 use retroglyph_core::tile::Tile;
734 use std::time::Duration;
735
736 #[test]
739 fn dpr_pointer_scale_no_correction_below_cap() {
740 assert!((dpr_pointer_scale(1.0, 1.5) - 1.0).abs() < 1e-9);
743 assert!((dpr_pointer_scale(1.5, 1.5) - 1.0).abs() < 1e-9);
744 }
745
746 #[test]
747 fn dpr_pointer_scale_corrects_above_cap() {
748 assert!((dpr_pointer_scale(3.0, 1.5) - 0.5).abs() < 1e-9);
752 assert!((dpr_pointer_scale(2.0, 1.5) - 0.75).abs() < 1e-9);
753 }
754
755 struct MockPresenter;
760
761 impl Presenter for MockPresenter {
762 type Error = core::convert::Infallible;
763 type SurfaceError = core::convert::Infallible;
764
765 fn draw<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
766 where
767 I: Iterator<Item = (Pos, &'a Tile)>,
768 {
769 Ok(())
770 }
771
772 fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
773 where
774 I: Iterator<Item = (u8, Pos, &'a Tile)>,
775 {
776 Ok(())
777 }
778
779 fn flush(&mut self) -> Result<(), Self::Error> {
780 Ok(())
781 }
782
783 fn size(&self) -> Size {
784 Size {
785 width: 10,
786 height: 5,
787 }
788 }
789
790 fn clear(&mut self) -> Result<(), Self::Error> {
791 Ok(())
792 }
793
794 fn resize(&mut self, _size: Size) {}
795
796 fn init_surface(
797 &mut self,
798 _window: Arc<dyn crate::presenter::WindowHandle>,
799 ) -> Result<(), Self::SurfaceError> {
800 Ok(())
801 }
802
803 fn resize_surface(&mut self, _width: u32, _height: u32) {}
804
805 fn present(&mut self) -> Result<(), Self::SurfaceError> {
806 Ok(())
807 }
808
809 fn cell_size(&self) -> (u32, u32) {
810 (8, 16)
811 }
812 }
813
814 type MockApp = WindowApp<MockPresenter, fn(&mut Terminal<WindowBackend<MockPresenter>>)>;
815
816 fn test_window_app() -> MockApp {
817 let terminal = Terminal::new(WindowBackend::new(MockPresenter));
818 WindowApp {
819 terminal: Some(terminal),
820 app_loop: |_| {},
821 window: None,
822 title: String::new(),
823 init_size: InitWindowSize {
824 width: 80,
825 height: 80,
826 },
827 current_modifiers: KeyModifiers::NONE,
828 cursor_px: (0.0, 0.0),
829 active_touch: None,
830 #[cfg(not(target_arch = "wasm32"))]
831 frame_interval: None,
832 #[cfg(not(target_arch = "wasm32"))]
833 next_frame: std::time::Instant::now(),
834 }
835 }
836
837 fn poll(app: &mut MockApp) -> Option<Event> {
838 app.terminal
839 .as_mut()
840 .unwrap()
841 .backend_mut()
842 .poll_event(Duration::ZERO)
843 }
844
845 #[test]
848 fn mouse_event_round_trips_through_event_buffer() {
849 let mut backend = WindowBackend::new(MockPresenter);
850 let ev = Event::Mouse(MouseEvent {
851 kind: MouseEventKind::Down(MouseButton::Left),
852 position: Pos { x: 3, y: 1 },
853 pixel_position: None,
854 modifiers: KeyModifiers::NONE,
855 });
856 backend.push_event(ev);
857 assert_eq!(backend.poll_event(Duration::ZERO), Some(ev));
858 assert_eq!(backend.poll_event(Duration::ZERO), None);
859 }
860
861 #[test]
862 fn multiple_mouse_events_preserve_fifo_order() {
863 let mut backend = WindowBackend::new(MockPresenter);
864 let moved = Event::Mouse(MouseEvent {
865 kind: MouseEventKind::Moved,
866 position: Pos { x: 1, y: 2 },
867 pixel_position: None,
868 modifiers: KeyModifiers::NONE,
869 });
870 let clicked = Event::Mouse(MouseEvent {
871 kind: MouseEventKind::Down(MouseButton::Left),
872 position: Pos { x: 1, y: 2 },
873 pixel_position: None,
874 modifiers: KeyModifiers::NONE,
875 });
876 backend.push_event(moved);
877 backend.push_event(clicked);
878 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved));
879 assert_eq!(backend.poll_event(Duration::ZERO), Some(clicked));
880 }
881
882 #[test]
885 fn cursor_moved_pushes_moved_event_at_correct_cell() {
886 let mut app = test_window_app();
888 app.handle_window_event(WindowEvent::CursorMoved {
889 device_id: winit::event::DeviceId::dummy(),
890 position: winit::dpi::PhysicalPosition::new(20.0_f64, 32.0_f64),
891 });
892 assert_eq!(
893 poll(&mut app),
894 Some(Event::Mouse(MouseEvent {
895 kind: MouseEventKind::Moved,
896 position: Pos { x: 2, y: 2 },
897 pixel_position: Some(PhysicalPos { x: 20, y: 32 }),
898 modifiers: KeyModifiers::NONE,
899 }))
900 );
901 }
902
903 #[test]
904 fn cursor_moved_caches_position_for_subsequent_click() {
905 let mut app = test_window_app();
908 app.handle_window_event(WindowEvent::CursorMoved {
909 device_id: winit::event::DeviceId::dummy(),
910 position: winit::dpi::PhysicalPosition::new(16.0_f64, 16.0_f64),
911 });
912 let _ = poll(&mut app); app.handle_window_event(WindowEvent::MouseInput {
914 device_id: winit::event::DeviceId::dummy(),
915 state: winit::event::ElementState::Pressed,
916 button: winit::event::MouseButton::Left,
917 });
918 assert_eq!(
919 poll(&mut app),
920 Some(Event::Mouse(MouseEvent {
921 kind: MouseEventKind::Down(MouseButton::Left),
922 position: Pos { x: 2, y: 1 },
923 pixel_position: Some(PhysicalPos { x: 16, y: 16 }),
924 modifiers: KeyModifiers::NONE,
925 }))
926 );
927 }
928
929 #[test]
930 fn mouse_button_release_produces_up_event() {
931 let mut app = test_window_app();
932 app.handle_window_event(WindowEvent::MouseInput {
933 device_id: winit::event::DeviceId::dummy(),
934 state: winit::event::ElementState::Released,
935 button: winit::event::MouseButton::Right,
936 });
937 assert_eq!(
938 poll(&mut app),
939 Some(Event::Mouse(MouseEvent {
940 kind: MouseEventKind::Up(MouseButton::Right),
941 position: Pos { x: 0, y: 0 },
942 pixel_position: Some(PhysicalPos { x: 0, y: 0 }),
943 modifiers: KeyModifiers::NONE,
944 }))
945 );
946 }
947
948 #[test]
949 fn unknown_mouse_button_produces_no_event() {
950 let mut app = test_window_app();
951 app.handle_window_event(WindowEvent::MouseInput {
952 device_id: winit::event::DeviceId::dummy(),
953 state: winit::event::ElementState::Pressed,
954 button: winit::event::MouseButton::Other(99),
955 });
956 assert_eq!(poll(&mut app), None);
957 }
958
959 fn touch(id: u64, phase: winit::event::TouchPhase, x: f64, y: f64) -> WindowEvent {
960 WindowEvent::Touch(winit::event::Touch {
961 device_id: winit::event::DeviceId::dummy(),
962 phase,
963 location: winit::dpi::PhysicalPosition::new(x, y),
964 force: None,
965 id,
966 })
967 }
968
969 #[test]
970 fn touch_tap_synthesizes_left_click() {
971 use winit::event::TouchPhase;
972 let mut app = test_window_app();
973 app.handle_window_event(touch(7, TouchPhase::Started, 20.0, 18.0));
975 assert!(matches!(
977 poll(&mut app),
978 Some(Event::Mouse(MouseEvent {
979 kind: MouseEventKind::Moved,
980 position: Pos { x: 2, y: 1 },
981 ..
982 }))
983 ));
984 assert!(matches!(
985 poll(&mut app),
986 Some(Event::Mouse(MouseEvent {
987 kind: MouseEventKind::Down(MouseButton::Left),
988 position: Pos { x: 2, y: 1 },
989 ..
990 }))
991 ));
992
993 app.handle_window_event(touch(7, TouchPhase::Ended, 20.0, 18.0));
994 assert!(matches!(
995 poll(&mut app),
996 Some(Event::Mouse(MouseEvent {
997 kind: MouseEventKind::Moved,
998 ..
999 }))
1000 ));
1001 assert!(matches!(
1002 poll(&mut app),
1003 Some(Event::Mouse(MouseEvent {
1004 kind: MouseEventKind::Up(MouseButton::Left),
1005 position: Pos { x: 2, y: 1 },
1006 ..
1007 }))
1008 ));
1009 assert_eq!(poll(&mut app), None);
1010 }
1011
1012 #[test]
1013 fn touch_drag_synthesizes_moves_between_down_and_up() {
1014 use winit::event::TouchPhase;
1015 let mut app = test_window_app();
1016 app.handle_window_event(touch(1, TouchPhase::Started, 0.0, 0.0));
1017 poll(&mut app); poll(&mut app); app.handle_window_event(touch(1, TouchPhase::Moved, 40.0, 32.0));
1021 assert!(matches!(
1022 poll(&mut app),
1023 Some(Event::Mouse(MouseEvent {
1024 kind: MouseEventKind::Moved,
1025 position: Pos { x: 5, y: 2 },
1026 ..
1027 }))
1028 ));
1029
1030 app.handle_window_event(touch(1, TouchPhase::Cancelled, 40.0, 32.0));
1031 poll(&mut app); assert!(matches!(
1033 poll(&mut app),
1034 Some(Event::Mouse(MouseEvent {
1035 kind: MouseEventKind::Up(MouseButton::Left),
1036 ..
1037 }))
1038 ));
1039 }
1040
1041 #[test]
1042 fn second_finger_is_ignored_while_first_is_down() {
1043 use winit::event::TouchPhase;
1044 let mut app = test_window_app();
1045 app.handle_window_event(touch(1, TouchPhase::Started, 0.0, 0.0));
1046 poll(&mut app); poll(&mut app); app.handle_window_event(touch(2, TouchPhase::Started, 80.0, 80.0));
1051 app.handle_window_event(touch(2, TouchPhase::Moved, 88.0, 80.0));
1052 app.handle_window_event(touch(2, TouchPhase::Ended, 88.0, 80.0));
1053 assert_eq!(poll(&mut app), None);
1054
1055 app.handle_window_event(touch(1, TouchPhase::Ended, 8.0, 0.0));
1057 poll(&mut app); assert!(matches!(
1059 poll(&mut app),
1060 Some(Event::Mouse(MouseEvent {
1061 kind: MouseEventKind::Up(MouseButton::Left),
1062 position: Pos { x: 1, y: 0 },
1063 ..
1064 }))
1065 ));
1066 }
1067
1068 #[test]
1069 fn scroll_up_line_delta() {
1070 let mut app = test_window_app();
1071 app.handle_window_event(WindowEvent::MouseWheel {
1072 device_id: winit::event::DeviceId::dummy(),
1073 delta: winit::event::MouseScrollDelta::LineDelta(0.0, 1.0),
1074 phase: winit::event::TouchPhase::Moved,
1075 });
1076 let ev = poll(&mut app).unwrap();
1077 assert!(matches!(
1078 ev,
1079 Event::Mouse(MouseEvent {
1080 kind: MouseEventKind::ScrollUp,
1081 ..
1082 })
1083 ));
1084 }
1085
1086 #[test]
1087 fn scroll_down_line_delta() {
1088 let mut app = test_window_app();
1089 app.handle_window_event(WindowEvent::MouseWheel {
1090 device_id: winit::event::DeviceId::dummy(),
1091 delta: winit::event::MouseScrollDelta::LineDelta(0.0, -1.0),
1092 phase: winit::event::TouchPhase::Moved,
1093 });
1094 let ev = poll(&mut app).unwrap();
1095 assert!(matches!(
1096 ev,
1097 Event::Mouse(MouseEvent {
1098 kind: MouseEventKind::ScrollDown,
1099 ..
1100 })
1101 ));
1102 }
1103
1104 #[test]
1105 fn scroll_up_pixel_delta() {
1106 let mut app = test_window_app();
1107 app.handle_window_event(WindowEvent::MouseWheel {
1108 device_id: winit::event::DeviceId::dummy(),
1109 delta: winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition::new(
1110 0.0_f64, 15.0_f64,
1111 )),
1112 phase: winit::event::TouchPhase::Moved,
1113 });
1114 let ev = poll(&mut app).unwrap();
1115 assert!(matches!(
1116 ev,
1117 Event::Mouse(MouseEvent {
1118 kind: MouseEventKind::ScrollUp,
1119 ..
1120 })
1121 ));
1122 }
1123
1124 #[test]
1125 fn modifiers_propagate_to_mouse_event() {
1126 let mut app = test_window_app();
1127 app.handle_window_event(WindowEvent::ModifiersChanged(
1129 winit::event::Modifiers::from(winit::keyboard::ModifiersState::SHIFT),
1130 ));
1131 let _ = poll(&mut app); app.handle_window_event(WindowEvent::MouseInput {
1133 device_id: winit::event::DeviceId::dummy(),
1134 state: winit::event::ElementState::Pressed,
1135 button: winit::event::MouseButton::Left,
1136 });
1137 let ev = poll(&mut app).unwrap();
1138 assert!(matches!(
1139 ev,
1140 Event::Mouse(MouseEvent {
1141 modifiers,
1142 ..
1143 }) if modifiers.contains(KeyModifiers::SHIFT)
1144 ));
1145 }
1146
1147 #[test]
1148 fn close_requested_pushes_close_event() {
1149 let mut app = test_window_app();
1150 app.handle_window_event(WindowEvent::CloseRequested);
1151 assert_eq!(poll(&mut app), Some(Event::Close));
1152 }
1153
1154 #[test]
1155 fn theme_changed_pushes_mapped_system_theme_event() {
1156 let mut app = test_window_app();
1157 app.handle_window_event(WindowEvent::ThemeChanged(winit::window::Theme::Light));
1158 assert_eq!(
1159 poll(&mut app),
1160 Some(Event::ThemeChanged(
1161 retroglyph_core::event::SystemTheme::Light
1162 ))
1163 );
1164
1165 app.handle_window_event(WindowEvent::ThemeChanged(winit::window::Theme::Dark));
1166 assert_eq!(
1167 poll(&mut app),
1168 Some(Event::ThemeChanged(
1169 retroglyph_core::event::SystemTheme::Dark
1170 ))
1171 );
1172 }
1173
1174 #[test]
1175 fn resized_pushes_resize_event_in_cells() {
1176 let mut app = test_window_app();
1178 app.handle_window_event(WindowEvent::Resized(winit::dpi::PhysicalSize::new(88, 80)));
1179 assert_eq!(poll(&mut app), Some(Event::Resize(11, 5)));
1180 }
1181}