1use std::collections::HashSet;
2use std::sync::{Arc, Mutex};
3
4use yrs::sync::time::Clock;
5use yrs::{Doc, Origin, ReadTxn, Transact};
6
7use crate::{COMMENTS, EditError, EditResult, META, SHAPES, SLIDE_ORDER, SLIDES, STORIES};
8
9const CAPTURE_TIMEOUT_MS: u64 = 500;
10
11#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13pub enum UndoCaptureMode {
14 #[default]
15 Auto,
16 Manual,
17}
18
19struct CaptureClock {
20 source: Arc<dyn Clock>,
21 state: Mutex<CaptureClockState>,
22}
23
24struct CaptureClockState {
25 mode: UndoCaptureMode,
26 ticks: u64,
27 last_source: u64,
28}
29
30impl CaptureClock {
31 fn new(source: Arc<dyn Clock>) -> Self {
32 Self {
33 state: Mutex::new(CaptureClockState {
34 mode: UndoCaptureMode::Auto,
35 ticks: 1,
36 last_source: source.now(),
37 }),
38 source,
39 }
40 }
41
42 fn mode(&self) -> UndoCaptureMode {
43 self.state.lock().unwrap().mode
44 }
45
46 fn set_mode(&self, mode: UndoCaptureMode) {
47 self.state.lock().unwrap().mode = mode;
48 }
49}
50
51impl Clock for CaptureClock {
52 fn now(&self) -> u64 {
53 let mut state = self.state.lock().unwrap();
54 let now = self.source.now();
55 let elapsed = match state.mode {
56 UndoCaptureMode::Auto => now.saturating_sub(state.last_source),
57 UndoCaptureMode::Manual => 0,
58 };
59 state.last_source = now;
60 state.ticks = state.ticks.saturating_add(elapsed);
61 state.ticks
62 }
63}
64
65pub struct DeckUndoManager {
66 clock: Arc<CaptureClock>,
67 inner: yrs::undo::UndoManager<()>,
68}
69
70impl DeckUndoManager {
71 pub(crate) fn new(doc: &Doc, client_id: u64) -> EditResult<Self> {
72 let txn = doc.transact();
73 let order = txn
74 .get_array(SLIDE_ORDER)
75 .ok_or_else(|| EditError::InvalidState("missing slide order".to_owned()))?;
76 let slides = txn
77 .get_map(SLIDES)
78 .ok_or_else(|| EditError::InvalidState("missing slides map".to_owned()))?;
79 let shapes = txn
80 .get_map(SHAPES)
81 .ok_or_else(|| EditError::InvalidState("missing shapes map".to_owned()))?;
82 let stories = txn
83 .get_map(STORIES)
84 .ok_or_else(|| EditError::InvalidState("missing stories map".to_owned()))?;
85 let comments = txn
86 .get_map(COMMENTS)
87 .ok_or_else(|| EditError::InvalidState("missing comments map".to_owned()))?;
88 let meta = txn
89 .get_map(META)
90 .ok_or_else(|| EditError::InvalidState("missing meta map".to_owned()))?;
91 drop(txn);
92 let clock = Arc::new(CaptureClock::new(default_clock()));
93 let options = yrs::undo::Options {
94 capture_timeout_millis: CAPTURE_TIMEOUT_MS,
95 tracked_origins: HashSet::from([Origin::from(client_id)]),
96 capture_transaction: None,
97 timestamp: clock.clone(),
98 init_undo_stack: Vec::new(),
99 init_redo_stack: Vec::new(),
100 };
101 let mut inner = yrs::undo::UndoManager::with_options(options);
102 inner.expand_scope(doc, &order);
103 inner.expand_scope(doc, &slides);
104 inner.expand_scope(doc, &shapes);
105 inner.expand_scope(doc, &stories);
106 inner.expand_scope(doc, &comments);
107 inner.expand_scope(doc, &meta);
108 Ok(Self { inner, clock })
109 }
110
111 pub fn capture_mode(&self) -> UndoCaptureMode {
112 self.clock.mode()
113 }
114
115 pub fn set_capture_mode(&mut self, mode: UndoCaptureMode) {
116 if self.capture_mode() != mode {
117 self.add_undo_barrier();
118 self.clock.set_mode(mode);
119 }
120 }
121
122 pub fn undo(&mut self) -> bool {
123 self.inner.undo_blocking()
124 }
125
126 pub fn redo(&mut self) -> bool {
127 self.inner.redo_blocking()
128 }
129
130 pub fn can_undo(&self) -> bool {
131 self.inner.can_undo()
132 }
133
134 pub fn can_redo(&self) -> bool {
135 self.inner.can_redo()
136 }
137
138 pub fn add_undo_barrier(&mut self) {
139 self.inner.reset();
140 }
141}
142
143fn default_clock() -> Arc<dyn Clock> {
144 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
145 {
146 Arc::new(|| {
147 std::time::SystemTime::now()
148 .duration_since(std::time::UNIX_EPOCH)
149 .map(|elapsed| elapsed.as_millis() as u64)
150 .unwrap_or_default()
151 })
152 }
153 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
154 {
155 use std::sync::atomic::{AtomicU64, Ordering};
156 let ticks = AtomicU64::new(0);
157 Arc::new(move || ticks.fetch_add(CAPTURE_TIMEOUT_MS + 1, Ordering::Relaxed))
158 }
159}