beuvy_runtime/input/
clipboard.rs1use 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 stack.record("");
108 let undo = stack.undo("a").expect("undo should work");
110 assert_eq!(undo, "");
111 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}