use super::translate::{
physical_pos_from, pixel_to_cell, translate_key, translate_modifiers, translate_mouse_button,
};
use crate::backend::WindowBackend;
use crate::presenter::Presenter;
use retroglyph_core::Terminal;
use retroglyph_core::backend::Backend;
use retroglyph_core::event::{Event, KeyModifiers, MouseEvent, MouseEventKind, PhysicalPos};
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};
pub struct WindowConfig {
pub title: String,
pub width: u32,
pub height: u32,
pub target_fps: Option<u32>,
pub fill_viewport: bool,
}
impl WindowConfig {
#[must_use]
pub fn fit<P: Presenter>(
presenter: &P,
title: impl Into<String>,
target_fps: Option<u32>,
) -> Self {
let grid = presenter.size();
let (cell_w, cell_h) = presenter.cell_size();
Self {
title: title.into(),
width: u32::from(grid.width) * cell_w,
height: u32::from(grid.height) * cell_h,
target_fps,
fill_viewport: false,
}
}
}
pub fn run_windowed<P, F>(
config: WindowConfig,
presenter: P,
app_loop: F,
) -> Result<(), winit::error::EventLoopError>
where
P: Presenter + 'static,
F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
{
let terminal = Terminal::new(WindowBackend::new(presenter));
let event_loop = EventLoop::new()?;
#[cfg(not(target_arch = "wasm32"))]
let frame_interval = config
.target_fps
.map(|fps| Duration::from_secs_f64(1.0 / f64::from(fps)));
let app = WindowApp {
terminal: Some(terminal),
app_loop,
window: None,
title: config.title,
init_size: InitWindowSize {
width: config.width,
height: config.height,
},
#[cfg(target_arch = "wasm32")]
fill_viewport: config.fill_viewport,
current_modifiers: KeyModifiers::NONE,
cursor_px: (0.0, 0.0),
active_touch: None,
#[cfg(not(target_arch = "wasm32"))]
frame_interval,
#[cfg(not(target_arch = "wasm32"))]
next_frame: std::time::Instant::now(),
};
#[cfg(not(target_arch = "wasm32"))]
{
let mut app = app;
event_loop.run_app(&mut app)
}
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::EventLoopExtWebSys;
event_loop.spawn_app(app);
Ok(())
}
}
pub fn run_app<P, A>(
config: WindowConfig,
presenter: P,
mut app: A,
) -> Result<(), winit::error::EventLoopError>
where
P: Presenter + 'static,
A: retroglyph_core::App<WindowBackend<P>> + 'static,
{
let mut frame_count = 0u64;
let mut last = web_time::Instant::now();
run_windowed(config, presenter, move |term| {
let now = web_time::Instant::now();
let delta = now.duration_since(last);
last = now;
let frame = retroglyph_core::Frame {
delta,
frame: frame_count,
};
frame_count = frame_count.wrapping_add(1);
if retroglyph_core::step(term, &mut app, &frame) == retroglyph_core::Flow::Exit {
#[cfg(not(target_arch = "wasm32"))]
std::process::exit(0);
}
})
}
struct InitWindowSize {
width: u32,
height: u32,
}
struct WindowApp<P: Presenter, F> {
terminal: Option<Terminal<WindowBackend<P>>>,
app_loop: F,
window: Option<Arc<Window>>,
title: String,
init_size: InitWindowSize,
#[cfg(target_arch = "wasm32")]
fill_viewport: bool,
current_modifiers: KeyModifiers,
cursor_px: (f64, f64),
active_touch: Option<u64>,
#[cfg(not(target_arch = "wasm32"))]
frame_interval: Option<Duration>,
#[cfg(not(target_arch = "wasm32"))]
next_frame: std::time::Instant,
}
impl<P: Presenter, F> WindowApp<P, F> {
fn create_window_and_surface(&mut self, event_loop: &ActiveEventLoop) -> Option<Arc<Window>> {
#[cfg(not(target_arch = "wasm32"))]
let physical_size =
winit::dpi::PhysicalSize::new(self.init_size.width, self.init_size.height);
#[cfg(target_arch = "wasm32")]
let physical_size = if self.fill_viewport {
web_viewport_layout_physical_size().unwrap_or_else(|| {
winit::dpi::PhysicalSize::new(self.init_size.width, self.init_size.height)
})
} else {
winit::dpi::PhysicalSize::new(self.init_size.width, self.init_size.height)
};
#[cfg(target_arch = "wasm32")]
let surface_physical_size = if self.fill_viewport {
web_viewport_surface_physical_size().unwrap_or(physical_size)
} else {
physical_size
};
#[cfg(not(target_arch = "wasm32"))]
let surface_physical_size = physical_size;
let attrs = Window::default_attributes()
.with_title(&self.title)
.with_inner_size(physical_size);
#[cfg(target_family = "wasm")]
let attrs = {
use winit::platform::web::WindowAttributesExtWebSys;
attrs.with_append(true)
};
let window = Arc::new(match event_loop.create_window(attrs) {
Ok(w) => w,
Err(e) => {
log::error!("window creation failed: {e}");
event_loop.exit();
return None;
}
});
if let Some(term) = self.terminal.as_mut() {
let handle: Arc<dyn crate::presenter::WindowHandle> = window.clone();
if let Err(e) = term.backend_mut().presenter_mut().init_surface(handle) {
log::error!("surface init failed: {e}");
event_loop.exit();
return None;
}
term.backend_mut()
.presenter_mut()
.resize_surface(surface_physical_size.width, surface_physical_size.height);
}
#[cfg(target_arch = "wasm32")]
if self.fill_viewport {
install_viewport_resize_listener(&window);
}
if let Some(theme) = window.theme()
&& let Some(term) = self.terminal.as_mut()
{
term.backend_mut().push_event(system_theme_event(theme));
}
Some(window)
}
}
const fn system_theme_event(theme: winit::window::Theme) -> Event {
use retroglyph_core::event::SystemTheme;
match theme {
winit::window::Theme::Light => Event::ThemeChanged(SystemTheme::Light),
winit::window::Theme::Dark => Event::ThemeChanged(SystemTheme::Dark),
}
}
#[cfg(target_arch = "wasm32")]
const MAX_DEVICE_PIXEL_RATIO: f64 = 1.5;
#[cfg(target_arch = "wasm32")]
fn web_viewport_css_size() -> Option<(f64, f64)> {
let window = web_sys::window()?;
let width = window.inner_width().ok()?.as_f64()?;
let height = window.inner_height().ok()?.as_f64()?;
Some((width, height))
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn web_viewport_layout_physical_size() -> Option<winit::dpi::PhysicalSize<u32>> {
let (width, height) = web_viewport_css_size()?;
let dpr = web_sys::window()?.device_pixel_ratio();
Some(winit::dpi::PhysicalSize::new(
(width * dpr).round() as u32,
(height * dpr).round() as u32,
))
}
#[cfg(any(target_arch = "wasm32", test))]
fn dpr_pointer_scale(real_dpr: f64, capped_dpr: f64) -> f64 {
(capped_dpr / real_dpr).min(1.0)
}
#[cfg(target_arch = "wasm32")]
fn wasm_pointer_scale() -> f64 {
web_sys::window().map_or(1.0, |w| {
dpr_pointer_scale(w.device_pixel_ratio(), MAX_DEVICE_PIXEL_RATIO)
})
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn web_viewport_surface_physical_size() -> Option<winit::dpi::PhysicalSize<u32>> {
let (width, height) = web_viewport_css_size()?;
let dpr = web_sys::window()?
.device_pixel_ratio()
.min(MAX_DEVICE_PIXEL_RATIO);
Some(winit::dpi::PhysicalSize::new(
(width * dpr).round() as u32,
(height * dpr).round() as u32,
))
}
#[cfg(target_arch = "wasm32")]
fn install_viewport_resize_listener(window: &Arc<Window>) {
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::Closure;
let Some(web_window) = web_sys::window() else {
return;
};
let window = window.clone();
let closure = Closure::<dyn FnMut()>::new(move || {
if let Some(size) = web_viewport_layout_physical_size() {
let _ = window.request_inner_size(size);
}
});
if web_window
.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref())
.is_ok()
{
closure.forget();
}
}
impl<P, F> ApplicationHandler for WindowApp<P, F>
where
P: Presenter,
F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
{
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if let Some(window) = self.create_window_and_surface(event_loop) {
self.window = Some(window);
}
}
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
self.handle_window_event(event);
}
fn about_to_wait(
&mut self,
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))] event_loop: &ActiveEventLoop,
) {
#[cfg(not(target_arch = "wasm32"))]
if let Some(interval) = self.frame_interval {
let now = std::time::Instant::now();
if self.next_frame > now {
event_loop
.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(self.next_frame));
return;
}
self.next_frame = (self.next_frame + interval).max(now);
}
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
impl<P, F> WindowApp<P, F>
where
P: Presenter,
F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
{
fn handle_window_event(&mut self, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
if let Some(term) = self.terminal.as_mut() {
term.backend_mut().push_event(Event::Close);
}
}
WindowEvent::Resized(size) => self.on_resized(size),
WindowEvent::CursorMoved { position, .. } => self.on_cursor_moved(position),
WindowEvent::MouseInput { state, button, .. } => self.on_mouse_input(state, button),
WindowEvent::MouseWheel { delta, .. } => self.on_mouse_wheel(delta),
WindowEvent::Touch(touch) => self.on_touch(touch),
WindowEvent::ModifiersChanged(mods) => {
self.current_modifiers = translate_modifiers(mods.state());
}
WindowEvent::ThemeChanged(theme) => {
if let Some(term) = self.terminal.as_mut() {
term.backend_mut().push_event(system_theme_event(theme));
}
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(term) = self.terminal.as_mut()
&& let Some(e) = translate_key(event, self.current_modifiers)
{
term.backend_mut().push_event(e);
}
}
WindowEvent::RedrawRequested => {
let Some(term) = self.terminal.as_mut() else {
return;
};
(self.app_loop)(term);
if let Err(e) = term.backend_mut().presenter_mut().present() {
log::error!("frame present failed: {e}");
}
}
_ => {}
}
}
fn on_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
let Some(term) = self.terminal.as_mut() else {
return;
};
#[cfg(target_arch = "wasm32")]
let size = if self.fill_viewport {
web_viewport_surface_physical_size().unwrap_or(size)
} else {
size
};
let (cell_w, cell_h) = term.backend().presenter().cell_size();
let cols = size.width / cell_w;
let rows = size.height / cell_h;
term.backend_mut()
.presenter_mut()
.resize_surface(cols * cell_w, rows * cell_h);
#[allow(clippy::cast_possible_truncation)]
term.backend_mut()
.push_event(Event::Resize(cols.max(1) as u16, rows.max(1) as u16));
}
fn on_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
#[cfg(target_arch = "wasm32")]
let scale = wasm_pointer_scale();
#[cfg(not(target_arch = "wasm32"))]
let scale = 1.0;
let (x, y) = (position.x * scale, position.y * scale);
self.cursor_px = (x, y);
let px = physical_pos_from(x, y);
let Some(term) = self.terminal.as_mut() else {
return;
};
let (cell_w, cell_h) = term.backend().presenter().cell_size();
let pos = pixel_to_cell(x, y, cell_w, cell_h);
term.backend_mut().push_event(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: pos,
pixel_position: Some(px),
modifiers: self.current_modifiers,
}));
}
fn on_mouse_input(
&mut self,
state: winit::event::ElementState,
button: winit::event::MouseButton,
) {
let Some(btn) = translate_mouse_button(button) else {
return;
};
let px = self.cursor_physical_pos();
let Some(term) = self.terminal.as_mut() else {
return;
};
let (cell_w, cell_h) = term.backend().presenter().cell_size();
let pos = pixel_to_cell(self.cursor_px.0, self.cursor_px.1, cell_w, cell_h);
let kind = if state.is_pressed() {
MouseEventKind::Down(btn)
} else {
MouseEventKind::Up(btn)
};
term.backend_mut().push_event(Event::Mouse(MouseEvent {
kind,
position: pos,
pixel_position: Some(px),
modifiers: self.current_modifiers,
}));
}
fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
let px = self.cursor_physical_pos();
let Some(term) = self.terminal.as_mut() else {
return;
};
let (cell_w, cell_h) = term.backend().presenter().cell_size();
let pos = pixel_to_cell(self.cursor_px.0, self.cursor_px.1, cell_w, cell_h);
let scroll_y = match delta {
winit::event::MouseScrollDelta::LineDelta(_, y) => f64::from(y),
winit::event::MouseScrollDelta::PixelDelta(p) => p.y,
};
let kind = if scroll_y > 0.0 {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
};
term.backend_mut().push_event(Event::Mouse(MouseEvent {
kind,
position: pos,
pixel_position: Some(px),
modifiers: self.current_modifiers,
}));
}
fn on_touch(&mut self, touch: winit::event::Touch) {
use winit::event::TouchPhase;
match touch.phase {
TouchPhase::Started => {
if self.active_touch.is_some() {
return; }
self.active_touch = Some(touch.id);
self.on_cursor_moved(touch.location);
self.on_mouse_input(
winit::event::ElementState::Pressed,
winit::event::MouseButton::Left,
);
}
TouchPhase::Moved => {
if self.active_touch == Some(touch.id) {
self.on_cursor_moved(touch.location);
}
}
TouchPhase::Ended | TouchPhase::Cancelled => {
if self.active_touch != Some(touch.id) {
return;
}
self.active_touch = None;
self.on_cursor_moved(touch.location);
self.on_mouse_input(
winit::event::ElementState::Released,
winit::event::MouseButton::Left,
);
}
}
}
const fn cursor_physical_pos(&self) -> PhysicalPos {
physical_pos_from(self.cursor_px.0, self.cursor_px.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use retroglyph_core::event::{MouseButton, MouseEvent, MouseEventKind};
use retroglyph_core::grid::{Pos, Size};
use retroglyph_core::tile::Tile;
use std::time::Duration;
#[test]
fn dpr_pointer_scale_no_correction_below_cap() {
assert!((dpr_pointer_scale(1.0, 1.5) - 1.0).abs() < 1e-9);
assert!((dpr_pointer_scale(1.5, 1.5) - 1.0).abs() < 1e-9);
}
#[test]
fn dpr_pointer_scale_corrects_above_cap() {
assert!((dpr_pointer_scale(3.0, 1.5) - 0.5).abs() < 1e-9);
assert!((dpr_pointer_scale(2.0, 1.5) - 0.75).abs() < 1e-9);
}
struct MockPresenter;
impl Presenter for MockPresenter {
type Error = core::convert::Infallible;
type SurfaceError = core::convert::Infallible;
fn draw<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (Pos, &'a Tile)>,
{
Ok(())
}
fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u8, Pos, &'a Tile)>,
{
Ok(())
}
fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn size(&self) -> Size {
Size {
width: 10,
height: 5,
}
}
fn clear(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn resize(&mut self, _size: Size) {}
fn init_surface(
&mut self,
_window: Arc<dyn crate::presenter::WindowHandle>,
) -> Result<(), Self::SurfaceError> {
Ok(())
}
fn resize_surface(&mut self, _width: u32, _height: u32) {}
fn present(&mut self) -> Result<(), Self::SurfaceError> {
Ok(())
}
fn cell_size(&self) -> (u32, u32) {
(8, 16)
}
}
type MockApp = WindowApp<MockPresenter, fn(&mut Terminal<WindowBackend<MockPresenter>>)>;
fn test_window_app() -> MockApp {
let terminal = Terminal::new(WindowBackend::new(MockPresenter));
WindowApp {
terminal: Some(terminal),
app_loop: |_| {},
window: None,
title: String::new(),
init_size: InitWindowSize {
width: 80,
height: 80,
},
current_modifiers: KeyModifiers::NONE,
cursor_px: (0.0, 0.0),
active_touch: None,
#[cfg(not(target_arch = "wasm32"))]
frame_interval: None,
#[cfg(not(target_arch = "wasm32"))]
next_frame: std::time::Instant::now(),
}
}
fn poll(app: &mut MockApp) -> Option<Event> {
app.terminal
.as_mut()
.unwrap()
.backend_mut()
.poll_event(Duration::ZERO)
}
#[test]
fn mouse_event_round_trips_through_event_buffer() {
let mut backend = WindowBackend::new(MockPresenter);
let ev = Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos { x: 3, y: 1 },
pixel_position: None,
modifiers: KeyModifiers::NONE,
});
backend.push_event(ev);
assert_eq!(backend.poll_event(Duration::ZERO), Some(ev));
assert_eq!(backend.poll_event(Duration::ZERO), None);
}
#[test]
fn multiple_mouse_events_preserve_fifo_order() {
let mut backend = WindowBackend::new(MockPresenter);
let moved = Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos { x: 1, y: 2 },
pixel_position: None,
modifiers: KeyModifiers::NONE,
});
let clicked = Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos { x: 1, y: 2 },
pixel_position: None,
modifiers: KeyModifiers::NONE,
});
backend.push_event(moved);
backend.push_event(clicked);
assert_eq!(backend.poll_event(Duration::ZERO), Some(moved));
assert_eq!(backend.poll_event(Duration::ZERO), Some(clicked));
}
#[test]
fn cursor_moved_pushes_moved_event_at_correct_cell() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::CursorMoved {
device_id: winit::event::DeviceId::dummy(),
position: winit::dpi::PhysicalPosition::new(20.0_f64, 32.0_f64),
});
assert_eq!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos { x: 2, y: 2 },
pixel_position: Some(PhysicalPos { x: 20, y: 32 }),
modifiers: KeyModifiers::NONE,
}))
);
}
#[test]
fn cursor_moved_caches_position_for_subsequent_click() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::CursorMoved {
device_id: winit::event::DeviceId::dummy(),
position: winit::dpi::PhysicalPosition::new(16.0_f64, 16.0_f64),
});
let _ = poll(&mut app); app.handle_window_event(WindowEvent::MouseInput {
device_id: winit::event::DeviceId::dummy(),
state: winit::event::ElementState::Pressed,
button: winit::event::MouseButton::Left,
});
assert_eq!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos { x: 2, y: 1 },
pixel_position: Some(PhysicalPos { x: 16, y: 16 }),
modifiers: KeyModifiers::NONE,
}))
);
}
#[test]
fn mouse_button_release_produces_up_event() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::MouseInput {
device_id: winit::event::DeviceId::dummy(),
state: winit::event::ElementState::Released,
button: winit::event::MouseButton::Right,
});
assert_eq!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Right),
position: Pos { x: 0, y: 0 },
pixel_position: Some(PhysicalPos { x: 0, y: 0 }),
modifiers: KeyModifiers::NONE,
}))
);
}
#[test]
fn unknown_mouse_button_produces_no_event() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::MouseInput {
device_id: winit::event::DeviceId::dummy(),
state: winit::event::ElementState::Pressed,
button: winit::event::MouseButton::Other(99),
});
assert_eq!(poll(&mut app), None);
}
fn touch(id: u64, phase: winit::event::TouchPhase, x: f64, y: f64) -> WindowEvent {
WindowEvent::Touch(winit::event::Touch {
device_id: winit::event::DeviceId::dummy(),
phase,
location: winit::dpi::PhysicalPosition::new(x, y),
force: None,
id,
})
}
#[test]
fn touch_tap_synthesizes_left_click() {
use winit::event::TouchPhase;
let mut app = test_window_app();
app.handle_window_event(touch(7, TouchPhase::Started, 20.0, 18.0));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos { x: 2, y: 1 },
..
}))
));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos { x: 2, y: 1 },
..
}))
));
app.handle_window_event(touch(7, TouchPhase::Ended, 20.0, 18.0));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
}))
));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
position: Pos { x: 2, y: 1 },
..
}))
));
assert_eq!(poll(&mut app), None);
}
#[test]
fn touch_drag_synthesizes_moves_between_down_and_up() {
use winit::event::TouchPhase;
let mut app = test_window_app();
app.handle_window_event(touch(1, TouchPhase::Started, 0.0, 0.0));
poll(&mut app); poll(&mut app);
app.handle_window_event(touch(1, TouchPhase::Moved, 40.0, 32.0));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos { x: 5, y: 2 },
..
}))
));
app.handle_window_event(touch(1, TouchPhase::Cancelled, 40.0, 32.0));
poll(&mut app); assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
..
}))
));
}
#[test]
fn second_finger_is_ignored_while_first_is_down() {
use winit::event::TouchPhase;
let mut app = test_window_app();
app.handle_window_event(touch(1, TouchPhase::Started, 0.0, 0.0));
poll(&mut app); poll(&mut app);
app.handle_window_event(touch(2, TouchPhase::Started, 80.0, 80.0));
app.handle_window_event(touch(2, TouchPhase::Moved, 88.0, 80.0));
app.handle_window_event(touch(2, TouchPhase::Ended, 88.0, 80.0));
assert_eq!(poll(&mut app), None);
app.handle_window_event(touch(1, TouchPhase::Ended, 8.0, 0.0));
poll(&mut app); assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
position: Pos { x: 1, y: 0 },
..
}))
));
}
#[test]
fn scroll_up_line_delta() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::MouseWheel {
device_id: winit::event::DeviceId::dummy(),
delta: winit::event::MouseScrollDelta::LineDelta(0.0, 1.0),
phase: winit::event::TouchPhase::Moved,
});
let ev = poll(&mut app).unwrap();
assert!(matches!(
ev,
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
})
));
}
#[test]
fn scroll_down_line_delta() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::MouseWheel {
device_id: winit::event::DeviceId::dummy(),
delta: winit::event::MouseScrollDelta::LineDelta(0.0, -1.0),
phase: winit::event::TouchPhase::Moved,
});
let ev = poll(&mut app).unwrap();
assert!(matches!(
ev,
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
})
));
}
#[test]
fn scroll_up_pixel_delta() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::MouseWheel {
device_id: winit::event::DeviceId::dummy(),
delta: winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition::new(
0.0_f64, 15.0_f64,
)),
phase: winit::event::TouchPhase::Moved,
});
let ev = poll(&mut app).unwrap();
assert!(matches!(
ev,
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
})
));
}
#[test]
fn modifiers_propagate_to_mouse_event() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::ModifiersChanged(
winit::event::Modifiers::from(winit::keyboard::ModifiersState::SHIFT),
));
let _ = poll(&mut app); app.handle_window_event(WindowEvent::MouseInput {
device_id: winit::event::DeviceId::dummy(),
state: winit::event::ElementState::Pressed,
button: winit::event::MouseButton::Left,
});
let ev = poll(&mut app).unwrap();
assert!(matches!(
ev,
Event::Mouse(MouseEvent {
modifiers,
..
}) if modifiers.contains(KeyModifiers::SHIFT)
));
}
#[test]
fn close_requested_pushes_close_event() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::CloseRequested);
assert_eq!(poll(&mut app), Some(Event::Close));
}
#[test]
fn theme_changed_pushes_mapped_system_theme_event() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::ThemeChanged(winit::window::Theme::Light));
assert_eq!(
poll(&mut app),
Some(Event::ThemeChanged(
retroglyph_core::event::SystemTheme::Light
))
);
app.handle_window_event(WindowEvent::ThemeChanged(winit::window::Theme::Dark));
assert_eq!(
poll(&mut app),
Some(Event::ThemeChanged(
retroglyph_core::event::SystemTheme::Dark
))
);
}
#[test]
fn resized_pushes_resize_event_in_cells() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::Resized(winit::dpi::PhysicalSize::new(88, 80)));
assert_eq!(poll(&mut app), Some(Event::Resize(11, 5)));
}
}