1use std::{mem, sync::{Arc, Mutex}};
2use wasm_bindgen::{prelude::*, closure::Closure};
3use web_sys::{Window, Event};
4use std::sync::mpsc;
5use wasm_bindgen_futures::spawn_local;
6
7type Callback = Box<dyn FnMut() + Send>;
8type JsCallback = Closure<dyn FnMut(Event)>;
9
10const EVENTS: [&str; 6] = ["load", "mousedown", "mousemove", "keydown", "touchstart", "wheel"];
11
12struct JsContext {
14 event_handlers: Vec<(String, JsCallback)>,
15 window: Window,
16 is_initialized: bool,
17}
18
19impl JsContext {
20 fn new() -> Self {
21 Self {
22 event_handlers: Vec::new(),
23 window: web_sys::window().expect("should have a Window"),
24 is_initialized: false,
25 }
26 }
27
28 fn add_event_listener(&mut self, event_type: &str, callback: JsCallback) {
29 self.window
30 .add_event_listener_with_callback(event_type, callback.as_ref().unchecked_ref())
31 .expect("should add event listener");
32 self.event_handlers.push((event_type.to_string(), callback));
33 }
34
35 fn remove_all_listeners(&mut self) {
36 for (event_type, handler) in self.event_handlers.drain(..) {
37 self.window
38 .remove_event_listener_with_callback(
39 &event_type,
40 handler.as_ref().unchecked_ref()
41 )
42 .expect("should remove event listener");
43 }
44 }
45
46 fn clear_timeout(&self, timeout_id: i32) {
47 self.window.clear_timeout_with_handle(timeout_id);
48 }
49
50 fn set_timeout(&self, closure: &Closure<dyn FnMut()>, timeout: i32) -> Result<i32, JsValue> {
51 self.window.set_timeout_with_callback_and_timeout_and_arguments_0(
52 closure.as_ref().unchecked_ref(),
53 timeout,
54 )
55 }
56}
57
58impl Drop for JsContext {
59 fn drop(&mut self) {
60 self.remove_all_listeners();
61 }
62}
63
64#[derive(Default)]
66struct Context {
67 callbacks: Arc<Mutex<Vec<Callback>>>,
68}
69
70enum JsMessage {
71 ResetTimer(u32),
72 Cleanup,
73 ScrollDebounce(u32),
74}
75
76struct JsHandler {
77 context: JsContext,
78 receiver: mpsc::Receiver<JsMessage>,
79 sender: mpsc::Sender<JsMessage>,
80 current_timer: Option<i32>,
81 current_scroll_debounce_timer: Option<i32>,
82 exit_closure: Option<Closure<dyn FnMut()>>,
83 reset_closure: Option<Closure<dyn FnMut()>>,
84}
85
86impl JsHandler {
87 fn new(receiver: mpsc::Receiver<JsMessage>, sender: mpsc::Sender<JsMessage>) -> Self {
88 Self {
89 context: JsContext::new(),
90 receiver,
91 sender,
92 current_timer: None,
93 current_scroll_debounce_timer: None,
94 exit_closure: None,
95 reset_closure: None,
96 }
97 }
98
99 fn get_handler_sender(&self) -> mpsc::Sender<JsMessage> {
100 self.sender.clone()
101 }
102
103 async fn run(&mut self) {
104 while let Ok(msg) = self.receiver.recv() {
105 match msg {
106 JsMessage::ResetTimer(timeout) => self.handle_reset_timer(timeout),
107 JsMessage::Cleanup => self.handle_cleanup(),
108 JsMessage::ScrollDebounce(delay) => self.handle_scroll_debounce(delay),
109 }
110 }
111 }
112
113 fn handle_reset_timer(&mut self, timeout: u32) {
114 if let Some(timer_id) = self.current_timer.take() {
116 self.context.clear_timeout(timer_id);
117 }
118
119 if timeout == 0 {
121 return;
122 }
123
124 let (sender, oneshot_receiver) = mpsc::channel();
126
127 let exit_closure = Closure::once(move || {
129 let _ = sender.send(());
131 });
132
133 match self.context.set_timeout(&exit_closure, timeout as i32) {
135 Ok(timer_id) => {
136 self.current_timer = Some(timer_id);
137 self.exit_closure = Some(exit_closure);
138
139 let sender = self.get_handler_sender();
141 spawn_local(async move {
142 let _ = oneshot_receiver.recv();
144 let _ = sender.send(JsMessage::Cleanup);
146 });
147 },
148 Err(_) => {
149 drop(exit_closure);
151 }
152 }
153 }
154
155 fn handle_cleanup(&mut self) {
156 if let Some(timer_id) = self.current_timer.take() {
158 self.context.clear_timeout(timer_id);
159 }
160
161 if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
162 self.context.clear_timeout(timer_id);
163 }
164
165 self.context.remove_all_listeners();
167
168 self.exit_closure = None;
170 self.reset_closure = None;
171 }
172
173 fn handle_scroll_debounce(&mut self, delay: u32) {
174 if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
176 self.context.clear_timeout(timer_id);
177 }
178
179 let (sender, oneshot_receiver) = mpsc::channel();
181
182 let reset_closure = Closure::once(move || {
184 let _ = sender.send(());
186 });
187
188 match self.context.set_timeout(&reset_closure, delay as i32) {
190 Ok(timer_id) => {
191 self.current_scroll_debounce_timer = Some(timer_id);
192 self.reset_closure = Some(reset_closure);
193
194 let sender = self.get_handler_sender();
196 spawn_local(async move {
197 let _ = oneshot_receiver.recv();
199 let _ = sender.send(JsMessage::ResetTimer(0));
201 });
202 },
203 Err(_) => {
204 drop(reset_closure);
206 }
207 }
208 }
209}
210
211#[derive(Clone)]
215pub struct IdleManager {
216 context: Arc<Mutex<Context>>,
217 idle_timeout: u32,
218 js_sender: Arc<Mutex<mpsc::Sender<JsMessage>>>,
219}
220
221impl std::fmt::Debug for IdleManager {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct("IdleManager")
224 .field("idle_timeout", &self.idle_timeout)
225 .field("callbacks", &{
226 if let Ok(context) = self.context.lock() {
227 if let Ok(callbacks) = context.callbacks.lock() {
228 callbacks.len()
229 } else {
230 0
231 }
232 } else {
233 0
234 }
235 })
236 .field("js_sender", &"<mpsc channel>")
237 .finish()
238 }
239}
240
241impl Drop for IdleManager {
242 fn drop(&mut self) {
243 if let Ok(sender) = self.js_sender.lock() {
244 let _ = sender.send(JsMessage::Cleanup);
245 }
246 }
247}
248
249impl IdleManager {
250 const DEFAULT_IDLE_TIMEOUT: u32 = 10 * 60 * 1000;
252 const DEFAULT_SCROLL_DEBOUNCE: u32 = 100;
254
255 pub fn new(options: Option<IdleManagerOptions>) -> Self {
257 let callbacks = options
258 .as_ref()
259 .map(|options| options.on_idle.clone())
260 .unwrap_or_else(|| Arc::new(Mutex::new(Vec::new())));
261
262 let idle_timeout = options
263 .as_ref()
264 .and_then(|options| options.idle_timeout)
265 .unwrap_or(Self::DEFAULT_IDLE_TIMEOUT);
266
267 let (sender, receiver) = mpsc::channel();
268 let js_sender = Arc::new(Mutex::new(sender.clone()));
269
270 let handler_receiver = receiver;
272 let handler_sender = sender;
273 spawn_local(async move {
274 let mut handler = JsHandler::new(handler_receiver, handler_sender);
275 handler.run().await;
276 });
277
278 let instance = Self {
279 context: Arc::new(Mutex::new(
280 Context {
281 callbacks,
282 }
283 )),
284 idle_timeout,
285 js_sender,
286 };
287
288 instance.initialize_event_listeners(&options);
289 instance.reset_timer();
290 instance
291 }
292
293 fn initialize_event_listeners(&self, options: &Option<IdleManagerOptions>) {
294 let mut js_context = JsContext::new();
296
297 if js_context.is_initialized {
298 return;
299 }
300
301 for event_type in EVENTS.iter() {
302 let sender = self.js_sender.clone();
303 let callback = Closure::wrap(Box::new(move |_: Event| {
304 if let Ok(sender) = sender.lock() {
305 let _ = sender.send(JsMessage::ResetTimer(0));
306 }
307 }) as Box<dyn FnMut(Event)>);
308
309 js_context.add_event_listener(event_type, callback);
310 }
311
312 if let Some(true) = options.as_ref().and_then(|options| options.capture_scroll) {
313 let sender = self.js_sender.clone();
314 let scroll_debounce = options.as_ref().and_then(|options| options.scroll_debounce)
315 .unwrap_or(Self::DEFAULT_SCROLL_DEBOUNCE);
316
317 let callback = Closure::wrap(Box::new(move |_: Event| {
318 if let Ok(sender) = sender.lock() {
319 let _ = sender.send(JsMessage::ScrollDebounce(scroll_debounce));
320 }
321 }) as Box<dyn FnMut(Event)>);
322
323 js_context.add_event_listener("scroll", callback);
324 }
325
326 js_context.is_initialized = true;
327
328 }
331
332 pub fn register_callback<F>(&self, callback: F)
334 where
335 F: FnMut() + Send + 'static,
336 {
337 self.context.lock().unwrap().callbacks.lock().unwrap().push(Box::new(callback));
338 }
339
340 pub fn exit(&mut self) {
342 if let Ok(sender) = self.js_sender.lock() {
344 let _ = sender.send(JsMessage::Cleanup);
345 }
346
347 let context = self.context.lock().unwrap();
349 for callback in context.callbacks.lock().unwrap().iter_mut() {
350 (callback)();
351 }
352 }
353
354 fn reset_timer(&self) {
356 if let Ok(sender) = self.js_sender.lock() {
357 let _ = sender.send(JsMessage::ResetTimer(self.idle_timeout));
358 }
359 }
360
361
362}
363
364#[derive(Default, Clone)]
366pub struct IdleManagerOptions {
367 pub on_idle: Arc<Mutex<Vec<Callback>>>,
369 pub idle_timeout: Option<u32>,
371 pub capture_scroll: Option<bool>,
373 pub scroll_debounce: Option<u32>,
375}
376
377impl std::fmt::Debug for IdleManagerOptions {
378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379 let callback_count = self.on_idle.lock().unwrap().len();
380 f.debug_struct("IdleManagerOptions")
381 .field("on_idle", &format!("{} callbacks", callback_count))
382 .field("idle_timeout", &self.idle_timeout)
383 .field("capture_scroll", &self.capture_scroll)
384 .field("scroll_debounce", &self.scroll_debounce)
385 .finish()
386 }
387}
388
389impl IdleManagerOptions {
390 pub fn builder() -> IdleManagerOptionsBuilder {
392 IdleManagerOptionsBuilder::default()
393 }
394}
395
396#[derive(Default)]
398pub struct IdleManagerOptionsBuilder {
399 on_idle: Vec<Callback>,
400 idle_timeout: Option<u32>,
401 capture_scroll: Option<bool>,
402 scroll_debounce: Option<u32>,
403}
404
405impl IdleManagerOptionsBuilder {
406 pub fn on_idle(&mut self, on_idle: fn()) -> &mut Self {
408 self.on_idle.push(Box::new(on_idle) as Box<dyn FnMut() + Send>);
409 self
410 }
411
412 pub fn idle_timeout(&mut self, idle_timeout: u32) -> &mut Self {
414 self.idle_timeout = Some(idle_timeout);
415 self
416 }
417
418 pub fn capture_scroll(&mut self, capture_scroll: bool) -> &mut Self {
420 self.capture_scroll = Some(capture_scroll);
421 self
422 }
423
424 pub fn scroll_debounce(&mut self, scroll_debounce: u32) -> &mut Self {
426 self.scroll_debounce = Some(scroll_debounce);
427 self
428 }
429
430 pub fn build(&mut self) -> IdleManagerOptions {
432 IdleManagerOptions {
433 on_idle: Arc::new(Mutex::new(mem::take(&mut self.on_idle))),
434 idle_timeout: self.idle_timeout,
435 capture_scroll: self.capture_scroll,
436 scroll_debounce: self.scroll_debounce,
437 }
438 }
439}
440
441#[allow(dead_code)]
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use wasm_bindgen_test::*;
446 use crate::util::sleep::sleep;
447
448 wasm_bindgen_test_configure!(run_in_browser);
449
450 #[wasm_bindgen_test]
451 async fn test_idle_manager() {
452 let options = IdleManagerOptions::builder()
453 .idle_timeout(500)
454 .build();
455
456 let idle_manager = IdleManager::new(Some(options));
457
458 let callback = Arc::new(Mutex::new(false));
459 let callback_clone = callback.clone();
460 idle_manager.register_callback(move || {
461 *callback_clone.lock().unwrap() = true;
462 });
463
464 assert!(!*callback.lock().unwrap());
465
466 sleep(2000).await;
468
469 assert!(*callback.lock().unwrap());
470 }
471
472 #[wasm_bindgen_test]
473 async fn test_idle_manager_with_reset_timer() {
474 let options = IdleManagerOptions::builder()
475 .idle_timeout(1000)
476 .build();
477
478 let idle_manager = IdleManager::new(Some(options));
479
480 let callback = Arc::new(Mutex::new(false));
481 let callback_clone = callback.clone();
482 idle_manager.register_callback(move || {
483 *callback_clone.lock().unwrap() = true;
484 });
485
486 assert!(!*callback.lock().unwrap());
487
488 sleep(500).await;
489
490 let window = web_sys::window().unwrap();
492 let event = window.document().unwrap().create_event("Event").unwrap();
493 event.init_event("mousemove");
494 window.dispatch_event(&event).unwrap();
495
496 sleep(700).await;
497
498 assert!(!*callback.lock().unwrap());
499
500 sleep(500).await;
502
503 assert!(*callback.lock().unwrap());
504 }
505
506 #[wasm_bindgen_test]
507 async fn test_idle_manager_with_scroll_debounce_1() {
508 let options = IdleManagerOptions::builder()
509 .idle_timeout(1000)
510 .capture_scroll(true)
511 .scroll_debounce(500)
512 .build();
513
514 let idle_manager = IdleManager::new(Some(options));
515
516 let callback = Arc::new(Mutex::new(false));
517 let callback_clone = callback.clone();
518 idle_manager.register_callback(move || {
519 *callback_clone.lock().unwrap() = true;
520 });
521
522 assert!(!*callback.lock().unwrap());
523
524 let window = web_sys::window().unwrap();
525 let event = window.document().unwrap().create_event("Event").unwrap();
526 event.init_event("scroll");
527
528 for _ in 0..7 {
529 sleep(200).await;
530 window.dispatch_event(&event).unwrap();
531 }
532
533 assert!(*callback.lock().unwrap());
534 }
535
536 #[wasm_bindgen_test]
537 async fn test_idle_manager_with_scroll_debounce_2() {
538 let options = IdleManagerOptions::builder()
539 .idle_timeout(1000)
540 .capture_scroll(true)
541 .scroll_debounce(500)
542 .build();
543
544 let idle_manager = IdleManager::new(Some(options));
545
546 let callback = Arc::new(Mutex::new(false));
547 let callback_clone = callback.clone();
548 idle_manager.register_callback(move || {
549 *callback_clone.lock().unwrap() = true;
550 });
551
552 let window = web_sys::window().unwrap();
553 let event = window.document().unwrap().create_event("Event").unwrap();
554 event.init_event("scroll");
555 window.dispatch_event(&event).unwrap();
556
557 assert!(!*callback.lock().unwrap());
558
559 sleep(1200).await;
560
561 assert!(!*callback.lock().unwrap());
562
563 sleep(700).await;
564
565 assert!(*callback.lock().unwrap());
566 }
567}