diff --git a/Cargo.toml b/Cargo.toml index 31636503..b084ac03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ objc2-core-foundation = { version = "0.3.2", default-features = false, features block2 = "0.6.2" dispatch2 = "0.3.1" objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSEnumerator", "block2", "NSOperation"] } +objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["CADisplayLink"] } objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSApplication", "NSCursor", @@ -90,11 +91,12 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSTrackingArea", "NSView", "NSWindow", + "objc2-quartz-core", "objc2-core-foundation" ] } [workspace] -members = ["examples/cursors","examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu", "examples/plugin_clack_femtovg", "examples/test-frame-pacing"] +members = ["examples/cursors", "examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu", "examples/plugin_clack_femtovg", "examples/test-frame-pacing", "examples/external-wakeup"] [lints.clippy] missing-safety-doc = "allow" diff --git a/examples/cursors/src/main.rs b/examples/cursors/src/main.rs index 11055d66..900ff2f8 100644 --- a/examples/cursors/src/main.rs +++ b/examples/cursors/src/main.rs @@ -6,13 +6,12 @@ use baseview::{ }; use femtovg::renderer::OpenGl; use femtovg::{Canvas, Color}; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; struct CursorsExample { window_context: WindowContext, gl_context: GlContext, canvas: RefCell>, - damaged: Cell, } impl CursorsExample { @@ -29,7 +28,7 @@ impl CursorsExample { canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32); unsafe { gl_context.make_not_current()? }; - Ok(Self { gl_context, window_context, canvas: canvas.into(), damaged: true.into() }) + Ok(Self { gl_context, window_context, canvas: canvas.into() }) } fn in_blue_area(&self, position: PhysicalPosition) -> bool { @@ -43,11 +42,7 @@ impl CursorsExample { } impl WindowHandler for CursorsExample { - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let context = &self.gl_context; unsafe { context.make_current()? }; @@ -72,7 +67,6 @@ impl WindowHandler for CursorsExample { canvas.flush(); context.swap_buffers()?; unsafe { context.make_not_current()? }; - self.damaged.set(false); Ok(()) } @@ -80,7 +74,6 @@ impl WindowHandler for CursorsExample { fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { let size = new_size.physical; self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); - self.damaged.set(true); Ok(()) } diff --git a/examples/external-wakeup/Cargo.toml b/examples/external-wakeup/Cargo.toml new file mode 100644 index 00000000..a40fc7e3 --- /dev/null +++ b/examples/external-wakeup/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "external-wakeup" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +baseview = { path = "../..", features = ["opengl"] } +femtovg = "0.27.0" +rand = "0.10.3" diff --git a/examples/external-wakeup/src/main.rs b/examples/external-wakeup/src/main.rs new file mode 100644 index 00000000..3d33c394 --- /dev/null +++ b/examples/external-wakeup/src/main.rs @@ -0,0 +1,147 @@ +use baseview::dpi::LogicalSize; +use baseview::gl::{GlConfig, GlContext}; +use baseview::{ + Event, EventStatus, HandlerError, Window, WindowContext, WindowHandler, WindowSettings, + WindowSize, WindowWaker, +}; +use femtovg::renderer::OpenGl; +use femtovg::{Canvas, Color}; +use std::cell::{Cell, RefCell}; +use std::sync::mpsc::*; +use std::time::Duration; + +#[derive(Copy, Clone, Debug)] +enum Message { + Hello, +} + +struct FemtovgExample { + window_context: WindowContext, + gl_context: GlContext, + canvas: RefCell>, + + green_rect_opacity: Cell, + + receiver: Receiver, +} + +impl FemtovgExample { + fn new( + window_context: WindowContext, receiver: Receiver, + ) -> Result { + let Some(gl_context) = window_context.gl_context() else { unreachable!() }; + unsafe { gl_context.make_current()? }; + + let renderer = + unsafe { OpenGl::new_from_function_cstr(|s| gl_context.get_proc_address(s)) }?; + + let mut canvas = Canvas::new(renderer)?; + let size = window_context.size(); + + canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32); + + unsafe { gl_context.make_not_current()? }; + Ok(Self { + gl_context, + window_context, + canvas: canvas.into(), + green_rect_opacity: 0.0.into(), + receiver, + }) + } +} + +impl WindowHandler for FemtovgExample { + fn draw(&self) -> Result<(), HandlerError> { + let context = &self.gl_context; + unsafe { context.make_current()? }; + + let mut canvas = self.canvas.borrow_mut(); + + let screen_height = canvas.height(); + let screen_width = canvas.width(); + + // Clear + canvas.clear_rect(0, 0, screen_width, screen_height, Color::rgb(0x0A, 0x0A, 0x0A)); + + if self.green_rect_opacity.get() <= 0.0 { + // Make orange rectangle + canvas.clear_rect( + (screen_width as f32 * 0.3).floor() as u32, + (screen_height as f32 * 0.45).floor() as u32, + (screen_width as f32 * 0.1).floor() as u32, + (screen_height as f32 * 0.1).floor() as u32, + Color::rgbf(1.0, 0.5, 0.), + ); + } else { + // Make green rectangle + canvas.clear_rect( + (screen_width as f32 * 0.5).floor() as u32, + (screen_height as f32 * 0.45).floor() as u32, + (screen_width as f32 * 0.1).floor() as u32, + (screen_height as f32 * 0.1).floor() as u32, + Color::rgbf(0.0, 1. * self.green_rect_opacity.get(), 0.), + ); + + // Prepare a next frame to animate it fading out + self.green_rect_opacity.set(self.green_rect_opacity.get() - 1.0 / 60.0); + self.window_context.request_redraw(); + } + + // Tell renderer to execute all drawing commands + canvas.flush(); + context.swap_buffers()?; + unsafe { context.make_not_current()? }; + + Ok(()) + } + + fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { + let size = new_size.physical; + self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); + + Ok(()) + } + + fn on_event(&self, _event: Event) -> EventStatus { + EventStatus::Ignored + } + + fn poll(&self) { + let msg = match self.receiver.try_recv() { + Err(TryRecvError::Empty) => return, + Err(TryRecvError::Disconnected) => return eprintln!("Channel disconnected!"), + Ok(msg) => msg, + }; + + eprintln!("Message received: {msg:?}!"); + self.green_rect_opacity.set(1.0); + self.window_context.request_redraw(); + } +} + +fn main() -> Result<(), baseview::Error> { + unsafe { baseview::assume_standalone_in_process() }; + let (sender, receiver) = channel(); + + let window_open_options = WindowSettings::new() + .with_title("Baseview Waker example") + .with_size(LogicalSize::new(512, 512)) + .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); + + let window = Window::create(window_open_options, |ctx| FemtovgExample::new(ctx, receiver))?; + let waker = window.waker(); + std::thread::spawn(|| run_thread(sender, waker)); + + window.run_until_closed()?; + Ok(()) +} + +fn run_thread(sender: Sender, waker: WindowWaker) { + loop { + let interval: f32 = rand::random_range(0.5..2.5); + std::thread::sleep(Duration::from_secs_f32(interval)); + sender.send(Message::Hello).unwrap(); + waker.request_poll(); + } +} diff --git a/examples/open_parented/src/main.rs b/examples/open_parented/src/main.rs index 812dd6fc..c4ecfd50 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -3,13 +3,11 @@ use baseview::{ Event, EventStatus, HandlerError, Window, WindowContext, WindowHandler, WindowSettings, WindowSize, }; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::num::NonZeroU32; struct ParentWindowHandler { surface: RefCell>, - damaged: Cell, - child_window: Window, } @@ -26,18 +24,15 @@ impl ParentWindowHandler { let child_window = Window::create(window_open_options, ChildWindowHandler::new)?; child_window.show()?; - Ok(Self { surface: surface.into(), damaged: true.into(), child_window }) + Ok(Self { surface: surface.into(), child_window }) } } impl WindowHandler for ParentWindowHandler { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut buf = surface.buffer_mut()?; - if self.damaged.get() { - buf.fill(0xFFAA0000); - self.damaged.set(false); - } + buf.fill(0xFFAA0000); buf.present()?; Ok(()) @@ -50,7 +45,6 @@ impl WindowHandler for ParentWindowHandler { (NonZeroU32::new(new_size.physical.width), NonZeroU32::new(new_size.physical.height)) { self.surface.borrow_mut().resize(width, height)?; - self.damaged.set(true); } self.child_window.suggest_fallback_scale_factor(new_size.scale_factor)?; @@ -71,7 +65,6 @@ impl WindowHandler for ParentWindowHandler { struct ChildWindowHandler { surface: RefCell>, - damaged: Cell, } impl ChildWindowHandler { @@ -81,18 +74,15 @@ impl ChildWindowHandler { let size = window.size().physical; surface.resize(size.width.try_into()?, size.height.try_into()?)?; - Ok(Self { surface: surface.into(), damaged: true.into() }) + Ok(Self { surface: surface.into() }) } } impl WindowHandler for ChildWindowHandler { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut buf = surface.buffer_mut()?; - if self.damaged.get() { - buf.fill(0xFFAAAAAA); - self.damaged.set(false); - } + buf.fill(0xFFAAAAAA); buf.present()?; Ok(()) @@ -105,7 +95,6 @@ impl WindowHandler for ChildWindowHandler { (NonZeroU32::new(new_size.physical.width), NonZeroU32::new(new_size.physical.height)) { self.surface.borrow_mut().resize(width, height)?; - self.damaged.set(true); } Ok(()) diff --git a/examples/open_window/src/main.rs b/examples/open_window/src/main.rs index f4019b4b..5074225e 100644 --- a/examples/open_window/src/main.rs +++ b/examples/open_window/src/main.rs @@ -24,7 +24,6 @@ struct OpenWindowExample { surface: RefCell>, mouse_pos: Cell>, is_cursor_inside: Cell, - damaged: Cell, } impl WindowHandler for OpenWindowExample { @@ -35,17 +34,12 @@ impl WindowHandler for OpenWindowExample { (NonZeroU32::new(new_size.physical.width), NonZeroU32::new(new_size.physical.height)) { self.surface.borrow_mut().resize(width, height)?; - self.damaged.set(true); } Ok(()) } - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut pixels = surface.buffer_mut()?; let size = self.window_context.size(); @@ -105,13 +99,15 @@ impl WindowHandler for OpenWindowExample { } pixels.present()?; - self.damaged.set(false); + Ok(()) + } + + fn poll(&self) { + eprintln!("Poll!"); while let Ok(message) = self.rx.borrow_mut().pop() { println!("Message: {:?}", message); } - - Ok(()) } fn on_event(&self, event: Event) -> EventStatus { @@ -120,21 +116,19 @@ impl WindowHandler for OpenWindowExample { Event::Mouse(MouseEvent::ButtonPressed { .. }) => copy_to_clipboard("This is a test!"), Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { self.mouse_pos.set(position); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::CursorEntered) => { self.is_cursor_inside.set(true); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::CursorLeft) => { self.is_cursor_inside.set(false); - self.damaged.set(true); + self.window_context.request_redraw(); } - _ => {} + event => log_event(&event), } - log_event(&event); - EventStatus::Captured } } @@ -146,15 +140,7 @@ fn main() -> Result<(), baseview::Error> { let (mut tx, rx) = RingBuffer::new(128); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_secs(5)); - - if tx.push(Message::Hello).is_err() { - println!("Failed sending message"); - } - }); - - Window::create(window_open_options, |window| { + let window = Window::create(window_open_options, |window| { let ctx = softbuffer::Context::new(window.clone())?; let mut surface = softbuffer::Surface::new(&ctx, window.clone())?; let size = window.size().physical; @@ -166,10 +152,21 @@ fn main() -> Result<(), baseview::Error> { rx: rx.into(), mouse_pos: PhysicalPosition::new(0., 0.).into(), is_cursor_inside: false.into(), - damaged: true.into(), }) - })? - .run_until_closed()?; + })?; + + let waker = window.waker(); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_secs(5)); + + if tx.push(Message::Hello).is_err() { + println!("Failed sending message"); + } else { + waker.request_poll(); + } + }); + + window.run_until_closed()?; Ok(()) } diff --git a/examples/plugin_clack/src/window_handler.rs b/examples/plugin_clack/src/window_handler.rs index 16800ba9..cab6a1d6 100644 --- a/examples/plugin_clack/src/window_handler.rs +++ b/examples/plugin_clack/src/window_handler.rs @@ -12,7 +12,6 @@ pub struct OpenWindowExample { surface: RefCell>, mouse_pos: Cell>, is_cursor_inside: Cell, - damaged: Cell, } impl WindowHandler for OpenWindowExample { @@ -23,17 +22,12 @@ impl WindowHandler for OpenWindowExample { (NonZeroU32::new(new_size.physical.width), NonZeroU32::new(new_size.physical.height)) { self.surface.borrow_mut().resize(width, height)?; - self.damaged.set(true); } Ok(()) } - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let mut pixels = surface.buffer_mut()?; let size = self.window_context.size(); @@ -93,7 +87,6 @@ impl WindowHandler for OpenWindowExample { } pixels.present()?; - self.damaged.set(false); Ok(()) } @@ -102,15 +95,15 @@ impl WindowHandler for OpenWindowExample { match event { Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { self.mouse_pos.set(position); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::CursorEntered) => { self.is_cursor_inside.set(true); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::CursorLeft) => { self.is_cursor_inside.set(false); - self.damaged.set(true); + self.window_context.request_redraw(); } Event::Mouse(MouseEvent::ButtonPressed { button: MouseButton::Left, .. }) => { let mut size = self.window_context.size().physical; @@ -141,7 +134,6 @@ impl OpenWindowExample { surface: surface.into(), mouse_pos: PhysicalPosition::new(0., 0.).into(), is_cursor_inside: false.into(), - damaged: true.into(), }) } } diff --git a/examples/plugin_clack_femtovg/src/window_handler.rs b/examples/plugin_clack_femtovg/src/window_handler.rs index 0ba8dcfa..8eabc12e 100644 --- a/examples/plugin_clack_femtovg/src/window_handler.rs +++ b/examples/plugin_clack_femtovg/src/window_handler.rs @@ -12,15 +12,10 @@ pub struct FemtovgExample { gl_context: GlContext, canvas: RefCell>, current_mouse_position: Cell>, - damaged: Cell, } impl WindowHandler for FemtovgExample { - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let context = &self.gl_context; unsafe { context.make_current()? }; @@ -56,7 +51,6 @@ impl WindowHandler for FemtovgExample { canvas.flush(); context.swap_buffers()?; unsafe { context.make_not_current()? }; - self.damaged.set(false); Ok(()) } @@ -64,7 +58,6 @@ impl WindowHandler for FemtovgExample { fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { let size = new_size.physical; self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); - self.damaged.set(true); Ok(()) } @@ -81,7 +74,7 @@ impl WindowHandler for FemtovgExample { if position.y > 400. && !self.window_context.has_focus() { let _ = self.window_context.focus(); } - self.damaged.set(true); + self.window_context.request_redraw(); }; EventStatus::Captured @@ -106,7 +99,6 @@ impl FemtovgExample { gl_context, window_context, canvas: canvas.into(), - damaged: true.into(), current_mouse_position: Cell::new(PhysicalPosition::default()), }) } diff --git a/examples/render_femtovg/src/main.rs b/examples/render_femtovg/src/main.rs index 98d27e90..d21c404a 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -14,7 +14,6 @@ struct FemtovgExample { gl_context: GlContext, canvas: RefCell>, current_mouse_position: Cell>, - damaged: Cell, } impl FemtovgExample { @@ -35,18 +34,13 @@ impl FemtovgExample { gl_context, window_context, canvas: canvas.into(), - damaged: true.into(), current_mouse_position: Cell::new(PhysicalPosition::default()), }) } } impl WindowHandler for FemtovgExample { - fn on_frame(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - + fn draw(&self) -> Result<(), HandlerError> { let context = &self.gl_context; unsafe { context.make_current()? }; @@ -82,7 +76,6 @@ impl WindowHandler for FemtovgExample { canvas.flush(); context.swap_buffers()?; unsafe { context.make_not_current()? }; - self.damaged.set(false); Ok(()) } @@ -90,7 +83,6 @@ impl WindowHandler for FemtovgExample { fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { let size = new_size.physical; self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); - self.damaged.set(true); Ok(()) } @@ -107,7 +99,7 @@ impl WindowHandler for FemtovgExample { if position.y > 400. && !self.window_context.has_focus() { let _ = self.window_context.focus(); } - self.damaged.set(true); + self.window_context.request_redraw(); } event => log_event(&event), }; diff --git a/examples/render_wgpu/src/main.rs b/examples/render_wgpu/src/main.rs index 41f45df3..c423c3c1 100644 --- a/examples/render_wgpu/src/main.rs +++ b/examples/render_wgpu/src/main.rs @@ -131,7 +131,7 @@ impl WgpuExample { } impl WindowHandler for WgpuExample { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let mut surface = self.surface.borrow_mut(); let surface_texture = match surface.get_current_texture() { diff --git a/examples/test-frame-pacing/src/main.rs b/examples/test-frame-pacing/src/main.rs index d8607833..84e454f7 100644 --- a/examples/test-frame-pacing/src/main.rs +++ b/examples/test-frame-pacing/src/main.rs @@ -18,6 +18,7 @@ const BAR_COUNT: u32 = 5; const BAR_SPEED_INCREMENTS: u32 = 3; struct FramePacingTest { + window_context: WindowContext, gl_context: GlContext, canvas: RefCell>, perf_graph: PerfGraph, @@ -25,6 +26,7 @@ struct FramePacingTest { bar_pos: Cell, bar_speed: Cell, + first: Cell, } impl FramePacingTest { @@ -46,18 +48,24 @@ impl FramePacingTest { unsafe { gl_context.make_not_current()? }; Ok(Self { + window_context, gl_context, canvas: canvas.into(), perf_graph: PerfGraph::new(), previous_frame_time: Instant::now().into(), bar_pos: 0.into(), bar_speed: 6.into(), + first: Cell::new(false), }) } } impl WindowHandler for FramePacingTest { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { + if !self.first.replace(true) { + //self.window_context.request_redraw(); + //return Ok(()); + } let now = Instant::now(); let dt = (now - self.previous_frame_time.get()).as_secs_f32(); self.previous_frame_time.set(now); @@ -118,6 +126,9 @@ impl WindowHandler for FramePacingTest { self.gl_context.swap_buffers()?; unsafe { self.gl_context.make_not_current()? }; + // Continuously schedule new frames + self.window_context.request_redraw(); + Ok(()) } diff --git a/src/context.rs b/src/context.rs index a83b8f1f..f757586b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,10 +1,12 @@ use super::*; use crate::dpi::Size; +use crate::waker::WindowWaker; use crate::{platform, MouseCursor, WindowSize}; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle, }; use std::fmt::Debug; +use std::time::Duration; /// A handle to the window given to a [`WindowHandler`](crate::WindowHandler), which it can then /// use to perform various operations on the window itself. @@ -34,6 +36,19 @@ impl WindowContext { self.inner.request_close(); } + pub fn request_redraw(&self) { + self.inner.request_redraw() + } + + pub fn request_redraw_after(&self, duration: Duration) { + self.inner.request_redraw_after(duration) + } + + #[must_use] + pub fn waker(&self) -> WindowWaker { + WindowWaker { inner: self.inner.waker() } + } + /// Returns `true` if this window currently has keyboard focus, `false` otherwise. pub fn has_focus(&self) -> bool { self.inner.has_focus() diff --git a/src/handler.rs b/src/handler.rs index 58bacf71..8758c55f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,12 +1,103 @@ use super::*; use crate::platform::Result; +#[non_exhaustive] +pub enum DamageArea { + FullWindow, + // Later: single rect, perhaps list of rects +} + pub trait WindowHandler: 'static { /// Requests the handler to draw a new frame immediately. /// + /// In order to reduce resource usage, this method is not called systematically at every frame + /// interval. + /// However, this method is automatically scheduled to be called in several situations: + /// + /// * When the window is first opened and shown to the user; + /// * When the window is shown after being previously hidden (if the platform did not keep the window's content in memory); + /// * When the window is resized; + /// * When the platform explicitly requests a window redraw (such as redrawing what was previously obscured by another window). + /// + /// In those situations, the [`draw`] call will be preceded by a [`damage`] call, specifying which + /// parts of the window need to be redrawn. + /// + /// This method can also be scheduled to be called from a call to [`WindowContext::request_redraw`], + /// as a result of [polling] or any kind of event. In this case, no [`damage`] call will occur. + /// + /// If this method itself calls [`WindowContext::request_redraw`], then a redraw will be + /// scheduled for the next frame interval. + /// This is useful for handling animations that need to run for successive frames in order to look smooth. + /// + /// Platforms may wait until this method completes before performing operations, such as + /// presenting the parent window or updating window decorations. + /// + /// Therefore, implementations should perform and complete all drawing inside this method + /// (or return an error), in order to minimize rendering artifacts. + /// + /// # Errors + /// /// If this returns an error, the window will be considered unable to render its contents, and /// will be subsequently closed. - fn on_frame(&self) -> core::result::Result<(), HandlerError>; + /// + /// [`draw`]: WindowHandler::draw + /// [`damage`]: WindowHandler::damage + /// [polling]: WindowHandler::poll + fn draw(&self) -> core::result::Result<(), HandlerError>; + + /// Notifies the handler that a given [`area`] of the window has been damaged by the platform + /// and needs to be redrawn. + /// + /// This is useful for renderers that support partial rendering, in order to not have to redraw + /// every single pixel on every frame. + /// + /// For instance, this will occur on non-compositing window managers (such as on X11), when + /// another window that initially obscured this one gets moved away. + /// + /// Additionally, this method will also be automatically called in the following situations: + /// * When the window is first opened and shown to the user; + /// * When the window is shown after being previously hidden (if the platform did not keep the window's content in memory); + /// * When the window is resized; + /// + /// Note that `baseview` only tracks and notifies about "external" (i.e. platform-originating) damage. + /// This method will *not* be called for "internal" damage, e.g. if moving the mouse over a button should redraw it. + /// Implementations should track this kind of damage internally (most GUI frameworks already do). + /// + /// Also note that depending on the platform, this method may be called multiple times with + /// different damage areas during a single frame interval. + /// Implementations should coalesce all the damaged areas received until the next [`draw`] call. + /// + /// Implementing this method is optional, as it's only useful if the handler supports partial rendering. + /// The default implementation of this method does nothing. + /// + /// [`area`]: DamageArea + /// [`draw`]: WindowHandler::draw + fn damage(&self, area: DamageArea) { + let _ = area; + } + + /// Requests the handler to poll updates from external sources, in preparation for rendering a new frame. + /// + /// This is useful when UIs need to check periodically on some sources (such as channels, queues, shared values, etc.) + /// to figure if they need updating. + /// + /// If received updates must result in the window updating its contents, then this method should + /// call [`WindowContext::request_redraw`]. + /// + /// Implementing this method is optional, and not needed if the window does not need to update itself + /// from external events. The default implementation for this method does nothing. + /// + /// # External update logic + /// + /// The goal is for all external update logic to be contained within this method. + /// Therefore, this method will be called in various cases, including (but not limited to): + /// + /// * As a result of calling [`Window::request_poll`], at the platform's earliest convenience; + /// * Whenever a new frame has been scheduled, right before actually calling [`WindowHandler::draw`]. + /// + /// This all means that this method will be invoked very regularly, possibly multiple times per frame interval. + /// Implementations should do their best to not block and finish their work as quick as possible. + fn poll(&self) {} /// Informs the handler that the window has been resized. /// diff --git a/src/lib.rs b/src/lib.rs index 80e16f94..06d387f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ mod keyboard; mod mouse_cursor; mod settings; mod tracing; +mod waker; mod window; pub(crate) mod platform; @@ -20,9 +21,10 @@ pub use clipboard::*; pub use context::{PlatformHandle, WindowContext}; pub use error::*; pub use event::*; -pub use handler::WindowHandler; +pub use handler::{DamageArea, WindowHandler}; pub use mouse_cursor::MouseCursor; pub use settings::*; +pub use waker::WindowWaker; pub use window::*; #[allow(unused, reason = "Some platforms may not use all exports from this mod")] diff --git a/src/platform/macos/context.rs b/src/platform/macos/context.rs index 4266a242..4629cb96 100644 --- a/src/platform/macos/context.rs +++ b/src/platform/macos/context.rs @@ -1,7 +1,7 @@ use crate::dpi::Size; use crate::platform::macos::view::BaseviewView; -use crate::platform::Result; use crate::platform::{PlatformHandle, WindowSharedState}; +use crate::platform::{Result, WindowWaker}; use crate::wrappers::appkit::{View, ViewRef}; use crate::*; use dispatch2::MainThreadBound; @@ -10,6 +10,7 @@ use objc2::runtime::NSObjectProtocol; use objc2::{MainThreadMarker, Message}; use raw_window_handle::DisplayHandle; use std::rc::Rc; +use std::time::Duration; #[derive(Clone)] pub struct WindowContext { @@ -33,6 +34,21 @@ impl WindowContext { BaseviewView::close(view, false); } + pub fn request_redraw(&self) { + self.state.redraw_requested.set(true); + let Some(view) = self.view.load() else { return }; + let Some(view) = view.inner() else { return }; + view.set_next_frame_needed(true); + } + + pub fn request_redraw_after(&self, duration: Duration) { + self.waker().request_redraw_after(duration); + } + + pub fn waker(&self) -> WindowWaker { + WindowWaker::new(Weak::clone(&self.view)) + } + pub fn has_focus(&self) -> bool { let Some(view) = self.view.load() else { return false }; let Some(window) = view.window() else { diff --git a/src/platform/macos/gl.rs b/src/platform/macos/gl.rs index dc8a70ad..bb1a7840 100644 --- a/src/platform/macos/gl.rs +++ b/src/platform/macos/gl.rs @@ -171,13 +171,11 @@ impl GlContext { pub fn swap_buffers(&self) -> Result<()> { self.context.flushBuffer(); - self.view.setNeedsDisplay(true); Ok(()) } /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. pub(crate) fn resize(&self, size: NSSize) { self.view.setFrameSize(size); - self.view.setNeedsDisplay(true); } } diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index cd2967f6..c9b6e618 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -3,6 +3,7 @@ mod cursor; mod error; mod keyboard; mod view; +mod waker; mod window; use crate::platform::macos::view::BaseviewView; @@ -17,7 +18,9 @@ use objc2_app_kit::NSView; use raw_window_handle::{DisplayHandle, HasWindowHandle}; use std::fmt; use std::fmt::Formatter; +pub use waker::WindowWaker; pub use window::*; + pub(crate) type Result = std::result::Result; #[cfg(feature = "opengl")] diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index bc9f8043..22d247b4 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -12,8 +12,8 @@ use crate::window::WindowInitializer; use crate::wrappers::appkit::*; use crate::MouseEvent::{ButtonPressed, ButtonReleased}; use crate::{ - DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, ScrollDelta, WindowEvent, - WindowHandler, WindowSize, + DropData, DropEffect, Event, EventStatus, HandlerError, MouseButton, MouseEvent, ScrollDelta, + WindowEvent, WindowHandler, WindowSize, }; use objc2::__framework_prelude::Retained; use objc2::rc::Weak; @@ -24,7 +24,8 @@ use objc2_app_kit::{ NSTrackingAreaOptions, NSView, NSWindow, }; use objc2_foundation::{NSArray, NSNotification, NSPoint, NSPointInRect, NSRect, NSSize, NSString}; -use std::cell::{Cell, RefCell}; +use objc2_quartz_core::CADisplayLink; +use std::cell::{Cell, OnceCell, RefCell}; use std::rc::Rc; pub enum ViewParentingType { @@ -65,19 +66,20 @@ pub(crate) struct BaseviewView { pub(crate) mtm: MainThreadMarker, window_handler: WindowHandlerContainer, - frame_timer: Cell>, notification_center_observer: Cell>, keyboard_state: KeyboardState, parenting: RefCell, pub(crate) lifetime_tied_to_app: Cell>>, + display_link: OnceCell>, + display_link_started: Cell, host: Host, pub(crate) cursor_manager: CursorManager, #[cfg(feature = "opengl")] - pub(crate) gl_context: std::cell::OnceCell, + pub(crate) gl_context: OnceCell, } impl BaseviewView { @@ -99,7 +101,8 @@ impl BaseviewView { state: Rc::clone(&state), keyboard_state: KeyboardState::new(), - frame_timer: None.into(), + display_link: OnceCell::new(), + display_link_started: false.into(), window_handler: WindowHandlerContainer::new(), notification_center_observer: None.into(), parenting: ViewParentingType::Uninitialized.into(), @@ -108,7 +111,7 @@ impl BaseviewView { cursor_manager: CursorManager::new(), #[cfg(feature = "opengl")] - gl_context: std::cell::OnceCell::new(), + gl_context: OnceCell::new(), }; let view = View::new(view_rect, inner, |view| { @@ -139,14 +142,9 @@ impl BaseviewView { let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType }; view.view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type])); - let timer_view = Weak::new(view.view); - view.frame_timer.set(TimerHandle::new(0.015, move || { - if let Some(view) = timer_view.load() { - if let Some(view) = view.inner_ref() { - Self::trigger_frame(view); - } - } - })); + let display_link = view.view.setup_display_link(); + display_link.setPaused(true); + let Ok(()) = view.display_link.set(display_link) else { unreachable!() }; let notifier_view = Weak::new(view.view); let observer = NotificationCenterObserver::register_window_key_change(move |n| { @@ -184,13 +182,32 @@ impl BaseviewView { } } + pub fn poll(this: ViewRef) { + this.window_handler.use_handler(|h| h.poll()); + } + + pub fn set_next_frame_needed(&self, needed: bool) { + if self.display_link_started.get() == needed { + return; + }; + + let Some(display_link) = self.display_link.get() else { return }; + + display_link.setPaused(!needed); + self.display_link_started.set(needed); + } + pub fn close(this: ViewRef, from_host: bool) { this.state.closed.set(true); this.view.removeFromSuperview(); this.notification_center_observer.take(); - this.frame_timer.take(); this.window_handler.destroy(); + if let Some(link) = this.display_link.get() { + link.setPaused(true); + link.invalidate(); + } + let parenting = this.parenting.replace(ViewParentingType::Uninitialized); parenting.teardown(); @@ -254,9 +271,20 @@ impl BaseviewView { } fn trigger_frame(this: ViewRef) { - if let Some(Err(e)) = this.window_handler.use_handler(|h| h.on_frame()) { - warn!("Error while rendering frame: {}", e); + let result = this.window_handler.use_handler(|h| { + this.state.redraw_requested.set(false); + h.draw()?; + + Ok::<(), HandlerError>(()) + }); + + let Some(result) = result else { return }; + + if let Err(e) = result { + warn!("Error while drawing: {}", e); Self::close(this, false); + } else { + this.set_next_frame_needed(this.state.redraw_requested.take()) } } @@ -356,6 +384,14 @@ impl ViewImpl for BaseviewView { } } + fn draw_rect(this: ViewRef, _rect: NSRect) { + this.set_next_frame_needed(true); + } + + fn display_link_fired(this: ViewRef, _sender: &CADisplayLink) { + Self::trigger_frame(this); + } + /// `hitTest:` override that collapses hits on baseview's internal /// OpenGL render subview to this NSView. /// diff --git a/src/platform/macos/waker.rs b/src/platform/macos/waker.rs new file mode 100644 index 00000000..2535062d --- /dev/null +++ b/src/platform/macos/waker.rs @@ -0,0 +1,36 @@ +use crate::platform::macos::view::BaseviewView; +use crate::wrappers::appkit::{MainThreadBoundWeak, View}; +use objc2::rc::Weak; +use std::time::Duration; + +#[derive(Clone)] +pub struct WindowWaker { + view: MainThreadBoundWeak>, +} + +impl WindowWaker { + pub fn new(reference: Weak>) -> Self { + Self { view: MainThreadBoundWeak::new(reference) } + } + + pub fn request_redraw(&self) { + self.request_redraw_after(Duration::ZERO) + } + + pub fn request_redraw_after(&self, duration: Duration) { + self.view.use_on_main_thread_after(duration, |view| { + let Some(view) = view.load() else { return }; + let Some(view) = view.inner() else { return }; + view.set_next_frame_needed(true); + }) + } + + pub fn request_poll(&self) { + self.view.use_on_main_thread(|view| { + let Some(view) = view.load() else { return }; + let Some(view) = view.inner_ref() else { return }; + + BaseviewView::poll(view); + }) + } +} diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index dcb09bf8..82c1717e 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -10,8 +10,8 @@ use std::cell::Cell; use std::rc::Rc; use crate::platform::macos::view::{BaseviewView, ViewParentingType}; -use crate::platform::ParentWindowHandle; use crate::platform::Result; +use crate::platform::{ParentWindowHandle, WindowWaker}; use crate::utils::SizingStrategy; use crate::wrappers::appkit::{create_window, View}; use crate::*; @@ -164,6 +164,18 @@ impl WindowHandle { pub fn sizing_strategy(&self) -> SizingStrategy { self.state.sizing_strategy } + + pub fn request_poll(&self) -> Result<()> { + let Some(view) = self.view.load() else { return Ok(()) }; + let Some(view) = view.inner_ref() else { return Ok(()) }; + + BaseviewView::poll(view); + Ok(()) + } + + pub fn waker(&self) -> WindowWaker { + WindowWaker::new(Weak::clone(&self.view)) + } } fn create_window_with_options( @@ -189,6 +201,7 @@ pub(crate) struct WindowSharedState { pub size: Cell>, pub scale_factor: Cell, pub sizing_strategy: SizingStrategy, + pub redraw_requested: Cell, } impl WindowSharedState { @@ -198,6 +211,7 @@ impl WindowSharedState { size: size.into(), scale_factor: scale_factor.into(), sizing_strategy, + redraw_requested: Cell::new(false), } } } diff --git a/src/platform/win/mod.rs b/src/platform/win/mod.rs index ef733ce8..912ceae6 100644 --- a/src/platform/win/mod.rs +++ b/src/platform/win/mod.rs @@ -3,6 +3,7 @@ mod drop_target; mod error; mod hook; mod keyboard; +mod waker; mod window; mod window_state; @@ -17,6 +18,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::num::NonZeroIsize; use std::ptr::NonNull; use std::rc::Rc; +pub use waker::WindowWaker; pub use window::*; #[cfg(feature = "opengl")] diff --git a/src/platform/win/waker.rs b/src/platform/win/waker.rs new file mode 100644 index 00000000..795b1398 --- /dev/null +++ b/src/platform/win/waker.rs @@ -0,0 +1,42 @@ +use crate::wrappers::win32::window::{HWnd, PostMessageExt, SyncHwnd}; +use std::sync::{Arc, OnceLock}; +use std::time::Duration; + +pub struct WindowWakerSource { + data: Arc>, +} + +impl WindowWakerSource { + pub fn new() -> Self { + Self { data: Arc::new(OnceLock::new()) } + } + + pub fn set(&self, hwnd: HWnd) { + let Ok(()) = self.data.set(hwnd.into()) else { unreachable!() }; + } + + pub fn waker(&self) -> WindowWaker { + WindowWaker { shared: Arc::clone(&self.data) } + } +} + +#[derive(Clone)] +pub struct WindowWaker { + shared: Arc>, +} + +impl WindowWaker { + pub fn request_redraw(&self) { + self.request_redraw_after(Duration::ZERO) + } + + pub fn request_redraw_after(&self, duration: Duration) { + let Some(hwnd) = self.shared.get() else { return }; + hwnd.post_request_redraw(duration) + } + + pub fn request_poll(&self) { + let Some(hwnd) = self.shared.get() else { return }; + hwnd.post_request_poll() + } +} diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index fecf22aa..0badccf2 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -7,11 +7,9 @@ use windows_sys::Win32::{ use crate::dpi::{PhysicalPosition, PhysicalSize, Size}; use crate::{warn, EventStatus, HandlerError, WindowHandler}; use std::cell::{Cell, OnceCell}; -use std::num::{NonZeroU32, NonZeroUsize}; +use std::num::NonZeroU32; use windows_sys::Win32::Foundation::POINT; -pub(crate) const BV_WINDOW_MUST_CLOSE: u32 = WM_USER + 1; - use super::drop_target::DropTarget; use super::*; use crate::handler::WindowHandlerBuilder; @@ -24,7 +22,7 @@ use crate::wrappers::win32::cursor::SystemCursor; use crate::wrappers::win32::window::*; use crate::wrappers::win32::{ ole_initialize, run_thread_message_loop_until, Dpi, DpiAwarenessGuard, LibraryModule, Rect, - WindowStyle, + TimerId, WindowStyle, }; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowSize}; @@ -36,11 +34,6 @@ fn lo_word(lparam: LPARAM) -> u16 { (lparam & 0xffff) as u16 } -const WIN_FRAME_TIMER: NonZeroUsize = match NonZeroUsize::new(4242) { - Some(x) => x, - None => unreachable!(), -}; - pub struct WindowHandle { init: Cell>, hwnd: Cell>, @@ -194,6 +187,18 @@ impl WindowHandle { Ok(()) } + + pub fn waker(&self) -> WindowWaker { + self.state.window_waker_source.waker() + } + + pub fn request_poll(&self) -> Result<()> { + if let Some(hwnd) = self.hwnd.get() { + hwnd.post_request_poll(); + } + + Ok(()) + } } impl Drop for WindowHandle { @@ -244,6 +249,8 @@ impl BaseviewWindow { let shared_state = Rc::clone(&shared_state); move |hwnd: HWnd| { + shared_state.set_hwnd(hwnd); + let window_state = Rc::new(WindowState::new( hwnd, shared_state.user32.clone(), @@ -271,13 +278,6 @@ impl BaseviewWindow { let title = HSTRING::from(init.settings.title); let window = create_window(&title, style, rect.size(), parent, &dpi_ctx, initializer)?; - // FIXME: this SetTimer call could be in after_create, but for some reason it changes the ordering - // for a parent+child window situation, which results in the parent drawing over the child. - // This timer should be replaced by proper window redrawing/damage/vsync handling, but this - // would be a breaking change, so we'll do that later. - // TODO: create a new timer instead of hard-coding a specific ID - window.set_timer(WIN_FRAME_TIMER, 15)?; - Ok(window) } @@ -299,13 +299,24 @@ impl BaseviewWindow { self.host.request_resize(new_size) } - pub(crate) fn handle_on_frame(&self) { + pub(crate) fn handle_draw(&self) { let Some(handler) = self.handler.get() else { return }; - if let Err(e) = handler.on_frame() { + handler.poll(); + + self.window_state.redraw_requested.set(false); + if let Err(e) = handler.draw() { warn!("Error while rendering frame: {}", e); self.window_state.request_close(); } + + self.window_state.setup_redraw_request_for_next_frame(); + } + + pub(crate) fn handle_poll(&self) { + let Some(handler) = self.handler.get() else { return }; + + handler.poll(); } pub(crate) fn handle_event(&self, event: Event) -> EventStatus { @@ -402,6 +413,7 @@ impl WindowImpl for BaseviewWindow { fn before_destroy(&self, window: HWnd) { let _ = window.revoke_drag_drop(); + // No need to destroy timers: they are all destroyed when the window is invalidated. } } @@ -530,13 +542,36 @@ unsafe fn wnd_proc_inner( None } - WM_TIMER => { - if wparam == WIN_FRAME_TIMER.get() { - window_bv.handle_on_frame() + WM_PAINT => { + if let Some(rect) = window.get_update_rect() { + window_bv.handle_draw(); + window.validate_rect(rect); } Some(0) } + WM_TIMER => { + let timer_id = TimerId::from_raw(wparam)?; + + if window_state.redraw_timer.matches_id(timer_id) + && window_state.redraw_timer.is_running() + { + window.invalidate_window(); + Some(0) + } else { + match window_state.shared.delayed_redraw_timers.remove_if_exists(window, timer_id) { + Ok(false) => None, + Err(e) => { + warn!("Could not remove timer: {}", e); + None + } + Ok(true) => { + window_state.request_redraw(); + Some(0) + } + } + } + } WM_CLOSE => { window_bv.handle_event(Event::Window(WindowEvent::WillClose)); @@ -613,6 +648,8 @@ unsafe fn wnd_proc_inner( return Some(-1); } + window.invalidate_window(); + None } WM_DPICHANGED => { @@ -732,6 +769,16 @@ unsafe fn wnd_proc_inner( let _ = window.destroy(); Some(0) } + + BV_REQUEST_REDRAW => { + window_state.request_redraw(); + Some(0) + } + + BV_REQUEST_POLL => { + window_bv.handle_poll(); + Some(0) + } _ => None, } } diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 58bd85d2..6065d90c 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -1,20 +1,25 @@ use crate::dpi::{PhysicalSize, Size}; use crate::platform::win::dpi::DpiScalingStrategy; use crate::platform::win::keyboard::KeyboardState; -use crate::platform::PlatformHandle; +use crate::platform::win::waker::WindowWakerSource; +use crate::platform::{PlatformHandle, WindowWaker}; use crate::utils::SizingStrategy; use crate::window::WindowInitializer; use crate::wrappers::win32::cursor::SystemCursor; use crate::wrappers::win32::h_instance::HInstance; -use crate::wrappers::win32::window::HWnd; -use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule}; +use crate::wrappers::win32::window::{HWnd, PostMessageExt}; +use crate::wrappers::win32::{ + Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList, TimerSlot, +}; use crate::WindowSettings; use crate::{MouseCursor, WindowSize}; use raw_window_handle::{DisplayHandle, Win32WindowHandle}; use std::cell::{Cell, Ref, RefCell}; use std::num::NonZeroIsize; use std::rc::Rc; -use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW; +use std::time::Duration; + +const REDRAW_TIMER_DELAY_MSEC: u32 = 15; /// All data associated with the window. pub(crate) struct WindowState { @@ -27,6 +32,8 @@ pub(crate) struct WindowState { pub user32: LibraryModule, pub shared: Rc, + pub(crate) redraw_timer: TimerSlot, + pub redraw_requested: Cell, #[cfg(feature = "opengl")] pub gl_context: std::cell::OnceCell, @@ -42,8 +49,10 @@ impl WindowState { mouse_button_counter: Cell::new(0), mouse_was_outside_window: true.into(), cursor_icon: Cell::new(MouseCursor::Default), + redraw_timer: TimerSlot::empty(hwnd), user32, shared, + redraw_requested: Cell::new(false), #[cfg(feature = "opengl")] gl_context: std::cell::OnceCell::new(), @@ -65,14 +74,7 @@ impl WindowState { } pub fn request_close(&self) { - unsafe { - PostMessageW( - self.hwnd.as_raw(), - crate::platform::win::window::BV_WINDOW_MUST_CLOSE, - 0, - 0, - ); - } + self.hwnd.post_must_close(); } pub fn has_focus(&self) -> bool { @@ -126,9 +128,33 @@ impl WindowState { let Some(hwnd) = NonZeroIsize::new(self.hwnd.as_raw() as _) else { unreachable!() }; PlatformHandle { hwnd } } + + pub fn request_redraw(&self) { + self.redraw_requested.set(true); + + self.redraw_timer.start_if_not_running(REDRAW_TIMER_DELAY_MSEC); + } + + pub fn setup_redraw_request_for_next_frame(&self) { + let should_redraw_next_frame = self.redraw_requested.take(); + + self.redraw_timer.set_running(should_redraw_next_frame, REDRAW_TIMER_DELAY_MSEC); + } + + pub fn request_redraw_after(&self, duration: Duration) { + if let Err(e) = self.shared.delayed_redraw_timers.add_new_timer(self.hwnd, duration) { + crate::warn!("Request Redraw failed: Could not add timer: {}", e) + } + } + + #[inline] + pub fn waker(&self) -> WindowWaker { + self.shared.window_waker_source.waker() + } } pub struct WindowSharedState { + pub hwnd: Cell>, pub parented: Cell, pub is_alive: Cell, pub current_size: Cell>, @@ -140,6 +166,8 @@ pub struct WindowSharedState { pub user32: LibraryModule, pub sizing_strategy: SizingStrategy, + pub delayed_redraw_timers: TimerList, + pub window_waker_source: WindowWakerSource, } impl WindowSharedState { @@ -155,6 +183,9 @@ impl WindowSharedState { sizing_strategy: SizingStrategy::from_settings(settings), user32, dpi_scaling_strategy: DpiScalingStrategy::default().into(), + delayed_redraw_timers: TimerList::new(), + window_waker_source: WindowWakerSource::new(), + hwnd: None.into(), } .into() } @@ -175,6 +206,11 @@ impl WindowSharedState { self.dpi_scaling_strategy.set(strategy); } + pub fn set_hwnd(&self, hwnd: HWnd) { + self.hwnd.set(Some(hwnd)); + self.window_waker_source.set(hwnd) + } + pub fn size(&self) -> WindowSize { WindowSize::from_physical(self.current_size.get(), self.scale_factor()) } diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 8332bd0a..99fb4921 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -153,25 +153,17 @@ impl EventLoop { Ok(()) } - fn handle_redraw(&mut self) { - if !self.draw_now { - return; - } + fn redraw(&mut self) -> Result<(), FatalError> { + self.window.present_notify_requested.set(false); self.draw_now = false; - if !self.window.visibility_state.own_window_is_viewable() { - return; - } - - if let Err(e) = self.handler.on_frame() { + if let Err(e) = self.handler.draw() { self.trigger_fatal_error(e.into()); - return; + return Ok(()); } - self.window.present_notify_requested.set(true); - - // Any socket error will be handled in the next poll - let _ = self.window.connection.conn.flush(); + self.window.connection.conn.flush()?; + Ok(()) } fn handle_present_notify(&mut self) -> Result<(), FatalError> { @@ -331,12 +323,10 @@ impl EventLoop { } WindowThreadRequest::Show => { self.window.xcb_window.map_window()?.check()?; - self.window.visibility_state.window_mapped(self.window.xcb_window.id()); Ok(()) } WindowThreadRequest::Hide => { self.window.xcb_window.unmap_window()?.check()?; - self.window.visibility_state.window_unmapped(self.window.xcb_window.id()); Ok(()) } } @@ -360,7 +350,23 @@ impl EventLoop { loop { self.handle_coalesced_resize_events()?; - self.handle_redraw(); + + // Consume all requests from above poll + if let Some(redraw_after) = self.window.main_thread_shared.take_redraw_request() { + self.window.request_redraw_after(redraw_after) + } + + let shared_poll_requested = self.window.main_thread_shared.take_poll_request(); + + if self.draw_now { + self.handler.poll(); + self.redraw()?; + self.window.poll_requested.set(false); + } else if shared_poll_requested || self.window.poll_requested.get() { + self.handler.poll(); + self.window.poll_requested.set(false); + } + self.handle_present_notify()?; if !self.drain_xcb_events()? { @@ -395,6 +401,10 @@ impl EventLoop { Ok(()) } + pub fn request_redraw(&self) { + self.window.request_redraw(); + } + fn handle_xcb_event(&mut self, event: XEvent) -> Result<(), FatalError> { // For all the keyboard and mouse events, you can fetch // `x`, `y`, `detail`, and `state`. @@ -459,15 +469,15 @@ impl EventLoop { } XEvent::ConfigureNotify(event) => { - // These are coalesced and then handled asynchronously at the end of the event loop - if event.window == self.window.raw_id() { - self.new_size = Some(PhysicalSize::new(event.width, event.height)); - } else if Some(event.window) - == self.window.visibility_state.parent_id().map(|i| i.get()) - { - // Also resize the window if the parent is resized - // This works around some hosts that might not call set_size() right away (or at all...) - self.new_parent_size = Some(PhysicalSize::new(event.width, event.height)); + if let Some(window_id) = NonZero::new(event.window) { + // These are coalesced and then handled asynchronously at the end of the event loop + if window_id == self.window.xcb_window.id() { + self.new_size = Some(PhysicalSize::new(event.width, event.height)); + } else if Some(window_id) == self.window.parent_id.get() { + // Also resize the window if the parent is resized + // This works around some hosts that might not call set_size() right away (or at all...) + self.new_parent_size = Some(PhysicalSize::new(event.width, event.height)); + } } } @@ -561,12 +571,6 @@ impl EventLoop { XEvent::MapNotify(e) => { if let Some(window_id) = NonZero::new(e.window) { if window_id == self.window.xcb_window.id() { - self.window.is_mapped.set(true); - } - - let became_viewable = self.window.visibility_state.window_mapped(window_id); - - if became_viewable { if self.window.xcb_window.present_supported() && self.window.xcb_window.present_select_input()? { @@ -578,31 +582,11 @@ impl EventLoop { } } - XEvent::UnmapNotify(e) => { + XEvent::ReparentNotify(e) => { if let Some(window_id) = NonZero::new(e.window) { if window_id == self.window.xcb_window.id() { - self.window.is_mapped.set(false) + self.window.parent_id.set(NonZero::new(e.parent)); } - - self.window.visibility_state.window_unmapped(window_id); - } - } - - XEvent::ReparentNotify(e) => { - if let Some(window_id) = NonZero::new(e.window) { - self.window.visibility_state.window_reparented( - window_id, - NonZero::new(e.parent), - &self.window.connection, - ) - } - } - - XEvent::DestroyNotify(e) => { - if let Some(window_id) = NonZero::new(e.window) { - self.window - .visibility_state - .window_destroyed(window_id, &self.window.connection) } } diff --git a/src/platform/x11/mod.rs b/src/platform/x11/mod.rs index b86140ca..ba0cb652 100644 --- a/src/platform/x11/mod.rs +++ b/src/platform/x11/mod.rs @@ -20,7 +20,7 @@ mod keyboard; mod visual_info; mod xcb_window; -mod visibility_tree; +mod waker; mod window_shared; mod window_thread; @@ -31,6 +31,7 @@ use crate::platform::x11::window_shared::WindowInner; use crate::wrappers::xlib::XlibXcbConnection; pub type WindowContext = Rc; +pub use waker::WindowWaker; #[cfg(feature = "opengl")] pub mod gl; diff --git a/src/platform/x11/visibility_tree.rs b/src/platform/x11/visibility_tree.rs deleted file mode 100644 index 1f9146f6..00000000 --- a/src/platform/x11/visibility_tree.rs +++ /dev/null @@ -1,362 +0,0 @@ -use crate::platform::X11Connection; -use std::cell::{Cell, RefCell}; -use std::num::NonZeroU32; -use x11rb::errors::ReplyError; -use x11rb::protocol::xproto::{ConnectionExt, MapState, QueryTreeReply}; -use x11rb::protocol::ErrorKind; -use x11rb::x11_utils::X11Error; - -pub enum AncestorVisibilityState { - Floating { - own_window_viewable: Cell, - own_window_id: NonZeroU32, - }, - Parented { - ancestry: AncestryList, - root_id: Cell>, - own_window_viewable: Cell, - }, -} - -pub struct AncestryList { - inner: RefCell>, -} - -impl AncestryList { - pub fn new(own_window: NonZeroU32) -> Self { - Self { inner: RefCell::new(vec![Ancestor { id: own_window, mapped: false.into() }]) } - } - - pub fn pop_id(&self) -> Option { - self.inner.borrow_mut().pop().map(|a| a.id) - } - - pub fn last_id(&self) -> Option { - self.inner.borrow().last().map(|a| a.id) - } - - pub fn push(&self, ancestor: Ancestor) { - self.inner.borrow_mut().push(ancestor); - } - - pub fn parent_id(&self) -> Option { - self.inner.borrow().get(1).map(|a| a.id) - } - pub fn own_window_id(&self) -> Option { - self.inner.borrow().first().map(|a| a.id) - } - - pub fn remove_window(&self, id: NonZeroU32) -> bool { - let mut inner = self.inner.borrow_mut(); - let Some(index) = inner.iter().position(|a| a.id == id) else { - return false; - }; - - inner.truncate(index.saturating_add(1)); - - true - } - - pub fn remove_after_window(&self, id: NonZeroU32) -> bool { - let mut inner = self.inner.borrow_mut(); - let Some(index) = inner.iter().position(|a| a.id == id) else { - return false; - }; - - inner.truncate(index.saturating_add(2)); - - true - } - - pub fn check_all_mapped(&self) -> bool { - self.inner.borrow().iter().all(|a| a.mapped.get()) - } - - pub fn set_mapped(&self, window: NonZeroU32, mapped: bool) -> bool { - let inner = self.inner.borrow(); - let Some(ancestor) = inner.iter().find(|a| a.id == window) else { - return false; - }; - - ancestor.mapped.set(mapped); - true - } -} - -#[cfg_attr(debug_assertions, derive(Debug))] -pub struct Ancestor { - id: NonZeroU32, - mapped: Cell, -} - -impl AncestorVisibilityState { - pub fn discover( - connection: &X11Connection, own_window_id: NonZeroU32, parented: bool, - ) -> Result { - if !parented { - return Ok(Self::Floating { own_window_viewable: Cell::new(false), own_window_id }); - } - - let this = Self::Parented { - ancestry: AncestryList::new(own_window_id), - own_window_viewable: Cell::new(false), - root_id: Cell::new(NonZeroU32::new(connection.default_screen().root)), - }; - - this.try_regenerate_from_last_window(connection)?; - - Ok(this) - } - - pub fn own_window_is_viewable(&self) -> bool { - match self { - Self::Parented { own_window_viewable, .. } => own_window_viewable.get(), - Self::Floating { own_window_viewable, .. } => own_window_viewable.get(), - } - } - - pub fn parent_id(&self) -> Option { - match self { - Self::Parented { ancestry, .. } => ancestry.parent_id(), - _ => None, - } - } - - /// Returns `true` if this operation made our own window visible. - pub fn window_mapped(&self, window_id: NonZeroU32) -> bool { - match self { - Self::Floating { own_window_id, own_window_viewable } => { - if *own_window_id != window_id { - return false; - } - - if own_window_viewable.get() { - return true; - } - - own_window_viewable.set(true); - true - } - Self::Parented { own_window_viewable, ancestry, .. } => { - if !ancestry.set_mapped(window_id, true) { - return false; - } - - if own_window_viewable.get() { - return false; - } - - let all_mapped = ancestry.check_all_mapped(); - if all_mapped { - own_window_viewable.set(true); - } - - all_mapped - } - } - } - - pub fn window_unmapped(&self, window_id: NonZeroU32) { - match self { - Self::Floating { own_window_id, own_window_viewable } => { - if *own_window_id != window_id { - return; - } - - own_window_viewable.set(false); - } - Self::Parented { own_window_viewable, ancestry, .. } => { - if !ancestry.set_mapped(window_id, false) { - return; - } - - own_window_viewable.set(false); - } - } - } - - pub fn window_destroyed(&self, window_id: NonZeroU32, connection: &X11Connection) { - let Self::Parented { ancestry, .. } = &self else { - return; - }; - - if !ancestry.remove_window(window_id) { - return; - } - - self.regenerate_from_last_window(connection); - } - - pub fn window_reparented( - &self, window_id: NonZeroU32, new_parent: Option, connection: &X11Connection, - ) { - let Self::Parented { ancestry, root_id, .. } = &self else { - return; - }; - - if !ancestry.remove_after_window(window_id) { - return; - } - - if let Some(new_parent) = new_parent { - if Some(new_parent) == root_id.get() { - return; - } - - ancestry.push(Ancestor { id: new_parent, mapped: Cell::new(false) }); - - self.regenerate_from_last_window(connection); - } - } - - pub fn regenerate_from_last_window(&self, connection: &X11Connection) { - if let Err(e) = self.try_regenerate_from_last_window(connection) { - crate::warn!("Failed to generate window ancestry list: {}", e) - } - } - - fn try_regenerate_from_last_window( - &self, connection: &X11Connection, - ) -> Result<(), ReplyError> { - let Self::Parented { ancestry, own_window_viewable, root_id } = &self else { - return Ok(()); - }; - - let Some(mut current_window) = ancestry.pop_id() else { return Ok(()) }; - - let mut shitlist = Vec::new(); - let mut rechecked_children = Vec::new(); - - loop { - let Some((mut mapped, tree)) = fetch_window_info(connection, current_window)? else { - // We got a BadWindow while trying to get a window's info, it must have been destroyed. - // Try to go back a layer and fetch the window's state and parent again - - crate::warn!("Failed to get info for window {}: XBadWindow", current_window); - - let Some(previous_parent) = ancestry.pop_id() else { - // No previous parent, this was the first window. Stop everything and return an empty state - break; - }; - - if shitlist.contains(&previous_parent) { - crate::warn!( - "Failed to get info for window {} in the past already. Stopping.", - previous_parent - ); - break; - } - - if shitlist.len() > 10 { - crate::warn!( - "Too many failures while trying to build X ancestry tree. Stopping." - ); - break; - } - - shitlist.push(previous_parent); - - current_window = previous_parent; - continue; - }; - - if tree.parent == current_window.get() { - // Weird, but that might also mean we're at the end of the tree (or the window has no parent yet) - break; - } - - // Sanity check if the current parent is actually registered to have the child in its children list - if let Some(child_id) = ancestry.last_id() { - if !tree.children.contains(&child_id.get()) { - // The child has been orphaned, it must have been reparented between our server queries. - // Go back a step and check again. - - if rechecked_children.contains(&child_id) { - crate::warn!( - "Children of parent {} does not contain {}: {:?}", - current_window, - child_id, - &tree.children - ); - } else { - rechecked_children.push(child_id); - } - - let Some(_) = ancestry.pop_id() else { unreachable!() }; - current_window = child_id; - continue; - } - } - - if ancestry.own_window_id().is_some_and(|id| id != current_window) { - // Despite what's documented, all windows down the parent tree must have the event mask - // bit set, otherwise events are not propagated through to us. - if let Err(e) = - connection.register_tree_structure_events_for_window(current_window)?.check() - { - crate::warn!( - "Could not register SubstructureNotify event for window {}: {}", - current_window, - e - ); - mapped = true; // Assume it is mapped, since we'll possibly not get any events from this window - } - } - - // All checks succeeded, now register the current window info and fetch info from the parent - ancestry.push(Ancestor { id: current_window, mapped: mapped.into() }); - - if tree.parent == tree.root { - // No need to get info for the root, we assume it's always there. We can just stop here. - break; - } - - // If parent == 0, assume there's no parent and just break - if let Some(parent) = NonZeroU32::new(tree.parent) { - current_window = parent; - } else { - break; - } - - if let Some(root) = NonZeroU32::new(tree.root) { - if Some(root) != root_id.get() { - root_id.set(Some(root)) - } - } - } - - own_window_viewable.set(ancestry.check_all_mapped()); - - Ok(()) - } -} - -/// Returns Ok(None) on BadWindow. -fn fetch_window_info( - connection: &X11Connection, window: NonZeroU32, -) -> Result, ReplyError> { - let attrs_cookie = connection.conn.get_window_attributes(window.get())?; - let tree_cookie = connection.conn.query_tree(window.get())?; - - let mapped = match attrs_cookie.reply() { - Ok(attr) => attr.map_state != MapState::UNMAPPED, - Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => { - tree_cookie.discard_reply_and_errors(); - return Ok(None); - } - Err(e) => { - tree_cookie.discard_reply_and_errors(); - return Err(e); - } - }; - - let tree = match tree_cookie.reply() { - Ok(tree) => tree, - Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => { - return Ok(None) - } - Err(e) => return Err(e), - }; - - Ok(Some((mapped, tree))) -} diff --git a/src/platform/x11/waker.rs b/src/platform/x11/waker.rs new file mode 100644 index 00000000..c5872720 --- /dev/null +++ b/src/platform/x11/waker.rs @@ -0,0 +1,26 @@ +use crate::platform::x11::window_thread::WindowThreadShared; +use calloop::LoopSignal; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone)] +pub struct WindowWaker { + pub(crate) loop_signal: LoopSignal, + pub(crate) shared: Arc, +} + +impl WindowWaker { + pub fn request_redraw(&self) { + self.request_redraw_after(Duration::ZERO) + } + + pub fn request_redraw_after(&self, duration: Duration) { + self.shared.request_redraw_after(duration); + self.loop_signal.wakeup(); + } + + pub fn request_poll(&self) { + self.shared.request_poll(); + self.loop_signal.wakeup(); + } +} diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 9a6f78a5..56fa7735 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,18 +1,21 @@ use crate::dpi::{PhysicalSize, Size}; use crate::platform::x11::event_loop::EventLoop; -use crate::platform::x11::visibility_tree::AncestorVisibilityState; use crate::platform::x11::visual_info::WindowVisualConfig; +use crate::platform::x11::waker::WindowWaker; use crate::platform::x11::window_thread::WindowThreadShared; use crate::platform::x11::xcb_connection::get_size_hints; use crate::platform::x11::xcb_window::XcbWindow; use crate::platform::*; use crate::utils::SizingStrategy; use crate::{warn, MouseCursor, WindowHandler, WindowSettings, WindowSize}; -use calloop::LoopSignal; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{LoopHandle, LoopSignal}; use raw_window_handle::{DisplayHandle, XlibWindowHandle}; use std::cell::Cell; +use std::num::NonZeroU32; use std::rc::Rc; use std::sync::Arc; +use std::time::Duration; use x11rb::protocol::xproto; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt, InputFocus, Visualid}; use x11rb::CURRENT_TIME; @@ -48,6 +51,7 @@ pub(crate) struct WindowInner { gl_context: Option, pub(crate) xcb_window: XcbWindow, + pub(crate) parent_id: Cell>, pub(crate) connection: Rc, pub(crate) scaling_factor: ScalingFactor, @@ -58,11 +62,10 @@ pub(crate) struct WindowInner { pub(crate) visual_id: Visualid, pub(crate) is_focused: Cell, - pub(crate) is_mapped: Cell, pub(crate) present_notify_requested: Cell, + pub(crate) poll_requested: Cell, pub(crate) loop_signal: LoopSignal, - - pub(crate) visibility_state: AncestorVisibilityState, + loop_handle: LoopHandle<'static, EventLoop>, pub(crate) main_thread_shared: Arc, } @@ -96,21 +99,15 @@ impl WindowInner { let visual_info = WindowVisualConfig::find_best_visual_config(&connection)?; let will_have_parent = options.parent.is_some() || options.wait_for_parent; + let parent_id = options.parent.map(|p| p.inner.window_id); - let xcb_window = XcbWindow::new( - Rc::clone(&connection), - physical_size, - &visual_info, - options.parent.map(|p| p.inner.window_id), - )?; + let xcb_window = + XcbWindow::new(Rc::clone(&connection), physical_size, &visual_info, parent_id)?; if will_have_parent { connection.register_tree_structure_events()?.check()?; } - let visibility_state = - AncestorVisibilityState::discover(&connection, xcb_window.id(), will_have_parent)?; - let cookies = [ xcb_window.set_title(&options.title)?, xcb_window.enable_wm_protocols()?, @@ -134,6 +131,7 @@ impl WindowInner { Ok(Rc::new(Self { connection, xcb_window, + parent_id: parent_id.into(), visual_id: visual_info.visual_id, window_size: physical_size.into(), scaling_factor: ScalingFactor { @@ -143,14 +141,13 @@ impl WindowInner { sizing_strategy, mouse_cursor: MouseCursor::default().into(), loop_signal: ev_loop.get_signal(), + loop_handle: ev_loop.handle(), is_focused: false.into(), - is_mapped: false.into(), present_notify_requested: false.into(), + poll_requested: false.into(), main_thread_shared: shared, - visibility_state, - #[cfg(feature = "opengl")] gl_context, })) @@ -197,6 +194,34 @@ impl WindowInner { self.loop_signal.wakeup(); } + pub fn request_redraw(&self) { + self.present_notify_requested.set(true) + } + + pub fn request_redraw_after(&self, duration: Duration) { + if duration.is_zero() || duration.as_millis() < 1 { + self.request_redraw(); + return; + } + + let result = self.loop_handle.insert_source(Timer::from_duration(duration), |_, _, e| { + e.request_redraw(); + TimeoutAction::Drop + }); + + if let Err(e) = result { + warn!("{}", e); + self.request_redraw(); + } + } + + pub fn waker(&self) -> WindowWaker { + WindowWaker { + loop_signal: self.loop_signal.clone(), + shared: Arc::clone(&self.main_thread_shared), + } + } + pub fn has_focus(&self) -> bool { self.is_focused.get() } diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs index 54c62565..bb805025 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -16,6 +16,7 @@ use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::{mpsc, Mutex, OnceLock}; use std::thread; use std::thread::JoinHandle; +use std::time::{Duration, Instant}; pub(crate) struct WindowThreadShared { stopped: AtomicBool, @@ -23,7 +24,38 @@ pub(crate) struct WindowThreadShared { size: AtomicU32, final_error: Mutex>, stopped_requested_from_host: AtomicBool, + poll_requested: AtomicBool, sizing_strategy: OnceLock, + + redraw_requested_after: Mutex>, +} + +pub enum RedrawRequested { + Now, + Later(Instant), +} + +impl RedrawRequested { + pub fn from_duration(duration: Duration) -> Self { + if duration.is_zero() { + RedrawRequested::Now + } else { + if let Some(dur) = Instant::now().checked_add(duration) { + RedrawRequested::Later(dur) + } else { + RedrawRequested::Now + } + } + } + + pub fn to_duration(&self) -> Duration { + match self { + RedrawRequested::Now => Duration::ZERO, + RedrawRequested::Later(instant) => { + instant.checked_duration_since(Instant::now()).unwrap_or(Duration::ZERO) + } + } + } } impl WindowThreadShared { @@ -35,6 +67,8 @@ impl WindowThreadShared { scaling_factor: 0.into(), stopped_requested_from_host: false.into(), sizing_strategy: OnceLock::new(), + redraw_requested_after: None.into(), + poll_requested: false.into(), } } @@ -73,6 +107,25 @@ impl WindowThreadShared { pub fn is_stop_host_requested(&self) -> bool { self.stopped_requested_from_host.load(Ordering::Relaxed) } + + pub fn request_redraw_after(&self, duration: Duration) { + // Ignore a poisoned mutex, we just fully override this value anyway. + let mut guard = self.redraw_requested_after.lock().unwrap_or_else(|g| g.into_inner()); + *guard = Some(RedrawRequested::from_duration(duration)); + } + + pub fn take_redraw_request(&self) -> Option { + let mut guard = self.redraw_requested_after.lock().unwrap_or_else(|g| g.into_inner()); + guard.take().map(|w| w.to_duration()) + } + + pub fn request_poll(&self) { + self.poll_requested.store(true, Ordering::Relaxed); + } + + pub fn take_poll_request(&self) -> bool { + self.poll_requested.swap(false, Ordering::Relaxed) + } } struct ThreadStopWatcher(Arc); @@ -239,6 +292,17 @@ impl WindowThreadHandle { self.request(WindowThreadRequest::SetParent(new_parent)) } + pub fn request_poll(&self) -> Result<()> { + self.shared.request_poll(); + self.loop_signal.wakeup(); + + Ok(()) + } + + pub fn waker(&self) -> WindowWaker { + WindowWaker { loop_signal: self.loop_signal.clone(), shared: Arc::clone(&self.shared) } + } + fn handle_main_thread_message(&self, msg: HostCallback) { let Some(host_callbacks) = self.host_callbacks.as_ref() else { return }; let mut host_callbacks = host_callbacks.borrow_mut(); diff --git a/src/platform/x11/xcb_connection.rs b/src/platform/x11/xcb_connection.rs index 2b6a4314..920661db 100644 --- a/src/platform/x11/xcb_connection.rs +++ b/src/platform/x11/xcb_connection.rs @@ -4,7 +4,6 @@ use crate::wrappers::xlib::XlibXcbConnection; use crate::MouseCursor; use std::cell::RefCell; use std::collections::hash_map::{Entry, HashMap}; -use std::num::NonZeroU32; use std::sync::Arc; use x11rb::connection::RequestConnection; use x11rb::cookie::VoidCookie; @@ -126,13 +125,4 @@ impl X11Connection { &ChangeWindowAttributesAux::new().event_mask(EventMask::SUBSTRUCTURE_NOTIFY), ) } - - pub fn register_tree_structure_events_for_window( - &self, window_id: NonZeroU32, - ) -> core::result::Result, ConnectionError> { - self.conn.change_window_attributes( - window_id.get(), - &ChangeWindowAttributesAux::new().event_mask(EventMask::SUBSTRUCTURE_NOTIFY), - ) - } } diff --git a/src/waker.rs b/src/waker.rs new file mode 100644 index 00000000..af205d22 --- /dev/null +++ b/src/waker.rs @@ -0,0 +1,26 @@ +use std::time::Duration; + +#[derive(Clone)] +pub struct WindowWaker { + pub(crate) inner: crate::platform::WindowWaker, +} + +// Assert that PlatformHandle implements both Send & Sync on all platforms +const _: () = { + const fn assert_impl_all() {} + let _: fn() = assert_impl_all::; +}; + +impl WindowWaker { + pub fn request_redraw(&self) { + self.inner.request_redraw() + } + + pub fn request_redraw_after(&self, duration: Duration) { + self.inner.request_redraw_after(duration) + } + + pub fn request_poll(&self) { + self.inner.request_poll() + } +} diff --git a/src/window.rs b/src/window.rs index 6a912335..2708e7c2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,6 +2,7 @@ use crate::dpi::*; use crate::handler::WindowHandlerBuilder; use crate::host::Host; use crate::platform; +use crate::waker::WindowWaker; use crate::*; use std::marker::PhantomData; @@ -225,6 +226,18 @@ impl Window { pub fn adjust_size + Into>(&self, size: S) -> S { self.inner.sizing_strategy().adjust_size(size.into(), self.size()).into() } + + #[inline] + pub fn request_poll(&self) -> Result<(), Error> { + self.inner.request_poll()?; + Ok(()) + } + + #[inline] + #[must_use] + pub fn waker(&self) -> WindowWaker { + WindowWaker { inner: self.inner.waker() } + } } pub(crate) struct WindowInitializer { diff --git a/src/wrappers/appkit.rs b/src/wrappers/appkit.rs index b6a8c3cc..d3100fe3 100644 --- a/src/wrappers/appkit.rs +++ b/src/wrappers/appkit.rs @@ -1,8 +1,10 @@ +mod main_thread; mod notification_center; mod timer; mod view; mod window; +pub use main_thread::*; pub use notification_center::*; use objc2::rc::Retained; use objc2_app_kit::NSView; diff --git a/src/wrappers/appkit/main_thread.rs b/src/wrappers/appkit/main_thread.rs new file mode 100644 index 00000000..a9657d99 --- /dev/null +++ b/src/wrappers/appkit/main_thread.rs @@ -0,0 +1,100 @@ +use dispatch2::DispatchQueue; +use objc2::rc::Weak; +use objc2::{MainThreadMarker, Message}; +use std::mem::ManuallyDrop; +use std::time::Duration; + +// This is like MainThreadBound>, but uses async dispatch +pub struct MainThreadBoundWeak(ManuallyDrop>); + +// SAFETY: The inner value is guaranteed to originate from the main thread +// because T is Message. +// +// Finally, the value is dropped on the main thread in `Drop`. +unsafe impl Send for MainThreadBoundWeak {} + +// SAFETY: We do not provide access to the inner value. +unsafe impl Sync for MainThreadBoundWeak {} + +impl Drop for MainThreadBoundWeak { + #[inline] + fn drop(&mut self) { + if MainThreadMarker::new().is_some() { + // SAFETY: The value is dropped on the main thread, which is + // the same thread that it originated from (guaranteed by + // `new` taking `MainThreadMarker`). + // + // Additionally, the value is never used again after this + // point. + unsafe { ManuallyDrop::drop(&mut self.0) }; + } else { + let weak = MTWrapper(unsafe { ManuallyDrop::take(&mut self.0) }); + + DispatchQueue::main().exec_async(move || { + drop(weak); + }); + } + } +} + +impl Clone for MainThreadBoundWeak { + fn clone(&self) -> Self { + // This is actually safe to do off of the main thread + Self(ManuallyDrop::new(Weak::clone(&self.0))) + } +} + +impl MainThreadBoundWeak { + pub fn new(inner: Weak) -> Self { + Self(ManuallyDrop::new(inner)) + } + + pub fn use_on_main_thread_after( + &self, duration: Duration, handler: impl FnOnce(Weak) + Send + 'static, + ) { + if duration.is_zero() { + self.use_on_main_thread(handler); + return; + } + + let Ok(time) = duration.try_into() else { + return; + }; + + let handler = { + let handle = MTWrapper(Weak::clone(&self.0)); + let handler = MTWrapper::new_handler(handler); + move || handler(handle) + }; + + let _ = DispatchQueue::main().after(time, handler); + } + + pub fn use_on_main_thread(&self, handler: impl FnOnce(Weak) + Send + 'static) { + if MainThreadMarker::new().is_some() { + handler(Weak::clone(&self.0)); + return; + } + + let handler = { + let handle = MTWrapper(Weak::clone(&self.0)); + let handler = MTWrapper::new_handler(handler); + move || handler(handle) + }; + + DispatchQueue::main().exec_async(handler); + } +} + +struct MTWrapper(Weak); + +impl MTWrapper { + pub fn new_handler( + handler: impl FnOnce(Weak) + Send + 'static, + ) -> impl FnOnce(MTWrapper) + Send + 'static { + |h| handler(h.0) + } +} + +unsafe impl Send for MTWrapper {} +unsafe impl Sync for MTWrapper {} diff --git a/src/wrappers/appkit/view.rs b/src/wrappers/appkit/view.rs index 85e3473c..410ba006 100644 --- a/src/wrappers/appkit/view.rs +++ b/src/wrappers/appkit/view.rs @@ -2,10 +2,13 @@ use crate::dpi::LogicalSize; use objc2::__framework_prelude::{Allocated, AnyClass, ProtocolObject, Retained}; use objc2::rc::Weak; use objc2::runtime::{AnyObject, Ivar}; -use objc2::{msg_send, Encoding, Message, RefEncode}; +use objc2::{msg_send, sel, Encoding, Message, RefEncode}; use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSEvent, NSView, NSWindow}; -use objc2_core_foundation::CGRect; -use objc2_foundation::{NSNotification, NSPoint}; +use objc2_core_foundation::{kCFRunLoopDefaultMode, CGRect}; +use objc2_foundation::{ + NSNotification, NSPoint, NSRect, NSRunLoop, NSRunLoopCommonModes, NSRunLoopMode, +}; +use objc2_quartz_core::CADisplayLink; use raw_window_handle::{AppKitWindowHandle, WindowHandle}; use std::ffi::{c_void, CStr}; use std::marker::PhantomData; @@ -119,6 +122,19 @@ impl View { let Some(ns_window) = self.window() else { return 1.0 }; ns_window.backingScaleFactor() } + + pub fn setup_display_link(&self) -> Retained { + let display_link = + unsafe { self.displayLinkWithTarget_selector(self, sel![displayLinkFired:]) }; + + let run_loop = NSRunLoop::currentRunLoop(); + + let loop_mode = unsafe { NSRunLoopCommonModes }; + + unsafe { display_link.addToRunLoop_forMode(&run_loop, loop_mode) }; + + display_link + } } pub struct ViewInner { @@ -156,6 +172,8 @@ pub trait ViewImpl: Sized { fn window_did_resize(this: ViewRef); fn view_did_change_backing_properties(this: ViewRef, from_host: bool); + fn draw_rect(this: ViewRef, rect: NSRect); + fn display_link_fired(this: ViewRef, sender: &CADisplayLink); fn hit_test(this: ViewRef<'_, Self>, point: NSPoint) -> Option<&NSView>; fn view_will_move_to_window(this: ViewRef, new_window: Option<&NSWindow>); fn update_tracking_areas(this: ViewRef); diff --git a/src/wrappers/appkit/view/implementation.rs b/src/wrappers/appkit/view/implementation.rs index af16e10f..eb2b3398 100644 --- a/src/wrappers/appkit/view/implementation.rs +++ b/src/wrappers/appkit/view/implementation.rs @@ -5,6 +5,7 @@ use objc2::ffi::objc_disposeClassPair; use objc2::runtime::ClassBuilder; use objc2::{msg_send, sel, ClassType}; use objc2_app_kit::{NSEvent, NSView}; +use objc2_foundation::NSRect; use std::ffi::c_void; /// # Safety @@ -94,6 +95,12 @@ pub unsafe fn create_view_class() -> &'static AnyClass { view_did_change_backing_properties:: as extern "C-unwind" fn(_, _) -> _, ); + class.add_method(sel!(drawRect:), draw_rect:: as extern "C-unwind" fn(_, _, _) -> _); + class.add_method( + sel!(displayLinkFired:), + display_link_fired:: as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( sel!(draggingEntered:), dragging_entered:: as extern "C-unwind" fn(_, _, _) -> _, @@ -194,6 +201,22 @@ extern "C-unwind" fn view_did_change_backing_properties(this: &View V::view_did_change_backing_properties(inner, true); } +extern "C-unwind" fn draw_rect(this: &View, _: Sel, dirty_rect: NSRect) { + let Some(inner) = this.inner_ref() else { + return; + }; + V::draw_rect(inner, dirty_rect); +} + +extern "C-unwind" fn display_link_fired( + this: &View, _sel: Sel, sender: &CADisplayLink, +) { + let Some(inner) = this.inner_ref() else { + return; + }; + V::display_link_fired(inner, sender); +} + extern "C-unwind" fn hit_test( this: &View, _sel: Sel, point: NSPoint, ) -> Option<&NSView> { diff --git a/src/wrappers/win32.rs b/src/wrappers/win32.rs index 3b62f2ca..80f15105 100644 --- a/src/wrappers/win32.rs +++ b/src/wrappers/win32.rs @@ -5,6 +5,7 @@ mod library; mod rect; mod shcore; mod style; +mod timer; mod user32; pub mod uuid; pub mod window; @@ -14,6 +15,7 @@ pub use library::*; pub use rect::Rect; pub use shcore::*; pub use style::*; +pub use timer::*; pub use user32::*; use std::ptr::null_mut; diff --git a/src/wrappers/win32/rect.rs b/src/wrappers/win32/rect.rs index c4bd73e6..724d0fe5 100644 --- a/src/wrappers/win32/rect.rs +++ b/src/wrappers/win32/rect.rs @@ -1,4 +1,5 @@ use crate::dpi::PhysicalSize; +use std::fmt::Debug; use windows_sys::Win32::Foundation::RECT; #[derive(Copy, Clone)] @@ -13,6 +14,22 @@ impl Rect { height: self.0.top.abs_diff(self.0.bottom), } } + + pub fn is_empty(&self) -> bool { + let size = self.size(); + size.width == 0 && size.height == 0 + } +} + +impl Debug for Rect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Rect") + .field("left", &self.0.left) + .field("top", &self.0.top) + .field("right", &self.0.right) + .field("bottom", &self.0.bottom) + .finish() + } } impl From> for Rect { diff --git a/src/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs new file mode 100644 index 00000000..bcc523b7 --- /dev/null +++ b/src/wrappers/win32/timer.rs @@ -0,0 +1,105 @@ +use crate::wrappers::win32::window::HWnd; +use std::cell::{Cell, RefCell}; +use std::num::NonZeroUsize; +use std::time::Duration; +use windows_core::Error; +use windows_sys::Win32::Foundation::WPARAM; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct TimerId(NonZeroUsize); + +impl TimerId { + pub fn from_raw(wparam: WPARAM) -> Option { + Some(Self(NonZeroUsize::new(wparam)?)) + } + + pub fn as_raw(&self) -> usize { + self.0.get() + } +} + +#[derive(PartialEq, Eq)] +pub struct TimerSlot { + id: Cell>, + hwnd: HWnd, +} + +impl TimerSlot { + pub fn empty(hwnd: HWnd) -> Self { + Self { hwnd, id: None.into() } + } + + pub fn is_running(&self) -> bool { + self.id.get().is_some() + } + + pub fn start_if_not_running(&self, timeout_msec: u32) { + if !self.is_running() { + self.start(timeout_msec); + } + } + + fn start(&self, timeout_msec: u32) { + match self.hwnd.create_timer(timeout_msec) { + Ok(timer_id) => self.id.set(Some(timer_id)), + Err(e) => crate::warn!("Failed to start timer: {}", e), + } + } + + pub fn set_running(&self, running: bool, timeout_msec: u32) { + match (running, self.is_running()) { + (true, true) | (false, false) => (), // Nothing to do + (false, true) => self.kill(), + (true, false) => self.start(timeout_msec), + } + } + + pub fn kill(&self) { + if let Some(timer_id) = self.id.take() { + if let Err(e) = self.hwnd.kill_timer(timer_id) { + crate::warn!("Failed to kill timer: {}", e); + } + } + } + + pub fn matches_id(&self, other: TimerId) -> bool { + match self.id.get() { + None => false, + Some(id) => id == other, + } + } +} + +pub struct TimerList { + timers: RefCell>, +} + +impl TimerList { + pub fn new() -> Self { + Self { timers: Vec::new().into() } + } + + pub fn add_new_timer(&self, window: HWnd, timeout: Duration) -> Result<(), Error> { + let timeout_msec = timeout.as_millis().try_into().unwrap_or(u32::MAX); + let new_timer_id = window.create_timer(timeout_msec)?; + self.timers.borrow_mut().push(new_timer_id); + Ok(()) + } + + pub fn remove_if_exists(&self, window: HWnd, id: TimerId) -> Result { + if !self.pop_if_exists(id) { + return Ok(false); + }; + + window.kill_timer(id)?; + + Ok(true) + } + + fn pop_if_exists(&self, id: TimerId) -> bool { + let mut timers = self.timers.borrow_mut(); + let Some(index) = timers.iter().position(|&t| t == id) else { return false }; + timers.swap_remove(index); + true + } +} diff --git a/src/wrappers/win32/window.rs b/src/wrappers/win32/window.rs index 826bc26a..53e3bb6e 100644 --- a/src/wrappers/win32/window.rs +++ b/src/wrappers/win32/window.rs @@ -14,7 +14,7 @@ pub use wgl::*; use crate::dpi::PhysicalSize; pub use data::WindowData; -pub use handle::HWnd; +pub use handle::*; pub use proc::wnd_proc; use std::ptr::{null_mut, NonNull}; use std::rc::Rc; diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs index 1f463d83..7de6ae7e 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -2,15 +2,17 @@ use crate::wrappers::win32::dpi::{Dpi, DpiAwarenessGuard}; use crate::wrappers::win32::style::WindowStyle; use crate::wrappers::win32::user32::ExtendedUser32; -use crate::wrappers::win32::{DpiAwarenessContext, ExtendedShCore, Rect}; +use crate::wrappers::win32::{DpiAwarenessContext, ExtendedShCore, Rect, TimerId}; use std::ffi::c_void; -use std::num::{NonZeroU32, NonZeroUsize}; +use std::num::NonZeroU32; use std::ptr::{null_mut, NonNull}; +use std::time::Duration; use windows::Win32::System::Ole::IDropTarget; use windows_core::{Error, Interface, InterfaceRef, Result, HRESULT}; -use windows_sys::Win32::Foundation::{SetLastError, FALSE, HWND, POINT, S_OK}; +use windows_sys::Win32::Foundation::{SetLastError, FALSE, HWND, LPARAM, POINT, S_OK, WPARAM}; use windows_sys::Win32::Graphics::Gdi::{ - MonitorFromWindow, ScreenToClient, MONITOR_DEFAULTTOPRIMARY, + GetUpdateRect, InvalidateRect, MonitorFromWindow, ScreenToClient, ValidateRect, + MONITOR_DEFAULTTOPRIMARY, }; use windows_sys::Win32::System::Ole::{RegisterDragDrop, RevokeDragDrop}; use windows_sys::Win32::UI::HiDpi::{DPI_HOSTING_BEHAVIOR_MIXED, MDT_DEFAULT}; @@ -18,11 +20,15 @@ use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ GetFocus, ReleaseCapture, SetCapture, SetFocus, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ - DestroyWindow, GetWindowLongPtrW, GetWindowLongW, SetParent, SetTimer, SetWindowLongPtrW, - SetWindowPos, ShowWindow, GWLP_USERDATA, GWL_EXSTYLE, GWL_STYLE, SWP_NOACTIVATE, SWP_NOMOVE, - SWP_NOZORDER, SW_HIDE, SW_SHOW, WINDOW_LONG_PTR_INDEX, + DestroyWindow, GetWindowLongPtrW, GetWindowLongW, KillTimer, PostMessageW, SetParent, SetTimer, + SetWindowLongPtrW, SetWindowPos, ShowWindow, GWLP_USERDATA, GWL_EXSTYLE, GWL_STYLE, + SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, SW_HIDE, SW_SHOW, WINDOW_LONG_PTR_INDEX, WM_USER, }; +pub(crate) const BV_WINDOW_MUST_CLOSE: u32 = WM_USER + 1; +pub(crate) const BV_REQUEST_REDRAW: u32 = WM_USER + 2; +pub(crate) const BV_REQUEST_POLL: u32 = WM_USER + 3; + /// A simple wrapper around a HWND. /// /// This type guarantees the HWND is safe to use, but not that it remains valid. (i.e. functions using @@ -216,12 +222,30 @@ impl HWnd { Ok(()) } - pub fn set_timer(&self, timer_id: NonZeroUsize, elapse: u32) -> Result<()> { - let result = unsafe { SetTimer(self.as_raw(), timer_id.get(), elapse, None) }; + pub fn create_timer(&self, elapse: u32) -> Result { + let result = unsafe { SetTimer(self.as_raw(), 0, elapse, None) }; + let timer_id = TimerId::from_raw(result).ok_or_else(Error::from_thread)?; + + self.reset_timer(timer_id, elapse)?; + + Ok(timer_id) + } + + pub fn reset_timer(&self, timer_id: TimerId, elapse: u32) -> Result<()> { + let result = unsafe { SetTimer(self.as_raw(), timer_id.as_raw(), elapse, None) }; if result == 0 { return Err(Error::from_thread()); } + Ok(()) + } + + pub fn kill_timer(&self, timer_id: TimerId) -> Result<()> { + let result = unsafe { KillTimer(self.as_raw(), timer_id.as_raw()) }; + + if result == FALSE { + return Err(Error::from_thread()); + } Ok(()) } @@ -294,6 +318,26 @@ impl HWnd { Ok(PhysicalPosition::new(pt.x, pt.y)) } + pub fn get_update_rect(&self) -> Option { + let mut rect = Rect::EMPTY; + + let result = unsafe { GetUpdateRect(self.as_raw(), &mut rect.0, FALSE) }; + + if result == 0 || rect.is_empty() { + return None; + } + + Some(rect) + } + + pub fn validate_rect(&self, rect: Rect) { + let _ = unsafe { ValidateRect(self.as_raw(), &rect.0) }; + } + + pub fn invalidate_window(&self) { + let _ = unsafe { InvalidateRect(self.as_raw(), null_mut(), FALSE) }; + } + #[cfg(feature = "opengl")] pub fn get_own_dc(&self) -> Result { super::OwnDeviceContext::from_window(*self) @@ -340,3 +384,59 @@ impl HWnd { } } } + +pub struct SyncHwnd(HWnd); + +// SAFETY: we only implement thread-safe operations on this handle +unsafe impl Send for SyncHwnd {} +// SAFETY: same as above +unsafe impl Sync for SyncHwnd {} + +impl From for SyncHwnd { + fn from(hwnd: HWnd) -> Self { + SyncHwnd(hwnd) + } +} + +pub trait PostMessageExt { + /// # Safety + /// + /// The message, wparam and lparam values must be valid. + unsafe fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM); + + #[inline] + fn post_must_close(&self) { + unsafe { self.post_message(BV_WINDOW_MUST_CLOSE, 0, 0) } + } + + #[inline] + fn post_request_redraw(&self, duration: Duration) { + let duration_msec = duration.as_millis(); + let duration_msec: usize = duration_msec.try_into().unwrap_or(usize::MAX); + + unsafe { self.post_message(BV_REQUEST_REDRAW, duration_msec, 0) } + } + + #[inline] + fn post_request_poll(&self) { + unsafe { self.post_message(BV_REQUEST_POLL, 0, 0) } + } +} + +impl PostMessageExt for HWnd { + unsafe fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { + let result = unsafe { PostMessageW(self.as_raw(), message, wparam, lparam) }; + + if result == 0 { + let error = Error::from_thread(); + crate::warn!("Failed to post message to window: {}", error) + } + } +} + +impl PostMessageExt for SyncHwnd { + #[inline] + unsafe fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { + self.0.post_message(message, wparam, lparam) + } +}