use crate::platform::time::Instant;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::platform::WindowRequest;
use crate::platform::wake;
type Update<S> = Box<dyn FnOnce(&mut S) + Send>;
type Tick<S> = Box<dyn FnMut(&mut S) + Send>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TimerId(u64);
struct Timer<S> {
id: TimerId,
due: Instant,
every: Option<Duration>,
tick: Tick<S>,
}
struct Shared<S> {
updates: Vec<Update<S>>,
timers: Vec<Timer<S>>,
next_id: u64,
exit: bool,
window: Vec<WindowRequest>,
}
pub struct AppHandle<S> {
shared: Arc<Mutex<Shared<S>>>,
}
impl<S> Clone for AppHandle<S> {
fn clone(&self) -> Self {
Self {
shared: self.shared.clone(),
}
}
}
impl<S> std::fmt::Debug for AppHandle<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppHandle").finish_non_exhaustive()
}
}
impl<S> Default for AppHandle<S> {
fn default() -> Self {
Self::new()
}
}
impl<S> AppHandle<S> {
pub fn new() -> Self {
Self {
shared: Arc::new(Mutex::new(Shared {
updates: Vec::new(),
timers: Vec::new(),
next_id: 1,
exit: false,
window: Vec::new(),
})),
}
}
fn with<R>(&self, f: impl FnOnce(&mut Shared<S>) -> R) -> R {
let mut shared = self.shared.lock().unwrap_or_else(|e| e.into_inner());
f(&mut shared)
}
pub fn update(&self, f: impl FnOnce(&mut S) + Send + 'static) {
self.with(|s| s.updates.push(Box::new(f)));
wake::wake();
}
pub fn after(&self, delay: Duration, f: impl FnOnce(&mut S) + Send + 'static) -> TimerId {
let mut f = Some(f);
self.add_timer(delay, None, move |s| {
if let Some(f) = f.take() {
f(s);
}
})
}
pub fn every(&self, interval: Duration, f: impl FnMut(&mut S) + Send + 'static) -> TimerId {
self.add_timer(interval, Some(interval), f)
}
fn add_timer(
&self,
delay: Duration,
every: Option<Duration>,
f: impl FnMut(&mut S) + Send + 'static,
) -> TimerId {
let id = self.with(|s| {
let id = TimerId(s.next_id);
s.next_id += 1;
s.timers.push(Timer {
id,
due: Instant::now() + delay,
every,
tick: Box::new(f),
});
id
});
wake::wake(); id
}
pub fn cancel(&self, id: TimerId) {
self.with(|s| s.timers.retain(|t| t.id != id));
}
pub fn exit(&self) {
self.with(|s| s.exit = true);
wake::wake();
}
pub fn window(&self, request: WindowRequest) {
self.with(|s| s.window.push(request));
wake::wake();
}
pub fn open_url(&self, url: &str) -> Result<(), crate::platform::PlatformError> {
crate::platform::open_url(url)
}
pub fn notify(
&self,
notification: &crate::platform::Notification,
) -> Result<(), crate::platform::PlatformError> {
crate::platform::notify(notification)
}
pub(crate) fn take_window_requests(&self) -> Vec<WindowRequest> {
self.with(|s| std::mem::take(&mut s.window))
}
pub(crate) fn pump(&self, state: &mut S, now: Instant) -> (bool, Option<Instant>) {
let mut ran = false;
let updates = self.with(|s| std::mem::take(&mut s.updates));
for update in updates {
update(state);
ran = true;
}
let due: Vec<Timer<S>> = self.with(|s| {
let (due, later) = std::mem::take(&mut s.timers)
.into_iter()
.partition(|t| t.due <= now);
s.timers = later;
due
});
for mut timer in due {
(timer.tick)(state);
ran = true;
if let Some(every) = timer.every {
let mut next = timer.due + every;
if next <= now {
next = now + every;
}
timer.due = next;
self.with(|s| s.timers.push(timer));
}
}
let next = self.with(|s| s.timers.iter().map(|t| t.due).min());
(ran, next)
}
pub(crate) fn exit_requested(&self) -> bool {
self.with(|s| s.exit)
}
}
#[cfg(all(feature = "tokio", not(target_arch = "wasm32")))]
impl<S> AppHandle<S> {
pub fn spawn<F>(
&self,
future: F,
apply: impl FnOnce(&mut S, F::Output) + Send + 'static,
) -> tokio::task::JoinHandle<()>
where
F: std::future::Future + Send + 'static,
F::Output: Send + 'static,
S: 'static,
{
let handle = self.clone();
runtime().spawn(async move {
let output = future.await;
handle.update(move |s| apply(s, output));
})
}
}
#[cfg(all(feature = "tokio", not(target_arch = "wasm32")))]
pub fn runtime() -> &'static tokio::runtime::Runtime {
static RUNTIME: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.thread_name("stipple-tokio")
.enable_all()
.build()
.expect("start the tokio runtime")
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn updates_run_in_order_on_pump() {
let h = AppHandle::<Vec<u32>>::new();
h.update(|s| s.push(1));
h.update(|s| s.push(2));
let mut state = Vec::new();
let (ran, next) = h.pump(&mut state, Instant::now());
assert!(ran);
assert_eq!(state, [1, 2]);
assert_eq!(next, None);
assert!(!h.pump(&mut state, Instant::now()).0, "updates run once");
}
#[test]
fn timers_fire_when_due_and_repeat() {
let h = AppHandle::<u32>::new();
let start = Instant::now();
h.after(Duration::from_millis(10), |s| *s += 100);
let tick = h.every(Duration::from_millis(5), |s| *s += 1);
let mut n = 0;
let (_, next) = h.pump(&mut n, start);
assert_eq!(n, 0, "nothing due yet");
assert!(next.is_some());
h.pump(&mut n, start + Duration::from_millis(6));
assert_eq!(n, 1);
h.pump(&mut n, start + Duration::from_millis(12));
assert_eq!(n, 102, "the repeat and the one-shot");
h.cancel(tick);
h.pump(&mut n, start + Duration::from_secs(1));
assert_eq!(n, 102, "cancelled, and the one-shot is gone");
assert_eq!(h.pump(&mut n, start + Duration::from_secs(2)).1, None);
}
#[test]
fn updates_can_use_the_handle() {
let h = AppHandle::<u32>::new();
let again = h.clone();
h.update(move |s| {
*s += 1;
again.update(|s| *s += 10); });
let mut n = 0;
h.pump(&mut n, Instant::now());
assert_eq!(n, 1);
h.pump(&mut n, Instant::now());
assert_eq!(n, 11);
}
#[test]
fn handles_cross_threads() {
let h = AppHandle::<u32>::new();
let remote = h.clone();
std::thread::spawn(move || remote.update(|s| *s = 7))
.join()
.unwrap();
let mut n = 0;
h.pump(&mut n, Instant::now());
assert_eq!(n, 7);
}
#[cfg(all(feature = "tokio", not(target_arch = "wasm32")))]
#[test]
fn spawn_applies_the_output() {
let h = AppHandle::<u32>::new();
let task = h.spawn(async { 21 * 2 }, |s, v| *s = v);
runtime().block_on(task).unwrap();
let mut n = 0;
h.pump(&mut n, Instant::now());
assert_eq!(n, 42);
}
}