From a66f423743d8d5360e47e89dc3529ad2ce5e4309 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:31:35 +0200 Subject: [PATCH 1/7] X11 impl for timers --- Cargo.toml | 1 + examples/timers/Cargo.toml | 11 +++ examples/timers/src/main.rs | 134 ++++++++++++++++++++++++++++++ src/context.rs | 6 ++ src/handler.rs | 5 ++ src/lib.rs | 2 + src/platform/x11/event_loop.rs | 4 + src/platform/x11/mod.rs | 2 + src/platform/x11/timer.rs | 41 +++++++++ src/platform/x11/window_shared.rs | 6 ++ src/timer.rs | 44 ++++++++++ 11 files changed, 256 insertions(+) create mode 100644 examples/timers/Cargo.toml create mode 100644 examples/timers/src/main.rs create mode 100644 src/platform/x11/timer.rs create mode 100644 src/timer.rs diff --git a/Cargo.toml b/Cargo.toml index 2f10bdbf..d1d9a171 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ members = [ "examples/render_wgpu", "examples/plugin_clack_femtovg", "examples/test-frame-pacing", + "examples/timers", "examples/external-wakeup", "tests/partial-rendering" ] diff --git a/examples/timers/Cargo.toml b/examples/timers/Cargo.toml new file mode 100644 index 00000000..76786180 --- /dev/null +++ b/examples/timers/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "timers" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +baseview = { path = "../..", features = ["opengl"] } +femtovg = "0.27.0" +rand = "0.10.3" +color = "0.3.3" diff --git a/examples/timers/src/main.rs b/examples/timers/src/main.rs new file mode 100644 index 00000000..880569fc --- /dev/null +++ b/examples/timers/src/main.rs @@ -0,0 +1,134 @@ +use baseview::dpi::LogicalSize; +use baseview::gl::{GlConfig, GlContext}; +use baseview::{ + Event, EventStatus, HandlerError, TimerHandle, Window, WindowContext, WindowHandler, + WindowSettings, WindowSize, +}; +use color::{Hsl, OpaqueColor}; +use femtovg::renderer::OpenGl; +use femtovg::{Canvas, Color}; +use std::cell::{Cell, RefCell}; +use std::time::Duration; + +struct FemtovgExample { + window_context: WindowContext, + gl_context: GlContext, + canvas: RefCell>, + + rect_1_color: Cell, + rect_2_color: Cell, + + rect_1_timer: TimerHandle, + rect_2_timer: TimerHandle, +} + +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)) }?; + + let mut canvas = Canvas::new(renderer)?; + let size = window_context.size(); + + canvas.set_size(size.physical.width, size.physical.height, size.scale_factor as f32); + + unsafe { gl_context.make_not_current()? }; + Ok(Self { + canvas: canvas.into(), + + rect_1_color: new_random_color().into(), + rect_2_color: new_random_color().into(), + + rect_1_timer: window_context.create_timer(Duration::from_millis(250))?, + rect_2_timer: window_context.create_timer(Duration::from_millis(1000))?, + + gl_context, + window_context, + }) + } +} + +impl WindowHandler for FemtovgExample { + fn draw(&self) -> Result<(), HandlerError> { + let context = &self.gl_context; + unsafe { context.make_current()? }; + + let mut canvas = self.canvas.borrow_mut(); + + let screen_height = canvas.height(); + let screen_width = canvas.width(); + + // Clear + canvas.clear_rect(0, 0, screen_width, screen_height, Color::rgb(0x0A, 0x0A, 0x0A)); + + // Make 1st rectangle + canvas.clear_rect( + (screen_width as f32 * 0.3).floor() as u32, + (screen_height as f32 * 0.45).floor() as u32, + (screen_width as f32 * 0.1).floor() as u32, + (screen_height as f32 * 0.1).floor() as u32, + self.rect_1_color.get(), + ); + + // Make 2nd rectangle + canvas.clear_rect( + (screen_width as f32 * 0.5).floor() as u32, + (screen_height as f32 * 0.45).floor() as u32, + (screen_width as f32 * 0.1).floor() as u32, + (screen_height as f32 * 0.1).floor() as u32, + self.rect_2_color.get(), + ); + + // Tell renderer to execute all drawing commands + canvas.flush(); + context.swap_buffers()?; + unsafe { context.make_not_current()? }; + + Ok(()) + } + + fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError> { + let size = new_size.physical; + self.canvas.borrow_mut().set_size(size.width, size.height, new_size.scale_factor as f32); + + Ok(()) + } + + fn on_event(&self, _event: Event) -> EventStatus { + EventStatus::Ignored + } + + fn on_timer(&self, timer: &TimerHandle) { + if timer == self.rect_1_timer { + self.rect_1_color.set(new_random_color()); + self.window_context.request_redraw(); + } + + if timer == self.rect_2_timer { + self.rect_2_color.set(new_random_color()); + self.window_context.request_redraw(); + } + } +} + +fn main() -> Result<(), baseview::Error> { + unsafe { baseview::assume_standalone_in_process() }; + + let window_open_options = WindowSettings::new() + .with_title("Baseview Waker example") + .with_size(LogicalSize::new(512, 512)) + .with_gl_config(GlConfig { alpha_bits: 8, ..GlConfig::default() }); + + Window::create(window_open_options, FemtovgExample::new)?.run_until_closed() +} + +fn new_random_color() -> Color { + let hue = rand::random_range(0.0..360.0); + let hsv_color = OpaqueColor::::new([hue, 100.0, 75.0]); + let color = hsv_color.to_rgba8(); + + Color::rgb(color.r, color.g, color.b) +} diff --git a/src/context.rs b/src/context.rs index f757586b..c85307f5 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,5 +1,6 @@ use super::*; use crate::dpi::Size; +use crate::timer::TimerHandle; use crate::waker::WindowWaker; use crate::{platform, MouseCursor, WindowSize}; use raw_window_handle::{ @@ -94,6 +95,11 @@ impl WindowContext { pub fn gl_context(&self) -> Option { self.inner.gl_context() } + + #[inline] + pub fn create_timer(&self, duration: Duration) -> Result { + Ok(self.inner.create_timer(duration)?.into()) + } } impl HasWindowHandle for WindowContext { diff --git a/src/handler.rs b/src/handler.rs index 8f6e628c..e49786dc 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,5 +1,6 @@ use super::*; use crate::platform::PlatformError; +use crate::timer::TimerHandle; pub trait WindowHandler: 'static { /// Requests the handler to draw a new frame immediately. @@ -107,6 +108,10 @@ pub trait WindowHandler: 'static { /// previous size, but this is only a best-effort attempt since those operations can also fail. fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError>; fn on_event(&self, event: Event) -> EventStatus; + + fn on_timer(&self, timer: &TimerHandle) { + let _ = timer; + } } type DynBuilderResult = Result, HandlerError>; diff --git a/src/lib.rs b/src/lib.rs index b50c3440..b43bbfca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod host; mod keyboard; mod mouse_cursor; mod settings; +mod timer; mod tracing; mod waker; mod window; @@ -26,6 +27,7 @@ pub use event::*; pub use handler::WindowHandler; pub use mouse_cursor::MouseCursor; pub use settings::*; +pub use timer::TimerHandle; pub use waker::WindowWaker; pub use window::*; diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index fc65e4ba..82b26fc8 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -112,6 +112,10 @@ impl EventLoop { }) } + pub(crate) fn handle_timer(&mut self, handle: &TimerHandle) { + self.handler.on_timer(handle.into()) + } + #[inline] fn drain_xcb_events(&mut self) -> Result { let mut event_received = false; diff --git a/src/platform/x11/mod.rs b/src/platform/x11/mod.rs index 582b87e1..9c106b6c 100644 --- a/src/platform/x11/mod.rs +++ b/src/platform/x11/mod.rs @@ -22,6 +22,7 @@ mod keyboard; mod visual_info; mod xcb_window; +mod timer; mod waker; mod window_shared; mod window_thread; @@ -33,6 +34,7 @@ use crate::platform::x11::window_shared::WindowInner; use crate::wrappers::xlib::XlibXcbConnection; pub type WindowContext = Rc; +pub use timer::TimerHandle; pub use waker::WindowWaker; #[cfg(feature = "opengl")] diff --git a/src/platform/x11/timer.rs b/src/platform/x11/timer.rs new file mode 100644 index 00000000..23150fb1 --- /dev/null +++ b/src/platform/x11/timer.rs @@ -0,0 +1,41 @@ +use crate::platform::x11::event_loop::EventLoop; +use crate::platform::PlatformError; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{LoopHandle, RegistrationToken}; +use std::rc::{Rc, Weak}; +use std::time::Duration; + +pub type TimerHandle = Rc; + +#[derive(PartialEq, Eq)] +pub struct TimerHandleInner { + token: RegistrationToken, +} + +impl TimerHandleInner {} + +pub(crate) fn insert_timer( + loop_handle: &LoopHandle, duration: Duration, +) -> Result { + let timer = Timer::from_duration(duration); + + let handle = Rc::new_cyclic(move |this| { + let this = Weak::clone(this); + + let result = loop_handle.insert_source(timer, move |_, _, e| { + if let Some(this) = this.upgrade() { + e.handle_timer(&this); + } + TimeoutAction::ToDuration(duration) + }); + + match result { + Err(e) => { + panic!("Failed to insert timer: {:?}", e); + } + Ok(token) => TimerHandleInner { token }, + } + }); + + Ok(handle) +} diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 56fa7735..3da45c2b 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,5 +1,6 @@ use crate::dpi::{PhysicalSize, Size}; use crate::platform::x11::event_loop::EventLoop; +use crate::platform::x11::timer::insert_timer; use crate::platform::x11::visual_info::WindowVisualConfig; use crate::platform::x11::waker::WindowWaker; use crate::platform::x11::window_thread::WindowThreadShared; @@ -255,6 +256,11 @@ impl WindowInner { Ok(()) } + #[inline] + pub fn create_timer(&self, duration: Duration) -> Result { + insert_timer(&self.loop_handle, duration) + } + pub fn resize_immediately( &self, new_size: PhysicalSize, handler: &dyn WindowHandler, ) -> Result<()> { diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 00000000..485ef467 --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,44 @@ +use crate::platform; +use std::marker::PhantomData; + +#[derive(PartialEq, Eq, Clone)] +#[repr(transparent)] +pub struct TimerHandle { + inner: platform::TimerHandle, + _nonsend: PhantomData<*mut ()>, // Ensures this is !Send & !Sync on all platforms +} + +impl TimerHandle { + pub(crate) fn from_ref(handle: &platform::TimerHandle) -> &TimerHandle { + // SAFETY: This is repr(transparent) + unsafe { core::mem::transmute::<&platform::TimerHandle, &TimerHandle>(handle) } + } +} + +impl From for TimerHandle { + #[inline] + fn from(value: platform::TimerHandle) -> Self { + Self { inner: value, _nonsend: PhantomData } + } +} + +impl<'a> From<&'a platform::TimerHandle> for &'a TimerHandle { + #[inline] + fn from(value: &'a platform::TimerHandle) -> Self { + TimerHandle::from_ref(value) + } +} + +impl PartialEq<&TimerHandle> for TimerHandle { + #[inline] + fn eq(&self, other: &&TimerHandle) -> bool { + self == *other + } +} + +impl PartialEq for &TimerHandle { + #[inline] + fn eq(&self, other: &TimerHandle) -> bool { + *self == other + } +} From f12847d451c1cf54b527c9d4eff70e1572ed0bed Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:41:28 +0200 Subject: [PATCH 2/7] optimize timer checks --- src/platform/x11/timer.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/platform/x11/timer.rs b/src/platform/x11/timer.rs index 23150fb1..5df8aba6 100644 --- a/src/platform/x11/timer.rs +++ b/src/platform/x11/timer.rs @@ -5,7 +5,15 @@ use calloop::{LoopHandle, RegistrationToken}; use std::rc::{Rc, Weak}; use std::time::Duration; -pub type TimerHandle = Rc; +#[derive(Clone, Eq)] +pub struct TimerHandle(Rc); + +impl PartialEq for TimerHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} #[derive(PartialEq, Eq)] pub struct TimerHandleInner { @@ -19,12 +27,12 @@ pub(crate) fn insert_timer( ) -> Result { let timer = Timer::from_duration(duration); - let handle = Rc::new_cyclic(move |this| { + let handle = Rc::::new_cyclic(move |this| { let this = Weak::clone(this); let result = loop_handle.insert_source(timer, move |_, _, e| { if let Some(this) = this.upgrade() { - e.handle_timer(&this); + e.handle_timer(&TimerHandle(this)); } TimeoutAction::ToDuration(duration) }); @@ -37,5 +45,5 @@ pub(crate) fn insert_timer( } }); - Ok(handle) + Ok(TimerHandle(handle)) } From 1a52871805f48a8095cd99649b08906b54e9857d Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:17:34 +0200 Subject: [PATCH 3/7] macos impl --- Cargo.toml | 2 +- src/platform/macos/context.rs | 15 +++++++- src/platform/macos/error.rs | 5 ++- src/platform/macos/mod.rs | 2 + src/platform/macos/timer.rs | 70 +++++++++++++++++++++++++++++++++++ src/platform/macos/view.rs | 19 ++++++++++ 6 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/platform/macos/timer.rs diff --git a/Cargo.toml b/Cargo.toml index d1d9a171..c2463c0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ objc2 = "0.6.4" objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std", "CFString", "CFUUID", "block2", "objc2"] } block2 = "0.6.2" dispatch2 = "0.3.1" -objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSEnumerator", "block2", "NSOperation"] } +objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSEnumerator", "block2", "NSTimer", "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", diff --git a/src/platform/macos/context.rs b/src/platform/macos/context.rs index 236c8e0c..bf447593 100644 --- a/src/platform/macos/context.rs +++ b/src/platform/macos/context.rs @@ -1,6 +1,6 @@ use crate::dpi::Size; use crate::platform::macos::view::BaseviewView; -use crate::platform::{PlatformHandle, WindowSharedState}; +use crate::platform::{PlatformError, PlatformHandle, WindowSharedState}; use crate::platform::{Result, WindowWaker}; use crate::wrappers::appkit::{View, ViewRef}; use crate::*; @@ -135,4 +135,17 @@ impl WindowContext { pub fn platform_handle(&self) -> PlatformHandle { PlatformHandle { inner: MainThreadBound::new(self.view.clone(), self.mtm) } } + + #[inline] + pub fn create_timer(&self, duration: Duration) -> Result { + let Some(view_retained) = self.view.load() else { + return Err(PlatformError::InvalidWindowContext); + }; + + let Some(view) = view_retained.inner_ref() else { + return Err(PlatformError::InvalidWindowContext); + }; + + autoreleasepool(|_| Ok(view.create_timer(&view_retained, duration))) + } } diff --git a/src/platform/macos/error.rs b/src/platform/macos/error.rs index 7d0672b2..8b7944e0 100644 --- a/src/platform/macos/error.rs +++ b/src/platform/macos/error.rs @@ -4,6 +4,7 @@ use std::fmt::Display; #[derive(Debug)] pub enum PlatformError { Handler(HandlerError), + InvalidWindowContext, #[cfg(feature = "opengl")] GlError(super::gl::GlError), } @@ -14,6 +15,9 @@ impl Display for PlatformError { #[cfg(feature = "opengl")] PlatformError::GlError(e) => e.fmt(fmt), PlatformError::Handler(e) => e.fmt(fmt), + PlatformError::InvalidWindowContext => { + write!(fmt, "Invalid operation: WindowContext is not valid anymore") + } } } } @@ -22,7 +26,6 @@ impl std::error::Error for PlatformError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { PlatformError::Handler(e) => Some(e.source()), - #[cfg(feature = "opengl")] _ => None, } } diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 4a983842..6f20e192 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -3,6 +3,7 @@ mod cursor; mod damage; mod error; mod keyboard; +mod timer; mod view; mod waker; mod window; @@ -20,6 +21,7 @@ use objc2_app_kit::NSView; use raw_window_handle::{DisplayHandle, HasWindowHandle}; use std::fmt; use std::fmt::Formatter; +pub use timer::TimerHandle; pub use waker::WindowWaker; pub use window::*; diff --git a/src/platform/macos/timer.rs b/src/platform/macos/timer.rs new file mode 100644 index 00000000..80244ab6 --- /dev/null +++ b/src/platform/macos/timer.rs @@ -0,0 +1,70 @@ +use crate::platform::macos::view::BaseviewView; +use crate::wrappers::appkit::View; +use block2::RcBlock; +use objc2::__framework_prelude::Retained; +use objc2::rc::Weak; +use objc2_foundation::NSTimer; +use std::cell::RefCell; +use std::ptr::NonNull; +use std::time::Duration; + +#[derive(Clone, Eq)] +pub struct TimerHandle { + timer: NonNull, +} + +impl PartialEq for TimerHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.timer == other.timer + } +} + +pub struct TimerManager { + timers: RefCell>>, +} + +impl TimerManager { + pub fn new() -> Self { + TimerManager { timers: vec![].into() } + } + + pub fn create_timer(&self, view: Weak>, duration: Duration) -> TimerHandle { + let timer = create_timer(view, duration); + + let Some(ptr) = NonNull::new(Retained::as_ptr(&timer).cast_mut()) else { unreachable!() }; + + let handle = TimerHandle { timer: ptr }; + + self.add_timer(timer); + + handle + } + + fn add_timer(&self, timer: Retained) { + self.timers.borrow_mut().push(timer); + } +} + +impl Drop for TimerManager { + fn drop(&mut self) { + for timer in self.timers.take() { + timer.invalidate(); + } + } +} + +fn create_timer(view: Weak>, duration: Duration) -> Retained { + let interval = duration.as_secs_f64(); + let block = RcBlock::new(move |t| { + let Some(view) = view.load() else { return }; + let Some(view) = view.inner_ref() else { return }; + + let handle = TimerHandle { timer: t }; + + BaseviewView::trigger_timer(view, &handle); + }); + + // SAFETY: block does not need to be sendable + unsafe { NSTimer::scheduledTimerWithTimeInterval_repeats_block(interval, true, &block) } +} diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index 05f885a4..7b53e4be 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -5,6 +5,7 @@ use super::window::WindowSharedState; use crate::dpi::{LogicalPosition, LogicalSize}; use crate::host::Host; use crate::platform::macos::cursor::CursorManager; +use crate::platform::macos::timer::TimerManager; use crate::platform::*; use crate::tracing::warn; use crate::utils::SizingStrategy; @@ -28,6 +29,7 @@ use objc2_quartz_core::CADisplayLink; use std::cell::{Cell, OnceCell, RefCell}; use std::ptr::null; use std::rc::Rc; +use std::time::Duration; pub enum ViewParentingType { Parented { parent_view: Weak }, @@ -79,6 +81,8 @@ pub(crate) struct BaseviewView { host: Host, pub(crate) cursor_manager: CursorManager, + timers: TimerManager, + #[cfg(feature = "opengl")] pub(crate) gl_context: OnceCell, } @@ -111,6 +115,8 @@ impl BaseviewView { lifetime_tied_to_app: None.into(), cursor_manager: CursorManager::new(), + timers: TimerManager::new(), + #[cfg(feature = "opengl")] gl_context: OnceCell::new(), }; @@ -289,6 +295,19 @@ impl BaseviewView { } } + pub(crate) fn trigger_timer(this: ViewRef, handle: &TimerHandle) { + this.window_handler.use_handler(|h| { + h.on_timer(handle.into()); + }); + } + + pub(crate) fn create_timer( + &self, this: &Retained>, duration: Duration, + ) -> TimerHandle { + let view = Weak::from_retained(this); + self.timers.create_timer(view, duration) + } + fn apply_size_constraints(this: ViewRef) { let ViewParentingType::Windowed { owned_window } = &*this.parenting.borrow() else { return; From db36e9664256fbfed872c491884c78369327c09e Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:13:12 +0200 Subject: [PATCH 4/7] win32 impl --- src/platform/win/mod.rs | 4 +++- src/platform/win/window.rs | 38 +++++++++++++++++++++++--------- src/platform/win/window_state.rs | 22 ++++++++++++++++-- src/wrappers/win32/timer.rs | 16 ++++++++++++++ 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/platform/win/mod.rs b/src/platform/win/mod.rs index 7b8ee6d7..6d38d505 100644 --- a/src/platform/win/mod.rs +++ b/src/platform/win/mod.rs @@ -9,7 +9,7 @@ mod window_state; use crate::wrappers::win32::h_instance::HInstance; use crate::wrappers::win32::window::HWnd; -use crate::wrappers::win32::Rect; +use crate::wrappers::win32::{Rect, TimerId}; pub(crate) use dpi::DpiScalingStrategy; pub use error::{PlatformError, Result}; use raw_window_handle::{ @@ -22,6 +22,8 @@ use std::rc::Rc; pub use waker::WindowWaker; pub use window::*; +pub type TimerHandle = TimerId; + #[cfg(feature = "opengl")] pub mod gl; diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index 4c412fc3..f41dd70d 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -355,6 +355,12 @@ impl BaseviewWindow { handler.poll(); } + pub(crate) fn handle_timer(&self, timer: &TimerHandle) { + let Some(handler) = self.handler.get() else { return }; + + handler.on_timer(timer.into()); + } + pub(crate) fn handle_event(&self, event: Event) -> EventStatus { let Some(handler) = self.handler.get() else { return EventStatus::Ignored; @@ -623,20 +629,30 @@ unsafe fn wnd_proc_inner( if window_state.redraw_timer.matches_id(timer_id) && window_state.redraw_timer.is_running() { + if !window_state.redraw_timer.is_running() { + return None; + }; window.invalidate_window(); + return Some(0); + } + + match window_state.shared.delayed_redraw_timers.remove_if_exists(window, timer_id) { + Ok(false) => {} + Err(e) => { + warn!("Could not remove timer: {}", e); + return None; + } + Ok(true) => { + window_state.request_redraw(); + return Some(0); + } + } + + if window_state.shared.user_timers.exists(timer_id) { + window_bv.handle_timer(&timer_id); Some(0) } else { - match window_state.shared.delayed_redraw_timers.remove_if_exists(window, timer_id) { - Ok(false) => None, - Err(e) => { - warn!("Could not remove timer: {}", e); - None - } - Ok(true) => { - window_state.request_redraw(); - Some(0) - } - } + None } } WM_CLOSE => { diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index f947bb88..3b6e0800 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -2,7 +2,7 @@ use crate::dpi::{PhysicalSize, Size}; use crate::platform::win::dpi::DpiScalingStrategy; use crate::platform::win::keyboard::KeyboardState; use crate::platform::win::waker::WindowWakerSource; -use crate::platform::{PlatformHandle, WindowWaker}; +use crate::platform::{PlatformHandle, TimerHandle, WindowWaker}; use crate::utils::SizingStrategy; use crate::window::WindowInitializer; use crate::wrappers::win32::cursor::SystemCursor; @@ -11,7 +11,7 @@ use crate::wrappers::win32::window::{HWnd, PostMessageExt}; use crate::wrappers::win32::{ Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList, TimerSlot, }; -use crate::{MouseCursor, WindowSize}; +use crate::{Error, MouseCursor, WindowSize}; use raw_window_handle::{DisplayHandle, Win32WindowHandle}; use std::cell::{Cell, Ref, RefCell}; use std::num::NonZeroIsize; @@ -150,6 +150,13 @@ impl WindowState { pub fn waker(&self) -> WindowWaker { self.shared.window_waker_source.waker() } + + #[inline] + pub fn create_timer(&self, duration: Duration) -> Result { + let duration_millis = duration.as_millis().try_into().unwrap_or(u32::MAX); + + Ok(self.hwnd.create_timer(duration_millis)?) + } } pub struct WindowSharedState { @@ -166,6 +173,7 @@ pub struct WindowSharedState { pub user32: LibraryModule, pub sizing_strategy: SizingStrategy, pub delayed_redraw_timers: TimerList, + pub user_timers: TimerList, pub window_waker_source: WindowWakerSource, } @@ -187,6 +195,7 @@ impl WindowSharedState { fallback_scale_factor: init.settings.fallback_scale_factor.into(), dpi_scaling_strategy: DpiScalingStrategy::default().into(), delayed_redraw_timers: TimerList::new(), + user_timers: TimerList::new(), window_waker_source: WindowWakerSource::new(), hwnd: None.into(), }; @@ -258,6 +267,15 @@ impl WindowSharedState { } } +impl Drop for WindowSharedState { + fn drop(&mut self) { + if let Some(hwnd) = self.hwnd.get() { + self.user_timers.stop_and_destroy_all(hwnd); + self.delayed_redraw_timers.stop_and_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 bcc523b7..45936b8c 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -86,6 +86,10 @@ impl TimerList { Ok(()) } + pub fn exists(&self, timer_id: TimerId) -> bool { + self.timers.borrow().contains(&timer_id) + } + pub fn remove_if_exists(&self, window: HWnd, id: TimerId) -> Result { if !self.pop_if_exists(id) { return Ok(false); @@ -102,4 +106,16 @@ impl TimerList { timers.swap_remove(index); true } + + fn pop(&self) -> Option { + self.timers.borrow_mut().pop() + } + + pub fn stop_and_destroy_all(&self, window: HWnd) { + while let Some(timer) = self.pop() { + if let Err(e) = window.kill_timer(timer) { + crate::warn!("Failed to kill timer: {}", e); + } + } + } } From 66e13fbf8a09a571470e5999c8ea796f5c84726f Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:33:03 +0200 Subject: [PATCH 5/7] fixes --- src/platform/win/window.rs | 4 +--- src/platform/win/window_state.rs | 4 +--- src/wrappers/win32/timer.rs | 4 ++-- src/wrappers/win32/window/handle.rs | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/platform/win/window.rs b/src/platform/win/window.rs index f41dd70d..e2a9561f 100644 --- a/src/platform/win/window.rs +++ b/src/platform/win/window.rs @@ -626,9 +626,7 @@ unsafe fn wnd_proc_inner( WM_TIMER => { let timer_id = TimerId::from_raw(wparam)?; - if window_state.redraw_timer.matches_id(timer_id) - && window_state.redraw_timer.is_running() - { + if window_state.redraw_timer.matches_id(timer_id) { if !window_state.redraw_timer.is_running() { return None; }; diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index 3b6e0800..b2af9bfb 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -153,9 +153,7 @@ impl WindowState { #[inline] pub fn create_timer(&self, duration: Duration) -> Result { - let duration_millis = duration.as_millis().try_into().unwrap_or(u32::MAX); - - Ok(self.hwnd.create_timer(duration_millis)?) + Ok(self.shared.user_timers.add_new_timer(self.hwnd, duration)?) } } diff --git a/src/wrappers/win32/timer.rs b/src/wrappers/win32/timer.rs index 45936b8c..44d1456b 100644 --- a/src/wrappers/win32/timer.rs +++ b/src/wrappers/win32/timer.rs @@ -79,11 +79,11 @@ impl TimerList { Self { timers: Vec::new().into() } } - pub fn add_new_timer(&self, window: HWnd, timeout: Duration) -> Result<(), Error> { + pub fn add_new_timer(&self, window: HWnd, timeout: Duration) -> Result { let timeout_msec = timeout.as_millis().try_into().unwrap_or(u32::MAX); let new_timer_id = window.create_timer(timeout_msec)?; self.timers.borrow_mut().push(new_timer_id); - Ok(()) + Ok(new_timer_id) } pub fn exists(&self, timer_id: TimerId) -> bool { diff --git a/src/wrappers/win32/window/handle.rs b/src/wrappers/win32/window/handle.rs index 4cb6897f..303446c9 100644 --- a/src/wrappers/win32/window/handle.rs +++ b/src/wrappers/win32/window/handle.rs @@ -223,7 +223,7 @@ impl HWnd { } pub fn create_timer(&self, elapse: u32) -> Result { - let result = unsafe { SetTimer(self.as_raw(), 0, elapse, None) }; + let result = unsafe { SetTimer(null_mut(), 0, elapse, None) }; let timer_id = TimerId::from_raw(result).ok_or_else(Error::from_thread)?; self.reset_timer(timer_id, elapse)?; From c6a761f84fe24403858b78fb253c159930f4690b Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:55:57 +0200 Subject: [PATCH 6/7] wip --- src/handler.rs | 7 +++++++ src/platform/win/window_state.rs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/handler.rs b/src/handler.rs index e49786dc..cb764d9d 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -109,6 +109,13 @@ pub trait WindowHandler: 'static { fn resized(&self, new_size: WindowSize) -> Result<(), HandlerError>; fn on_event(&self, event: Event) -> EventStatus; + /// Notifies the handler that a [timer](WindowContext::create_timer) has been triggered. + /// + /// A [`TimerHandle`] is given for the handler to match and figure out which timer was triggered, + /// and act accordingly. + /// + /// See [`WindowContext::create_timer`] to create a new timer that will schedule this method + /// to be called at regular intervals, and for more information. fn on_timer(&self, timer: &TimerHandle) { let _ = timer; } diff --git a/src/platform/win/window_state.rs b/src/platform/win/window_state.rs index b2af9bfb..4a9610c8 100644 --- a/src/platform/win/window_state.rs +++ b/src/platform/win/window_state.rs @@ -11,7 +11,7 @@ use crate::wrappers::win32::window::{HWnd, PostMessageExt}; use crate::wrappers::win32::{ Dpi, DpiAwarenessGuard, ExtendedUser32, LibraryModule, TimerList, TimerSlot, }; -use crate::{Error, MouseCursor, WindowSize}; +use crate::{MouseCursor, WindowSize}; use raw_window_handle::{DisplayHandle, Win32WindowHandle}; use std::cell::{Cell, Ref, RefCell}; use std::num::NonZeroIsize; From a05e5baa292767c46563d7572ce4218294d8ac69 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:19:32 +0200 Subject: [PATCH 7/7] Docs --- src/context.rs | 32 ++++++++++++++++++++++++++++++-- src/timer.rs | 18 ++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/context.rs b/src/context.rs index c85307f5..0ad95034 100644 --- a/src/context.rs +++ b/src/context.rs @@ -96,9 +96,37 @@ impl WindowContext { self.inner.gl_context() } + /// Creates a new timer that will trigger regularly at the specified `interval`. + /// + /// When the timer is triggered, the [`WindowHandler::on_timer`] callback will be called with a + /// handle to the timer that was triggered. + /// + /// This returns a [`TimerHandle`], which can be stored and later matched against the one given in + /// [`WindowHandler::on_timer`] to identify which timer was triggered, and process it accordingly. + /// + /// The returned [`TimerHandle`] is a weak handle and has no effect on the underlying timer's + /// lifetime. See the [`TimerHandle`] documentation for more information. + /// + /// The created timer is automatically stopped and cleaned up when the window is destroyed. + /// + /// # Timer precision + /// + /// The timers created by this function rely on the platform's GUI timers implementation, which + /// are often imprecise. + /// + /// They often have a minimum firing interval that can be as high as 10ms (e.g. on Windows). + /// They can also drift, so two timers with the same interval may not trigger in sync forever. + /// + /// Moreover, all platforms have something akin to some kind of "event queue", which can back + /// up if the thread is overloaded. + /// When that happens, depending on the platform, timer firings may be skipped, coalesced, or + /// even have multiple firings come all flooding at once when said queue is emptying. + /// + /// Therefore, this function should *not* be used if you need high-precision timers, e.g. for + /// hardware synchronization, MIDI, etc. #[inline] - pub fn create_timer(&self, duration: Duration) -> Result { - Ok(self.inner.create_timer(duration)?.into()) + pub fn create_timer(&self, interval: Duration) -> Result { + Ok(self.inner.create_timer(interval)?.into()) } } diff --git a/src/timer.rs b/src/timer.rs index 485ef467..eb2824a5 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -1,6 +1,24 @@ use crate::platform; use std::marker::PhantomData; +/// A weak handle to a [timer]. +/// +/// This handle can be used to identify which timer has been triggered during a [`on_timer`] event +/// using its [`PartialEq`] implementation. +/// +/// It can also be cheaply [cloned](Clone) for convenience. All cloned handles represent the same +/// timer instance, and will always compare equal to one another. +/// +/// Note this is a weak handle and is not tied at all to the actual timer's lifetime. A timer may +/// be invalidated and/or destroyed while this handle is still alive. +/// +/// Conversely, dropping this handle has no effect on the lifetime of the timer, and it may still +/// fire after all handles have been dropped. +/// +/// All timers are automatically destroyed and stopped when the associated window is destroyed. +/// +/// [timer]: crate::WindowContext::create_timer +/// [`on_timer`]: crate::WindowHandler::on_timer #[derive(PartialEq, Eq, Clone)] #[repr(transparent)] pub struct TimerHandle {