diff --git a/Cargo.toml b/Cargo.toml index 461eb535..35d4f3c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ tracing = { version = "0.1", optional = true } x11rb = { version = "0.13.2", features = ["cursor", "resource_manager", "allow-unsafe-code", "dl-libxcb"], default-features = false } xkbcommon-dl = { version = "0.4.2", features = ["x11"] } x11-dl = { version = "2.21.0" } -polling = "3.11.0" +calloop = "0.14.4" percent-encoding = "2.3.2" bytemuck = "1.25.0" @@ -99,3 +99,6 @@ missing-safety-doc = "allow" [package.metadata.docs.rs] all-features = true + +[workspace.dependencies] +tracing-subscriber = "0.3.23" diff --git a/examples/open_parented/src/main.rs b/examples/open_parented/src/main.rs index ef2def04..b5e13eaa 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -130,7 +130,7 @@ impl WindowHandler for ChildWindowHandler { fn main() -> Result<(), baseview::Error> { let window_open_options = WindowOpenOptions::new().with_size(LogicalSize::new(512.0, 512.0)); - baseview::create_window(window_open_options, ParentWindowHandler::new)?.run_until_closed(); + baseview::create_window(window_open_options, ParentWindowHandler::new)?.run_until_closed()?; Ok(()) } diff --git a/examples/open_window/src/main.rs b/examples/open_window/src/main.rs index fed1ecd5..93f1bfd6 100644 --- a/examples/open_window/src/main.rs +++ b/examples/open_window/src/main.rs @@ -167,7 +167,7 @@ fn main() -> Result<(), baseview::Error> { damaged: true.into(), }) })? - .run_until_closed(); + .run_until_closed()?; Ok(()) } diff --git a/examples/render_femtovg/Cargo.toml b/examples/render_femtovg/Cargo.toml index 2271a344..72b19485 100644 --- a/examples/render_femtovg/Cargo.toml +++ b/examples/render_femtovg/Cargo.toml @@ -5,5 +5,6 @@ edition = "2021" publish = false [dependencies] -baseview = { path = "../..", features = ["opengl"] } +baseview = { path = "../..", features = ["opengl", "tracing"] } femtovg = "0.25.1" +tracing-subscriber = { workspace = true } diff --git a/examples/render_femtovg/src/main.rs b/examples/render_femtovg/src/main.rs index c609f829..f1f34d4e 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -116,12 +116,14 @@ impl WindowHandler for FemtovgExample { } fn main() -> Result<(), baseview::Error> { + tracing_subscriber::fmt::init(); + let window_open_options = WindowOpenOptions::new() .with_title("Femtovg on Baseview") .with_size(LogicalSize::new(512, 512)) .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); - baseview::create_window(window_open_options, FemtovgExample::new)?.run_until_closed(); + baseview::create_window(window_open_options, FemtovgExample::new)?.run_until_closed()?; Ok(()) } diff --git a/examples/render_wgpu/src/main.rs b/examples/render_wgpu/src/main.rs index 1828e1cb..b10fad7b 100644 --- a/examples/render_wgpu/src/main.rs +++ b/examples/render_wgpu/src/main.rs @@ -214,7 +214,7 @@ fn main() -> Result<(), baseview::Error> { .with_size(LogicalSize::new(512, 512)); baseview::create_window(window_open_options, |c| pollster::block_on(WgpuExample::new(c)))? - .run_until_closed(); + .run_until_closed()?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index a781de81..b5aa10ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,6 @@ pub use window::*; pub use window_open_options::*; #[allow(unused)] -pub(crate) use tracing::warn; +pub(crate) use tracing::*; pub(crate) mod wrappers; diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index ccc3e477..4d47a294 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -83,8 +83,9 @@ impl WindowHandle { Ok(Self { mtm, state, view: Weak::from_retained(&view), _window: Some(window) }) } - pub fn run_until_closed(self) { + pub fn run_until_closed(self) -> Result<()> { NSApplication::sharedApplication(self.mtm).run(); + Ok(()) } pub fn is_open(&self) -> bool { diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index 8f5abbee..21ace30e 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -57,8 +57,9 @@ pub struct WindowHandle { } impl WindowHandle { - pub fn run_until_closed(self) { - run_thread_message_loop_until(|| !self.is_open()).unwrap(); + pub fn run_until_closed(self) -> Result<()> { + run_thread_message_loop_until(|| !self.is_open())?; + Ok(()) } pub fn is_open(&self) -> bool { diff --git a/src/platform/x11/drag_n_drop.rs b/src/platform/x11/drag_n_drop.rs index 892d4246..1b3b0eac 100644 --- a/src/platform/x11/drag_n_drop.rs +++ b/src/platform/x11/drag_n_drop.rs @@ -95,8 +95,7 @@ pub(crate) enum DragNDropState { // Other errors (protocol errors, transfer errors) should be dealt with as gracefully as possible. impl DragNDropState { pub fn handle_enter_event( - &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, - event: &ClientMessageEvent, + &mut self, window: &WindowInner, handler: &dyn WindowHandler, event: &ClientMessageEvent, ) -> Result<(), ConnectionError> { let data = event.data.as_data32(); @@ -155,8 +154,7 @@ impl DragNDropState { } pub fn handle_position_event( - &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, - event: &ClientMessageEvent, + &mut self, window: &WindowInner, handler: &dyn WindowHandler, event: &ClientMessageEvent, ) -> Result<(), ConnectionError> { let event_data = event.data.as_data32(); @@ -262,9 +260,7 @@ impl DragNDropState { } } - pub fn handle_leave_event( - &mut self, handler: &mut dyn WindowHandler, event: &ClientMessageEvent, - ) { + pub fn handle_leave_event(&mut self, handler: &dyn WindowHandler, event: &ClientMessageEvent) { let data = event.data.as_data32(); let event_source_window = data[0] as xproto::Window; @@ -295,8 +291,7 @@ impl DragNDropState { } pub fn handle_drop_event( - &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, - event: &ClientMessageEvent, + &mut self, window: &WindowInner, handler: &dyn WindowHandler, event: &ClientMessageEvent, ) -> Result<(), ConnectionError> { let data = event.data.as_data32(); @@ -400,8 +395,7 @@ impl DragNDropState { } pub fn handle_selection_notify_event( - &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, - event: &SelectionNotifyEvent, + &mut self, window: &WindowInner, handler: &dyn WindowHandler, event: &SelectionNotifyEvent, ) -> Result<(), ConnectionError> { // Ignore the event if we weren't actually waiting for a selection notify event let WaitingForData { diff --git a/src/platform/x11/error.rs b/src/platform/x11/error.rs index f1f622b1..a5c0674c 100644 --- a/src/platform/x11/error.rs +++ b/src/platform/x11/error.rs @@ -13,6 +13,7 @@ use x11rb::x11_utils::{TryParse, X11Error}; #[derive(Debug)] pub enum Error { CreationFailed(String), + Run(String), Io(std::io::Error), DylibOpen(OpenError), InitThreadsFailed(InitThreadsFailedError), @@ -25,6 +26,7 @@ pub enum Error { DisplayOpenFailed(DisplayOpenFailedError), Handler(HandlerError), Channel(RecvError), + Calloop(calloop::Error), #[cfg(feature = "opengl")] XLib(crate::wrappers::xlib::XLibError), #[cfg(feature = "opengl")] @@ -98,6 +100,18 @@ impl From for Error { } } +impl From for Error { + fn from(value: calloop::Error) -> Self { + Self::Calloop(value) + } +} + +impl From for Error { + fn from(value: RecvError) -> Self { + Self::Channel(value) + } +} + #[cfg(feature = "opengl")] impl From for Error { fn from(value: crate::wrappers::xlib::XLibError) -> Self { diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index e5ce2edc..36aa1a8d 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -1,11 +1,14 @@ use super::drag_n_drop::DragNDropState; use super::keyboard::{convert_key_press_event, convert_key_release_event, key_mods}; use super::*; +use std::result::Result; use crate::warn; -use crate::wrappers::connection_poller::{ConnectionPoller, PollStatus}; use crate::wrappers::xkbcommon::XkbcommonState; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowHandler, WindowSize}; +use calloop::generic::Generic; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{Interest, LoopSignal, Mode, PostAction}; use dpi::{PhysicalPosition, PhysicalSize}; use std::rc::Rc; use std::time::{Duration, Instant}; @@ -16,47 +19,57 @@ use x11rb::protocol::Event as XEvent; pub(crate) struct EventLoop { handler: Box, window: Rc, - parent_handle: Option, new_physical_size: Option>, - frame_interval: Duration, - event_loop_running: bool, - drag_n_drop: DragNDropState, + loop_signal: LoopSignal, + drag_n_drop: DragNDropState, xkb_state: Option, + + run_error: Option, } +const FRAME_INTERVAL: Duration = Duration::from_millis(15); + impl EventLoop { pub fn new( window: Rc, handler: Box, - parent_handle: Option, - ) -> Self { - Self { - xkb_state: XkbcommonState::new(&window.connection), - window, + inner: &mut calloop::EventLoop<'static, Self>, + ) -> Result { + let loop_handle = inner.handle(); + + loop_handle + .insert_source(Timer::from_duration(FRAME_INTERVAL), |i, _, e| e.handle_frame(i)) + .map_err(|e| e.error)?; + + loop_handle + .insert_source( + Generic::new_with_error(window.connection.conn.clone(), Interest::READ, Mode::Edge), + |_, _, e| e.handle_connection_event_ready(), + ) + .map_err(|e| e.error)?; + + Ok(Self { + loop_signal: inner.get_signal(), handler, - parent_handle, - frame_interval: Duration::from_millis(15), - event_loop_running: false, new_physical_size: None, drag_n_drop: DragNDropState::NoCurrentSession, - } - } - - pub fn window_id(&self) -> NonZeroU32 { - self.window.xcb_window.id() + xkb_state: XkbcommonState::new(&window.connection), + run_error: None, + window, + }) } #[inline] - fn drain_xcb_events(&mut self) -> core::result::Result<(), ConnectionError> { + fn drain_xcb_events(&mut self) -> Result<(), ConnectionError> { // the X server has a tendency to send spurious/extraneous configure notify events when a // window is resized, and we need to batch those together and just send one resize event // when they've all been coalesced. self.new_physical_size = None; while let Some(event) = self.window.connection.conn.poll_for_event()? { - self.handle_xcb_event(event); + self.handle_xcb_event(event)?; } if let Some(size) = self.new_physical_size.take() { @@ -75,58 +88,51 @@ impl EventLoop { Ok(()) } - // Event loop - pub fn run(mut self) -> Result<()> { - let connection = Rc::clone(&self.window.connection); - let mut poller = ConnectionPoller::new(&connection.conn)?; - - let mut last_frame = Instant::now(); - self.event_loop_running = true; - - while self.event_loop_running { - // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in - // the expected frame time, this will throttle down to prevent multiple frames from - // being queued up. The conditional here is needed because event handling and frame - // drawing is interleaved. The `poll()` function below will wait until the next frame - // can be drawn, or until the window receives an event. We thus need to manually check - // if it's already time to draw a new frame. - let next_frame = last_frame + self.frame_interval; - if Instant::now() >= next_frame { - self.handler.on_frame()?; - last_frame = Instant::max(next_frame, Instant::now() - self.frame_interval); - } + fn handle_connection_event_ready(&mut self) -> Result { + self.drain_xcb_events()?; - // Check for any events in the internal buffers - // before going to sleep: - self.drain_xcb_events()?; + Ok(PostAction::Continue) + } - // FIXME: handle errors - if let PollStatus::ReadAvailable = poller.wait(next_frame)? { - self.drain_xcb_events()?; - } + fn handle_frame(&mut self, previous_deadline: Instant) -> TimeoutAction { + if let Err(e) = self.handler.on_frame() { + self.run_error = Some(e.into()); + self.loop_signal.stop(); + return TimeoutAction::Drop; + } - // Check if the parents's handle was dropped (such as when the host - // requested the window to close) - if let Some(parent_handle) = &self.parent_handle { - if parent_handle.parent_did_drop() { - self.handle_must_close(); - self.window.close_requested.set(false); - } - } + // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in + // the expected frame time, this will throttle down to prevent multiple frames from + // being queued up. - // Check if the user has requested the window to close - if self.window.close_requested.get() { - self.handle_must_close(); - self.window.close_requested.set(false); - } - } + let now = Instant::now(); + let next_deadline = if previous_deadline + FRAME_INTERVAL >= now { + now + FRAME_INTERVAL + } else { + previous_deadline + FRAME_INTERVAL + }; - poller.delete()?; + TimeoutAction::ToInstant(next_deadline) + } + + fn handle_idle(&mut self) { + // Check for any events in the internal buffers before going to sleep: + let _ = self.drain_xcb_events(); + } + + pub fn run(mut self, mut inner: calloop::EventLoop) -> Result<(), Error> { + inner.run(None, &mut self, Self::handle_idle)?; + + self.handle_event(Event::Window(WindowEvent::WillClose)); + + if let Some(err) = self.run_error { + return Err(err); + }; Ok(()) } - fn handle_xcb_event(&mut self, event: XEvent) { + fn handle_xcb_event(&mut self, event: XEvent) -> Result<(), ConnectionError> { // For all the keyboard and mouse events, you can fetch // `x`, `y`, `detail`, and `state`. // - `x` and `y` are the position inside the window where the cursor currently is @@ -153,53 +159,35 @@ impl EventLoop { //// XEvent::ClientMessage(event) => { if event.format != 32 { - return; + return Ok(()); } if event.data.as_data32()[0] == self.window.connection.atoms.WM_DELETE_WINDOW { - self.handle_close_requested(); - return; + self.window.request_close(); + return Ok(()); } //// // drag n drop //// if event.type_ == self.window.connection.atoms.XdndEnter { - if let Err(_e) = self.drag_n_drop.handle_enter_event( - &self.window, - &mut *self.handler, - &event, - ) { - // TODO: log warning - } + self.drag_n_drop.handle_enter_event(&self.window, &*self.handler, &event)?; } else if event.type_ == self.window.connection.atoms.XdndPosition { - if let Err(_e) = self.drag_n_drop.handle_position_event( - &self.window, - &mut *self.handler, - &event, - ) { - // TODO: log warning - } + self.drag_n_drop.handle_position_event(&self.window, &*self.handler, &event)?; } else if event.type_ == self.window.connection.atoms.XdndDrop { - if let Err(_e) = - self.drag_n_drop.handle_drop_event(&self.window, &mut *self.handler, &event) - { - // TODO: log warning - } + self.drag_n_drop.handle_drop_event(&self.window, &*self.handler, &event)?; } else if event.type_ == self.window.connection.atoms.XdndLeave { - self.drag_n_drop.handle_leave_event(&mut *self.handler, &event); + self.drag_n_drop.handle_leave_event(&*self.handler, &event); } } XEvent::SelectionNotify(event) => { if event.property == self.window.connection.atoms.XdndSelection { - if let Err(_e) = self.drag_n_drop.handle_selection_notify_event( + self.drag_n_drop.handle_selection_notify_event( &self.window, - &mut *self.handler, + &*self.handler, &event, - ) { - // TODO: Log warning - } + )?; } } @@ -254,9 +242,8 @@ impl EventLoop { })); } detail => { - let button_id = mouse_id(detail); self.handle_event(Event::Mouse(MouseEvent::ButtonPressed { - button: button_id, + button: mouse_id(detail), modifiers: key_mods(event.state), })); } @@ -295,22 +282,13 @@ impl EventLoop { _ => {} } + + Ok(()) } fn handle_event(&mut self, event: Event) { self.handler.on_event(event); } - - fn handle_close_requested(&mut self) { - // FIXME: handler should decide whether window stays open or not - self.handle_must_close(); - } - - fn handle_must_close(&mut self) { - self.handle_event(Event::Window(WindowEvent::WillClose)); - - self.event_loop_running = false; - } } fn mouse_id(id: u8) -> MouseButton { diff --git a/src/platform/x11/mod.rs b/src/platform/x11/mod.rs index 858915e8..009970c4 100644 --- a/src/platform/x11/mod.rs +++ b/src/platform/x11/mod.rs @@ -21,6 +21,7 @@ mod visual_info; mod xcb_window; mod window_shared; +mod window_thread; pub use error::{CookieExt as _, Error}; pub(crate) type Result = std::result::Result; diff --git a/src/platform/x11/window.rs b/src/platform/x11/window.rs index cd99e990..d5326e17 100644 --- a/src/platform/x11/window.rs +++ b/src/platform/x11/window.rs @@ -1,176 +1,6 @@ -use std::cell::Cell; -use std::num::NonZero; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; -use std::sync::Arc; -use std::thread::{self, JoinHandle}; +use crate::platform::x11::window_thread::WindowThreadHandle; -use super::X11Connection; -use super::{event_loop::EventLoop, visual_info::WindowVisualConfig}; -use crate::handler::WindowHandlerBuilder; -use crate::platform::x11::window_shared::WindowInner; -use crate::platform::x11::xcb_window::XcbWindow; -use crate::platform::Result; -use crate::*; - -pub struct WindowHandle { - window_id: Option>, - event_loop_handle: Cell>>, - close_requested: Arc, - is_open: Arc, -} - -impl WindowHandle { - pub fn create_window( - options: WindowOpenOptions, handler: WindowHandlerBuilder, - ) -> Result { - let (tx, rx) = mpsc::sync_channel::(1); - let (parent_handle, mut window_handle) = ParentHandle::new(); - - let join_handle = - thread::spawn(move || match create_window(options, handler, Some(parent_handle)) { - Ok(ev_loop) => { - tx.send(Ok(ev_loop.window_id())).unwrap(); - ev_loop.run().unwrap(); - } - Err(e) => { - tx.send(Err(format!("{}", e))).unwrap(); - } - }); - - let id = match rx.recv() { - Ok(Ok(id)) => id, - Err(e) => return Err(super::Error::Channel(e)), - Ok(Err(s)) => return Err(super::error::Error::CreationFailed(s)), - }; - - window_handle.window_id = Some(id); - window_handle.event_loop_handle = Some(join_handle).into(); - Ok(window_handle) - } - - pub fn run_until_closed(self) { - let Some(thread) = self.event_loop_handle.take() else { return }; - - thread.join().unwrap_or_else(|err| { - eprintln!("Window thread panicked: {:#?}", err); - }); - } - - pub fn is_open(&self) -> bool { - self.is_open.load(Ordering::Relaxed) - } -} - -impl Drop for WindowHandle { - fn drop(&mut self) { - self.close_requested.store(true, Ordering::Relaxed); - if let Some(event_loop) = self.event_loop_handle.take() { - let _ = event_loop.join(); - } - } -} - -pub(crate) struct ParentHandle { - close_requested: Arc, - is_open: Arc, -} - -impl ParentHandle { - pub fn new() -> (Self, WindowHandle) { - let close_requested = Arc::new(AtomicBool::new(false)); - let is_open = Arc::new(AtomicBool::new(true)); - let handle = WindowHandle { - window_id: None, - event_loop_handle: None.into(), - close_requested: Arc::clone(&close_requested), - is_open: Arc::clone(&is_open), - }; - - (Self { close_requested, is_open }, handle) - } - - pub fn parent_did_drop(&self) -> bool { - self.close_requested.load(Ordering::Relaxed) - } -} - -impl Drop for ParentHandle { - fn drop(&mut self) { - self.is_open.store(false, Ordering::Relaxed); - } -} - -type WindowOpenResult = core::result::Result, String>; - -fn create_window( - options: WindowOpenOptions, build: WindowHandlerBuilder, parent_handle: Option, -) -> Result { - // Connect to the X server - let xcb_connection = X11Connection::new()?; - - let scaling = match options.scale { - WindowScalePolicy::SystemScaleFactor => xcb_connection.get_scaling(), - WindowScalePolicy::ScaleFactor(scale) => scale, - }; - - let physical_size = options.size.to_physical(scaling); - - #[cfg(feature = "opengl")] - let visual_info = - WindowVisualConfig::find_best_visual_config_for_gl(&xcb_connection, options.gl_config)?; - - #[cfg(not(feature = "opengl"))] - let visual_info = WindowVisualConfig::find_best_visual_config(&xcb_connection)?; - - let xcb_connection = Rc::new(xcb_connection); - - let x_window = XcbWindow::new( - Rc::clone(&xcb_connection), - physical_size, - &visual_info, - options.parent.map(|p| p.window_id), - )?; - - let cookies = [ - x_window.map_window()?, - x_window.set_title(&options.title)?, - x_window.enable_wm_protocols()?, - x_window.enable_dnd_protocols()?, - ]; - - for cookie in cookies { - cookie.check()?; - } - - #[cfg(feature = "opengl")] - let gl_context = match visual_info.fb_config { - None => None, - Some(fb_config) => { - // Because of the visual negotation we had to take some extra steps to create this context - Some(super::gl::GlContextInner::create( - &x_window, - Rc::clone(&xcb_connection), - fb_config, - )?) - } - }; - - let inner = Rc::new(WindowInner::new( - xcb_connection, - x_window, - physical_size, - scaling, - visual_info.visual_id, - #[cfg(feature = "opengl")] - gl_context, - )); - - let handler = build.build(WindowContext::new(Rc::clone(&inner)))?; - - Ok(EventLoop::new(inner, handler, parent_handle)) -} +pub type WindowHandle = WindowThreadHandle; pub fn copy_to_clipboard(_data: &str) { todo!() diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 8b69cd0b..a5390fc0 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,6 +1,9 @@ +use crate::platform::x11::event_loop::EventLoop; +use crate::platform::x11::visual_info::WindowVisualConfig; use crate::platform::x11::xcb_window::XcbWindow; use crate::platform::*; -use crate::{MouseCursor, WindowSize}; +use crate::{MouseCursor, WindowOpenOptions, WindowScalePolicy, WindowSize}; +use calloop::LoopSignal; use dpi::{PhysicalSize, Size}; use raw_window_handle::{DisplayHandle, XlibWindowHandle}; use std::cell::Cell; @@ -21,30 +24,79 @@ pub(crate) struct WindowInner { mouse_cursor: Cell, pub(crate) visual_id: Visualid, - pub(crate) close_requested: Cell, pub(crate) is_focused: Cell, + pub(crate) loop_signal: LoopSignal, } impl WindowInner { - pub(crate) fn new( - connection: Rc, xcb_window: XcbWindow, window_size: PhysicalSize, - scale_factor: f64, visual_id: Visualid, - #[cfg(feature = "opengl")] gl_context: Option, - ) -> Self { - Self { + pub(crate) fn create( + options: WindowOpenOptions, ev_loop: &calloop::EventLoop<'static, EventLoop>, + ) -> Result> { + // Connect to the X server + let xcb_connection = X11Connection::new()?; + + let scaling = match options.scale { + WindowScalePolicy::SystemScaleFactor => xcb_connection.get_scaling(), + WindowScalePolicy::ScaleFactor(scale) => scale, + }; + + let physical_size = options.size.to_physical(scaling); + + #[cfg(feature = "opengl")] + let visual_info = + WindowVisualConfig::find_best_visual_config_for_gl(&xcb_connection, options.gl_config)?; + + #[cfg(not(feature = "opengl"))] + let visual_info = WindowVisualConfig::find_best_visual_config(&xcb_connection)?; + + let connection = Rc::new(xcb_connection); + + let xcb_window = XcbWindow::new( + Rc::clone(&connection), + physical_size, + &visual_info, + options.parent.map(|p| p.window_id), + )?; + + let cookies = [ + xcb_window.map_window()?, + xcb_window.set_title(&options.title)?, + xcb_window.enable_wm_protocols()?, + xcb_window.enable_dnd_protocols()?, + ]; + + for cookie in cookies { + cookie.check()?; + } + + #[cfg(feature = "opengl")] + let gl_context = match visual_info.fb_config { + None => None, + Some(fb_config) => { + // Because of the visual negotation we had to take some extra steps to create this context + Some(super::gl::GlContextInner::create( + &xcb_window, + Rc::clone(&connection), + fb_config, + )?) + } + }; + + let visual_id = visual_info.visual_id; + Ok(Rc::new(Self { connection, xcb_window, visual_id, - window_size: window_size.into(), - scaling_factor: scale_factor.into(), + window_size: physical_size.into(), + scaling_factor: scaling.into(), mouse_cursor: MouseCursor::default().into(), + loop_signal: ev_loop.get_signal(), - close_requested: false.into(), is_focused: false.into(), #[cfg(feature = "opengl")] gl_context, - } + })) } pub fn set_mouse_cursor(&self, mouse_cursor: MouseCursor) -> Result<()> { @@ -70,7 +122,8 @@ impl WindowInner { } pub fn request_close(&self) { - self.close_requested.set(true); + self.loop_signal.stop(); + self.loop_signal.wakeup(); } pub fn has_focus(&self) -> bool { diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs new file mode 100644 index 00000000..cf167317 --- /dev/null +++ b/src/platform/x11/window_thread.rs @@ -0,0 +1,158 @@ +use super::*; +use crate::handler::WindowHandlerBuilder; +use crate::platform::x11::event_loop::EventLoop; +use crate::platform::x11::window_shared::WindowInner; +use crate::{WindowContext, WindowOpenOptions}; +use calloop::LoopSignal; +use std::cell::Cell; +use std::panic::resume_unwind; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Mutex}; +use std::thread; +use std::thread::JoinHandle; + +pub(crate) struct WindowThreadShared { + stopped: AtomicBool, + final_error: Mutex>, +} + +impl WindowThreadShared { + pub fn new() -> Self { + Self { stopped: false.into(), final_error: None.into() } + } +} + +pub struct WindowThreadHandle { + shared: Arc, + loop_signal: LoopSignal, + event_loop_handle: Cell>>, +} + +impl WindowThreadHandle { + pub fn create_window( + options: WindowOpenOptions, handler: WindowHandlerBuilder, + ) -> Result { + let (tx, rx) = result_channel(); + let shared = Arc::new(WindowThreadShared::new()); + + let join_handle = { + let shared = shared.clone(); + + thread::spawn(move || { + let thread = match WindowThread::create(options, handler, shared) { + Err(e) => return tx.send_error(e), + Ok(thread) => thread, + }; + + if tx.send_success(&thread) { + thread.run() + } + }) + }; + + rx.receive(join_handle, shared) + } + + pub fn run_until_closed(&self) -> Result<()> { + let Some(thread) = self.event_loop_handle.take() else { return Ok(()) }; + + if let Err(panic) = thread.join() { + resume_unwind(panic); + } + + if let Some(e) = self.shared.final_error.lock().unwrap().take() { + return Err(Error::Run(e)); + } + + Ok(()) + } + + pub fn is_open(&self) -> bool { + !self.shared.stopped.load(Ordering::Relaxed) + } +} + +impl Drop for WindowThreadHandle { + fn drop(&mut self) { + self.loop_signal.stop(); + self.loop_signal.wakeup(); + + if let Err(e) = self.run_until_closed() { + crate::warn!("Error while closing window: {}", e) + } + } +} + +enum WindowOpenResult { + Success { loop_signal: LoopSignal }, + Error(String), +} + +struct WindowThread { + event_loop: EventLoop, + ev_loop: calloop::EventLoop<'static, EventLoop>, + shared: Arc, +} + +impl WindowThread { + pub fn create( + options: WindowOpenOptions, handler: WindowHandlerBuilder, shared: Arc, + ) -> Result { + let mut ev_loop = calloop::EventLoop::try_new()?; + let inner = WindowInner::create(options, &ev_loop)?; + let handler = handler.build(WindowContext::new(Rc::clone(&inner)))?; + let event_loop = EventLoop::new(inner, handler, &mut ev_loop)?; + + Ok(Self { event_loop, ev_loop, shared }) + } + + pub fn run(self) { + if let Err(e) = self.event_loop.run(self.ev_loop) { + self.shared.final_error.lock().unwrap().replace(e.to_string()); + } + } +} + +fn result_channel() -> (WindowResultSender, WindowResultReceiver) { + let (tx, rx) = mpsc::sync_channel::(1); + (WindowResultSender(tx), WindowResultReceiver(rx)) +} + +struct WindowResultSender(mpsc::SyncSender); +impl WindowResultSender { + pub fn send_error(self, error: Error) { + if let Err(err) = self.0.send(WindowOpenResult::Error(format!("{}", error))) { + crate::error!("Window creation failed: {}", error); + crate::warn!("Failed to send error to main thread: {}", err); + } + } + + pub fn send_success(self, thread: &WindowThread) -> bool { + let msg = WindowOpenResult::Success { loop_signal: thread.ev_loop.get_signal() }; + + if let Err(err) = self.0.send(msg) { + crate::error!("Failed to send created window to main thread: {}. Aborting.", err); + return false; + } + + true + } +} + +struct WindowResultReceiver(mpsc::Receiver); +impl WindowResultReceiver { + pub fn receive( + self, join_handle: JoinHandle<()>, shared: Arc, + ) -> Result { + let result = self.0.recv()?; + match result { + WindowOpenResult::Error(e) => Err(Error::CreationFailed(e)), + WindowOpenResult::Success { loop_signal } => Ok(WindowThreadHandle { + event_loop_handle: Some(join_handle).into(), + shared, + loop_signal, + }), + } + } +} diff --git a/src/platform/x11/xcb_window.rs b/src/platform/x11/xcb_window.rs index 7e390d9a..7e012645 100644 --- a/src/platform/x11/xcb_window.rs +++ b/src/platform/x11/xcb_window.rs @@ -111,6 +111,7 @@ impl XcbWindow { impl Drop for XcbWindow { fn drop(&mut self) { + // TODO: log error let Ok(cookie) = self.connection.conn.destroy_window(self.window_id.get()) else { return }; let _ = cookie.check(); } diff --git a/src/tracing.rs b/src/tracing.rs index bc23b156..6eea95c7 100644 --- a/src/tracing.rs +++ b/src/tracing.rs @@ -1,7 +1,9 @@ #[cfg(feature = "tracing")] -pub use tracing::*; +#[allow(unused)] +pub use tracing::{error, warn}; #[cfg(not(feature = "tracing"))] +#[allow(unused)] mod tracing_impl { macro_rules! __warn { ($($f:tt)*) => { @@ -13,6 +15,7 @@ mod tracing_impl { } pub(crate) use __warn as warn; + pub(crate) use __warn as error; } #[cfg(not(feature = "tracing"))] diff --git a/src/window.rs b/src/window.rs index bedd4e67..5234ca5e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -15,8 +15,9 @@ impl WindowHandle { Self { window_handle, phantom: PhantomData } } - pub fn run_until_closed(self) { - self.window_handle.run_until_closed() + pub fn run_until_closed(self) -> Result<(), Error> { + self.window_handle.run_until_closed()?; + Ok(()) } /// Close the window diff --git a/src/wrappers.rs b/src/wrappers.rs index d5d30536..01483e1e 100644 --- a/src/wrappers.rs +++ b/src/wrappers.rs @@ -23,9 +23,6 @@ pub mod xkbcommon; #[cfg(all(target_os = "linux", feature = "opengl"))] pub mod glx; -#[cfg(target_os = "linux")] -pub mod connection_poller; - /// Wrappers and utilities around the Win32 API #[cfg(target_os = "windows")] pub mod win32; diff --git a/src/wrappers/connection_poller.rs b/src/wrappers/connection_poller.rs deleted file mode 100644 index 19a8b913..00000000 --- a/src/wrappers/connection_poller.rs +++ /dev/null @@ -1,66 +0,0 @@ -use polling::{Event, Events, Poller}; -use std::io; -use std::os::fd::{AsFd, BorrowedFd}; -use std::time::Instant; - -pub struct ConnectionPoller<'a> { - poller: Poller, - events: Events, - fd: BorrowedFd<'a>, -} - -const CONNECTION_KEY: usize = 42; - -impl<'a> ConnectionPoller<'a> { - pub fn new(source: &'a impl AsFd) -> io::Result { - let poller = Poller::new()?; - let fd = source.as_fd(); - unsafe { poller.add(&fd, Event::readable(CONNECTION_KEY))? }; - - Ok(Self { poller, fd, events: Events::new() }) - } - - pub fn wait(&mut self, deadline: Instant) -> io::Result { - self.events.clear(); - // NOTE: polling crate already handles retrying on EINTR - let new_events_count = self.poller.wait_deadline(&mut self.events, deadline)?; - - if new_events_count == 0 { - return Ok(PollStatus::Nothing); - } - - for event in self.events.iter() { - if event.key != CONNECTION_KEY { - continue; - } - - if let Some(true) = event.is_err() { - panic!("xcb connection poll error") - } - - if event.is_interrupt() { - return Ok(PollStatus::ConnectionClosed); - } - - return Ok(PollStatus::ReadAvailable); - } - - Ok(PollStatus::Nothing) - } - - pub fn delete(self) -> io::Result<()> { - self.poller.delete(self.fd) - } -} - -impl<'a> Drop for ConnectionPoller<'a> { - fn drop(&mut self) { - let _ = self.poller.delete(self.fd); - } -} - -pub enum PollStatus { - Nothing, - ReadAvailable, - ConnectionClosed, -}