Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -105,6 +105,7 @@ members = [
"examples/render_wgpu",
"examples/plugin_clack_femtovg",
"examples/test-frame-pacing",
"examples/timers",
"examples/external-wakeup",
"tests/partial-rendering"
]
Expand Down
11 changes: 11 additions & 0 deletions examples/timers/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
134 changes: 134 additions & 0 deletions examples/timers/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<Canvas<OpenGl>>,

rect_1_color: Cell<Color>,
rect_2_color: Cell<Color>,

rect_1_timer: TimerHandle,
rect_2_timer: TimerHandle,
}

impl FemtovgExample {
fn new(window_context: WindowContext) -> Result<Self, HandlerError> {
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::<Hsl>::new([hue, 100.0, 75.0]);
let color = hsv_color.to_rgba8();

Color::rgb(color.r, color.g, color.b)
}
34 changes: 34 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -94,6 +95,39 @@ impl WindowContext {
pub fn gl_context(&self) -> Option<crate::gl::GlContext> {
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, interval: Duration) -> Result<TimerHandle, Error> {
Ok(self.inner.create_timer(interval)?.into())
}
}

impl HasWindowHandle for WindowContext {
Expand Down
12 changes: 12 additions & 0 deletions src/handler.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -107,6 +108,17 @@ 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;

/// 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;
}
}

type DynBuilderResult = Result<Box<dyn WindowHandler>, HandlerError>;
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod host;
mod keyboard;
mod mouse_cursor;
mod settings;
mod timer;
mod tracing;
mod waker;
mod window;
Expand All @@ -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::*;

Expand Down
15 changes: 14 additions & 1 deletion src/platform/macos/context.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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<super::TimerHandle> {
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)))
}
}
5 changes: 4 additions & 1 deletion src/platform/macos/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::fmt::Display;
#[derive(Debug)]
pub enum PlatformError {
Handler(HandlerError),
InvalidWindowContext,
#[cfg(feature = "opengl")]
GlError(super::gl::GlError),
}
Expand All @@ -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")
}
}
}
}
Expand All @@ -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,
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/platform/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod cursor;
mod damage;
mod error;
mod keyboard;
mod timer;
mod view;
mod waker;
mod window;
Expand All @@ -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::*;

Expand Down
70 changes: 70 additions & 0 deletions src/platform/macos/timer.rs
Original file line number Diff line number Diff line change
@@ -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<NSTimer>,
}

impl PartialEq for TimerHandle {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.timer == other.timer
}
}

pub struct TimerManager {
timers: RefCell<Vec<Retained<NSTimer>>>,
}

impl TimerManager {
pub fn new() -> Self {
TimerManager { timers: vec![].into() }
}

pub fn create_timer(&self, view: Weak<View<BaseviewView>>, 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<NSTimer>) {
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<View<BaseviewView>>, duration: Duration) -> Retained<NSTimer> {
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) }
}
Loading
Loading