use std::{borrow::BorrowMut, cell::RefCell, mem, rc::Rc};
use gloo_utils::window;
use gloo_timers::callback::Timeout;
use gloo_events::EventListener;
type Callback = Box<dyn FnMut()>;
const EVENTS: [&str; 6] = ["load", "mousedown", "mousemove", "keydown", "touchstart", "wheel"];
#[derive(Clone)]
pub struct IdleManager {
callbacks: Rc<RefCell<Vec<Callback>>>,
idle_timeout: u32,
timeout: Rc<RefCell<Option<Timeout>>>,
scroll_debounce_timeout: Rc<RefCell<Option<Timeout>>>,
event_handlers: Rc<RefCell<Vec<EventListener>>>,
}
impl IdleManager {
const DEFAULT_IDLE_TIMEOUT: u32 = 10 * 60 * 1000;
const DEFAULT_SCROLL_DEBOUNCE: u32 = 100;
pub fn new(options: Option<IdleManagerOptions>) -> Self {
let callbacks = options
.as_ref()
.and_then(|options| options.on_idle.clone().borrow_mut().take())
.map_or_else(Vec::new, |callback| vec![callback]);
let idle_timeout = options
.as_ref()
.and_then(|options| options.idle_timeout)
.unwrap_or(Self::DEFAULT_IDLE_TIMEOUT);
let mut instance = Self {
callbacks: Rc::new(RefCell::new(callbacks)),
idle_timeout,
timeout: Rc::new(RefCell::new(None)),
scroll_debounce_timeout: Rc::new(RefCell::new(None)),
event_handlers: Rc::new(RefCell::new(Vec::new())),
};
EVENTS.iter().for_each(|event| {
let mut instance_clone = instance.clone();
let listener = EventListener::new(&window(), *event, move |_| instance_clone.reset_timer());
instance.event_handlers.as_ref().borrow_mut().push(listener);
});
if let Some(true) = options.as_ref().and_then(|options| options.capture_scroll) {
let mut instance_clone = instance.clone();
let listener = EventListener::new(&window(), "scroll", move |_| instance_clone.scroll_debounce(&options));
instance.event_handlers.as_ref().borrow_mut().push(listener);
}
instance.reset_timer();
instance
}
pub fn register_callback<F>(&self, callback: F)
where
F: FnMut() + 'static,
{
self.callbacks.as_ref().borrow_mut().push(Box::new(callback));
}
pub fn exit(&mut self) {
if let Some(timeout) = self.timeout.borrow_mut().take() {
timeout.cancel();
}
self.event_handlers.as_ref().borrow_mut().clear();
let mut callbacks = self.callbacks.as_ref().borrow_mut();
for callback in callbacks.iter_mut() {
(callback)();
}
}
fn reset_timer(&mut self) {
if let Some(timeout) = self.timeout.borrow_mut().take() {
timeout.cancel();
}
let mut self_clone = self.clone();
self.timeout.borrow_mut().replace(
Some(Timeout::new(
self.idle_timeout,
move || self_clone.exit()
))
);
}
fn scroll_debounce(&mut self, options: &Option<IdleManagerOptions>) {
let delay = options
.as_ref()
.and_then(|options| options.scroll_debounce)
.unwrap_or(Self::DEFAULT_SCROLL_DEBOUNCE);
let mut self_clone = self.clone();
if let Some(timeout) = self.scroll_debounce_timeout.borrow_mut().replace(
Some(Timeout::new(
delay,
move || self_clone.reset_timer()
))
) {
timeout.cancel();
};
}
}
#[derive(Default, Clone)]
pub struct IdleManagerOptions {
pub on_idle: Rc<RefCell<Option<Callback>>>,
pub idle_timeout: Option<u32>,
pub capture_scroll: Option<bool>,
pub scroll_debounce: Option<u32>,
}
impl IdleManagerOptions {
pub fn builder() -> IdleManagerOptionsBuilder {
IdleManagerOptionsBuilder::default()
}
}
#[derive(Default)]
pub struct IdleManagerOptionsBuilder {
on_idle: Option<Callback>,
idle_timeout: Option<u32>,
capture_scroll: Option<bool>,
scroll_debounce: Option<u32>,
}
impl IdleManagerOptionsBuilder {
pub fn on_idle(&mut self, on_idle: fn()) -> &mut Self {
self.on_idle = Some(Box::new(on_idle) as Box<dyn FnMut()>);
self
}
pub fn idle_timeout(&mut self, idle_timeout: u32) -> &mut Self {
self.idle_timeout = Some(idle_timeout);
self
}
pub fn capture_scroll(&mut self, capture_scroll: bool) -> &mut Self {
self.capture_scroll = Some(capture_scroll);
self
}
pub fn scroll_debounce(&mut self, scroll_debounce: u32) -> &mut Self {
self.scroll_debounce = Some(scroll_debounce);
self
}
pub fn build(&mut self) -> IdleManagerOptions {
IdleManagerOptions {
on_idle: Rc::new(RefCell::new(mem::take(&mut self.on_idle))),
idle_timeout: self.idle_timeout,
capture_scroll: self.capture_scroll,
scroll_debounce: self.scroll_debounce,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_idle_manager() {
let options = IdleManagerOptions::builder()
.idle_timeout(500)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Rc::new(RefCell::new(false));
let mut callback_clone = callback.clone();
idle_manager.register_callback(move || {
callback_clone.borrow_mut().replace(true);
});
assert!(!*callback.borrow());
wasm_timer::Delay::new(std::time::Duration::from_millis(2000)).await.unwrap();
assert!(*callback.borrow());
}
#[wasm_bindgen_test]
async fn test_idle_manager_with_reset_timer() {
let options = IdleManagerOptions::builder()
.idle_timeout(1000)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Rc::new(RefCell::new(false));
let mut callback_clone = callback.clone();
idle_manager.register_callback(move || {
callback_clone.borrow_mut().replace(true);
});
assert!(!*callback.borrow());
wasm_timer::Delay::new(std::time::Duration::from_millis(500)).await.unwrap();
let window = web_sys::window().unwrap();
let event = window.document().unwrap().create_event("Event").unwrap();
event.init_event("mousemove");
window.dispatch_event(&event).unwrap();
wasm_timer::Delay::new(std::time::Duration::from_millis(700)).await.unwrap();
assert!(!*callback.borrow());
wasm_timer::Delay::new(std::time::Duration::from_millis(500)).await.unwrap();
assert!(*callback.borrow());
}
#[wasm_bindgen_test]
async fn test_idle_manager_with_scroll_debounce_1() {
let options = IdleManagerOptions::builder()
.idle_timeout(1000)
.capture_scroll(true)
.scroll_debounce(500)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Rc::new(RefCell::new(false));
let mut callback_clone = callback.clone();
idle_manager.register_callback(move || {
callback_clone.borrow_mut().replace(true);
});
assert!(!*callback.borrow());
let window = window();
let event = window.document().unwrap().create_event("Event").unwrap();
event.init_event("scroll");
for _ in 0..7 {
wasm_timer::Delay::new(std::time::Duration::from_millis(200)).await.unwrap();
window.dispatch_event(&event).unwrap();
}
assert!(*callback.borrow());
}
#[wasm_bindgen_test]
async fn test_idle_manager_with_scroll_debounce_2() {
let options = IdleManagerOptions::builder()
.idle_timeout(1000)
.capture_scroll(true)
.scroll_debounce(500)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Rc::new(RefCell::new(false));
let mut callback_clone = callback.clone();
idle_manager.register_callback(move || {
callback_clone.borrow_mut().replace(true);
});
let window = window();
let event = window.document().unwrap().create_event("Event").unwrap();
event.init_event("scroll");
window.dispatch_event(&event).unwrap();
assert!(!*callback.borrow());
wasm_timer::Delay::new(std::time::Duration::from_millis(1200)).await.unwrap();
assert!(!*callback.borrow());
wasm_timer::Delay::new(std::time::Duration::from_millis(700)).await.unwrap();
assert!(*callback.borrow());
}
}