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