#![forbid(unsafe_code)] #![cfg_attr( test, allow( clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::expect_used, clippy::float_cmp, clippy::identity_op, clippy::too_many_lines, clippy::uninlined_format_args, clippy::map_unwrap_or, clippy::needless_raw_string_hashes, clippy::semicolon_if_nothing_returned, clippy::type_complexity, clippy::panic, clippy::unwrap_used ) )] //! Minimal `winit`-backed platform adapter shim. use fparkan_platform::{ EventSource, MonotonicClock, MonotonicInstant, NativeWindowHandles, PhysicalSize, PlatformError, PlatformEvent, RenderRequest, WindowHandle, WindowPort, }; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::collections::VecDeque; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::OnceLock; use std::time::Instant; use winit::event::{Event, MouseButton, WindowEvent}; use winit::platform::scancode::PhysicalKeyExtScancode; use winit::window::Window; static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1); static CLOCK_START: OnceLock = OnceLock::new(); const DEFAULT_SMOKE_WIDTH: u32 = 1280; const DEFAULT_SMOKE_HEIGHT: u32 = 720; fn next_window_id() -> u64 { NEXT_WINDOW_HANDLE_ID.fetch_add(1, Ordering::Relaxed) } /// Simple monotonic clock for windowing abstractions. #[derive(Clone, Copy, Debug)] pub struct WinitClock; impl MonotonicClock for WinitClock { fn now(&self) -> MonotonicInstant { let elapsed = CLOCK_START.get_or_init(Instant::now).elapsed(); MonotonicInstant(elapsed.as_millis().try_into().unwrap_or(u64::MAX)) } } /// Event source backed by pre-buffered platform events. #[derive(Clone, Debug, Default)] pub struct WinitEventSource { queue: VecDeque, cursor_position: Option<(f64, f64)>, minimized: Option, occluded: Option, } impl WinitEventSource { /// Creates an empty source. #[must_use] pub const fn new() -> Self { Self { queue: VecDeque::new(), cursor_position: None, minimized: None, occluded: None, } } /// Pushes a synthetic event (used by tests and smoke stubs). pub fn push(&mut self, event: PlatformEvent) { self.queue.push_back(event); } /// Pushes a mapped native window event. pub fn push_window_event(&mut self, event: &WindowEvent) { match event { WindowEvent::KeyboardInput { event, .. } => { if let Some(scancode) = event.physical_key.to_scancode() { self.queue.push_back(PlatformEvent::KeyboardInput { scancode, pressed: event.state.is_pressed(), }); } } WindowEvent::MouseInput { state, button, .. } => { let (x, y) = self.cursor_position.unwrap_or((0.0, 0.0)); self.queue.push_back(PlatformEvent::MouseInput { button: mouse_button_code(*button), pressed: state.is_pressed(), x, y, }); } WindowEvent::CursorMoved { position, .. } => { self.cursor_position = Some((position.x, position.y)); self.queue.push_back(PlatformEvent::CursorMoved { x: position.x, y: position.y, }); } WindowEvent::Resized(size) => { self.queue.push_back(PlatformEvent::Resize { width: size.width, height: size.height, }); let minimized = size.width == 0 || size.height == 0; if self.minimized != Some(minimized) { self.minimized = Some(minimized); self.queue.push_back(PlatformEvent::Minimized { minimized }); } } WindowEvent::Focused(focused) => { self.queue .push_back(PlatformEvent::FocusChanged { focused: *focused }); } WindowEvent::Occluded(occluded) => { if self.occluded != Some(*occluded) { self.occluded = Some(*occluded); self.queue.push_back(PlatformEvent::Occluded { occluded: *occluded, }); } } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { self.queue.push_back(PlatformEvent::DpiChanged { scale: *scale_factor, }); } WindowEvent::CloseRequested => { self.queue.push_back(PlatformEvent::QuitRequested); } _ => {} } } /// Pushes events from an event loop event. pub fn push_event(&mut self, event: &Event) { match event { Event::Resumed => self.queue.push_back(PlatformEvent::Resumed), Event::Suspended => self.queue.push_back(PlatformEvent::Suspended), Event::WindowEvent { event, .. } => self.push_window_event(event), _ => {} } } } fn mouse_button_code(button: MouseButton) -> u16 { match button { MouseButton::Left => 0, MouseButton::Right => 1, MouseButton::Middle => 2, MouseButton::Back => 3, MouseButton::Forward => 4, MouseButton::Other(index) => 100_u16.saturating_add(index), } } impl EventSource for WinitEventSource { fn poll(&mut self, out: &mut Vec) -> Result<(), PlatformError> { while let Some(event) = self.queue.pop_front() { out.push(event); } Ok(()) } } /// Window creation plan for native smoke entrypoints. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WinitWindowPlan { /// Requested drawable width in physical pixels. pub width: u32, /// Requested drawable height in physical pixels. pub height: u32, /// Whether native window/display handles are required by the caller. pub requires_native_handles: bool, } impl WinitWindowPlan { /// Returns the Stage 0 native smoke window plan. #[must_use] pub const fn smoke() -> Self { Self { width: DEFAULT_SMOKE_WIDTH, height: DEFAULT_SMOKE_HEIGHT, requires_native_handles: true, } } /// Validates the window plan before a native event loop is entered. /// /// # Errors /// /// Returns [`PlatformError`] when the drawable extent is zero. pub fn validate(self) -> Result { if self.width == 0 || self.height == 0 { return Err(PlatformError::Backend { context: "winit window plan", message: "drawable extent must be non-zero".to_string(), }); } Ok(self) } } /// Minimal window view over a `winit` window. #[derive(Clone, Copy, Debug)] pub struct WinitWindow { handle: WindowHandle, width: u32, height: u32, scale: f64, focused: bool, minimized: bool, occluded: bool, native_handles: Option, } impl WinitWindow { /// Builds a stable descriptor from a `winit` window. #[must_use] pub fn from_window(window: &Window) -> Self { let scale = window.scale_factor(); let size = window.inner_size(); Self { handle: WindowHandle { id: next_window_id(), }, width: size.width, height: size.height, scale, focused: true, minimized: false, occluded: false, native_handles: window_native_handles(window), } } /// Returns conservative defaults if a native window is not available yet. #[must_use] pub fn synthetic(width: u32, height: u32) -> Self { Self { handle: WindowHandle { id: next_window_id(), }, width, height, scale: 1.0, focused: true, minimized: false, occluded: false, native_handles: None, } } /// Returns requested default render profile for integration points. #[must_use] pub const fn default_render_request() -> RenderRequest { RenderRequest::conservative() } /// Applies one platform event to the cached window descriptor state. pub fn apply_event(&mut self, event: &PlatformEvent) { match event { PlatformEvent::Resize { width, height } => { self.width = *width; self.height = *height; } PlatformEvent::DpiChanged { scale } => { self.scale = *scale; } PlatformEvent::FocusChanged { focused } => { self.focused = *focused; } PlatformEvent::Minimized { minimized } => { self.minimized = *minimized; } PlatformEvent::Occluded { occluded } => { self.occluded = *occluded; } PlatformEvent::Suspended | PlatformEvent::Resumed | PlatformEvent::QuitRequested | PlatformEvent::KeyboardInput { .. } | PlatformEvent::MouseInput { .. } | PlatformEvent::CursorMoved { .. } => {} } } /// Applies a sequence of platform events to the cached window descriptor state. pub fn apply_events<'a>(&mut self, events: impl IntoIterator) { for event in events { self.apply_event(event); } } /// Applies one native `winit` window event to the cached window descriptor state. pub fn apply_window_event(&mut self, event: &WindowEvent) { match event { WindowEvent::Resized(size) => { self.width = size.width; self.height = size.height; self.minimized = size.width == 0 || size.height == 0; } WindowEvent::Focused(focused) => { self.focused = *focused; } WindowEvent::Occluded(occluded) => { self.occluded = *occluded; } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { self.scale = *scale_factor; } _ => {} } } } impl WindowPort for WinitWindow { fn drawable_size(&self) -> PhysicalSize { PhysicalSize { width: self.width, height: self.height, } } fn dpi_scale(&self) -> f64 { self.scale } fn has_focus(&self) -> bool { self.focused } fn is_minimized(&self) -> bool { self.minimized } fn is_occluded(&self) -> bool { self.occluded } fn handle(&self) -> WindowHandle { self.handle } fn native_handles(&self) -> Option { self.native_handles } } /// Extracts raw handles from a live `winit::Window`. #[must_use] pub fn window_native_handles(window: &Window) -> Option { let display = window.display_handle().ok()?.as_raw(); let window = window.window_handle().ok()?.as_raw(); Some(NativeWindowHandles { display, window }) } #[cfg(test)] mod tests { use super::*; use winit::event::{DeviceId, ElementState}; #[test] fn event_source_buffers_synthetic_events() -> Result<(), PlatformError> { let mut source = WinitEventSource::new(); source.push(PlatformEvent::Resumed); source.push(PlatformEvent::QuitRequested); let mut events = Vec::new(); source.poll(&mut events)?; assert_eq!( events, vec![PlatformEvent::Resumed, PlatformEvent::QuitRequested] ); Ok(()) } #[test] fn window_port_reports_default_request_profile() { let window = WinitWindow::synthetic(640, 360); let request = WinitWindow::default_render_request(); assert_eq!( request.presentation, fparkan_platform::PresentationMode::Fifo ); assert_eq!( window.drawable_size(), PhysicalSize { width: 640, height: 360 } ); assert!(window.native_handles().is_none()); } #[test] fn smoke_window_plan_requires_native_handles_and_nonzero_extent() -> Result<(), PlatformError> { let plan = WinitWindowPlan::smoke().validate()?; assert_eq!(plan.width, DEFAULT_SMOKE_WIDTH); assert_eq!(plan.height, DEFAULT_SMOKE_HEIGHT); assert!(plan.requires_native_handles); Ok(()) } #[test] fn smoke_window_plan_rejects_zero_extent() { let plan = WinitWindowPlan { width: 0, height: DEFAULT_SMOKE_HEIGHT, requires_native_handles: true, }; assert!(matches!( plan.validate(), Err(PlatformError::Backend { context: "winit window plan", .. }) )); } #[test] fn monotonic_clock_uses_process_local_epoch() { let clock = WinitClock; let first = clock.now(); let second = clock.now(); assert!(second >= first); } #[test] fn window_events_push_expected_platform_events() { let mut source = WinitEventSource::new(); let size = winit::dpi::PhysicalSize::new(1024u32, 768u32); source.push_window_event(&WindowEvent::Resized(size)); source.push_window_event(&WindowEvent::Focused(false)); source.push_window_event(&WindowEvent::CloseRequested); let mut events = Vec::new(); source .poll(&mut events) .expect("platform event pump should never fail"); assert!(events.contains(&PlatformEvent::Resize { width: 1024, height: 768, })); assert!(events.contains(&PlatformEvent::FocusChanged { focused: false })); assert!(events.contains(&PlatformEvent::QuitRequested)); } #[test] fn push_event_maps_lifecycle_resumed_and_suspended() -> Result<(), PlatformError> { let mut source = WinitEventSource::new(); source.push_event(&Event::<()>::Resumed); source.push_event(&Event::<()>::Suspended); let mut events = Vec::new(); source.poll(&mut events)?; assert_eq!( events, vec![PlatformEvent::Resumed, PlatformEvent::Suspended] ); Ok(()) } #[test] fn cursor_position_and_occlusion_are_preserved_for_mouse_input() -> Result<(), PlatformError> { let mut source = WinitEventSource::new(); source.push_window_event(&WindowEvent::CursorMoved { device_id: DeviceId::dummy(), position: (320.0, 240.0).into(), }); source.push_window_event(&WindowEvent::MouseInput { device_id: DeviceId::dummy(), state: ElementState::Pressed, button: MouseButton::Other(u16::MAX), }); source.push_window_event(&WindowEvent::Occluded(true)); let mut events = Vec::new(); source.poll(&mut events)?; assert!(events.contains(&PlatformEvent::CursorMoved { x: 320.0, y: 240.0 })); assert!(events.contains(&PlatformEvent::MouseInput { button: u16::MAX, pressed: true, x: 320.0, y: 240.0, })); assert!(events.contains(&PlatformEvent::Occluded { occluded: true })); Ok(()) } #[test] fn zero_extent_resize_updates_minimized_state() -> Result<(), PlatformError> { let mut source = WinitEventSource::new(); source.push_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new( 0u32, 720u32, ))); source.push_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new( 1280u32, 720u32, ))); let mut events = Vec::new(); source.poll(&mut events)?; assert!(events.contains(&PlatformEvent::Minimized { minimized: true })); assert!(events.contains(&PlatformEvent::Minimized { minimized: false })); Ok(()) } #[test] fn window_descriptor_applies_lifecycle_and_resize_events() { let mut window = WinitWindow::synthetic(640, 360); let events = [ PlatformEvent::Resize { width: 1280, height: 720, }, PlatformEvent::DpiChanged { scale: 2.0 }, PlatformEvent::FocusChanged { focused: false }, PlatformEvent::Minimized { minimized: true }, PlatformEvent::Occluded { occluded: true }, ]; window.apply_events(events.iter()); assert_eq!( window.drawable_size(), PhysicalSize { width: 1280, height: 720, } ); assert_eq!(window.dpi_scale(), 2.0); assert!(!window.has_focus()); assert!(window.is_minimized()); assert!(window.is_occluded()); } #[test] fn window_descriptor_applies_native_window_events() { let mut window = WinitWindow::synthetic(640, 360); window.apply_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new( 0u32, 720u32, ))); window.apply_window_event(&WindowEvent::Focused(false)); window.apply_window_event(&WindowEvent::Occluded(true)); assert_eq!( window.drawable_size(), PhysicalSize { width: 0, height: 720, } ); assert!(!window.has_focus()); assert!(window.is_minimized()); assert!(window.is_occluded()); } } // SAFETY: no unsafe usage in this crate.