use super::translate::{
physical_pos_from, pixel_to_cell, translate_key, translate_modifiers, translate_mouse_button,
};
#[cfg(target_arch = "wasm32")]
use super::web;
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::cell::Cell;
use std::fmt;
use std::rc::Rc;
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};
type UserEvent = u64;
#[derive(Clone, Debug)]
pub struct EventProxy(winit::event_loop::EventLoopProxy<UserEvent>);
impl EventProxy {
pub fn send_event(&self, id: u64) -> Result<(), EventProxyClosed> {
self.0.send_event(id).map_err(|e| EventProxyClosed(e.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EventProxyClosed(u64);
impl EventProxyClosed {
#[must_use]
pub const fn into_inner(self) -> u64 {
self.0
}
}
impl fmt::Display for EventProxyClosed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "event loop closed")
}
}
impl std::error::Error for EventProxyClosed {}
#[allow(clippy::struct_excessive_bools)]
pub struct WindowConfig {
title: String,
width: u32,
height: u32,
target_fps: Option<u32>,
fill_viewport: bool,
resizable: bool,
decorations: bool,
min_size: Option<(u32, u32)>,
max_size: Option<(u32, u32)>,
initial_position: Option<(i32, i32)>,
fullscreen: bool,
transparency: 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,
resizable: true,
decorations: true,
min_size: None,
max_size: None,
initial_position: None,
fullscreen: false,
transparency: false,
}
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[must_use]
pub const fn target_fps(&self) -> Option<u32> {
self.target_fps
}
#[must_use]
pub const fn fill_viewport(mut self, fill_viewport: bool) -> Self {
self.fill_viewport = fill_viewport;
self
}
#[must_use]
pub const fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
#[must_use]
pub const fn decorations(mut self, decorations: bool) -> Self {
self.decorations = decorations;
self
}
#[must_use]
pub const fn min_size(mut self, width: u32, height: u32) -> Self {
self.min_size = Some((width, height));
self
}
#[must_use]
pub const fn max_size(mut self, width: u32, height: u32) -> Self {
self.max_size = Some((width, height));
self
}
#[must_use]
pub const fn initial_position(mut self, x: i32, y: i32) -> Self {
self.initial_position = Some((x, y));
self
}
#[must_use]
pub const fn fullscreen(mut self, fullscreen: bool) -> Self {
self.fullscreen = fullscreen;
self
}
#[must_use]
pub const fn transparency(mut self, transparency: bool) -> Self {
self.transparency = transparency;
self
}
}
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,
{
run_windowed_with_proxy(config, presenter, app_loop, |_proxy| {})
}
pub fn run_windowed_with_proxy<P, F, O>(
config: WindowConfig,
presenter: P,
app_loop: F,
on_proxy: O,
) -> Result<(), winit::error::EventLoopError>
where
P: Presenter + 'static,
F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
O: FnOnce(EventProxy),
{
run_windowed_with_proxy_and_exit_flag(
config,
presenter,
app_loop,
on_proxy,
Rc::new(Cell::new(false)),
)
}
fn run_windowed_with_proxy_and_exit_flag<P, F, O>(
config: WindowConfig,
presenter: P,
app_loop: F,
on_proxy: O,
exit_requested: Rc<Cell<bool>>,
) -> Result<(), winit::error::EventLoopError>
where
P: Presenter + 'static,
F: FnMut(&mut Terminal<WindowBackend<P>>) + 'static,
O: FnOnce(EventProxy),
{
let terminal = Terminal::new(WindowBackend::new(presenter));
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
on_proxy(EventProxy(event_loop.create_proxy()));
#[cfg(not(target_arch = "wasm32"))]
let frame_interval = config
.target_fps
.map(|fps| Duration::from_secs_f64(1.0 / f64::from(fps)));
let attrs = WindowAttrs::from(&config);
let app = WindowApp {
terminal: Some(terminal),
app_loop,
window: None,
title: config.title,
init_size: InitWindowSize {
width: config.width,
height: config.height,
},
attrs,
#[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(),
exit_requested,
needs_redraw: true,
consecutive_present_errors: 0,
};
#[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,
app: A,
) -> Result<(), winit::error::EventLoopError>
where
P: Presenter + 'static,
A: retroglyph_core::App<WindowBackend<P>> + 'static,
{
run_app_with_proxy(config, presenter, app, |_proxy| {})
}
pub fn run_app_with_proxy<P, A, O>(
config: WindowConfig,
presenter: P,
mut app: A,
on_proxy: O,
) -> Result<(), winit::error::EventLoopError>
where
P: Presenter + 'static,
A: retroglyph_core::App<WindowBackend<P>> + 'static,
O: FnOnce(EventProxy),
{
let mut frame_count = 0u64;
let mut last = web_time::Instant::now();
let exit_requested = Rc::new(Cell::new(false));
let exit_requested_in_loop = exit_requested.clone();
run_windowed_with_proxy_and_exit_flag(
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 {
exit_requested_in_loop.set(true);
}
},
on_proxy,
exit_requested,
)
}
struct InitWindowSize {
width: u32,
height: u32,
}
#[allow(clippy::struct_excessive_bools)]
struct WindowAttrs {
resizable: bool,
decorations: bool,
min_size: Option<(u32, u32)>,
max_size: Option<(u32, u32)>,
initial_position: Option<(i32, i32)>,
fullscreen: bool,
transparency: bool,
}
impl From<&WindowConfig> for WindowAttrs {
fn from(config: &WindowConfig) -> Self {
Self {
resizable: config.resizable,
decorations: config.decorations,
min_size: config.min_size,
max_size: config.max_size,
initial_position: config.initial_position,
fullscreen: config.fullscreen,
transparency: config.transparency,
}
}
}
impl Default for WindowAttrs {
fn default() -> Self {
Self {
resizable: true,
decorations: true,
min_size: None,
max_size: None,
initial_position: None,
fullscreen: false,
transparency: false,
}
}
}
struct WindowApp<P: Presenter, F> {
terminal: Option<Terminal<WindowBackend<P>>>,
app_loop: F,
window: Option<Arc<Window>>,
title: String,
init_size: InitWindowSize,
attrs: WindowAttrs,
#[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,
exit_requested: Rc<Cell<bool>>,
needs_redraw: bool,
consecutive_present_errors: u32,
}
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 = {
let scale_factor = event_loop
.primary_monitor()
.map_or(1.0, |monitor| monitor.scale_factor());
let (width, height) =
physical_size_for(self.init_size.width, self.init_size.height, scale_factor);
winit::dpi::PhysicalSize::new(width, height)
};
#[cfg(target_arch = "wasm32")]
let physical_size = if self.fill_viewport {
web::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::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)
.with_resizable(self.attrs.resizable)
.with_decorations(self.attrs.decorations)
.with_transparent(self.attrs.transparency);
let attrs = match self.attrs.min_size {
Some((w, h)) => attrs.with_min_inner_size(winit::dpi::PhysicalSize::new(w, h)),
None => attrs,
};
let attrs = match self.attrs.max_size {
Some((w, h)) => attrs.with_max_inner_size(winit::dpi::PhysicalSize::new(w, h)),
None => attrs,
};
let attrs = match self.attrs.initial_position {
Some((x, y)) => attrs.with_position(winit::dpi::PhysicalPosition::new(x, y)),
None => attrs,
};
let attrs = if self.attrs.fullscreen {
attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)))
} else {
attrs
};
#[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 {
web::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)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn physical_size_for(logical_width: u32, logical_height: u32, scale_factor: f64) -> (u32, u32) {
(
(f64::from(logical_width) * scale_factor).round() as u32,
(f64::from(logical_height) * scale_factor).round() as u32,
)
}
const PRESENT_FAILURE_RECOVERY_THRESHOLD: u32 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PresentFailureAction {
Ok { was_failing: bool },
Log { at_error_level: bool },
Recover,
}
const fn present_failure_action(
consecutive_failures: u32,
succeeded: bool,
) -> PresentFailureAction {
if succeeded {
return PresentFailureAction::Ok {
was_failing: consecutive_failures > 0,
};
}
if (consecutive_failures + 1).is_multiple_of(PRESENT_FAILURE_RECOVERY_THRESHOLD) {
return PresentFailureAction::Recover;
}
PresentFailureAction::Log {
at_error_level: consecutive_failures == 0,
}
}
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),
}
}
impl<P, F> ApplicationHandler<UserEvent> 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);
}
self.needs_redraw = true;
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
self.handle_window_event(event);
if self.exit_requested.get() {
event_loop.exit();
}
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
self.handle_user_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();
}
return;
}
if self.needs_redraw {
self.needs_redraw = false;
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_user_event(&mut self, event: UserEvent) {
if let Some(term) = self.terminal.as_mut() {
term.backend_mut().push_event(Event::Custom(event));
}
self.needs_redraw = true;
}
fn handle_window_event(&mut self, event: WindowEvent) {
if !matches!(event, WindowEvent::RedrawRequested) {
self.needs_redraw = true;
}
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::Focused(gained) => self.on_focus_changed(gained),
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::ScaleFactorChanged { scale_factor, .. } => {
self.on_scale_factor_changed(scale_factor);
}
WindowEvent::RedrawRequested => self.handle_redraw_requested(),
_ => {}
}
}
fn handle_redraw_requested(&mut self) {
let Some(term) = self.terminal.as_mut() else {
return;
};
(self.app_loop)(term);
let result = term.backend_mut().presenter_mut().present();
let succeeded = result.is_ok();
match present_failure_action(self.consecutive_present_errors, succeeded) {
PresentFailureAction::Ok { was_failing } => {
if was_failing {
log::info!(
"frame present recovered after {} consecutive failures",
self.consecutive_present_errors
);
}
self.consecutive_present_errors = 0;
}
PresentFailureAction::Log { at_error_level } => {
self.consecutive_present_errors += 1;
let e = result.unwrap_err();
if at_error_level {
log::error!("frame present failed: {e}");
} else {
log::debug!("frame present still failing: {e}");
}
}
PresentFailureAction::Recover => {
self.consecutive_present_errors += 1;
let e = result.unwrap_err();
log::warn!(
"frame present failed {} times consecutively ({e}); attempting surface recovery",
self.consecutive_present_errors
);
self.try_recover_surface();
}
}
}
fn try_recover_surface(&mut self) {
let Some(window) = self.window.clone() else {
return;
};
let Some(term) = self.terminal.as_mut() else {
return;
};
let handle: Arc<dyn crate::presenter::WindowHandle> = window;
if let Err(e) = term.backend_mut().presenter_mut().init_surface(handle) {
log::error!("surface recovery failed: {e}");
}
}
fn on_resized(&mut self, size: winit::dpi::PhysicalSize<u32>) {
#[cfg(target_arch = "wasm32")]
let size = if self.fill_viewport {
web::web_viewport_surface_physical_size().unwrap_or(size)
} else {
size
};
self.resize_to(size);
}
fn on_scale_factor_changed(&mut self, scale_factor: f64) {
if let Some(term) = self.terminal.as_mut() {
term.backend_mut()
.presenter_mut()
.scale_factor_changed(scale_factor);
}
let Some(window) = self.window.clone() else {
return;
};
self.resize_to(window.inner_size());
}
fn resize_to(&mut self, size: winit::dpi::PhysicalSize<u32>) {
let Some(term) = self.terminal.as_mut() else {
return;
};
let (cell_w, cell_h) = term.backend().presenter().cell_size();
let cols = (size.width / cell_w).max(1);
let rows = (size.height / cell_h).max(1);
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 as u16, rows as u16));
}
fn on_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
#[cfg(target_arch = "wasm32")]
let scale = if self.fill_viewport {
web::wasm_pointer_scale()
} else {
1.0
};
#[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)
}
fn on_focus_changed(&mut self, gained: bool) {
if let Some(term) = self.terminal.as_mut() {
let event = if gained {
Event::FocusGained
} else {
Event::FocusLost
};
term.backend_mut().push_event(event);
}
if !gained {
self.current_modifiers = KeyModifiers::NONE;
if self.active_touch.take().is_some() {
self.on_mouse_input(
winit::event::ElementState::Released,
winit::event::MouseButton::Left,
);
}
}
}
}
#[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::cell::RefCell;
use std::time::Duration;
#[test]
fn physical_size_for_unscaled_monitor_is_unchanged() {
assert_eq!(physical_size_for(80, 80, 1.0), (80, 80));
}
#[test]
fn physical_size_for_hidpi_monitor_scales_up() {
assert_eq!(physical_size_for(80, 80, 2.0), (160, 160));
}
#[test]
fn physical_size_for_fractional_scale_rounds() {
assert_eq!(physical_size_for(81, 81, 1.5), (122, 122));
}
#[test]
fn fit_defaults_match_winit_defaults() {
let presenter = MockPresenter::default();
let config = WindowConfig::fit(&presenter, "test", None);
assert!(config.resizable);
assert!(config.decorations);
assert_eq!(config.min_size, None);
assert_eq!(config.max_size, None);
assert_eq!(config.initial_position, None);
assert!(!config.fullscreen);
assert!(!config.transparency);
assert!(!config.fill_viewport);
}
#[test]
fn builder_chain_sets_each_attribute() {
let presenter = MockPresenter::default();
let config = WindowConfig::fit(&presenter, "test", None)
.resizable(false)
.decorations(false)
.min_size(320, 240)
.max_size(1920, 1080)
.initial_position(10, 20)
.fullscreen(true)
.transparency(true);
assert!(!config.resizable);
assert!(!config.decorations);
assert_eq!(config.min_size, Some((320, 240)));
assert_eq!(config.max_size, Some((1920, 1080)));
assert_eq!(config.initial_position, Some((10, 20)));
assert!(config.fullscreen);
assert!(config.transparency);
}
#[test]
fn window_attrs_from_config_copies_all_fields() {
let presenter = MockPresenter::default();
let config = WindowConfig::fit(&presenter, "test", None)
.resizable(false)
.decorations(false)
.min_size(1, 2)
.max_size(3, 4)
.initial_position(5, 6)
.fullscreen(true)
.transparency(true);
let attrs = WindowAttrs::from(&config);
assert!(!attrs.resizable);
assert!(!attrs.decorations);
assert_eq!(attrs.min_size, Some((1, 2)));
assert_eq!(attrs.max_size, Some((3, 4)));
assert_eq!(attrs.initial_position, Some((5, 6)));
assert!(attrs.fullscreen);
assert!(attrs.transparency);
}
#[test]
fn present_success_with_no_prior_failures_is_plain_ok() {
assert_eq!(
present_failure_action(0, true),
PresentFailureAction::Ok { was_failing: false }
);
}
#[test]
fn present_success_after_a_failure_streak_reports_recovery() {
assert_eq!(
present_failure_action(5, true),
PresentFailureAction::Ok { was_failing: true }
);
}
#[test]
fn first_failure_in_a_streak_logs_at_error_level() {
assert_eq!(
present_failure_action(0, false),
PresentFailureAction::Log {
at_error_level: true
}
);
}
#[test]
fn subsequent_failures_below_threshold_log_below_error_level() {
for count in 1..PRESENT_FAILURE_RECOVERY_THRESHOLD - 1 {
assert_eq!(
present_failure_action(count, false),
PresentFailureAction::Log {
at_error_level: false
},
"consecutive_failures = {count}"
);
}
}
#[test]
fn failure_crossing_the_threshold_triggers_recovery() {
assert_eq!(
present_failure_action(PRESENT_FAILURE_RECOVERY_THRESHOLD - 1, false),
PresentFailureAction::Recover
);
}
#[test]
fn failure_recovers_again_every_full_threshold_after_the_first() {
assert_eq!(
present_failure_action(2 * PRESENT_FAILURE_RECOVERY_THRESHOLD - 1, false),
PresentFailureAction::Recover
);
for count in
PRESENT_FAILURE_RECOVERY_THRESHOLD..(2 * PRESENT_FAILURE_RECOVERY_THRESHOLD - 1)
{
assert_eq!(
present_failure_action(count, false),
PresentFailureAction::Log {
at_error_level: false
},
"consecutive_failures = {count}"
);
}
}
#[derive(Default)]
struct MockPresenter {
last_scale_factor: Cell<Option<f64>>,
}
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, Option<&'a str>)>,
{
Ok(())
}
fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u8, Pos, &'a Tile, Option<&'a str>)>,
{
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)
}
fn scale_factor_changed(&mut self, scale_factor: f64) {
self.last_scale_factor.set(Some(scale_factor));
}
}
#[derive(Default)]
struct RecordingPresenter {
resize_calls: Rc<RefCell<Vec<(u32, u32)>>>,
}
impl Presenter for RecordingPresenter {
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, Option<&'a str>)>,
{
Ok(())
}
fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u8, Pos, &'a Tile, Option<&'a str>)>,
{
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) {
self.resize_calls.borrow_mut().push((width, height));
}
fn present(&mut self) -> Result<(), Self::SurfaceError> {
Ok(())
}
fn cell_size(&self) -> (u32, u32) {
(8, 16)
}
}
#[derive(Default)]
struct FailingPresenter {
failing: Rc<Cell<bool>>,
init_surface_calls: Rc<Cell<u32>>,
}
impl Presenter for FailingPresenter {
type Error = core::convert::Infallible;
type SurfaceError = &'static str;
fn draw<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (Pos, &'a Tile, Option<&'a str>)>,
{
Ok(())
}
fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u8, Pos, &'a Tile, Option<&'a str>)>,
{
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> {
self.init_surface_calls
.set(self.init_surface_calls.get() + 1);
Ok(())
}
fn resize_surface(&mut self, _width: u32, _height: u32) {}
fn present(&mut self) -> Result<(), Self::SurfaceError> {
if self.failing.get() {
Err("simulated present failure")
} else {
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::default()));
WindowApp {
terminal: Some(terminal),
app_loop: |_| {},
window: None,
title: String::new(),
init_size: InitWindowSize {
width: 80,
height: 80,
},
attrs: WindowAttrs::default(),
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(),
exit_requested: Rc::new(Cell::new(false)),
needs_redraw: false,
consecutive_present_errors: 0,
}
}
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::default());
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.clone());
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::default());
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.clone());
backend.push_event(clicked.clone());
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 user_event_pushes_custom_event() {
let mut app = test_window_app();
app.handle_user_event(42);
assert_eq!(poll(&mut app), Some(Event::Custom(42)));
}
#[test]
fn multiple_user_events_preserve_fifo_order() {
let mut app = test_window_app();
app.handle_user_event(1);
app.handle_user_event(2);
assert_eq!(poll(&mut app), Some(Event::Custom(1)));
assert_eq!(poll(&mut app), Some(Event::Custom(2)));
assert_eq!(poll(&mut app), None);
}
#[test]
fn user_events_interleave_with_window_events_in_arrival_order() {
let mut app = test_window_app();
app.handle_user_event(7);
app.handle_window_event(WindowEvent::CloseRequested);
assert_eq!(poll(&mut app), Some(Event::Custom(7)));
assert_eq!(poll(&mut app), Some(Event::Close));
}
#[test]
fn event_proxy_closed_reports_the_undelivered_id() {
let err = EventProxyClosed(42);
assert_eq!(err.into_inner(), 42);
assert_eq!(err.to_string(), "event loop closed");
}
#[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));
}
type BoxedAppLoop = Box<dyn FnMut(&mut Terminal<WindowBackend<MockPresenter>>)>;
type BoxedApp = WindowApp<MockPresenter, BoxedAppLoop>;
#[test]
fn redraw_requested_runs_app_loop_and_does_not_set_exit_by_default() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::RedrawRequested);
assert!(!app.exit_requested.get());
}
#[test]
fn app_loop_setting_exit_requested_is_observed_after_redraw() {
let terminal = Terminal::new(WindowBackend::new(MockPresenter::default()));
let exit_requested = Rc::new(Cell::new(false));
let exit_requested_in_loop = exit_requested.clone();
let app_loop: BoxedAppLoop = Box::new(move |_term| exit_requested_in_loop.set(true));
let mut app: BoxedApp = WindowApp {
terminal: Some(terminal),
app_loop,
window: None,
title: String::new(),
init_size: InitWindowSize {
width: 80,
height: 80,
},
attrs: WindowAttrs::default(),
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(),
exit_requested,
needs_redraw: false,
consecutive_present_errors: 0,
};
assert!(!app.exit_requested.get());
app.handle_window_event(WindowEvent::RedrawRequested);
assert!(app.exit_requested.get());
}
#[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 focused_pushes_focus_gained_and_lost_events() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::Focused(true));
assert_eq!(poll(&mut app), Some(Event::FocusGained));
app.handle_window_event(WindowEvent::Focused(false));
assert_eq!(poll(&mut app), Some(Event::FocusLost));
}
#[test]
fn focus_lost_resets_stuck_modifiers() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::ModifiersChanged(
winit::event::Modifiers::from(winit::keyboard::ModifiersState::SHIFT),
));
let _ = poll(&mut app); assert_eq!(app.current_modifiers, KeyModifiers::SHIFT);
app.handle_window_event(WindowEvent::Focused(false));
assert_eq!(poll(&mut app), Some(Event::FocusLost));
assert_eq!(app.current_modifiers, KeyModifiers::NONE);
app.handle_window_event(WindowEvent::Focused(true));
assert_eq!(poll(&mut app), Some(Event::FocusGained));
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 == KeyModifiers::NONE
));
}
#[test]
fn focus_lost_releases_stuck_active_touch() {
use winit::event::TouchPhase;
let mut app = test_window_app();
app.handle_window_event(touch(3, TouchPhase::Started, 20.0, 18.0));
poll(&mut app); poll(&mut app); assert_eq!(app.active_touch, Some(3));
app.handle_window_event(WindowEvent::Focused(false));
assert_eq!(poll(&mut app), Some(Event::FocusLost));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
..
}))
));
assert_eq!(poll(&mut app), None);
assert_eq!(app.active_touch, None);
app.handle_window_event(WindowEvent::Focused(true));
assert_eq!(poll(&mut app), Some(Event::FocusGained));
app.handle_window_event(touch(4, TouchPhase::Started, 40.0, 32.0));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
}))
));
assert!(matches!(
poll(&mut app),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
..
}))
));
assert_eq!(app.active_touch, Some(4));
}
#[test]
fn focus_lost_without_active_touch_pushes_no_extra_events() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::Focused(false));
assert_eq!(poll(&mut app), Some(Event::FocusLost));
assert_eq!(poll(&mut app), None);
}
#[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)));
}
#[test]
fn scale_factor_changed_notifies_presenter() {
let mut app = test_window_app();
app.on_scale_factor_changed(2.0);
assert_eq!(
app.terminal
.as_ref()
.unwrap()
.backend()
.presenter()
.last_scale_factor
.get(),
Some(2.0)
);
}
#[test]
fn scale_factor_changed_without_a_window_is_a_no_op_resize() {
let mut app = test_window_app();
app.on_scale_factor_changed(2.0);
assert_eq!(poll(&mut app), None);
}
#[test]
fn resize_to_clamps_to_whole_cells_and_pushes_resize_event() {
let mut app = test_window_app();
app.resize_to(winit::dpi::PhysicalSize::new(90, 81));
assert_eq!(poll(&mut app), Some(Event::Resize(11, 5)));
}
#[test]
fn resized_below_one_cell_clamps_surface_and_event_to_1x1() {
type RecordingApp =
WindowApp<RecordingPresenter, fn(&mut Terminal<WindowBackend<RecordingPresenter>>)>;
let resize_calls = Rc::new(RefCell::new(Vec::new()));
let presenter = RecordingPresenter {
resize_calls: resize_calls.clone(),
};
let terminal = Terminal::new(WindowBackend::new(presenter));
let mut app: RecordingApp = WindowApp {
terminal: Some(terminal),
app_loop: |_| {},
window: None,
title: String::new(),
init_size: InitWindowSize {
width: 80,
height: 80,
},
attrs: WindowAttrs::default(),
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(),
exit_requested: Rc::new(Cell::new(false)),
needs_redraw: false,
consecutive_present_errors: 0,
};
app.handle_window_event(WindowEvent::Resized(winit::dpi::PhysicalSize::new(4, 4)));
assert_eq!(resize_calls.borrow().as_slice(), &[(8, 16)]);
assert_eq!(
app.terminal
.as_mut()
.unwrap()
.backend_mut()
.poll_event(Duration::ZERO),
Some(Event::Resize(1, 1))
);
}
#[test]
fn fresh_app_does_not_need_a_redraw() {
let app = test_window_app();
assert!(!app.needs_redraw);
}
#[test]
fn window_event_sets_needs_redraw() {
let mut app = test_window_app();
assert!(!app.needs_redraw);
app.handle_window_event(WindowEvent::CursorMoved {
device_id: winit::event::DeviceId::dummy(),
position: winit::dpi::PhysicalPosition::new(1.0_f64, 1.0_f64),
});
assert!(app.needs_redraw);
}
#[test]
fn redraw_requested_does_not_itself_set_needs_redraw() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::RedrawRequested);
assert!(!app.needs_redraw);
}
#[test]
fn user_event_sets_needs_redraw() {
let mut app = test_window_app();
assert!(!app.needs_redraw);
app.handle_user_event(1);
assert!(app.needs_redraw);
}
#[test]
fn unhandled_window_events_still_set_needs_redraw() {
let mut app = test_window_app();
app.handle_window_event(WindowEvent::Occluded(true));
assert!(app.needs_redraw);
}
type FailingApp =
WindowApp<FailingPresenter, fn(&mut Terminal<WindowBackend<FailingPresenter>>)>;
fn failing_app() -> (FailingApp, Rc<Cell<bool>>, Rc<Cell<u32>>) {
let failing = Rc::new(Cell::new(false));
let init_surface_calls = Rc::new(Cell::new(0));
let presenter = FailingPresenter {
failing: failing.clone(),
init_surface_calls: init_surface_calls.clone(),
};
let terminal = Terminal::new(WindowBackend::new(presenter));
let app = WindowApp {
terminal: Some(terminal),
app_loop: (|_| {}) as fn(&mut Terminal<WindowBackend<FailingPresenter>>),
window: None,
title: String::new(),
init_size: InitWindowSize {
width: 80,
height: 80,
},
attrs: WindowAttrs::default(),
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(),
exit_requested: Rc::new(Cell::new(false)),
needs_redraw: false,
consecutive_present_errors: 0,
};
(app, failing, init_surface_calls)
}
#[test]
fn successful_presents_never_increment_the_failure_counter() {
let (mut app, _failing, _init_calls) = failing_app();
for _ in 0..5 {
app.handle_redraw_requested();
}
assert_eq!(app.consecutive_present_errors, 0);
}
#[test]
fn failing_presents_increment_the_counter_and_stop_short_of_recovery() {
let (mut app, failing, init_calls) = failing_app();
failing.set(true);
for _ in 0..PRESENT_FAILURE_RECOVERY_THRESHOLD - 1 {
app.handle_redraw_requested();
}
assert_eq!(
app.consecutive_present_errors,
PRESENT_FAILURE_RECOVERY_THRESHOLD - 1
);
assert_eq!(init_calls.get(), 0);
}
#[test]
fn counter_resets_after_recovering_from_a_failure_streak() {
let (mut app, failing, _init_calls) = failing_app();
failing.set(true);
for _ in 0..5 {
app.handle_redraw_requested();
}
assert_eq!(app.consecutive_present_errors, 5);
failing.set(false);
app.handle_redraw_requested();
assert_eq!(app.consecutive_present_errors, 0);
}
#[test]
fn crossing_the_recovery_threshold_attempts_recovery_without_panicking() {
let (mut app, failing, init_calls) = failing_app();
failing.set(true);
for _ in 0..PRESENT_FAILURE_RECOVERY_THRESHOLD {
app.handle_redraw_requested();
}
assert_eq!(
app.consecutive_present_errors,
PRESENT_FAILURE_RECOVERY_THRESHOLD
);
assert_eq!(
init_calls.get(),
0,
"no window means try_recover_surface's guard skips init_surface"
);
}
#[test]
fn try_recover_surface_without_a_window_is_a_no_op() {
let (mut app, _failing, init_calls) = failing_app();
app.try_recover_surface();
assert_eq!(init_calls.get(), 0);
}
}