From f404fae3185cbd0509a0ae1c507ef0275166ce21 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:15:31 +0200 Subject: [PATCH 01/19] wip --- examples/cursors/src/main.rs | 2 +- examples/open_parented/src/main.rs | 4 +- examples/open_window/src/main.rs | 22 ++---- examples/plugin_clack/src/window_handler.rs | 2 +- .../src/window_handler.rs | 2 +- examples/render_femtovg/src/main.rs | 2 +- examples/render_wgpu/src/main.rs | 2 +- examples/test-frame-pacing/src/main.rs | 2 +- src/context.rs | 4 + src/handler.rs | 76 ++++++++++++++++++- src/platform/win/window.rs | 2 +- src/platform/x11/event_loop.rs | 8 +- src/platform/x11/window_shared.rs | 4 + src/platform/x11/window_thread.rs | 4 + src/window.rs | 6 ++ 15 files changed, 115 insertions(+), 27 deletions(-) diff --git a/examples/cursors/src/main.rs b/examples/cursors/src/main.rs index 11055d66..7ea21a5b 100644 --- a/examples/cursors/src/main.rs +++ b/examples/cursors/src/main.rs @@ -43,7 +43,7 @@ impl CursorsExample { } impl WindowHandler for CursorsExample { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { if !self.damaged.get() { return Ok(()); } diff --git a/examples/open_parented/src/main.rs b/examples/open_parented/src/main.rs index 812dd6fc..a5e326a4 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -31,7 +31,7 @@ impl ParentWindowHandler { } 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() { @@ -86,7 +86,7 @@ impl ChildWindowHandler { } 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() { diff --git a/examples/open_window/src/main.rs b/examples/open_window/src/main.rs index f4019b4b..1ea17388 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,14 @@ impl WindowHandler for OpenWindowExample { } pixels.present()?; - self.damaged.set(false); + Ok(()) + } + + fn poll(&self) { while let Ok(message) = self.rx.borrow_mut().pop() { println!("Message: {:?}", message); } - - Ok(()) } fn on_event(&self, event: Event) -> EventStatus { @@ -120,15 +115,15 @@ 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(); } _ => {} } @@ -166,7 +161,6 @@ 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()?; diff --git a/examples/plugin_clack/src/window_handler.rs b/examples/plugin_clack/src/window_handler.rs index 16800ba9..f74b5c32 100644 --- a/examples/plugin_clack/src/window_handler.rs +++ b/examples/plugin_clack/src/window_handler.rs @@ -29,7 +29,7 @@ impl WindowHandler for OpenWindowExample { Ok(()) } - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { if !self.damaged.get() { return Ok(()); } diff --git a/examples/plugin_clack_femtovg/src/window_handler.rs b/examples/plugin_clack_femtovg/src/window_handler.rs index 0ba8dcfa..31ebdd9b 100644 --- a/examples/plugin_clack_femtovg/src/window_handler.rs +++ b/examples/plugin_clack_femtovg/src/window_handler.rs @@ -16,7 +16,7 @@ pub struct FemtovgExample { } impl WindowHandler for FemtovgExample { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { if !self.damaged.get() { return Ok(()); } diff --git a/examples/render_femtovg/src/main.rs b/examples/render_femtovg/src/main.rs index 98d27e90..23873c27 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -42,7 +42,7 @@ impl FemtovgExample { } impl WindowHandler for FemtovgExample { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { if !self.damaged.get() { return Ok(()); } 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..342f2c1a 100644 --- a/examples/test-frame-pacing/src/main.rs +++ b/examples/test-frame-pacing/src/main.rs @@ -57,7 +57,7 @@ impl FramePacingTest { } impl WindowHandler for FramePacingTest { - fn on_frame(&self) -> Result<(), HandlerError> { + fn draw(&self) -> Result<(), HandlerError> { let now = Instant::now(); let dt = (now - self.previous_frame_time.get()).as_secs_f32(); self.previous_frame_time.set(now); diff --git a/src/context.rs b/src/context.rs index a83b8f1f..7a4a53f6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -34,6 +34,10 @@ impl WindowContext { self.inner.request_close(); } + pub fn request_redraw(&self) { + self.inner.request_redraw() + } + /// 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..8313b448 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,12 +1,86 @@ 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. + /// + /// 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; + /// * 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. + /// + /// 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 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. + /// + /// 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 + 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 many 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/platform/win/window.rs b/src/platform/win/window.rs index fecf22aa..d826d855 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -302,7 +302,7 @@ impl BaseviewWindow { pub(crate) fn handle_on_frame(&self) { let Some(handler) = self.handler.get() else { return }; - if let Err(e) = handler.on_frame() { + if let Err(e) = handler.draw() { warn!("Error while rendering frame: {}", e); self.window_state.request_close(); } diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 8332bd0a..dd49eabd 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -127,6 +127,7 @@ impl EventLoop { const FRAME_INTERVAL: Duration = Duration::from_millis(15); fn handle_frame(evloop: &mut EventLoop, previous_deadline: Instant) -> TimeoutAction { + dbg!("frame"); evloop.draw_now = true; // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in @@ -163,13 +164,11 @@ impl EventLoop { return; } - if let Err(e) = self.handler.on_frame() { + if let Err(e) = self.handler.draw() { self.trigger_fatal_error(e.into()); return; } - self.window.present_notify_requested.set(true); - // Any socket error will be handled in the next poll let _ = self.window.connection.conn.flush(); } @@ -472,6 +471,7 @@ impl EventLoop { } XEvent::Expose(e) if e.window == self.window.raw_id() => { + dbg!(e); self.window.present_notify_requested.set(true) } @@ -635,6 +635,8 @@ impl EventLoop { } } + dbg!((e.serial, e.msc)); + self.last_received_present = Some((e.serial, e.msc)); self.draw_now = true; } diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 9a6f78a5..22403878 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -197,6 +197,10 @@ impl WindowInner { self.loop_signal.wakeup(); } + pub fn request_redraw(&self) { + self.present_notify_requested.set(true) + } + 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..0a6690e9 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -239,6 +239,10 @@ impl WindowThreadHandle { self.request(WindowThreadRequest::SetParent(new_parent)) } + pub fn request_poll(&self) -> Result<()> { + todo!() + } + 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/window.rs b/src/window.rs index 6a912335..f60cab89 100644 --- a/src/window.rs +++ b/src/window.rs @@ -225,6 +225,12 @@ 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(()) + } } pub(crate) struct WindowInitializer { From a45ec6ae6032885990bb88fc1e031579de0d5bf1 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:33:55 +0200 Subject: [PATCH 02/19] wip --- examples/cursors/src/main.rs | 11 ++--------- examples/open_parented/src/main.rs | 19 ++++--------------- examples/open_window/src/main.rs | 4 +--- examples/plugin_clack/src/window_handler.rs | 14 +++----------- .../src/window_handler.rs | 10 +--------- examples/render_femtovg/src/main.rs | 11 +---------- examples/test-frame-pacing/src/main.rs | 5 +++++ src/platform/x11/event_loop.rs | 3 --- 8 files changed, 17 insertions(+), 60 deletions(-) diff --git a/examples/cursors/src/main.rs b/examples/cursors/src/main.rs index 7ea21a5b..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 { @@ -44,10 +43,6 @@ impl CursorsExample { impl WindowHandler for CursorsExample { fn draw(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - 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/open_parented/src/main.rs b/examples/open_parented/src/main.rs index a5e326a4..6c4b4afa 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -8,8 +8,6 @@ use std::num::NonZeroU32; struct ParentWindowHandler { surface: RefCell>, - damaged: Cell, - child_window: Window, } @@ -26,7 +24,7 @@ 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 }) } } @@ -34,10 +32,7 @@ impl WindowHandler for ParentWindowHandler { 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,7 +74,7 @@ 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() }) } } @@ -89,10 +82,7 @@ impl WindowHandler for ChildWindowHandler { 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 1ea17388..0670dab3 100644 --- a/examples/open_window/src/main.rs +++ b/examples/open_window/src/main.rs @@ -125,11 +125,9 @@ impl WindowHandler for OpenWindowExample { self.is_cursor_inside.set(false); self.window_context.request_redraw(); } - _ => {} + event => log_event(&event), } - log_event(&event); - EventStatus::Captured } } diff --git a/examples/plugin_clack/src/window_handler.rs b/examples/plugin_clack/src/window_handler.rs index f74b5c32..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 draw(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - 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 31ebdd9b..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 draw(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - 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 23873c27..6bd0a215 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -14,13 +14,11 @@ struct FemtovgExample { gl_context: GlContext, canvas: RefCell>, current_mouse_position: Cell>, - damaged: Cell, } impl FemtovgExample { fn new(window_context: WindowContext) -> 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)) }?; @@ -35,7 +33,6 @@ impl FemtovgExample { gl_context, window_context, canvas: canvas.into(), - damaged: true.into(), current_mouse_position: Cell::new(PhysicalPosition::default()), }) } @@ -43,10 +40,6 @@ impl FemtovgExample { impl WindowHandler for FemtovgExample { fn draw(&self) -> Result<(), HandlerError> { - if !self.damaged.get() { - return Ok(()); - } - let context = &self.gl_context; unsafe { context.make_current()? }; @@ -82,7 +75,6 @@ impl WindowHandler for FemtovgExample { canvas.flush(); context.swap_buffers()?; unsafe { context.make_not_current()? }; - self.damaged.set(false); Ok(()) } @@ -90,7 +82,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 +98,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/test-frame-pacing/src/main.rs b/examples/test-frame-pacing/src/main.rs index 342f2c1a..5ee7b627 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, @@ -46,6 +47,7 @@ impl FramePacingTest { unsafe { gl_context.make_not_current()? }; Ok(Self { + window_context, gl_context, canvas: canvas.into(), perf_graph: PerfGraph::new(), @@ -118,6 +120,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/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index dd49eabd..7de60979 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -471,7 +471,6 @@ impl EventLoop { } XEvent::Expose(e) if e.window == self.window.raw_id() => { - dbg!(e); self.window.present_notify_requested.set(true) } @@ -635,8 +634,6 @@ impl EventLoop { } } - dbg!((e.serial, e.msc)); - self.last_received_present = Some((e.serial, e.msc)); self.draw_now = true; } From e6b9ba98bed4588dbec7cc75b7a2c6e96429c05e Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:12:44 +0200 Subject: [PATCH 03/19] wip --- src/context.rs | 11 +++++++++++ src/handler.rs | 2 +- src/lib.rs | 3 ++- src/platform/x11/event_loop.rs | 9 ++++++++- src/platform/x11/mod.rs | 2 ++ src/platform/x11/waker.rs | 25 +++++++++++++++++++++++++ src/platform/x11/window_shared.rs | 31 ++++++++++++++++++++++++++++++- src/platform/x11/window_thread.rs | 20 ++++++++++++++++++++ src/waker.rs | 26 ++++++++++++++++++++++++++ src/window.rs | 7 +++++++ 10 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 src/platform/x11/waker.rs create mode 100644 src/waker.rs diff --git a/src/context.rs b/src/context.rs index 7a4a53f6..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. @@ -38,6 +40,15 @@ impl WindowContext { 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 8313b448..e42f0c5b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -73,7 +73,7 @@ pub trait WindowHandler: 'static { /// # External update logic /// /// The goal is for all external update logic to be contained within this method. - /// Therefore, this method will be called in many cases, including (but not limited to): + /// 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`]. diff --git a/src/lib.rs b/src/lib.rs index 80e16f94..9dd581e8 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,7 +21,7 @@ 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 window::*; diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 7de60979..dfe954c3 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -127,7 +127,6 @@ impl EventLoop { const FRAME_INTERVAL: Duration = Duration::from_millis(15); fn handle_frame(evloop: &mut EventLoop, previous_deadline: Instant) -> TimeoutAction { - dbg!("frame"); evloop.draw_now = true; // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in @@ -155,6 +154,10 @@ impl EventLoop { } fn handle_redraw(&mut self) { + if let Some(redraw_after) = self.window.main_thread_shared.take_redraw_request() { + self.window.request_redraw_after(redraw_after) + } + if !self.draw_now { return; } @@ -394,6 +397,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`. diff --git a/src/platform/x11/mod.rs b/src/platform/x11/mod.rs index b86140ca..cfe22047 100644 --- a/src/platform/x11/mod.rs +++ b/src/platform/x11/mod.rs @@ -21,6 +21,7 @@ mod visual_info; mod xcb_window; mod visibility_tree; +mod waker; mod window_shared; mod window_thread; @@ -31,6 +32,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/waker.rs b/src/platform/x11/waker.rs new file mode 100644 index 00000000..9c0e6d63 --- /dev/null +++ b/src/platform/x11/waker.rs @@ -0,0 +1,25 @@ +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.loop_signal.wakeup(); + } +} diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 22403878..f3698301 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -2,17 +2,20 @@ 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::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; @@ -61,6 +64,7 @@ pub(crate) struct WindowInner { pub(crate) is_mapped: Cell, pub(crate) present_notify_requested: Cell, pub(crate) loop_signal: LoopSignal, + loop_handle: LoopHandle<'static, super::event_loop::EventLoop>, pub(crate) visibility_state: AncestorVisibilityState, @@ -143,6 +147,7 @@ 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(), @@ -201,6 +206,30 @@ impl WindowInner { 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 0a6690e9..791d4342 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; pub(crate) struct WindowThreadShared { stopped: AtomicBool, @@ -24,6 +25,9 @@ pub(crate) struct WindowThreadShared { final_error: Mutex>, stopped_requested_from_host: AtomicBool, sizing_strategy: OnceLock, + + // TODO: use instant here instead of duration + redraw_requested_after: Mutex>, } impl WindowThreadShared { @@ -35,6 +39,7 @@ impl WindowThreadShared { scaling_factor: 0.into(), stopped_requested_from_host: false.into(), sizing_strategy: OnceLock::new(), + redraw_requested_after: None.into(), } } @@ -73,6 +78,17 @@ 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(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() + } } struct ThreadStopWatcher(Arc); @@ -243,6 +259,10 @@ impl WindowThreadHandle { todo!() } + 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/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 f60cab89..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; @@ -231,6 +232,12 @@ impl Window { self.inner.request_poll()?; Ok(()) } + + #[inline] + #[must_use] + pub fn waker(&self) -> WindowWaker { + WindowWaker { inner: self.inner.waker() } + } } pub(crate) struct WindowInitializer { From 21c5fc23f89dbf423585100160c4baf3359528ab Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:26:35 +0200 Subject: [PATCH 04/19] Remove visibility tree --- examples/open_parented/src/main.rs | 2 +- src/platform/x11/event_loop.rs | 54 +---- src/platform/x11/mod.rs | 1 - src/platform/x11/visibility_tree.rs | 362 ---------------------------- src/platform/x11/window_shared.rs | 24 +- src/platform/x11/xcb_connection.rs | 10 - 6 files changed, 19 insertions(+), 434 deletions(-) delete mode 100644 src/platform/x11/visibility_tree.rs diff --git a/examples/open_parented/src/main.rs b/examples/open_parented/src/main.rs index 6c4b4afa..c4ecfd50 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -3,7 +3,7 @@ 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 { diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index dfe954c3..40fc9919 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -163,10 +163,6 @@ impl EventLoop { } self.draw_now = false; - if !self.window.visibility_state.own_window_is_viewable() { - return; - } - if let Err(e) = self.handler.draw() { self.trigger_fatal_error(e.into()); return; @@ -333,12 +329,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(()) } } @@ -465,15 +459,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)); + } } } @@ -567,12 +561,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()? { @@ -584,31 +572,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 cfe22047..ba0cb652 100644 --- a/src/platform/x11/mod.rs +++ b/src/platform/x11/mod.rs @@ -20,7 +20,6 @@ mod keyboard; mod visual_info; mod xcb_window; -mod visibility_tree; mod waker; mod window_shared; mod window_thread; 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/window_shared.rs b/src/platform/x11/window_shared.rs index f3698301..52cd1f69 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,6 +1,5 @@ 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; @@ -13,6 +12,7 @@ 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; @@ -51,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, @@ -61,12 +62,9 @@ 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) loop_signal: LoopSignal, - loop_handle: LoopHandle<'static, super::event_loop::EventLoop>, - - pub(crate) visibility_state: AncestorVisibilityState, + loop_handle: LoopHandle<'static, EventLoop>, pub(crate) main_thread_shared: Arc, } @@ -100,21 +98,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()?, @@ -138,6 +130,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 { @@ -150,12 +143,9 @@ impl WindowInner { loop_handle: ev_loop.handle(), is_focused: false.into(), - is_mapped: false.into(), present_notify_requested: false.into(), main_thread_shared: shared, - visibility_state, - #[cfg(feature = "opengl")] gl_context, })) 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), - ) - } } From d47ab45b18c1779490a0f168c807e6c04071386b Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:53:53 +0200 Subject: [PATCH 05/19] Update docs --- src/handler.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e42f0c5b..8d73e31c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -12,8 +12,7 @@ pub trait WindowHandler: 'static { /// /// In order to reduce resource usage, this method is not called systematically at every frame /// interval. - /// - /// This method is automatically scheduled to be called in several situations: + /// 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; From 668da065d7862dee0a1ece664e3d2f39922b41a7 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:31:49 +0200 Subject: [PATCH 06/19] macOS wip --- src/context.rs | 5 ++- src/platform/macos/context.rs | 6 +++ src/platform/macos/gl.rs | 2 - src/platform/macos/mod.rs | 3 ++ src/platform/macos/view.rs | 24 +++++++----- src/platform/macos/waker.rs | 45 ++++++++++++++++++++++ src/window.rs | 5 ++- src/wrappers/appkit/view.rs | 3 +- src/wrappers/appkit/view/implementation.rs | 10 +++++ 9 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 src/platform/macos/waker.rs diff --git a/src/context.rs b/src/context.rs index f757586b..bed1a320 100644 --- a/src/context.rs +++ b/src/context.rs @@ -41,12 +41,13 @@ impl WindowContext { } pub fn request_redraw_after(&self, duration: Duration) { - self.inner.request_redraw_after(duration) + //self.inner.request_redraw_after(duration) } #[must_use] pub fn waker(&self) -> WindowWaker { - WindowWaker { inner: self.inner.waker() } + //WindowWaker { inner: self.inner.waker() } + todo!() } /// Returns `true` if this window currently has keyboard focus, `false` otherwise. diff --git a/src/platform/macos/context.rs b/src/platform/macos/context.rs index 4266a242..5e05eec2 100644 --- a/src/platform/macos/context.rs +++ b/src/platform/macos/context.rs @@ -33,6 +33,12 @@ impl WindowContext { BaseviewView::close(view, false); } + pub fn request_redraw(&self) { + let Some(view) = self.view.load() else { return }; + + view.setNeedsDisplay(true); + } + 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..261683f1 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -2,7 +2,7 @@ use super::keyboard::{make_modifiers, KeyboardState}; use super::window::WindowSharedState; -use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::dpi::{LogicalPosition, LogicalSize, Size}; use crate::host::Host; use crate::platform::macos::cursor::CursorManager; use crate::platform::*; @@ -142,9 +142,7 @@ impl BaseviewView { 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); - } + view.setNeedsDisplay(true); } })); @@ -184,6 +182,10 @@ impl BaseviewView { } } + pub fn poll(this: ViewRef) { + this.window_handler.use_handler(|h| h.poll()); + } + pub fn close(this: ViewRef, from_host: bool) { this.state.closed.set(true); this.view.removeFromSuperview(); @@ -254,8 +256,8 @@ 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); + if let Some(Err(e)) = this.window_handler.use_handler(|h| h.draw()) { + warn!("Error while drawing: {}", e); Self::close(this, false); } } @@ -316,7 +318,7 @@ impl ViewImpl for BaseviewView { let size = window.contentRectForFrameRect(window.frame()).size; let size = LogicalSize::new(size.width, size.height); - BaseviewView::resize(this, size, true, true); + BaseviewView::resize(this, size.into(), true, true); } fn view_did_change_backing_properties(this: ViewRef, notify_host: bool) { @@ -342,7 +344,7 @@ impl ViewImpl for BaseviewView { warn!("Window Handler failed to resize: {}", e); this.state.size.set(previous); - Self::resize(this, previous, false, false); + Self::resize(this, previous.into(), false, false); return; } @@ -350,12 +352,16 @@ impl ViewImpl for BaseviewView { if let Err(e) = this.host.request_resize(new_size) { warn!("Host failed to resize parent view: {}", e); - Self::resize(this, previous, false, false); + Self::resize(this, previous.into(), false, false); } } } } + fn draw_rect(this: ViewRef, _rect: NSRect) { + 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..dd184f7b --- /dev/null +++ b/src/platform/macos/waker.rs @@ -0,0 +1,45 @@ +use crate::platform::macos::view::BaseviewView; +use crate::wrappers::appkit::View; +use dispatch2::MainThreadBound; +use objc2::rc::Weak; +use objc2::MainThreadMarker; +use std::time::Duration; + +pub struct WindowWaker { + view: MainThreadBound>>, +} + +impl Clone for WindowWaker { + fn clone(&self) -> Self { + // SAFETY: we only use this to clone the inner Weak handle, which is always thread-safe. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + + Self { view: MainThreadBound::new(Weak::clone(self.view.get(mtm)), mtm) } + } +} + +impl WindowWaker { + pub fn request_redraw(&self) { + self.request_redraw_after(Duration::ZERO) + } + + pub fn request_redraw_after(&self, duration: Duration) { + self.view.get_on_main(|view| { + let Some(view) = view.load() else { return }; + if duration.is_zero() { + view.setNeedsDisplay(true) + } else { + todo!() + } + }) + } + + pub fn request_poll(&self) { + self.view.get_on_main(|view| { + let Some(view) = view.load() else { return }; + let Some(view) = view.inner_ref() else { return }; + + BaseviewView::poll(view); + }) + } +} diff --git a/src/window.rs b/src/window.rs index 2708e7c2..33f56618 100644 --- a/src/window.rs +++ b/src/window.rs @@ -229,14 +229,15 @@ impl Window { #[inline] pub fn request_poll(&self) -> Result<(), Error> { - self.inner.request_poll()?; + //self.inner.request_poll()?; Ok(()) } #[inline] #[must_use] pub fn waker(&self) -> WindowWaker { - WindowWaker { inner: self.inner.waker() } + todo!() + //WindowWaker { inner: self.inner.waker() } } } diff --git a/src/wrappers/appkit/view.rs b/src/wrappers/appkit/view.rs index 85e3473c..c9044cb3 100644 --- a/src/wrappers/appkit/view.rs +++ b/src/wrappers/appkit/view.rs @@ -5,7 +5,7 @@ use objc2::runtime::{AnyObject, Ivar}; use objc2::{msg_send, Encoding, Message, RefEncode}; use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSEvent, NSView, NSWindow}; use objc2_core_foundation::CGRect; -use objc2_foundation::{NSNotification, NSPoint}; +use objc2_foundation::{NSNotification, NSPoint, NSRect}; use raw_window_handle::{AppKitWindowHandle, WindowHandle}; use std::ffi::{c_void, CStr}; use std::marker::PhantomData; @@ -156,6 +156,7 @@ 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 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..b26c346b 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,8 @@ 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!(draggingEntered:), dragging_entered:: as extern "C-unwind" fn(_, _, _) -> _, @@ -194,6 +197,13 @@ 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 hit_test( this: &View, _sel: Sel, point: NSPoint, ) -> Option<&NSView> { From bb31a1a0a2b34497007bdd52484ee1b86b264ab0 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:47:54 +0200 Subject: [PATCH 07/19] macOS wip 2 --- src/context.rs | 5 +- src/platform/macos/context.rs | 11 +++- src/platform/macos/view.rs | 8 +-- src/platform/macos/waker.rs | 28 +++----- src/platform/macos/window.rs | 14 +++- src/window.rs | 5 +- src/wrappers/appkit.rs | 2 + src/wrappers/appkit/main_thread.rs | 100 +++++++++++++++++++++++++++++ 8 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 src/wrappers/appkit/main_thread.rs diff --git a/src/context.rs b/src/context.rs index bed1a320..f757586b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -41,13 +41,12 @@ impl WindowContext { } pub fn request_redraw_after(&self, duration: Duration) { - //self.inner.request_redraw_after(duration) + self.inner.request_redraw_after(duration) } #[must_use] pub fn waker(&self) -> WindowWaker { - //WindowWaker { inner: self.inner.waker() } - todo!() + WindowWaker { inner: self.inner.waker() } } /// Returns `true` if this window currently has keyboard focus, `false` otherwise. diff --git a/src/platform/macos/context.rs b/src/platform/macos/context.rs index 5e05eec2..bd3491ea 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 { @@ -39,6 +40,14 @@ impl WindowContext { view.setNeedsDisplay(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/view.rs b/src/platform/macos/view.rs index 261683f1..9bd3ea66 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -2,7 +2,7 @@ use super::keyboard::{make_modifiers, KeyboardState}; use super::window::WindowSharedState; -use crate::dpi::{LogicalPosition, LogicalSize, Size}; +use crate::dpi::{LogicalPosition, LogicalSize}; use crate::host::Host; use crate::platform::macos::cursor::CursorManager; use crate::platform::*; @@ -318,7 +318,7 @@ impl ViewImpl for BaseviewView { let size = window.contentRectForFrameRect(window.frame()).size; let size = LogicalSize::new(size.width, size.height); - BaseviewView::resize(this, size.into(), true, true); + BaseviewView::resize(this, size, true, true); } fn view_did_change_backing_properties(this: ViewRef, notify_host: bool) { @@ -344,7 +344,7 @@ impl ViewImpl for BaseviewView { warn!("Window Handler failed to resize: {}", e); this.state.size.set(previous); - Self::resize(this, previous.into(), false, false); + Self::resize(this, previous, false, false); return; } @@ -352,7 +352,7 @@ impl ViewImpl for BaseviewView { if let Err(e) = this.host.request_resize(new_size) { warn!("Host failed to resize parent view: {}", e); - Self::resize(this, previous.into(), false, false); + Self::resize(this, previous, false, false); } } } diff --git a/src/platform/macos/waker.rs b/src/platform/macos/waker.rs index dd184f7b..740f354b 100644 --- a/src/platform/macos/waker.rs +++ b/src/platform/macos/waker.rs @@ -1,41 +1,31 @@ use crate::platform::macos::view::BaseviewView; -use crate::wrappers::appkit::View; -use dispatch2::MainThreadBound; +use crate::wrappers::appkit::{MainThreadBoundWeak, View}; use objc2::rc::Weak; -use objc2::MainThreadMarker; use std::time::Duration; +#[derive(Clone)] pub struct WindowWaker { - view: MainThreadBound>>, + view: MainThreadBoundWeak>, } -impl Clone for WindowWaker { - fn clone(&self) -> Self { - // SAFETY: we only use this to clone the inner Weak handle, which is always thread-safe. - let mtm = unsafe { MainThreadMarker::new_unchecked() }; - - Self { view: MainThreadBound::new(Weak::clone(self.view.get(mtm)), mtm) } +impl WindowWaker { + pub fn new(reference: Weak>) -> Self { + Self { view: MainThreadBoundWeak::new(reference) } } -} -impl WindowWaker { pub fn request_redraw(&self) { self.request_redraw_after(Duration::ZERO) } pub fn request_redraw_after(&self, duration: Duration) { - self.view.get_on_main(|view| { + self.view.use_on_main_thread_after(duration, |view| { let Some(view) = view.load() else { return }; - if duration.is_zero() { - view.setNeedsDisplay(true) - } else { - todo!() - } + view.setNeedsDisplay(true); }) } pub fn request_poll(&self) { - self.view.get_on_main(|view| { + self.view.use_on_main_thread(|view| { let Some(view) = view.load() else { return }; let Some(view) = view.inner_ref() else { return }; diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index dcb09bf8..d61b0b8c 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( diff --git a/src/window.rs b/src/window.rs index 33f56618..2708e7c2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -229,15 +229,14 @@ impl Window { #[inline] pub fn request_poll(&self) -> Result<(), Error> { - //self.inner.request_poll()?; + self.inner.request_poll()?; Ok(()) } #[inline] #[must_use] pub fn waker(&self) -> WindowWaker { - todo!() - //WindowWaker { inner: self.inner.waker() } + WindowWaker { inner: self.inner.waker() } } } 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 {} From 54b468458eb13c61a457a244d80a43a177389c88 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:46:28 +0200 Subject: [PATCH 08/19] X11: properly implement poll --- examples/open_window/src/main.rs | 26 +++++++++++++++----------- src/platform/x11/event_loop.rs | 12 ++++++++++++ src/platform/x11/window_shared.rs | 2 ++ 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/examples/open_window/src/main.rs b/examples/open_window/src/main.rs index 0670dab3..d1dfe7b4 100644 --- a/examples/open_window/src/main.rs +++ b/examples/open_window/src/main.rs @@ -139,15 +139,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; @@ -160,8 +152,20 @@ fn main() -> Result<(), baseview::Error> { mouse_pos: PhysicalPosition::new(0., 0.).into(), is_cursor_inside: false.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/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 40fc9919..dd5e5148 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -154,6 +154,10 @@ impl EventLoop { } fn handle_redraw(&mut self) { + self.handler.poll(); + self.window.poll_requested.set(false); + + // 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) } @@ -161,6 +165,9 @@ impl EventLoop { if !self.draw_now { return; } + + self.window.present_notify_requested.set(false); + self.draw_now = false; if let Err(e) = self.handler.draw() { @@ -359,6 +366,11 @@ impl EventLoop { self.handle_redraw(); self.handle_present_notify()?; + if self.window.poll_requested.get() { + self.handler.poll(); + self.window.poll_requested.set(true); + } + if !self.drain_xcb_events()? { break; } diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 52cd1f69..56fa7735 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -63,6 +63,7 @@ pub(crate) struct WindowInner { pub(crate) is_focused: Cell, pub(crate) present_notify_requested: Cell, + pub(crate) poll_requested: Cell, pub(crate) loop_signal: LoopSignal, loop_handle: LoopHandle<'static, EventLoop>, @@ -144,6 +145,7 @@ impl WindowInner { is_focused: false.into(), present_notify_requested: false.into(), + poll_requested: false.into(), main_thread_shared: shared, #[cfg(feature = "opengl")] From 72086078c67e02dff713641adfcfecec39430747 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:56:49 +0200 Subject: [PATCH 09/19] X11 poll fixes --- examples/open_window/src/main.rs | 1 + src/platform/x11/event_loop.rs | 40 +++++++++++++++---------------- src/platform/x11/waker.rs | 1 + src/platform/x11/window_thread.rs | 10 ++++++++ 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/examples/open_window/src/main.rs b/examples/open_window/src/main.rs index d1dfe7b4..5074225e 100644 --- a/examples/open_window/src/main.rs +++ b/examples/open_window/src/main.rs @@ -104,6 +104,7 @@ impl WindowHandler for OpenWindowExample { } fn poll(&self) { + eprintln!("Poll!"); while let Ok(message) = self.rx.borrow_mut().pop() { println!("Message: {:?}", message); } diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index dd5e5148..99fb4921 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -153,30 +153,17 @@ impl EventLoop { Ok(()) } - fn handle_redraw(&mut self) { - self.handler.poll(); - self.window.poll_requested.set(false); - - // 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) - } - - if !self.draw_now { - return; - } - + fn redraw(&mut self) -> Result<(), FatalError> { self.window.present_notify_requested.set(false); - self.draw_now = false; if let Err(e) = self.handler.draw() { self.trigger_fatal_error(e.into()); - return; + return Ok(()); } - // 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> { @@ -363,14 +350,25 @@ impl EventLoop { loop { self.handle_coalesced_resize_events()?; - self.handle_redraw(); - self.handle_present_notify()?; - if self.window.poll_requested.get() { + // 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(true); + self.window.poll_requested.set(false); } + self.handle_present_notify()?; + if !self.drain_xcb_events()? { break; } diff --git a/src/platform/x11/waker.rs b/src/platform/x11/waker.rs index 9c0e6d63..c5872720 100644 --- a/src/platform/x11/waker.rs +++ b/src/platform/x11/waker.rs @@ -20,6 +20,7 @@ impl WindowWaker { } pub fn request_poll(&self) { + self.shared.request_poll(); self.loop_signal.wakeup(); } } diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs index 791d4342..c258306b 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -24,6 +24,7 @@ pub(crate) struct WindowThreadShared { size: AtomicU32, final_error: Mutex>, stopped_requested_from_host: AtomicBool, + poll_requested: AtomicBool, sizing_strategy: OnceLock, // TODO: use instant here instead of duration @@ -40,6 +41,7 @@ impl WindowThreadShared { stopped_requested_from_host: false.into(), sizing_strategy: OnceLock::new(), redraw_requested_after: None.into(), + poll_requested: false.into(), } } @@ -89,6 +91,14 @@ impl WindowThreadShared { let mut guard = self.redraw_requested_after.lock().unwrap_or_else(|g| g.into_inner()); guard.take() } + + 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); From ad3d319797d35702dcb9590ea1672926d6285a46 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:02:09 +0200 Subject: [PATCH 10/19] fixes --- src/platform/x11/window_thread.rs | 42 ++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/platform/x11/window_thread.rs b/src/platform/x11/window_thread.rs index c258306b..bb805025 100644 --- a/src/platform/x11/window_thread.rs +++ b/src/platform/x11/window_thread.rs @@ -16,7 +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; +use std::time::{Duration, Instant}; pub(crate) struct WindowThreadShared { stopped: AtomicBool, @@ -27,8 +27,35 @@ pub(crate) struct WindowThreadShared { poll_requested: AtomicBool, sizing_strategy: OnceLock, - // TODO: use instant here instead of duration - redraw_requested_after: Mutex>, + 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 { @@ -84,12 +111,12 @@ impl WindowThreadShared { 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(duration); + *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() + guard.take().map(|w| w.to_duration()) } pub fn request_poll(&self) { @@ -266,7 +293,10 @@ impl WindowThreadHandle { } pub fn request_poll(&self) -> Result<()> { - todo!() + self.shared.request_poll(); + self.loop_signal.wakeup(); + + Ok(()) } pub fn waker(&self) -> WindowWaker { From 599860f1e6e28b3f75f0fe5dd352a4ffc1903c11 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:35:28 +0200 Subject: [PATCH 11/19] Docs update --- src/handler.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 8d73e31c..8758c55f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -15,7 +15,7 @@ pub trait WindowHandler: 'static { /// 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; + /// * 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). /// @@ -23,7 +23,7 @@ pub trait WindowHandler: 'static { /// 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. + /// 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. @@ -45,15 +45,33 @@ pub trait WindowHandler: 'static { /// [polling]: WindowHandler::poll fn draw(&self) -> core::result::Result<(), HandlerError>; - /// Notifies the handler that a given [`area`] of the window has been damaged and needs to be redrawn. + /// 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; } From 92823058b02f6b2da48df713bf55a28d5851b93b Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:17:00 +0200 Subject: [PATCH 12/19] win32 timer wip --- src/platform/win/window.rs | 39 ++++++++++++------ src/platform/win/window_state.rs | 4 +- src/wrappers/win32.rs | 2 + src/wrappers/win32/timer.rs | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 src/wrappers/win32/timer.rs diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index d826d855..927a8060 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -24,7 +24,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, + Timer, TimerId, WindowStyle, }; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowSize}; @@ -219,6 +219,7 @@ pub struct BaseviewWindow { handler_builder: Cell>, handler: OnceCell>, host: Host, + redraw_timer: OnceCell, // Things not directly used, but kept so their Drop impl runs when the window is destroyed _keyboard_hook: Cell>, @@ -257,6 +258,7 @@ impl BaseviewWindow { handler: OnceCell::new(), shared_state, host: init.host, + redraw_timer: OnceCell::new(), _drop_target: None.into(), _keyboard_hook: None.into(), @@ -271,13 +273,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) } @@ -324,6 +319,8 @@ impl Drop for BaseviewWindow { } } +const REDRAW_TIMER_DELAY_MSEC: u32 = 15; + impl WindowImpl for BaseviewWindow { fn non_client_create(&self, window: HWnd) -> std::result::Result<(), PlatformError> { if self.shared_state.dpi_scaling_strategy.get().assume_96_dpi { @@ -391,6 +388,10 @@ impl WindowImpl for BaseviewWindow { }; let Ok(()) = self.handler.set(handler) else { unreachable!() }; + let Ok(()) = self.redraw_timer.set(Timer::new(window, REDRAW_TIMER_DELAY_MSEC)?) else { + unreachable!() + }; + Ok(()) } @@ -531,11 +532,25 @@ unsafe fn wnd_proc_inner( None } WM_TIMER => { - if wparam == WIN_FRAME_TIMER.get() { - window_bv.handle_on_frame() - } + let Some(timer_id) = TimerId::from_wparam(wparam) else { + return None; + }; - Some(0) + if Some(timer_id) == window_bv.redraw_timer.get().map(|t| t.id()) { + window_bv.handle_on_frame(); + // check if redraw requested, if not then kill the timer + Some(0) + } else { + let Ok(true) = + window_state.shared.delayed_redraw_timers.remove_if_exists(window, timer_id) + else { + return None; + }; + // Schedule new frame, reset + + window_bv.redraw_timer.get() + Some(0) + } } WM_CLOSE => { window_bv.handle_event(Event::Window(WindowEvent::WillClose)); diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 58bd85d2..cc7bae64 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -7,7 +7,7 @@ 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::{Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList}; use crate::WindowSettings; use crate::{MouseCursor, WindowSize}; use raw_window_handle::{DisplayHandle, Win32WindowHandle}; @@ -140,6 +140,7 @@ pub struct WindowSharedState { pub user32: LibraryModule, pub sizing_strategy: SizingStrategy, + pub delayed_redraw_timers: TimerList, } impl WindowSharedState { @@ -155,6 +156,7 @@ impl WindowSharedState { sizing_strategy: SizingStrategy::from_settings(settings), user32, dpi_scaling_strategy: DpiScalingStrategy::default().into(), + delayed_redraw_timers: TimerList::new(), } .into() } 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/timer.rs b/src/wrappers/win32/timer.rs new file mode 100644 index 00000000..5f2ab337 --- /dev/null +++ b/src/wrappers/win32/timer.rs @@ -0,0 +1,68 @@ +use crate::wrappers::win32::window::HWnd; +use std::cell::RefCell; +use std::num::NonZeroUsize; +use windows_core::Error; +use windows_sys::Win32::Foundation::WPARAM; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct TimerId(NonZeroUsize); + +impl TimerId { + pub fn from_wparam(wparam: WPARAM) -> Option { + Some(Self(NonZeroUsize::new(wparam)?)) + } +} + +#[derive(PartialEq, Eq)] +pub struct Timer { + id: TimerId, + hwnd: HWnd, +} + +impl Timer { + pub fn new(window: HWnd, timeout_msec: u32) -> Result { + todo!() + } + + pub fn reset(&self, timeout_msec: u32) -> Result<(), Error> { + todo!() + } + + pub fn id(&self) -> TimerId { + self.id + } +} + +impl PartialEq for Timer { + fn eq(&self, other: &TimerId) -> bool { + self.id == *other + } +} + +impl Drop for Timer { + fn drop(&mut self) { + todo!() + } +} + +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_msec: u32) -> Result<(), Error> { + todo!() + } + + pub fn remove_if_exists(&self, window: HWnd, id: TimerId) -> Result { + todo!() + } + + pub fn destroy_all(&self, window: HWnd) { + todo!() + } +} From 7409c82e26961839bb63cfbfe0baa915969351ca Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:07:54 +0200 Subject: [PATCH 13/19] win32 timer wip --- src/platform/win/mod.rs | 2 + src/platform/win/waker.rs | 42 +++++++++++++ src/platform/win/window.rs | 63 +++++++++++++------ src/platform/win/window_state.rs | 32 +++++++--- src/wrappers/win32/timer.rs | 38 +++++++----- src/wrappers/win32/window.rs | 2 +- src/wrappers/win32/window/handle.rs | 95 ++++++++++++++++++++++++++--- 7 files changed, 225 insertions(+), 49 deletions(-) create mode 100644 src/platform/win/waker.rs 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 927a8060..ed2b200c 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -1,4 +1,4 @@ -use windows_core::{ComObject, HSTRING}; +use windows_core::{ComObject, Error, HSTRING}; use windows_sys::Win32::{ Foundation::{LPARAM, LRESULT, RECT, WPARAM}, UI::{Controls::WM_MOUSELEAVE, WindowsAndMessaging::*}, @@ -10,8 +10,6 @@ use std::cell::{Cell, OnceCell}; use std::num::{NonZeroU32, NonZeroUsize}; 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, - Timer, TimerId, WindowStyle, + TimerId, TimerSlot, WindowStyle, }; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowSize}; @@ -194,6 +192,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 { @@ -219,7 +229,7 @@ pub struct BaseviewWindow { handler_builder: Cell>, handler: OnceCell>, host: Host, - redraw_timer: OnceCell, + redraw_timer: TimerSlot, // Things not directly used, but kept so their Drop impl runs when the window is destroyed _keyboard_hook: Cell>, @@ -245,6 +255,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(), @@ -258,7 +270,7 @@ impl BaseviewWindow { handler: OnceCell::new(), shared_state, host: init.host, - redraw_timer: OnceCell::new(), + redraw_timer: TimerSlot::empty(hwnd), _drop_target: None.into(), _keyboard_hook: None.into(), @@ -388,9 +400,7 @@ impl WindowImpl for BaseviewWindow { }; let Ok(()) = self.handler.set(handler) else { unreachable!() }; - let Ok(()) = self.redraw_timer.set(Timer::new(window, REDRAW_TIMER_DELAY_MSEC)?) else { - unreachable!() - }; + self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC)?; Ok(()) } @@ -536,20 +546,25 @@ unsafe fn wnd_proc_inner( return None; }; - if Some(timer_id) == window_bv.redraw_timer.get().map(|t| t.id()) { + if window_bv.redraw_timer.matches_id(timer_id) { window_bv.handle_on_frame(); // check if redraw requested, if not then kill the timer Some(0) } else { - let Ok(true) = - window_state.shared.delayed_redraw_timers.remove_if_exists(window, timer_id) - else { - return None; - }; - // Schedule new frame, reset - - window_bv.redraw_timer.get() - Some(0) + 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) => { + // If the timer is already running, do nothing, we'll get a new frame anyway + if !window_bv.redraw_timer.is_running() { + window_bv.handle_on_frame(); + } + Some(0) + } + } } } WM_CLOSE => { @@ -747,6 +762,16 @@ unsafe fn wnd_proc_inner( let _ = window.destroy(); Some(0) } + + BV_REQUEST_REDRAW => { + todo!(); + Some(0) + } + + BV_REQUEST_POLL => { + todo!(); + Some(0) + } _ => None, } } diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index cc7bae64..4e66ec75 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -1,12 +1,13 @@ 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::window::{HWnd, BV_WINDOW_MUST_CLOSE}; use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList}; use crate::WindowSettings; use crate::{MouseCursor, WindowSize}; @@ -14,6 +15,7 @@ use raw_window_handle::{DisplayHandle, Win32WindowHandle}; use std::cell::{Cell, Ref, RefCell}; use std::num::NonZeroIsize; use std::rc::Rc; +use std::time::Duration; use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW; /// All data associated with the window. @@ -66,12 +68,7 @@ impl WindowState { pub fn request_close(&self) { unsafe { - PostMessageW( - self.hwnd.as_raw(), - crate::platform::win::window::BV_WINDOW_MUST_CLOSE, - 0, - 0, - ); + PostMessageW(self.hwnd.as_raw(), BV_WINDOW_MUST_CLOSE, 0, 0); } } @@ -126,6 +123,19 @@ impl WindowState { let Some(hwnd) = NonZeroIsize::new(self.hwnd.as_raw() as _) else { unreachable!() }; PlatformHandle { hwnd } } + + pub fn request_redraw(&self) {} + + 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 { @@ -141,6 +151,7 @@ pub struct WindowSharedState { pub user32: LibraryModule, pub sizing_strategy: SizingStrategy, pub delayed_redraw_timers: TimerList, + pub window_waker_source: WindowWakerSource, } impl WindowSharedState { @@ -157,6 +168,7 @@ impl WindowSharedState { user32, dpi_scaling_strategy: DpiScalingStrategy::default().into(), delayed_redraw_timers: TimerList::new(), + window_waker_source: WindowWakerSource::new(), } .into() } @@ -177,6 +189,10 @@ impl WindowSharedState { self.dpi_scaling_strategy.set(strategy); } + pub fn set_hwnd(&self, hwnd: 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/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs index 5f2ab337..f2df634b 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -1,6 +1,7 @@ use crate::wrappers::win32::window::HWnd; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::num::NonZeroUsize; +use std::time::Duration; use windows_core::Error; use windows_sys::Win32::Foundation::WPARAM; @@ -11,35 +12,44 @@ impl TimerId { pub fn from_wparam(wparam: WPARAM) -> Option { Some(Self(NonZeroUsize::new(wparam)?)) } + + pub fn as_raw(&self) -> usize { + self.0.get() + } } #[derive(PartialEq, Eq)] -pub struct Timer { - id: TimerId, +pub struct TimerSlot { + id: Cell>, hwnd: HWnd, } -impl Timer { - pub fn new(window: HWnd, timeout_msec: u32) -> Result { +impl TimerSlot { + pub fn empty(hwnd: HWnd) -> Self { + Self { hwnd, id: None.into() } + } + + pub fn is_running(&self) -> bool { todo!() } - pub fn reset(&self, timeout_msec: u32) -> Result<(), Error> { + pub fn restart(&self, timeout_msec: u32) -> Result<(), Error> { todo!() } - pub fn id(&self) -> TimerId { - self.id + pub fn kill(&self) { + todo!() } -} -impl PartialEq for Timer { - fn eq(&self, other: &TimerId) -> bool { - self.id == *other + pub fn matches_id(&self, other: TimerId) -> bool { + match self.id.get() { + None => false, + Some(id) => id == other, + } } } -impl Drop for Timer { +impl Drop for TimerSlot { fn drop(&mut self) { todo!() } @@ -54,7 +64,7 @@ impl TimerList { Self { timers: Vec::new().into() } } - pub fn add_new_timer(&self, window: HWnd, timeout_msec: u32) -> Result<(), Error> { + pub fn add_new_timer(&self, window: HWnd, timeout: Duration) -> Result<(), Error> { todo!() } 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..a7b5d6f0 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -2,13 +2,14 @@ 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::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, }; @@ -18,11 +19,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,8 +221,14 @@ 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 set_timer(&self, elapse: u32) -> Result { + let result = unsafe { SetTimer(self.as_raw(), 0, elapse, None) }; + + TimerId::from_wparam(result).ok_or_else(Error::from_thread) + } + + pub fn kill_timer(&self, timer_id: TimerId) -> Result<()> { + let result = unsafe { KillTimer(self.as_raw(), timer_id.as_raw()) }; if result == 0 { return Err(Error::from_thread()); @@ -340,3 +351,73 @@ 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) + } +} + +impl SyncHwnd { + /// # Safety + /// + /// The message, wparam and lparam values must be valid + pub unsafe fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { + let result = unsafe { PostMessageW(self.0.as_raw(), message, wparam, lparam) }; + + if result == 0 { + let error = Error::from_thread(); + crate::warn!("Failed to post message to window: {}", error) + } + } +} + +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) + } +} From 3254af912d0596536e2f339475e719291cab8c70 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:21:12 +0200 Subject: [PATCH 14/19] wip --- src/platform/win/window_state.rs | 11 ++++++++ src/wrappers/win32/timer.rs | 48 +++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 4e66ec75..84dbeb3a 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -139,6 +139,7 @@ impl WindowState { } pub struct WindowSharedState { + pub hwnd: Cell>, pub parented: Cell, pub is_alive: Cell, pub current_size: Cell>, @@ -169,6 +170,7 @@ impl WindowSharedState { dpi_scaling_strategy: DpiScalingStrategy::default().into(), delayed_redraw_timers: TimerList::new(), window_waker_source: WindowWakerSource::new(), + hwnd: None.into(), } .into() } @@ -190,6 +192,7 @@ impl WindowSharedState { } pub fn set_hwnd(&self, hwnd: HWnd) { + self.hwnd.set(Some(hwnd)); self.window_waker_source.set(hwnd) } @@ -216,6 +219,14 @@ impl WindowSharedState { } } +impl Drop for WindowSharedState { + fn drop(&mut self) { + if let Some(hwnd) = self.hwnd.get() { + self.delayed_redraw_timers.destroy_all(hwnd) + } + } +} + struct Guard<'a>(&'a Cell); impl<'a> Drop for Guard<'a> { fn drop(&mut self) { diff --git a/src/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs index f2df634b..fed3e25e 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -30,15 +30,27 @@ impl TimerSlot { } pub fn is_running(&self) -> bool { - todo!() + self.id.get().is_some() } pub fn restart(&self, timeout_msec: u32) -> Result<(), Error> { - todo!() + if let Some(timer_id) = self.id.take() { + self.hwnd.kill_timer(timer_id)?; + } + + let timer_id = self.hwnd.set_timer(timeout_msec)?; + self.id.set(Some(timer_id)); + + Ok(()) } pub fn kill(&self) { - todo!() + if let Some(timer_id) = self.id.get() { + if let Err(e) = self.hwnd.kill_timer(timer_id) { + crate::warn!("Failed to kill timer: {}", e); + } + self.id.set(None); + } } pub fn matches_id(&self, other: TimerId) -> bool { @@ -51,7 +63,7 @@ impl TimerSlot { impl Drop for TimerSlot { fn drop(&mut self) { - todo!() + self.kill() } } @@ -65,14 +77,36 @@ impl TimerList { } pub fn add_new_timer(&self, window: HWnd, timeout: Duration) -> Result<(), Error> { - todo!() + let timeout_msec = timeout.as_millis().try_into().unwrap_or(u32::MAX); + let new_timer_id = window.set_timer(timeout_msec)?; + self.timers.borrow_mut().push(new_timer_id); + Ok(()) } pub fn remove_if_exists(&self, window: HWnd, id: TimerId) -> Result { - todo!() + 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 } pub fn destroy_all(&self, window: HWnd) { - todo!() + let timers = self.timers.take(); + + for timer_id in timers { + if let Err(e) = window.kill_timer(timer_id) { + crate::warn!("Could not remove timer: {e}") + } + } } } From 49bb8f9349d120a34fd384b304399e7ebab84411 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:03:30 +0200 Subject: [PATCH 15/19] wip --- src/platform/win/window.rs | 44 ++++++++++++++------------------ src/platform/win/window_state.rs | 20 +++++++++++++-- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index ed2b200c..89fe0572 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -1,4 +1,4 @@ -use windows_core::{ComObject, Error, HSTRING}; +use windows_core::{ComObject, HSTRING}; use windows_sys::Win32::{ Foundation::{LPARAM, LRESULT, RECT, WPARAM}, UI::{Controls::WM_MOUSELEAVE, WindowsAndMessaging::*}, @@ -7,7 +7,7 @@ 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; use super::drop_target::DropTarget; @@ -22,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, - TimerId, TimerSlot, WindowStyle, + TimerId, WindowStyle, }; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowSize}; @@ -34,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>, @@ -229,7 +224,6 @@ pub struct BaseviewWindow { handler_builder: Cell>, handler: OnceCell>, host: Host, - redraw_timer: TimerSlot, // Things not directly used, but kept so their Drop impl runs when the window is destroyed _keyboard_hook: Cell>, @@ -270,7 +264,6 @@ impl BaseviewWindow { handler: OnceCell::new(), shared_state, host: init.host, - redraw_timer: TimerSlot::empty(hwnd), _drop_target: None.into(), _keyboard_hook: None.into(), @@ -309,12 +302,19 @@ impl BaseviewWindow { pub(crate) fn handle_on_frame(&self) { let Some(handler) = self.handler.get() else { return }; + handler.poll(); if let Err(e) = handler.draw() { warn!("Error while rendering frame: {}", e); self.window_state.request_close(); } } + 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 { let Some(handler) = self.handler.get() else { return EventStatus::Ignored; @@ -331,8 +331,6 @@ impl Drop for BaseviewWindow { } } -const REDRAW_TIMER_DELAY_MSEC: u32 = 15; - impl WindowImpl for BaseviewWindow { fn non_client_create(&self, window: HWnd) -> std::result::Result<(), PlatformError> { if self.shared_state.dpi_scaling_strategy.get().assume_96_dpi { @@ -400,8 +398,6 @@ impl WindowImpl for BaseviewWindow { }; let Ok(()) = self.handler.set(handler) else { unreachable!() }; - self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC)?; - Ok(()) } @@ -542,13 +538,14 @@ unsafe fn wnd_proc_inner( None } WM_TIMER => { - let Some(timer_id) = TimerId::from_wparam(wparam) else { - return None; - }; + let timer_id = TimerId::from_wparam(wparam)?; - if window_bv.redraw_timer.matches_id(timer_id) { + if window_state.redraw_timer.matches_id(timer_id) { + window_state.redraw_requested.set(false); window_bv.handle_on_frame(); - // check if redraw requested, if not then kill the timer + if !window_state.redraw_requested.get() { + window_state.redraw_timer.kill(); + } Some(0) } else { match window_state.shared.delayed_redraw_timers.remove_if_exists(window, timer_id) { @@ -558,10 +555,7 @@ unsafe fn wnd_proc_inner( None } Ok(true) => { - // If the timer is already running, do nothing, we'll get a new frame anyway - if !window_bv.redraw_timer.is_running() { - window_bv.handle_on_frame(); - } + window_state.request_redraw(); Some(0) } } @@ -764,12 +758,12 @@ unsafe fn wnd_proc_inner( } BV_REQUEST_REDRAW => { - todo!(); + window_state.request_redraw(); Some(0) } BV_REQUEST_POLL => { - todo!(); + window_bv.handle_poll(); Some(0) } _ => None, diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 84dbeb3a..b82da214 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -8,7 +8,9 @@ use crate::window::WindowInitializer; use crate::wrappers::win32::cursor::SystemCursor; use crate::wrappers::win32::h_instance::HInstance; use crate::wrappers::win32::window::{HWnd, BV_WINDOW_MUST_CLOSE}; -use crate::wrappers::win32::{Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList}; +use crate::wrappers::win32::{ + Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList, TimerSlot, +}; use crate::WindowSettings; use crate::{MouseCursor, WindowSize}; use raw_window_handle::{DisplayHandle, Win32WindowHandle}; @@ -18,6 +20,8 @@ use std::rc::Rc; use std::time::Duration; use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW; +const REDRAW_TIMER_DELAY_MSEC: u32 = 15; + /// All data associated with the window. pub(crate) struct WindowState { /// The HWND belonging to this window. @@ -29,6 +33,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, @@ -44,8 +50,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(), @@ -124,7 +132,15 @@ impl WindowState { PlatformHandle { hwnd } } - pub fn request_redraw(&self) {} + pub fn request_redraw(&self) { + self.redraw_requested.set(true); + + if !self.redraw_timer.is_running() { + if let Err(e) = self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC) { + crate::warn!("Could not schedule redraw: {}", e) + } + } + } pub fn request_redraw_after(&self, duration: Duration) { if let Err(e) = self.shared.delayed_redraw_timers.add_new_timer(self.hwnd, duration) { From 439945e94df5d050705faee44e25f9c16df25713 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:29:23 +0200 Subject: [PATCH 16/19] wip --- src/platform/win/window.rs | 28 +++++++++++++++------ src/platform/win/window_state.rs | 13 +++++++--- src/wrappers/win32/rect.rs | 17 +++++++++++++ src/wrappers/win32/timer.rs | 20 +++++++-------- src/wrappers/win32/window/handle.rs | 39 +++++++++++++++++------------ 5 files changed, 80 insertions(+), 37 deletions(-) diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index 89fe0572..f72b81dd 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -299,14 +299,19 @@ 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 }; handler.poll(); + + self.window_state.redraw_requested.set(false); + eprintln!("DRAW"); 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) { @@ -537,15 +542,22 @@ unsafe fn wnd_proc_inner( None } + 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_wparam(wparam)?; + dbg!(timer_id); - if window_state.redraw_timer.matches_id(timer_id) { - window_state.redraw_requested.set(false); - window_bv.handle_on_frame(); - if !window_state.redraw_requested.get() { - window_state.redraw_timer.kill(); - } + 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) { @@ -637,6 +649,8 @@ unsafe fn wnd_proc_inner( return Some(-1); } + window.invalidate_window(); + None } WM_DPICHANGED => { diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index b82da214..d9422d69 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -136,9 +136,16 @@ impl WindowState { self.redraw_requested.set(true); if !self.redraw_timer.is_running() { - if let Err(e) = self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC) { - crate::warn!("Could not schedule redraw: {}", e) - } + self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC) + } + } + + pub fn setup_redraw_request_for_next_frame(&self) { + dbg!(self.redraw_requested.get(), self.redraw_timer.is_running()); + match (self.redraw_requested.take(), self.redraw_timer.is_running()) { + (true, true) | (false, false) => (), // Nothing to do + (false, true) => self.redraw_timer.kill(), + (true, false) => self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC), } } 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 index fed3e25e..d4777e3a 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -5,7 +5,7 @@ use std::time::Duration; use windows_core::Error; use windows_sys::Win32::Foundation::WPARAM; -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct TimerId(NonZeroUsize); impl TimerId { @@ -33,23 +33,21 @@ impl TimerSlot { self.id.get().is_some() } - pub fn restart(&self, timeout_msec: u32) -> Result<(), Error> { - if let Some(timer_id) = self.id.take() { - self.hwnd.kill_timer(timer_id)?; - } - - let timer_id = self.hwnd.set_timer(timeout_msec)?; - self.id.set(Some(timer_id)); + pub fn restart(&self, timeout_msec: u32) { + self.kill(); - Ok(()) + eprintln!("timer start"); + match self.hwnd.set_timer(timeout_msec) { + Ok(timer_id) => self.id.set(Some(dbg!(timer_id))), + Err(e) => crate::warn!("Failed to start timer: {}", e), + } } pub fn kill(&self) { - if let Some(timer_id) = self.id.get() { + 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); } - self.id.set(None); } } diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs index a7b5d6f0..4ccf27e6 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -11,7 +11,8 @@ use windows::Win32::System::Ole::IDropTarget; use windows_core::{Error, Interface, InterfaceRef, Result, HRESULT}; 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}; @@ -230,7 +231,7 @@ impl HWnd { pub fn kill_timer(&self, timer_id: TimerId) -> Result<()> { let result = unsafe { KillTimer(self.as_raw(), timer_id.as_raw()) }; - if result == 0 { + if result == FALSE { return Err(Error::from_thread()); } @@ -305,6 +306,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) @@ -365,20 +386,6 @@ impl From for SyncHwnd { } } -impl SyncHwnd { - /// # Safety - /// - /// The message, wparam and lparam values must be valid - pub unsafe fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) { - let result = unsafe { PostMessageW(self.0.as_raw(), message, wparam, lparam) }; - - if result == 0 { - let error = Error::from_thread(); - crate::warn!("Failed to post message to window: {}", error) - } - } -} - pub trait PostMessageExt { /// # Safety /// From 29507bdd47e6b0b4c504f6b9a25b6a190058e059 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:07:35 +0200 Subject: [PATCH 17/19] wip --- examples/render_femtovg/src/main.rs | 1 + src/platform/win/window.rs | 5 ++-- src/platform/win/window_state.rs | 28 +++++--------------- src/wrappers/win32/timer.rs | 41 +++++++++++++---------------- src/wrappers/win32/window/handle.rs | 20 +++++++++++--- 5 files changed, 43 insertions(+), 52 deletions(-) diff --git a/examples/render_femtovg/src/main.rs b/examples/render_femtovg/src/main.rs index 6bd0a215..d21c404a 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -19,6 +19,7 @@ struct FemtovgExample { impl FemtovgExample { fn new(window_context: WindowContext) -> 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)) }?; diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index f72b81dd..0badccf2 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -305,7 +305,6 @@ impl BaseviewWindow { handler.poll(); self.window_state.redraw_requested.set(false); - eprintln!("DRAW"); if let Err(e) = handler.draw() { warn!("Error while rendering frame: {}", e); self.window_state.request_close(); @@ -414,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. } } @@ -551,8 +551,7 @@ unsafe fn wnd_proc_inner( Some(0) } WM_TIMER => { - let timer_id = TimerId::from_wparam(wparam)?; - dbg!(timer_id); + let timer_id = TimerId::from_raw(wparam)?; if window_state.redraw_timer.matches_id(timer_id) && window_state.redraw_timer.is_running() diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index d9422d69..6065d90c 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -7,7 +7,7 @@ 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, BV_WINDOW_MUST_CLOSE}; +use crate::wrappers::win32::window::{HWnd, PostMessageExt}; use crate::wrappers::win32::{ Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList, TimerSlot, }; @@ -18,7 +18,6 @@ use std::cell::{Cell, Ref, RefCell}; use std::num::NonZeroIsize; use std::rc::Rc; use std::time::Duration; -use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW; const REDRAW_TIMER_DELAY_MSEC: u32 = 15; @@ -75,9 +74,7 @@ impl WindowState { } pub fn request_close(&self) { - unsafe { - PostMessageW(self.hwnd.as_raw(), BV_WINDOW_MUST_CLOSE, 0, 0); - } + self.hwnd.post_must_close(); } pub fn has_focus(&self) -> bool { @@ -135,18 +132,13 @@ impl WindowState { pub fn request_redraw(&self) { self.redraw_requested.set(true); - if !self.redraw_timer.is_running() { - self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC) - } + self.redraw_timer.start_if_not_running(REDRAW_TIMER_DELAY_MSEC); } pub fn setup_redraw_request_for_next_frame(&self) { - dbg!(self.redraw_requested.get(), self.redraw_timer.is_running()); - match (self.redraw_requested.take(), self.redraw_timer.is_running()) { - (true, true) | (false, false) => (), // Nothing to do - (false, true) => self.redraw_timer.kill(), - (true, false) => self.redraw_timer.restart(REDRAW_TIMER_DELAY_MSEC), - } + 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) { @@ -242,14 +234,6 @@ impl WindowSharedState { } } -impl Drop for WindowSharedState { - fn drop(&mut self) { - if let Some(hwnd) = self.hwnd.get() { - self.delayed_redraw_timers.destroy_all(hwnd) - } - } -} - struct Guard<'a>(&'a Cell); impl<'a> Drop for Guard<'a> { fn drop(&mut self) { diff --git a/src/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs index d4777e3a..bcc523b7 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -9,7 +9,7 @@ use windows_sys::Win32::Foundation::WPARAM; pub struct TimerId(NonZeroUsize); impl TimerId { - pub fn from_wparam(wparam: WPARAM) -> Option { + pub fn from_raw(wparam: WPARAM) -> Option { Some(Self(NonZeroUsize::new(wparam)?)) } @@ -33,16 +33,27 @@ impl TimerSlot { self.id.get().is_some() } - pub fn restart(&self, timeout_msec: u32) { - self.kill(); + pub fn start_if_not_running(&self, timeout_msec: u32) { + if !self.is_running() { + self.start(timeout_msec); + } + } - eprintln!("timer start"); - match self.hwnd.set_timer(timeout_msec) { - Ok(timer_id) => self.id.set(Some(dbg!(timer_id))), + 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) { @@ -59,12 +70,6 @@ impl TimerSlot { } } -impl Drop for TimerSlot { - fn drop(&mut self) { - self.kill() - } -} - pub struct TimerList { timers: RefCell>, } @@ -76,7 +81,7 @@ impl TimerList { 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.set_timer(timeout_msec)?; + let new_timer_id = window.create_timer(timeout_msec)?; self.timers.borrow_mut().push(new_timer_id); Ok(()) } @@ -97,14 +102,4 @@ impl TimerList { timers.swap_remove(index); true } - - pub fn destroy_all(&self, window: HWnd) { - let timers = self.timers.take(); - - for timer_id in timers { - if let Err(e) = window.kill_timer(timer_id) { - crate::warn!("Could not remove timer: {e}") - } - } - } } diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs index 4ccf27e6..7de6ae7e 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -4,7 +4,7 @@ use crate::wrappers::win32::style::WindowStyle; use crate::wrappers::win32::user32::ExtendedUser32; 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; @@ -222,10 +222,22 @@ impl HWnd { Ok(()) } - pub fn set_timer(&self, elapse: u32) -> Result { + 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)?; - TimerId::from_wparam(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<()> { @@ -389,7 +401,7 @@ impl From for SyncHwnd { pub trait PostMessageExt { /// # Safety /// - /// The message, wparam and lparam values must be valid + /// The message, wparam and lparam values must be valid. unsafe fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM); #[inline] From 84d4d05c29ac4ce7c0ba2996f5b70eb406906184 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:18:31 +0200 Subject: [PATCH 18/19] macOS wip: use CADisplayLink --- Cargo.toml | 4 +++- examples/test-frame-pacing/src/main.rs | 6 ++++++ src/platform/macos/view.rs | 23 +++++++++++----------- src/wrappers/appkit/view.rs | 23 +++++++++++++++++++--- src/wrappers/appkit/view/implementation.rs | 13 ++++++++++++ 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 31636503..747ad881 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"] [lints.clippy] missing-safety-doc = "allow" diff --git a/examples/test-frame-pacing/src/main.rs b/examples/test-frame-pacing/src/main.rs index 5ee7b627..84e454f7 100644 --- a/examples/test-frame-pacing/src/main.rs +++ b/examples/test-frame-pacing/src/main.rs @@ -26,6 +26,7 @@ struct FramePacingTest { bar_pos: Cell, bar_speed: Cell, + first: Cell, } impl FramePacingTest { @@ -54,12 +55,17 @@ impl FramePacingTest { previous_frame_time: Instant::now().into(), bar_pos: 0.into(), bar_speed: 6.into(), + first: Cell::new(false), }) } } impl WindowHandler for FramePacingTest { 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); diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index 9bd3ea66..1c6ea943 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -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,19 @@ 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>, 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 +100,7 @@ impl BaseviewView { state: Rc::clone(&state), keyboard_state: KeyboardState::new(), - frame_timer: None.into(), + display_link: OnceCell::new(), window_handler: WindowHandlerContainer::new(), notification_center_observer: None.into(), parenting: ViewParentingType::Uninitialized.into(), @@ -139,12 +140,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() { - view.setNeedsDisplay(true); - } - })); + let Ok(()) = view.display_link.set(view.view.setup_display_link()) else { + unreachable!() + }; let notifier_view = Weak::new(view.view); let observer = NotificationCenterObserver::register_window_key_change(move |n| { @@ -190,7 +188,6 @@ impl BaseviewView { this.state.closed.set(true); this.view.removeFromSuperview(); this.notification_center_observer.take(); - this.frame_timer.take(); this.window_handler.destroy(); let parenting = this.parenting.replace(ViewParentingType::Uninitialized); @@ -362,6 +359,10 @@ impl ViewImpl for BaseviewView { Self::trigger_frame(this); } + 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/wrappers/appkit/view.rs b/src/wrappers/appkit/view.rs index c9044cb3..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, NSRect}; +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 { @@ -157,6 +173,7 @@ pub trait ViewImpl: Sized { 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 b26c346b..eb2b3398 100644 --- a/src/wrappers/appkit/view/implementation.rs +++ b/src/wrappers/appkit/view/implementation.rs @@ -96,6 +96,10 @@ pub unsafe fn create_view_class() -> &'static AnyClass { ); 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:), @@ -204,6 +208,15 @@ extern "C-unwind" fn draw_rect(this: &View, _: Sel, dirty_rect: 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> { From 7ce98758c2665f0e546e1a8b1500feca595479f7 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:34:13 +0200 Subject: [PATCH 19/19] macOS fixes --- src/platform/macos/context.rs | 5 ++-- src/platform/macos/view.rs | 45 ++++++++++++++++++++++++++++------- src/platform/macos/waker.rs | 3 ++- src/platform/macos/window.rs | 2 ++ 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/platform/macos/context.rs b/src/platform/macos/context.rs index bd3491ea..4629cb96 100644 --- a/src/platform/macos/context.rs +++ b/src/platform/macos/context.rs @@ -35,9 +35,10 @@ impl WindowContext { } pub fn request_redraw(&self) { + self.state.redraw_requested.set(true); let Some(view) = self.view.load() else { return }; - - view.setNeedsDisplay(true); + let Some(view) = view.inner() else { return }; + view.set_next_frame_needed(true); } pub fn request_redraw_after(&self, duration: Duration) { diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index 1c6ea943..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; @@ -73,6 +73,7 @@ pub(crate) struct BaseviewView { parenting: RefCell, pub(crate) lifetime_tied_to_app: Cell>>, display_link: OnceCell>, + display_link_started: Cell, host: Host, pub(crate) cursor_manager: CursorManager, @@ -101,6 +102,7 @@ impl BaseviewView { keyboard_state: KeyboardState::new(), display_link: OnceCell::new(), + display_link_started: false.into(), window_handler: WindowHandlerContainer::new(), notification_center_observer: None.into(), parenting: ViewParentingType::Uninitialized.into(), @@ -109,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| { @@ -140,9 +142,9 @@ impl BaseviewView { let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType }; view.view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type])); - let Ok(()) = view.display_link.set(view.view.setup_display_link()) else { - unreachable!() - }; + 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,12 +186,28 @@ impl BaseviewView { 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.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(); @@ -253,9 +271,20 @@ impl BaseviewView { } fn trigger_frame(this: ViewRef) { - if let Some(Err(e)) = this.window_handler.use_handler(|h| h.draw()) { + 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,7 +385,7 @@ impl ViewImpl for BaseviewView { } fn draw_rect(this: ViewRef, _rect: NSRect) { - Self::trigger_frame(this); + this.set_next_frame_needed(true); } fn display_link_fired(this: ViewRef, _sender: &CADisplayLink) { diff --git a/src/platform/macos/waker.rs b/src/platform/macos/waker.rs index 740f354b..2535062d 100644 --- a/src/platform/macos/waker.rs +++ b/src/platform/macos/waker.rs @@ -20,7 +20,8 @@ impl WindowWaker { pub fn request_redraw_after(&self, duration: Duration) { self.view.use_on_main_thread_after(duration, |view| { let Some(view) = view.load() else { return }; - view.setNeedsDisplay(true); + let Some(view) = view.inner() else { return }; + view.set_next_frame_needed(true); }) } diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index d61b0b8c..82c1717e 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -201,6 +201,7 @@ pub(crate) struct WindowSharedState { pub size: Cell>, pub scale_factor: Cell, pub sizing_strategy: SizingStrategy, + pub redraw_requested: Cell, } impl WindowSharedState { @@ -210,6 +211,7 @@ impl WindowSharedState { size: size.into(), scale_factor: scale_factor.into(), sizing_strategy, + redraw_requested: Cell::new(false), } } }