Skip to main content

pptx_edit/
undo.rs

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/// Policy for grouping tracked local transactions.
12#[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}