From 7083a19bf1029bff21a9550d40cc3260e99aac53 Mon Sep 17 00:00:00 2001 From: HampusM Date: Sat, 18 Oct 2025 17:04:28 +0200 Subject: refactor(engine): use winit instead of glfw --- engine/src/windowing.rs | 669 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 669 insertions(+) create mode 100644 engine/src/windowing.rs (limited to 'engine/src/windowing.rs') diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs new file mode 100644 index 0000000..69adae9 --- /dev/null +++ b/engine/src/windowing.rs @@ -0,0 +1,669 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::thread::{spawn, JoinHandle as ThreadJoinHandle}; + +use crossbeam_channel::{ + bounded as bounded_channel, + Receiver as ChannelReceiver, + Sender as ChannelSender, + TrySendError, +}; +use ecs::actions::Actions; +use ecs::component::Component; +use ecs::entity::obtainer::Obtainer as EntityObtainer; +use ecs::event::component::{Added, Changed, Removed}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; +use ecs::sole::Single; +use ecs::system::observer::Observe; +use ecs::uid::Uid; +use ecs::{declare_entity, Query, Sole}; +use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle}; +use winit::application::ApplicationHandler; +use winit::dpi::PhysicalPosition; +use winit::error::EventLoopError; +use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use winit::event_loop::{ + ActiveEventLoop, + ControlFlow as EventLoopControlFlow, + EventLoop, + OwnedDisplayHandle, +}; +use winit::keyboard::PhysicalKey; +use winit::window::{Window as WinitWindow, WindowId as WinitWindowId}; + +use crate::data_types::dimens::Dimens; +use crate::util::MapVec; +use crate::vector::Vec2; +use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError}; +use crate::windowing::mouse::{ + Button as MouseButton, + ButtonState as MouseButtonState, + Buttons as MouseButtons, + Motion as MouseMotion, +}; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady as WindowCreationReady, + CursorGrabMode, + Id as WindowId, + Window, +}; + +pub mod keyboard; +pub mod mouse; +pub mod window; + +const MESSAGE_FROM_APP_CHANNEL_CAP: usize = 128; + +const MESSAGE_TO_APP_CHANNEL_CAP: usize = 16; // Increase if more messages are added + +declare_entity!( + pub PHASE, + ( + Phase, + Pair::builder() + .relation::() + .target_id(*UPDATE_PHASE) + .build() + ) +); + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + collector.add_sole(Context::default()).ok(); + collector.add_sole(Keyboard::default()).ok(); + collector.add_sole(MouseMotion::default()).ok(); + collector.add_sole(MouseButtons::default()).ok(); + + collector.add_declared_entity(&PHASE); + + collector.add_system(*PHASE, update_stuff); + + collector.add_observer(handle_window_changed); + collector.add_observer(handle_window_removed); + collector.add_observer(handle_window_creation_ready); + } +} + +fn handle_window_creation_ready( + observe: Observe>, + context: Single, +) +{ + for evt_match in &observe { + let Some(ent) = evt_match.get_entity() else { + unreachable!(); + }; + + if ent.has_component(Window::id()) || ent.has_component(WindowClosed::id()) { + continue; + } + + let Some(window_creation_attrs) = ent.get::() else { + unreachable!(); + }; + + context.try_send_message_to_app(MessageToApp::CreateWindow( + ent.uid(), + window_creation_attrs.clone(), + )); + } +} + +#[tracing::instrument(skip_all)] +fn update_stuff( + mut context: Single, + mut keyboard: Single, + mut mouse_motion: Single, + mut mouse_buttons: Single, + mut actions: Actions, + entity_obtainer: EntityObtainer, +) +{ + keyboard.make_key_states_previous(); + mouse_buttons.make_states_previous(); + mouse_motion.position_delta = Vec2::default(); + + let Context { + ref message_from_app_receiver, + ref mut display_handle, + ref mut windows, + .. + } = *context; + + for message in message_from_app_receiver.try_iter() { + match message { + MessageFromApp::Init(new_display_handle) => { + *display_handle = Some(new_display_handle); + } + MessageFromApp::WindowCreated( + window_ent_id, + winit_window, + window_creation_attrs, + ) => { + actions.add_components( + window_ent_id, + (Window::new(&winit_window, &window_creation_attrs),), + ); + + actions.remove_comps::<(WindowCreationReady,)>(window_ent_id); + + tracing::debug!("Added window component to window entity"); + + windows.insert( + WindowId::from_inner(winit_window.id()), + (winit_window, window_ent_id), + ); + } + MessageFromApp::WindowResized(window_id, new_window_size) => { + let Some(window_ent_id) = + windows.get(&window_id).map(|(_, ent_id)| ent_id) + else { + continue; + }; + + let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else { + continue; + }; + + let Some(mut window) = window_ent.get_mut::() else { + continue; + }; + + window.set_inner_size(new_window_size); + + window.set_changed(); + } + MessageFromApp::WindowCloseRequested(window_id) => { + let Some(window_ent_id) = + windows.get(&window_id).map(|(_, ent_id)| ent_id) + else { + tracing::error!( + wid = ?window_id, + "Window does not exist in windowing context" + ); + continue; + }; + + actions.remove_comps::<(Window,)>(*window_ent_id); + } + MessageFromApp::KeyboardKeyStateChanged(key, key_state) => { + keyboard.set_key_state(key, key_state); + } + MessageFromApp::MouseMoved { position_delta } => { + mouse_motion.position_delta += position_delta; + } + MessageFromApp::MouseButtonStateChanged(mouse_button, mouse_button_state) => { + mouse_buttons.set(mouse_button, mouse_button_state); + } + } + } +} + +fn handle_window_changed( + observe: Observe<'_, Pair>, + context: Single, +) +{ + for evt_match in &observe { + let window_ent_id = evt_match.id(); + + let window = evt_match.get_changed_comp(); + + let Some((winit_window, _)) = context.windows.get(&window.wid()) else { + tracing::error!( + wid = ?window.wid(), + entity_id = %window_ent_id, + "Window does not exist in windowing context", + ); + continue; + }; + + window.apply(winit_window); + + context.try_send_message_to_app(MessageToApp::SetWindowCursorGrabMode( + window.wid(), + window.cursor_grab_mode, + )); + } +} + +fn handle_window_removed( + observe: Observe>, + window_query: Query<(&Window,)>, + mut context: Single, + mut actions: Actions, +) +{ + for evt_match in &observe { + let window = evt_match.get_removed_comp(); + + context.windows.remove(window.wid()); + + actions.add_components(evt_match.id(), (WindowClosed,)); + } + + if window_query.iter().count() == 1 { + actions.stop(); + } +} + +#[derive(Debug, Sole)] +pub struct Context +{ + _thread: ThreadJoinHandle<()>, + is_dropped: Arc, + message_from_app_receiver: ChannelReceiver, + message_to_app_sender: ChannelSender, + display_handle: Option, + windows: MapVec, Uid)>, +} + +impl Context +{ + pub fn display_handle(&self) -> Option> + { + let display_handle = self.display_handle.as_ref()?; + + display_handle.display_handle().ok() + } + + /// Returns the specified window as a window handle, if it exists. + /// + /// # Safety + /// The Window handle must only be used with thread safe APIs. + pub unsafe fn get_window_as_handle( + &self, + window_id: &WindowId, + ) -> Option, HandleError>> + { + self.windows.get(window_id).map(|(winit_window, _)| { + #[cfg(windows)] + { + use winit::platform::windows::WindowExtWindows; + + // SAFETY: I don't care + unsafe { winit_window.window_handle_any_thread() } + } + + #[cfg(not(windows))] + { + use raw_window_handle::HasWindowHandle; + + winit_window.window_handle() + } + }) + } + + fn try_send_message_to_app(&self, message: MessageToApp) + { + if let Err(err) = self.message_to_app_sender.try_send(message) { + let error = match &err { + TrySendError::Full(_) => TrySendError::Full(()), + TrySendError::Disconnected(_) => TrySendError::Disconnected(()), + }; + + let message = err.into_inner(); + + tracing::error!("Failed to send message {error}: {message:?}"); + } + } +} + +impl Default for Context +{ + fn default() -> Self + { + let is_dropped = Arc::new(AtomicBool::new(false)); + + let is_dropped_b = is_dropped.clone(); + + let (message_from_app_sender, message_from_app_receiver) = + bounded_channel::(MESSAGE_FROM_APP_CHANNEL_CAP); + + let message_from_app_receiver_b = message_from_app_receiver.clone(); + + let (message_to_app_sender, message_to_app_receiver) = + bounded_channel::(MESSAGE_TO_APP_CHANNEL_CAP); + + Self { + _thread: spawn(move || { + let mut app = App { + message_from_app_sender, + message_from_app_receiver: message_from_app_receiver_b, + message_to_app_receiver, + is_dropped: is_dropped_b, + windows: MapVec::default(), + focused_window_id: None, + }; + + let event_loop = match create_event_loop() { + Ok(event_loop) => event_loop, + Err(err) => { + tracing::error!("Failed to create event loop: {err}"); + return; + } + }; + + event_loop.set_control_flow(EventLoopControlFlow::Poll); + + if let Err(err) = event_loop.run_app(&mut app) { + tracing::error!("Event loop error occurred: {err}"); + } + }), + is_dropped, + message_from_app_receiver, + message_to_app_sender, + display_handle: None, + windows: MapVec::default(), + } + } +} + +impl Drop for Context +{ + fn drop(&mut self) + { + self.is_dropped.store(true, Ordering::Relaxed); + } +} + +fn create_event_loop() -> Result, EventLoopError> +{ + let mut event_loop_builder = EventLoop::builder(); + + #[cfg(any(x11_platform, wayland_platform))] + winit::platform::x11::EventLoopBuilderExtX11::with_any_thread( + &mut event_loop_builder, + true, + ); + + #[cfg(windows)] + winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread( + &mut event_loop_builder, + true, + ); + + #[cfg(not(any(x11_platform, wayland_platform, windows)))] + compile_error!("Unsupported platform"); + + event_loop_builder.build() +} + +#[derive(Debug)] +enum MessageFromApp +{ + Init(OwnedDisplayHandle), + WindowCreated(Uid, Arc, WindowCreationAttributes), + WindowResized(WindowId, Dimens), + WindowCloseRequested(WindowId), + KeyboardKeyStateChanged(Key, KeyState), + MouseMoved + { + position_delta: Vec2, + }, + MouseButtonStateChanged(MouseButton, MouseButtonState), +} + +#[derive(Debug)] +enum MessageToApp +{ + CreateWindow(Uid, WindowCreationAttributes), + SetWindowCursorGrabMode(WindowId, CursorGrabMode), +} + +#[derive(Debug)] +struct App +{ + message_from_app_sender: ChannelSender, + message_from_app_receiver: ChannelReceiver, + message_to_app_receiver: ChannelReceiver, + is_dropped: Arc, + windows: MapVec, WindowSettings)>, + focused_window_id: Option, +} + +impl App +{ + fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop) + { + for message in self.message_to_app_receiver.try_iter() { + match message { + MessageToApp::CreateWindow(window_ent_id, window_creation_attrs) => { + tracing::info!( + "Creating window with title {}", + window_creation_attrs.title() + ); + + let winit_window = Arc::new( + match event_loop + .create_window(window_creation_attrs.clone().into_inner()) + { + Ok(window) => window, + Err(err) => { + tracing::error!("Failed to create window: {err}"); + continue; + } + }, + ); + + tracing::info!("Created window has title {}", winit_window.title()); + + self.windows.insert( + WindowId::from_inner(winit_window.id()), + (Arc::downgrade(&winit_window), WindowSettings::default()), + ); + + self.send_message(MessageFromApp::WindowCreated( + window_ent_id, + winit_window, + window_creation_attrs, + )); + } + MessageToApp::SetWindowCursorGrabMode(window_id, cursor_grab_mode) => { + let Some((_, window_settings)) = self.windows.get_mut(&window_id) + else { + tracing::warn!( + window_id=?window_id, + "Cannot set window cursor grab mode. Window not found" + ); + + continue; + }; + + window_settings.cursor_grab_mode = cursor_grab_mode; + } + } + } + } + + fn send_message(&self, message: MessageFromApp) + { + if self.message_from_app_sender.is_full() { + tracing::warn!( + "Message channel is full! Dropping oldest message from channel" + ); + + self.message_from_app_receiver.try_recv().ok(); + } + + if let Err(err) = self.message_from_app_sender.try_send(message) { + let error = match &err { + TrySendError::Full(_) => TrySendError::Full(()), + TrySendError::Disconnected(_) => TrySendError::Disconnected(()), + }; + + let message = err.into_inner(); + + tracing::error!("Failed to send message {error}: {message:?}"); + } + } +} + +impl ApplicationHandler for App +{ + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) + { + match cause { + StartCause::Init => { + self.send_message(MessageFromApp::Init( + event_loop.owned_display_handle(), + )); + } + StartCause::Poll => { + if self.is_dropped.load(Ordering::Relaxed) { + event_loop.exit(); + return; + } + + self.handle_received_messages(event_loop); + } + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) + { + for (window, _) in self.windows.values() { + let Some(window) = window.upgrade() else { + continue; + }; + + window.request_redraw(); + } + } + + fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + window_id: WinitWindowId, + event: WindowEvent, + ) + { + match event { + WindowEvent::Resized(new_window_size) => { + self.send_message(MessageFromApp::WindowResized( + WindowId::from_inner(window_id), + new_window_size.into(), + )); + } + WindowEvent::CloseRequested => { + self.send_message(MessageFromApp::WindowCloseRequested( + WindowId::from_inner(window_id), + )); + } + WindowEvent::KeyboardInput { + device_id: _, + event: keyboard_event, + is_synthetic: _, + } => { + if keyboard_event.repeat { + return; + } + + let key_code = match keyboard_event.physical_key { + PhysicalKey::Code(key_code) => key_code, + PhysicalKey::Unidentified(native_key) => { + tracing::warn!("Ignoring unidentified key: {native_key:?}"); + return; + } + }; + + let key: Key = match key_code.try_into() { + Ok(key) => key, + Err(UnknownKeyCodeError) => { + tracing::warn!("Ignoring key with unknown key code {key_code:?}"); + return; + } + }; + + self.send_message(MessageFromApp::KeyboardKeyStateChanged( + key, + keyboard_event.state.into(), + )); + } + WindowEvent::MouseInput { device_id: _, state, button } => { + self.send_message(MessageFromApp::MouseButtonStateChanged( + button.into(), + state.into(), + )); + } + WindowEvent::Focused(is_focused) => { + if is_focused { + self.focused_window_id = Some(WindowId::from_inner(window_id)); + } else { + self.focused_window_id = None; + } + } + _ => {} + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + _device_id: DeviceId, + device_event: DeviceEvent, + ) + { + match device_event { + DeviceEvent::MouseMotion { delta } => { + self.send_message(MessageFromApp::MouseMoved { + position_delta: Vec2 { x: delta.0, y: delta.1 }, + }); + + let Some(focused_window_id) = self.focused_window_id else { + return; + }; + + let Some((focused_window, focused_window_settings)) = + self.windows.get(&focused_window_id) + else { + tracing::error!( + window_id=?focused_window_id, + "Focused window not found" + ); + return; + }; + + if focused_window_settings.cursor_grab_mode != CursorGrabMode::Locked { + return; + } + + // TODO: This might need to be optimized + let Some(focused_window) = focused_window.upgrade() else { + return; + }; + + let focused_window_size = focused_window.inner_size(); + + if let Err(err) = focused_window.set_cursor_position(PhysicalPosition { + x: focused_window_size.width / 2, + y: focused_window_size.height / 2, + }) { + tracing::error!( + window_id=?focused_window_id, + "Failed to set cursor position in focused window: {err}" + ); + }; + } + _ => {} + } + } +} + +#[derive(Debug, Default)] +struct WindowSettings +{ + cursor_grab_mode: CursorGrabMode, +} -- cgit v1.2.3-18-g5258