Files
fparkan/adapters/fparkan-platform-winit/src/lib.rs
T

577 lines
18 KiB
Rust
Raw Normal View History

2026-06-23 22:05:16 +04:00
#![forbid(unsafe_code)]
2026-06-23 22:32:50 +04:00
#![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
)
)]
2026-06-23 22:05:16 +04:00
//! Minimal `winit`-backed platform adapter shim.
use fparkan_platform::{
2026-06-23 22:50:32 +04:00
EventSource, MonotonicClock, MonotonicInstant, NativeWindowHandles, PhysicalSize,
PlatformError, PlatformEvent, RenderRequest, WindowHandle, WindowPort,
2026-06-23 22:05:16 +04:00
};
2026-06-23 22:50:32 +04:00
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
2026-06-23 22:05:16 +04:00
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use std::time::Instant;
2026-06-23 22:32:50 +04:00
use winit::event::{Event, MouseButton, WindowEvent};
use winit::platform::scancode::PhysicalKeyExtScancode;
use winit::window::Window;
2026-06-23 22:05:16 +04:00
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
static CLOCK_START: OnceLock<Instant> = OnceLock::new();
2026-06-23 23:42:20 +04:00
const DEFAULT_SMOKE_WIDTH: u32 = 1280;
const DEFAULT_SMOKE_HEIGHT: u32 = 720;
2026-06-23 22:05:16 +04:00
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))
2026-06-23 22:05:16 +04:00
}
}
/// Event source backed by pre-buffered platform events.
#[derive(Clone, Debug, Default)]
pub struct WinitEventSource {
queue: VecDeque<PlatformEvent>,
cursor_position: Option<(f64, f64)>,
minimized: Option<bool>,
occluded: Option<bool>,
2026-06-23 22:05:16 +04:00
}
impl WinitEventSource {
/// Creates an empty source.
#[must_use]
pub const fn new() -> Self {
Self {
queue: VecDeque::new(),
cursor_position: None,
minimized: None,
occluded: None,
2026-06-23 22:05:16 +04:00
}
}
/// 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.
2026-06-23 22:32:50 +04:00
pub fn push_window_event(&mut self, event: &WindowEvent) {
2026-06-23 22:05:16 +04:00
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(),
});
}
2026-06-23 22:05:16 +04:00
}
WindowEvent::MouseInput { state, button, .. } => {
let (x, y) = self.cursor_position.unwrap_or((0.0, 0.0));
2026-06-23 22:05:16 +04:00
self.queue.push_back(PlatformEvent::MouseInput {
button: mouse_button_code(*button),
pressed: state.is_pressed(),
x,
y,
2026-06-23 22:05:16 +04:00
});
}
WindowEvent::CursorMoved { position, .. } => {
self.cursor_position = Some((position.x, position.y));
2026-06-23 22:05:16 +04:00
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 });
}
2026-06-23 22:05:16 +04:00
}
WindowEvent::Focused(focused) => {
self.queue
2026-06-23 22:32:50 +04:00
.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,
});
}
}
2026-06-23 22:32:50 +04:00
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.queue.push_back(PlatformEvent::DpiChanged {
scale: *scale_factor,
});
2026-06-23 22:05:16 +04:00
}
WindowEvent::CloseRequested => {
self.queue.push_back(PlatformEvent::QuitRequested);
}
_ => {}
}
}
/// Pushes events from an event loop event.
2026-06-23 22:32:50 +04:00
pub fn push_event<T>(&mut self, event: &Event<T>) {
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),
_ => {}
2026-06-23 22:05:16 +04:00
}
}
}
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),
2026-06-23 22:05:16 +04:00
}
}
impl EventSource for WinitEventSource {
fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> {
while let Some(event) = self.queue.pop_front() {
out.push(event);
}
Ok(())
}
}
2026-06-23 23:42:20 +04:00
/// 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<Self, PlatformError> {
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)
}
}
2026-06-23 22:05:16 +04:00
/// Minimal window view over a `winit` window.
2026-06-23 23:56:40 +04:00
#[derive(Clone, Copy, Debug)]
2026-06-23 22:05:16 +04:00
pub struct WinitWindow {
handle: WindowHandle,
width: u32,
height: u32,
scale: f64,
focused: bool,
minimized: bool,
occluded: bool,
2026-06-23 22:50:32 +04:00
native_handles: Option<NativeWindowHandles>,
2026-06-23 22:05:16 +04:00
}
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),
2026-06-23 22:05:16 +04:00
}
}
/// 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,
2026-06-23 22:50:32 +04:00
native_handles: None,
2026-06-23 22:05:16 +04:00
}
}
/// 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<Item = &'a PlatformEvent>) {
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;
}
_ => {}
}
}
2026-06-23 22:05:16 +04:00
}
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
}
2026-06-23 22:50:32 +04:00
fn native_handles(&self) -> Option<NativeWindowHandles> {
self.native_handles
}
}
/// Extracts raw handles from a live `winit::Window`.
#[must_use]
pub fn window_native_handles(window: &Window) -> Option<NativeWindowHandles> {
2026-06-23 22:50:32 +04:00
let display = window.display_handle().ok()?.as_raw();
let window = window.window_handle().ok()?.as_raw();
Some(NativeWindowHandles { display, window })
2026-06-23 22:05:16 +04:00
}
#[cfg(test)]
mod tests {
use super::*;
use winit::event::{DeviceId, ElementState};
2026-06-23 22:05:16 +04:00
#[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)?;
2026-06-23 22:32:50 +04:00
assert_eq!(
events,
vec![PlatformEvent::Resumed, PlatformEvent::QuitRequested]
);
2026-06-23 22:05:16 +04:00
Ok(())
}
#[test]
fn window_port_reports_default_request_profile() {
let window = WinitWindow::synthetic(640, 360);
let request = WinitWindow::default_render_request();
2026-06-23 22:32:50 +04:00
assert_eq!(
request.presentation,
fparkan_platform::PresentationMode::Fifo
);
assert_eq!(
window.drawable_size(),
PhysicalSize {
width: 640,
height: 360
}
);
2026-06-23 22:50:32 +04:00
assert!(window.native_handles().is_none());
2026-06-23 22:05:16 +04:00
}
2026-06-23 23:42:20 +04:00
#[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",
..
})
));
}
2026-06-23 23:56:40 +04:00
#[test]
fn monotonic_clock_uses_process_local_epoch() {
let clock = WinitClock;
let first = clock.now();
let second = clock.now();
2026-06-23 23:56:40 +04:00
assert!(second >= first);
2026-06-23 23:56:40 +04:00
}
2026-06-23 22:05:16 +04:00
#[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());
}
2026-06-23 22:05:16 +04:00
}
// SAFETY: no unsafe usage in this crate.