worldwideportal/src/timer_host.rs

174 lines
5.1 KiB
Rust
Raw Normal View History

use anyhow::bail;
use piccolo::{Context, IntoValue, Table, Value};
use wasm_bindgen::{closure::Closure, JsCast};
use crate::{
command_handler::execute_queue,
parsing::{parse_commands, ParsedCommand},
FrameId, GlobalMemoCell,
};
pub struct TimerHost {
timers: Vec<TimerRecord>,
frame_id: FrameId,
next_id: u64,
}
impl Drop for TimerHost {
fn drop(&mut self) {
if let Some(window) = web_sys::window() {
for timer in &self.timers {
if timer.recurring {
window.clear_interval_with_handle(timer.js_id);
} else {
window.clear_timeout_with_handle(timer.js_id);
}
}
}
}
}
impl TimerHost {
pub fn new(frame_id: FrameId) -> Self {
Self {
timers: vec![],
frame_id,
next_id: 1,
}
}
pub fn add_record<AC>(
&mut self,
access_context: AC,
global: &GlobalMemoCell,
recurring: bool,
interval_millis: u32,
commands: String,
timer_name: Option<String>,
) -> anyhow::Result<String>
where
AC: TimerHostAccessContext + 'static,
{
let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?;
let new_id = self.next_id;
self.next_id += 1;
let global = global.clone();
if timer_name
.as_ref()
.map(|tn| tn.starts_with('#'))
.unwrap_or(false)
{
bail!(
"Timer name can't start with #, it is reserved for referencing timers by number."
);
}
let frame = self.frame_id.clone();
let callback_commands: Vec<ParsedCommand> = parse_commands(&commands).commands;
let callback: Closure<dyn FnMut()> = Closure::new(move || {
let mut cq = global.command_queue.borrow_mut();
for cmd in &callback_commands {
cq.push_back((frame.clone(), cmd.clone()));
}
drop(cq);
execute_queue(&global);
if !recurring {
access_context.with_ref(|timer_host| {
timer_host.timers.retain(|t| t.id != new_id);
});
}
});
let js_id = if recurring {
window
.set_interval_with_callback_and_timeout_and_arguments_0(
callback.as_ref().unchecked_ref(),
interval_millis as i32,
)
.map_err(|_| anyhow::Error::msg("setInterval failed"))?
} else {
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.as_ref().unchecked_ref(),
interval_millis as i32,
)
.map_err(|_| anyhow::Error::msg("setTimeout failed"))?
};
self.timers.push(TimerRecord {
id: new_id,
js_id,
timer_name,
recurring,
interval_millis,
commands,
retained_closure: callback,
});
Ok(format!("#{}", new_id))
}
pub fn remove_timer(&mut self, timer_name: String) -> anyhow::Result<()> {
let timer = if let Some(suffix) = timer_name.strip_prefix('#') {
let id = suffix
.parse::<u64>()
.map_err(|_| anyhow::Error::msg("Should follow # with a number"))?;
self.timers.iter().enumerate().find(|t| t.1.id == id)
} else {
self.timers
.iter()
.enumerate()
.find(|t| t.1.timer_name.as_ref() == Some(&timer_name))
};
let timer = timer.ok_or_else(|| anyhow::Error::msg("No matching timer found."))?;
let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?;
if timer.1.recurring {
window.clear_interval_with_handle(timer.1.js_id);
} else {
window.clear_timeout_with_handle(timer.1.js_id);
}
self.timers.remove(timer.0);
Ok(())
}
pub fn to_value<'gc>(&self, ctx: Context<'gc>) -> anyhow::Result<Value<'gc>> {
let tbl = Table::new(&ctx);
for timer in self.timers.iter() {
let timer_tbl = Table::new(&ctx);
timer_tbl.set(
ctx,
"name",
timer
.timer_name
.clone()
.unwrap_or_else(|| format!("#{}", timer.id)),
)?;
timer_tbl.set(ctx, "commands", timer.commands.clone())?;
timer_tbl.set(ctx, "recurring", timer.recurring)?;
timer_tbl.set(ctx, "interval_millis", timer.interval_millis)?;
tbl.set(ctx, timer.id as i64, timer_tbl)?;
}
Ok(tbl.into_value(ctx))
}
}
pub trait TimerHostAccessContext {
fn with_ref<F>(&self, f: F)
where
F: Fn(&mut TimerHost);
}
pub struct TimerRecord {
id: u64,
js_id: i32,
timer_name: Option<String>,
recurring: bool,
interval_millis: u32,
commands: String,
#[allow(dead_code)]
retained_closure: Closure<dyn FnMut()>,
}