aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-platform-winit/src/lib.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 21:05:16 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 21:05:16 +0300
commitf8e447ffee746cfe6580cc0e78a8a225aa39b546 (patch)
treee37ebc6c5edd908fd9f44cd3aaf7bffed8de8a88 /adapters/fparkan-platform-winit/src/lib.rs
parent83d763dd70ef20b7d30a905c15cad3d5531ebc6a (diff)
downloadfparkan-f8e447ffee746cfe6580cc0e78a8a225aa39b546.tar.xz
fparkan-f8e447ffee746cfe6580cc0e78a8a225aa39b546.zip
feat: close stage 0-2 audit groundwork
Remove legacy SDL/OpenGL adapters from the workspace and introduce winit/Vulkan adapter boundaries for the rendered composition root. Add reproducible toolchain and xtask CI coverage for formatting, tests, clippy, docs, policy, deny, acceptance auditing, and hosted OS matrix evidence. Strengthen Stage 1 data contracts with byte-first paths, VFS hardening, structured diagnostics, RsLi writer/edit scaffolding, corpus reporting, and resource error classification. Advance Stage 2 asset preparation by moving mission loading through assets/runtime boundaries, materializing prototype graph data, preserving provenance, and adding inspection/viewer integration. Record the Stage 0-2 audit input, acceptance roadmap, coverage updates, and documentation notes for follow-up evidence.
Diffstat (limited to 'adapters/fparkan-platform-winit/src/lib.rs')
-rw-r--r--adapters/fparkan-platform-winit/src/lib.rs257
1 files changed, 257 insertions, 0 deletions
diff --git a/adapters/fparkan-platform-winit/src/lib.rs b/adapters/fparkan-platform-winit/src/lib.rs
new file mode 100644
index 0000000..ec30908
--- /dev/null
+++ b/adapters/fparkan-platform-winit/src/lib.rs
@@ -0,0 +1,257 @@
+#![forbid(unsafe_code)]
+//! Minimal `winit`-backed platform adapter shim.
+
+use fparkan_platform::{
+ EventSource, MonotonicClock, MonotonicInstant, PlatformEvent, PlatformError, PhysicalSize,
+ RenderRequest, WindowHandle, WindowPort,
+};
+use winit::event::{MouseButton, WindowEvent};
+use winit::event_loop::Event;
+use std::collections::VecDeque;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+use winit::window::Window;
+
+static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
+
+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 duration = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default();
+ MonotonicInstant(duration.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<PlatformEvent>,
+}
+
+impl WinitEventSource {
+ /// Creates an empty source.
+ #[must_use]
+ pub const fn new() -> Self {
+ Self {
+ queue: VecDeque::new(),
+ }
+ }
+
+ /// 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, .. } => {
+ self.queue.push_back(PlatformEvent::KeyboardInput {
+ scancode: event.physical_key.to_scancode().unwrap_or(0),
+ pressed: event.state.is_pressed(),
+ });
+ }
+ WindowEvent::MouseInput { state, button, .. } => {
+ self.queue.push_back(PlatformEvent::MouseInput {
+ button: mouse_button_code(*button),
+ pressed: state.is_pressed(),
+ x: 0.0,
+ y: 0.0,
+ });
+ }
+ WindowEvent::CursorMoved { position, .. } => {
+ 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,
+ });
+ }
+ WindowEvent::Focused(focused) => {
+ self.queue.push_back(PlatformEvent::FocusChanged { focused: *focused });
+ }
+ 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<T>(&mut self, event: &Event<'_, T>) {
+ if let Event::WindowEvent { event, .. } = 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::try_from(index).unwrap_or(0),
+ }
+}
+
+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(())
+ }
+}
+
+/// Minimal window view over a `winit` window.
+#[derive(Clone, Debug)]
+pub struct WinitWindow {
+ handle: WindowHandle,
+ width: u32,
+ height: u32,
+ scale: f64,
+ focused: bool,
+ minimized: bool,
+ occluded: bool,
+}
+
+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,
+ }
+ }
+
+ /// 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,
+ }
+ }
+
+ /// Returns requested default render profile for integration points.
+ #[must_use]
+ pub const fn default_render_request() -> RenderRequest {
+ RenderRequest::conservative()
+ }
+}
+
+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
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[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 });
+ }
+
+ #[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));
+ }
+}
+
+// SAFETY: no unsafe usage in this crate.