Skip to main content

beuvy_runtime/input/
clipboard.rs

1use std::collections::VecDeque;
2
3const MAX_UNDO_DEPTH: usize = 100;
4
5pub(crate) struct InputClipboard {
6    inner: Option<arboard::Clipboard>,
7}
8
9impl InputClipboard {
10    pub fn new() -> Self {
11        Self {
12            inner: arboard::Clipboard::new().ok(),
13        }
14    }
15
16    pub fn get_text(&mut self) -> Option<String> {
17        self.inner.as_mut()?.get_text().ok()
18    }
19
20    pub fn set_text(&mut self, text: &str) {
21        if let Some(clipboard) = self.inner.as_mut() {
22            let _ = clipboard.set_text(text.to_string());
23        }
24    }
25}
26
27#[derive(Debug, Clone, Default)]
28pub struct UndoStack {
29    undo: VecDeque<String>,
30    redo: VecDeque<String>,
31    last_state: Option<String>,
32}
33
34impl UndoStack {
35    pub fn record(&mut self, text: &str) {
36        let text = text.to_string();
37        if self.last_state.as_deref() == Some(&text) {
38            return;
39        }
40        self.last_state = Some(text.clone());
41        self.redo.clear();
42        while self.undo.len() >= MAX_UNDO_DEPTH {
43            self.undo.pop_front();
44        }
45        self.undo.push_back(text);
46    }
47
48    pub fn undo(&mut self, current: &str) -> Option<String> {
49        let current = current.to_string();
50        let popped = self.undo.pop_back()?;
51        self.redo.push_front(current);
52        self.last_state = self.undo.back().cloned();
53        Some(popped)
54    }
55
56    pub fn redo(&mut self, current: &str) -> Option<String> {
57        let current = current.to_string();
58        if self.redo.is_empty() {
59            return None;
60        }
61        let next = self.redo.pop_front()?;
62        self.undo.push_back(current);
63        self.last_state = Some(next.clone());
64        Some(next)
65    }
66
67    #[allow(dead_code)]
68    pub fn clear(&mut self) {
69        self.undo.clear();
70        self.redo.clear();
71        self.last_state = None;
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn undo_restores_pre_edit_state_after_typing() {
81        let mut stack = UndoStack::default();
82        stack.record("");
83        let restored = stack.undo("a").expect("should restore pre-edit");
84        assert_eq!(restored, "");
85    }
86
87    #[test]
88    fn undo_restores_pre_edit_state_after_deleting_selection() {
89        let mut stack = UndoStack::default();
90        stack.record("hello");
91        let restored = stack.undo("").expect("should restore pre-delete");
92        assert_eq!(restored, "hello");
93    }
94
95    #[test]
96    fn undo_after_backspace_returns_previous_value() {
97        let mut stack = UndoStack::default();
98        stack.record("abc");
99        let restored = stack.undo("ab").expect("should restore pre-backspace");
100        assert_eq!(restored, "abc");
101    }
102
103    #[test]
104    fn redo_restores_undone_state() {
105        let mut stack = UndoStack::default();
106        // User at "" about to type "a"
107        stack.record("");
108        // Now at "a", press undo
109        let undo = stack.undo("a").expect("undo should work");
110        assert_eq!(undo, "");
111        // Now at "", press redo
112        let redo = stack.redo("").expect("redo should work");
113        assert_eq!(redo, "a");
114    }
115
116    #[test]
117    fn undo_and_redo_walk_multiple_edits() {
118        let mut stack = UndoStack::default();
119        stack.record("");
120        stack.record("a");
121
122        assert_eq!(stack.undo("ab").as_deref(), Some("a"));
123        assert_eq!(stack.undo("a").as_deref(), Some(""));
124        assert_eq!(stack.redo("").as_deref(), Some("a"));
125        assert_eq!(stack.redo("a").as_deref(), Some("ab"));
126    }
127
128    #[test]
129    fn undo_without_pre_record_returns_none() {
130        let mut stack = UndoStack::default();
131        assert!(stack.undo("current").is_none());
132    }
133}