summaryrefslogtreecommitdiff
path: root/engine/src/windowing.rs
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2025-10-18 17:04:28 +0200
committerHampusM <hampus@hampusmat.com>2025-10-18 17:04:28 +0200
commit7083a19bf1029bff21a9550d40cc3260e99aac53 (patch)
tree524a8bd2e75ca712b0536218089804cf9838553b /engine/src/windowing.rs
parent7f3072ed7e016dff359439d7580403e36ad6b325 (diff)
refactor(engine): use winit instead of glfw
Diffstat (limited to 'engine/src/windowing.rs')
-rw-r--r--engine/src/windowing.rs669
1 files changed, 669 insertions, 0 deletions
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::<ChildOf>()
+ .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<Pair<Added, WindowCreationReady>>,
+ context: Single<Context>,
+)
+{
+ 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::<WindowCreationAttributes>() 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<Context>,
+ mut keyboard: Single<Keyboard>,
+ mut mouse_motion: Single<MouseMotion>,
+ mut mouse_buttons: Single<MouseButtons>,
+ 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::<Window>() 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<Changed, Window>>,
+ context: Single<Context>,
+)
+{
+ 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<Pair<Removed, Window>>,
+ window_query: Query<(&Window,)>,
+ mut context: Single<Context>,
+ 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<AtomicBool>,
+ message_from_app_receiver: ChannelReceiver<MessageFromApp>,
+ message_to_app_sender: ChannelSender<MessageToApp>,
+ display_handle: Option<OwnedDisplayHandle>,
+ windows: MapVec<WindowId, (Arc<winit::window::Window>, Uid)>,
+}
+
+impl Context
+{
+ pub fn display_handle(&self) -> Option<DisplayHandle<'_>>
+ {
+ 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<Result<WindowHandle<'_>, 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::<MessageFromApp>(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::<MessageToApp>(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<EventLoop<()>, 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<WinitWindow>, WindowCreationAttributes),
+ WindowResized(WindowId, Dimens<u32>),
+ WindowCloseRequested(WindowId),
+ KeyboardKeyStateChanged(Key, KeyState),
+ MouseMoved
+ {
+ position_delta: Vec2<f64>,
+ },
+ MouseButtonStateChanged(MouseButton, MouseButtonState),
+}
+
+#[derive(Debug)]
+enum MessageToApp
+{
+ CreateWindow(Uid, WindowCreationAttributes),
+ SetWindowCursorGrabMode(WindowId, CursorGrabMode),
+}
+
+#[derive(Debug)]
+struct App
+{
+ message_from_app_sender: ChannelSender<MessageFromApp>,
+ message_from_app_receiver: ChannelReceiver<MessageFromApp>,
+ message_to_app_receiver: ChannelReceiver<MessageToApp>,
+ is_dropped: Arc<AtomicBool>,
+ windows: MapVec<WindowId, (Weak<WinitWindow>, WindowSettings)>,
+ focused_window_id: Option<WindowId>,
+}
+
+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,
+}