Add timestamps to events

This commit is contained in:
Mads Marquart
2026-03-18 17:31:57 +01:00
parent 4d81f4aa62
commit 99f7bb6b44
8 changed files with 109 additions and 20 deletions

View File

@@ -60,6 +60,7 @@ objc2-core-foundation = { version = "0.3.2", default-features = false }
objc2-core-graphics = { version = "0.3.2", default-features = false }
objc2-core-video = { version = "0.3.2", default-features = false }
objc2-foundation = { version = "0.3.2", default-features = false }
objc2-quartz-core = { version = "0.3.2", default-features = false }
objc2-ui-kit = { version = "0.3.2", default-features = false }
# Windows dependencies.

View File

@@ -107,6 +107,7 @@ objc2-foundation = { workspace = true, features = [
"NSThread",
"NSValue",
] }
objc2-quartz-core = { workspace = true, features = ["std", "CABase"] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler"] }
[dev-dependencies]

View File

@@ -98,6 +98,7 @@ pub(crate) fn override_send_event(global_app: &NSApplication) {
}
fn maybe_dispatch_device_event(app_state: &Rc<AppState>, event: &NSEvent) {
let time = app_state.event_time(event);
let event_type = event.r#type();
#[allow(non_upper_case_globals)]
match event_type {
@@ -110,7 +111,7 @@ fn maybe_dispatch_device_event(app_state: &Rc<AppState>, event: &NSEvent) {
if delta_x != 0.0 || delta_y != 0.0 {
app_state.maybe_queue_with_handler(move |app, event_loop| {
app.device_event(event_loop, None, DeviceEvent::PointerMotion {
app.device_event(event_loop, None, time, DeviceEvent::PointerMotion {
delta: (delta_x, delta_y),
});
});
@@ -119,7 +120,7 @@ fn maybe_dispatch_device_event(app_state: &Rc<AppState>, event: &NSEvent) {
NSEventType::LeftMouseDown | NSEventType::RightMouseDown | NSEventType::OtherMouseDown => {
let button = event.buttonNumber() as u32;
app_state.maybe_queue_with_handler(move |app, event_loop| {
app.device_event(event_loop, None, DeviceEvent::Button {
app.device_event(event_loop, None, time, DeviceEvent::Button {
button,
state: ElementState::Pressed,
});
@@ -128,7 +129,7 @@ fn maybe_dispatch_device_event(app_state: &Rc<AppState>, event: &NSEvent) {
NSEventType::LeftMouseUp | NSEventType::RightMouseUp | NSEventType::OtherMouseUp => {
let button = event.buttonNumber() as u32;
app_state.maybe_queue_with_handler(move |app, event_loop| {
app.device_event(event_loop, None, DeviceEvent::Button {
app.device_event(event_loop, None, time, DeviceEvent::Button {
button,
state: ElementState::Released,
});

View File

@@ -2,12 +2,14 @@ use std::cell::{Cell, OnceCell, RefCell};
use std::mem;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use std::time::{Duration, Instant};
use dispatch2::MainThreadBound;
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication};
use objc2_foundation::NSNotification;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEvent, NSRunningApplication};
use objc2_foundation::{NSNotification, NSTimeInterval};
use objc2_quartz_core::CACurrentMediaTime;
use tracing::warn;
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
use winit_common::event_handler::EventHandler;
use winit_core::application::ApplicationHandler;
@@ -43,6 +45,8 @@ pub(super) struct AppState {
start_time: Cell<Option<Instant>>,
wait_timeout: Cell<Option<Instant>>,
pending_redraw: RefCell<Vec<WindowId>>,
startup_instant: Instant,
startup_timestamp: NSTimeInterval,
// NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and
// as such should be careful to not add fields that, in turn, strongly reference those.
}
@@ -63,6 +67,17 @@ impl AppState {
Self::get(mtm).with_handler(|app, event_loop| app.proxy_wake_up(event_loop));
}));
// Prime dylib caches etc.
let _ = CACurrentMediaTime();
// Find the current Rust timestamp.
let startup_instant = Instant::now();
// Find the timestamp
//
// `NSProcessInfo::processInfo().systemUptime()` needs the required reason manifest,
// `CACurrentMediaTime` (currently) doesn't, so we use that instead.
let startup_timestamp = CACurrentMediaTime();
let this = Rc::new(Self {
mtm,
activation_policy,
@@ -83,6 +98,8 @@ impl AppState {
start_time: Cell::new(None),
wait_timeout: Cell::new(None),
pending_redraw: RefCell::new(vec![]),
startup_instant,
startup_timestamp,
});
GLOBAL.get(mtm).set(this.clone()).ok().and(Some(this))
@@ -96,6 +113,16 @@ impl AppState {
.clone()
}
pub(crate) fn event_time(&self, event: &NSEvent) -> Instant {
if event.timestamp() == 0.0 {
warn!(?event, "got zero timestamp");
return Instant::now();
}
let duration_since_startup = event.timestamp() - self.startup_timestamp;
let duration_since_startup = Duration::from_secs_f64(duration_since_startup as f64);
self.startup_instant + duration_since_startup
}
// NOTE: This notification will, globally, only be emitted once,
// no matter how many `EventLoop`s the user creates.
pub fn did_finish_launching(self: &Rc<Self>, _notification: &NSNotification) {
@@ -247,7 +274,12 @@ impl AppState {
// -> Don't go back into the event handler when our callstack originates from there
if !self.event_handler.in_use() {
self.with_handler(|app, event_loop| {
app.window_event(event_loop, window_id, WindowEvent::RedrawRequested);
app.window_event(
event_loop,
window_id,
Instant::now(),
WindowEvent::RedrawRequested,
);
});
// `pump_events` will request to stop immediately _after_ dispatching RedrawRequested
@@ -347,7 +379,12 @@ impl AppState {
let redraw = mem::take(&mut *self.pending_redraw.borrow_mut());
for window_id in redraw {
self.with_handler(|app, event_loop| {
app.window_event(event_loop, window_id, WindowEvent::RedrawRequested);
app.window_event(
event_loop,
window_id,
Instant::now(),
WindowEvent::RedrawRequested,
);
});
}
self.with_handler(|app, event_loop| {

View File

@@ -2,11 +2,12 @@
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use std::time::Instant;
use dpi::{LogicalPosition, LogicalSize};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Sel};
use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send};
use objc2::{AnyThread, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSWindow,
@@ -16,6 +17,7 @@ use objc2_foundation::{
NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString,
NSNotFound, NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
};
use tracing::warn;
use winit_core::event::{
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta,
PointerKind, PointerSource, TouchPhase, WindowEvent,
@@ -680,8 +682,9 @@ define_class!(
self.update_modifiers(event, false);
let time = self.ivars().app_state.event_time(event);
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.device_event(event_loop, None, DeviceEvent::MouseWheel { delta })
app.device_event(event_loop, None, time, DeviceEvent::MouseWheel { delta })
});
self.queue_event(WindowEvent::MouseWheel { device_id: None, delta, phase });
}
@@ -844,9 +847,16 @@ impl WinitView {
}
fn queue_event(&self, event: WindowEvent) {
let app = NSApplication::sharedApplication(self.mtm());
let window_id = window_id(&self.window());
let time = if let Some(nsevent) = app.currentEvent() {
self.ivars().app_state.event_time(&nsevent)
} else {
warn!("queued event with wrong timestamp, no active NSEvent found");
Instant::now()
};
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, event);
app.window_event(event_loop, window_id, time, event);
});
}

View File

@@ -5,6 +5,7 @@ use std::ffi::c_void;
use std::ptr;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use dpi::{
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
@@ -903,9 +904,16 @@ impl WindowDelegate {
}
pub(crate) fn queue_event(&self, event: WindowEvent) {
let app = NSApplication::sharedApplication(self.mtm());
let window_id = window_id(self.window());
let time = if let Some(nsevent) = app.currentEvent() {
self.ivars().app_state.event_time(&nsevent)
} else {
warn!("queued event with wrong timestamp, no active NSEvent found");
Instant::now()
};
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, event);
app.window_event(event_loop, window_id, time, event);
});
}

View File

@@ -1,4 +1,9 @@
//! End user application handling.
#[cfg(not(target_family = "wasm"))]
use std::time::Instant;
#[cfg(target_family = "wasm")]
use web_time::Instant;
use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
use crate::event_loop::ActiveEventLoop;
@@ -192,10 +197,14 @@ pub trait ApplicationHandler {
}
/// Emitted when the OS sends an event to a winit window.
///
/// Contains the ID of the window, the event, and the time the event was received. Note that
/// since events are queued, the time will differ from [`Instant::now()`].
fn window_event(
&mut self,
event_loop: &dyn ActiveEventLoop,
window_id: WindowId,
timestamp: Instant,
event: WindowEvent,
);
@@ -206,9 +215,10 @@ pub trait ApplicationHandler {
&mut self,
event_loop: &dyn ActiveEventLoop,
device_id: Option<DeviceId>,
timestamp: Instant,
event: DeviceEvent,
) {
let _ = (event_loop, device_id, event);
let _ = (event_loop, device_id, timestamp, event);
}
/// Emitted when the event loop is about to block and wait for new events.
@@ -372,9 +382,10 @@ impl<A: ?Sized + ApplicationHandler> ApplicationHandler for &mut A {
&mut self,
event_loop: &dyn ActiveEventLoop,
window_id: WindowId,
timestamp: Instant,
event: WindowEvent,
) {
(**self).window_event(event_loop, window_id, event);
(**self).window_event(event_loop, window_id, timestamp, event);
}
#[inline]
@@ -382,9 +393,10 @@ impl<A: ?Sized + ApplicationHandler> ApplicationHandler for &mut A {
&mut self,
event_loop: &dyn ActiveEventLoop,
device_id: Option<DeviceId>,
timestamp: Instant,
event: DeviceEvent,
) {
(**self).device_event(event_loop, device_id, event);
(**self).device_event(event_loop, device_id, timestamp, event);
}
#[inline]
@@ -440,9 +452,10 @@ impl<A: ?Sized + ApplicationHandler> ApplicationHandler for Box<A> {
&mut self,
event_loop: &dyn ActiveEventLoop,
window_id: WindowId,
timestamp: Instant,
event: WindowEvent,
) {
(**self).window_event(event_loop, window_id, event);
(**self).window_event(event_loop, window_id, timestamp, event);
}
#[inline]
@@ -450,9 +463,10 @@ impl<A: ?Sized + ApplicationHandler> ApplicationHandler for Box<A> {
&mut self,
event_loop: &dyn ActiveEventLoop,
device_id: Option<DeviceId>,
timestamp: Instant,
event: DeviceEvent,
) {
(**self).device_event(event_loop, device_id, event);
(**self).device_event(event_loop, device_id, timestamp, event);
}
#[inline]

View File

@@ -1,6 +1,7 @@
//! Simple winit window example.
use std::error::Error;
use std::time::Instant;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
@@ -36,8 +37,14 @@ impl ApplicationHandler for App {
}
}
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) {
println!("{event:?}");
fn window_event(
&mut self,
event_loop: &dyn ActiveEventLoop,
_: WindowId,
timestamp: Instant,
event: WindowEvent,
) {
::tracing::info!("{:?}: {event:?}", timestamp.elapsed());
match event {
WindowEvent::CloseRequested => {
println!("Close was requested; stopping");
@@ -62,11 +69,21 @@ impl ApplicationHandler for App {
fill::fill_window(window.as_ref());
// For contiguous redraw loop you can request a redraw from here.
// window.request_redraw();
window.request_redraw();
},
_ => (),
}
}
fn device_event(
&mut self,
_event_loop: &dyn ActiveEventLoop,
_device_id: Option<winit::event::DeviceId>,
timestamp: Instant,
event: winit::event::DeviceEvent,
) {
::tracing::info!("{:?}: {event:?}", timestamp.elapsed());
}
}
fn main() -> Result<(), Box<dyn Error>> {