gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Undo and redo by snapshots: what the editors edit — a definition, a patch —
//! is small enough to keep whole copies of it.

/// Snapshots before each edit, and the ones undone since.
#[derive(Debug)]
pub(crate) struct UndoStack<T> {
    past: Vec<T>,
    future: Vec<T>,
    /// What the last snapshot took off `future`. A snapshot nothing changed
    /// after — a field focused and left, a drag that did not move — is no
    /// edit, and must not cost the undone edits.
    stashed_future: Vec<T>,
}

impl<T> Default for UndoStack<T> {
    fn default() -> Self {
        Self {
            past: vec![],
            future: vec![],
            stashed_future: vec![],
        }
    }
}

impl<T: Clone + PartialEq> UndoStack<T> {
    /// How many edits can be undone.
    const CAP: usize = 64;

    /// Remembers `before`, the value as it was before an edit. A new edit
    /// makes the undone ones unreachable.
    pub(crate) fn record(&mut self, before: &T) {
        if self.past.last() == Some(before) {
            return;
        }
        self.past.push(before.clone());
        if self.past.len() > Self::CAP {
            self.past.remove(0);
        }
        self.stashed_future = std::mem::take(&mut self.future);
    }

    /// Puts `current` back to how it was before the last edit. Returns
    /// whether there was one.
    pub(crate) fn undo(&mut self, current: &mut T) -> bool {
        let Some(previous) = self.past.pop() else {
            return false;
        };
        self.stashed_future.clear();
        self.future.push(std::mem::replace(current, previous));
        true
    }

    /// Makes the last undone edit again. Returns whether there was one.
    pub(crate) fn redo(&mut self, current: &mut T) -> bool {
        let Some(next) = self.future.pop() else {
            return false;
        };
        self.stashed_future.clear();
        self.past.push(std::mem::replace(current, next));
        true
    }

    pub(crate) fn can_undo(&self) -> bool {
        !self.past.is_empty()
    }

    pub(crate) fn can_redo(&self) -> bool {
        !self.future.is_empty()
    }

    /// Drops the last snapshot if `current` still equals it: a drag that
    /// moved nothing is no edit, so the edits undone before it can still be
    /// redone.
    pub(crate) fn discard_if_unchanged(&mut self, current: &T) {
        if self.past.last() == Some(current) {
            self.past.pop();
            if self.future.is_empty() {
                self.future = std::mem::take(&mut self.stashed_future);
            }
        }
    }

    /// Forgets everything, for another value or one reset from disk.
    pub(crate) fn clear(&mut self) {
        self.past.clear();
        self.future.clear();
        self.stashed_future.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn undo_and_redo_walk_the_edits() {
        let mut stack = UndoStack::default();
        let mut value = 1;
        stack.record(&value);
        value = 2;

        assert!(stack.undo(&mut value));
        assert_eq!(value, 1);
        assert!(stack.redo(&mut value));
        assert_eq!(value, 2);
        assert!(!stack.redo(&mut value));
    }

    #[test]
    fn a_new_edit_drops_what_was_undone() {
        let mut stack = UndoStack::default();
        let mut value = 1;
        stack.record(&value);
        value = 2;
        stack.undo(&mut value);

        stack.record(&value);
        assert!(!stack.can_redo());
    }

    #[test]
    fn only_the_last_edits_are_kept() {
        let mut stack = UndoStack::default();
        let mut value = 0;
        for next in 1..=70 {
            stack.record(&value);
            value = next;
        }
        let mut undone = 0;
        while stack.undo(&mut value) {
            undone += 1;
        }
        assert_eq!(undone, UndoStack::<i32>::CAP);
    }

    #[test]
    fn recording_the_same_state_twice_is_one_step() {
        let mut stack = UndoStack::default();
        let mut value = 1;
        stack.record(&value);
        stack.record(&value);
        value = 2;
        stack.undo(&mut value);
        assert!(!stack.can_undo());
    }

    /// Regression: focusing a field and leaving it, or a drag that did not
    /// move, took a snapshot that dropped the undone edits, and redo greyed
    /// out although nothing had changed.
    #[test]
    fn an_unchanged_edit_keeps_the_redo_history() {
        let mut stack = UndoStack::default();
        let mut value = 1;
        stack.record(&value);
        value = 2;
        stack.undo(&mut value);

        stack.record(&value);
        stack.discard_if_unchanged(&value);

        assert!(stack.can_redo());
        assert!(stack.redo(&mut value));
        assert_eq!(value, 2);
    }

    #[test]
    fn a_real_change_after_undo_drops_the_redo_history() {
        let mut stack = UndoStack::default();
        let mut value = 1;
        stack.record(&value);
        value = 2;
        stack.undo(&mut value);

        stack.record(&value);
        value = 3;
        stack.discard_if_unchanged(&value);

        assert!(!stack.can_redo());
        assert!(stack.undo(&mut value));
        assert_eq!(value, 1);
    }

    #[test]
    fn a_snapshot_nothing_changed_is_dropped() {
        let mut stack = UndoStack::default();
        let value = 1;
        stack.record(&value);
        stack.discard_if_unchanged(&value);
        assert!(!stack.can_undo());
    }
}