Skip to main content

incular_runtime/
undo.rs

1//! Retained undo/redo state for editable text.
2//!
3//! The controller owns history independently from any widget element. It
4//! observes the authoritative `TextEditingController`, stores complete
5//! editing values (including selection), and exposes availability through the
6//! runtime's `Signal` type so only consumers that read history state rebuild.
7//!
8//! History policy — what enters the stacks:
9//! Undoable: observed committed text transitions, including typing,
10//! deletion, cut/paste, programmatic sets and committed IME composition.
11//! Historical notification arguments are not authoritative current state;
12//! reentrant edits before this observer runs can coalesce intermediate values.
13//! Transient, never a step: preedit set/clear, selection and caret moves,
14//! and the composing marker. A cancelled composition leaves no trace, and
15//! any committed value change clears the transient overlay so preedit never
16//! describes a stale value. Material installs Widgets formatters through
17//! retained subscriptions; Text applies them before committing or notifying
18//! history, so rejected intermediate values are not recorded.
19//! Redo clears on the next committed text change, including after an undo.
20//! Controller replacement starts fresh: the old history drops with its
21//! listener and the new controller tracks from its current value.
22//! Restoration is a baseline, not an edit: binding rebaselines current and
23//! clears both stacks instead of recording an undo step.
24//! Reentrant edits made by listeners during notification commit normally —
25//! newer wins and snapshots are never restored over them; edits made while
26//! an undo/redo application is dispatching are absorbed into current without
27//! creating steps, so current always tracks the editor.
28
29use crate::Signal;
30use incular_text::{TextEditingController, TextEditingValue};
31use std::{
32    cell::{Cell, RefCell},
33    rc::{Rc, Weak},
34};
35
36/// Observable availability state for an [`UndoHistoryController`].
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct UndoHistoryState {
39    pub can_undo: bool,
40    pub can_redo: bool,
41}
42
43struct History {
44    current: TextEditingValue,
45    undo: Vec<TextEditingValue>,
46    redo: Vec<TextEditingValue>,
47    max_entries: usize,
48}
49
50const DEFAULT_MAX_ENTRIES: usize = 100;
51
52fn trim_history(entries: &mut Vec<TextEditingValue>, max_entries: usize) {
53    if entries.len() > max_entries {
54        let remove = entries.len() - max_entries;
55        entries.drain(..remove);
56    }
57}
58
59struct Inner {
60    editor: TextEditingController,
61    history: RefCell<History>,
62    applying: Cell<bool>,
63    restoration_generation: Cell<u64>,
64    state: Signal<UndoHistoryState>,
65    listener: usize,
66}
67
68impl Inner {
69    fn availability(&self) -> UndoHistoryState {
70        let history = self.history.borrow();
71        UndoHistoryState {
72            can_undo: !history.undo.is_empty(),
73            can_redo: !history.redo.is_empty(),
74        }
75    }
76
77    fn sync_state(&self) {
78        let _ = self.state.set(self.availability());
79    }
80
81    fn record(&self, value: TextEditingValue) {
82        // Restoration is a baseline, not an edit: rebaseline instead of
83        // recording an undo step when the generation changed.
84        let generation = self.editor.restoration_generation();
85        if generation != self.restoration_generation.get() {
86            self.restoration_generation.set(generation);
87            let mut history = self.history.borrow_mut();
88            history.current = value;
89            history.undo.clear();
90            history.redo.clear();
91            drop(history);
92            self.sync_state();
93            return;
94        }
95        if self.applying.get() {
96            self.history.borrow_mut().current = value;
97            return;
98        }
99
100        let changed = {
101            let mut history = self.history.borrow_mut();
102            if history.current == value {
103                false
104            } else if history.current.text == value.text {
105                // Selection/caret changes are part of the current editing
106                // state, but should not create an undo step by themselves.
107                history.current = value;
108                false
109            } else {
110                let previous = std::mem::replace(&mut history.current, value);
111                if history.max_entries > 0 {
112                    history.undo.push(previous);
113                    let max_entries = history.max_entries;
114                    trim_history(&mut history.undo, max_entries);
115                }
116                history.redo.clear();
117                true
118            }
119        };
120        if changed {
121            self.sync_state();
122        }
123    }
124
125    fn apply_history_value(&self, value: TextEditingValue) {
126        struct RestorePrevious<'a> {
127            inner: &'a Inner,
128            previous: bool,
129        }
130
131        impl Drop for RestorePrevious<'_> {
132            fn drop(&mut self) {
133                self.inner.history.borrow_mut().current = self.inner.editor.value();
134                self.inner.applying.set(self.previous);
135                if !std::thread::panicking() {
136                    self.inner.sync_state();
137                }
138            }
139        }
140
141        let previous = self.applying.replace(true);
142        let _guard = RestorePrevious {
143            inner: self,
144            previous,
145        };
146        self.editor.set_value(value);
147    }
148}
149
150impl Drop for Inner {
151    fn drop(&mut self) {
152        self.editor.remove_listener(self.listener);
153    }
154}
155
156/// A cloneable, widget-independent undo/redo controller for text editing.
157#[derive(Clone)]
158pub struct UndoHistoryController {
159    inner: Rc<Inner>,
160}
161
162impl std::fmt::Debug for UndoHistoryController {
163    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        formatter
165            .debug_struct("UndoHistoryController")
166            .field("editor", &self.inner.editor)
167            .field("state", &self.status())
168            .finish()
169    }
170}
171
172impl UndoHistoryController {
173    /// Starts tracking an existing editor without adding its initial value to
174    /// history.
175    #[must_use]
176    pub fn new(editor: TextEditingController) -> Self {
177        let initial = editor.value();
178        let inner = Rc::new_cyclic(|weak: &Weak<Inner>| {
179            let listener = {
180                let weak = weak.clone();
181                editor.add_listener(move |_| {
182                    if let Some(inner) = weak.upgrade() {
183                        inner.record(inner.editor.value());
184                    }
185                })
186            };
187            Inner {
188                editor: editor.clone(),
189                history: RefCell::new(History {
190                    current: initial.clone(),
191                    undo: Vec::new(),
192                    redo: Vec::new(),
193                    max_entries: DEFAULT_MAX_ENTRIES,
194                }),
195                applying: Cell::new(false),
196                restoration_generation: Cell::new(editor.restoration_generation()),
197                state: Signal::new(UndoHistoryState::default()),
198                listener,
199            }
200        });
201        Self { inner }
202    }
203
204    #[must_use]
205    pub fn editor(&self) -> TextEditingController {
206        self.inner.editor.clone()
207    }
208
209    /// Returns the signal used by reactive consumers to observe availability.
210    #[must_use]
211    pub fn state_signal(&self) -> Signal<UndoHistoryState> {
212        self.inner.state.clone()
213    }
214
215    /// Short alias for [`Self::state_signal`].
216    #[must_use]
217    pub fn signal(&self) -> Signal<UndoHistoryState> {
218        self.state_signal()
219    }
220
221    #[must_use]
222    pub fn status(&self) -> UndoHistoryState {
223        self.inner.state.get()
224    }
225
226    #[must_use]
227    pub fn can_undo(&self) -> bool {
228        self.status().can_undo
229    }
230
231    #[must_use]
232    pub fn can_redo(&self) -> bool {
233        self.status().can_redo
234    }
235
236    /// Limits the number of committed text states retained by this history.
237    /// A value of zero disables recording while keeping the current editor
238    /// value usable. Existing entries are trimmed immediately.
239    pub fn set_max_entries(&self, max_entries: usize) {
240        let mut history = self.inner.history.borrow_mut();
241        history.max_entries = max_entries;
242        trim_history(&mut history.undo, max_entries);
243        trim_history(&mut history.redo, max_entries);
244        drop(history);
245        self.inner.sync_state();
246    }
247
248    #[must_use]
249    pub fn max_entries(&self) -> usize {
250        self.inner.history.borrow().max_entries
251    }
252
253    /// Moves the editor to the previous value, if one exists.
254    #[must_use]
255    pub fn undo(&self) -> bool {
256        let target = {
257            let mut history = self.inner.history.borrow_mut();
258            let Some(target) = history.undo.pop() else {
259                return false;
260            };
261            let current = std::mem::replace(&mut history.current, target.clone());
262            if history.max_entries > 0 {
263                history.redo.push(current);
264                let max_entries = history.max_entries;
265                trim_history(&mut history.redo, max_entries);
266            }
267            target
268        };
269        self.inner.apply_history_value(target);
270        true
271    }
272
273    /// Moves the editor to the next value, if one exists.
274    #[must_use]
275    pub fn redo(&self) -> bool {
276        let target = {
277            let mut history = self.inner.history.borrow_mut();
278            let Some(target) = history.redo.pop() else {
279                return false;
280            };
281            let current = std::mem::replace(&mut history.current, target.clone());
282            if history.max_entries > 0 {
283                history.undo.push(current);
284                let max_entries = history.max_entries;
285                trim_history(&mut history.undo, max_entries);
286            }
287            target
288        };
289        self.inner.apply_history_value(target);
290        true
291    }
292
293    /// Discards undo and redo entries while retaining the current editor text.
294    pub fn clear(&self) {
295        let mut history = self.inner.history.borrow_mut();
296        history.current = self.inner.editor.value();
297        history.undo.clear();
298        history.redo.clear();
299        drop(history);
300        self.inner.sync_state();
301    }
302}