gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Changing what an editor edits so it can be undone: every change goes
//! through an [`Edit`], which keeps the snapshot from before it.

use crate::canvas::undo::UndoStack;
use egui::Response;

/// The value being edited this frame, with its undo history.
#[derive(Debug)]
pub(crate) struct Edit<'a, T> {
    pub(crate) value: &'a mut T,
    undo: &'a mut UndoStack<T>,
    changed: bool,
}

impl<'a, T: Clone + PartialEq> Edit<'a, T> {
    pub(crate) fn new(value: &'a mut T, undo: &'a mut UndoStack<T>) -> Self {
        Self {
            value,
            undo,
            changed: false,
        }
    }

    /// Applies `change` as one undo step. If it changed nothing, it leaves
    /// none.
    pub(crate) fn apply<R>(&mut self, change: impl FnOnce(&mut T) -> R) -> R {
        let before = self.value.clone();
        let result = change(self.value);
        if *self.value != before {
            self.undo.record(&before);
            self.changed = true;
        }
        result
    }

    /// Takes the undo snapshot for an edit spread over several frames, a drag
    /// or typing; the frames after it change the value with
    /// [`Edit::continue_change`], and [`Edit::end`] closes it.
    pub(crate) fn begin(&mut self) {
        self.undo.record(self.value);
    }

    /// Changes the value within an edit [`Edit::begin`] started.
    pub(crate) fn continue_change(&mut self, change: impl FnOnce(&mut T)) {
        change(self.value);
        self.changed = true;
    }

    /// Closes an edit [`Edit::begin`] started. One that changed nothing
    /// leaves no undo step.
    pub(crate) fn end(&mut self) {
        self.undo.discard_if_unchanged(self.value);
    }

    /// Applies what a widget changed. Dragging a value or typing into a field
    /// takes many frames and becomes one undo step; a focus or drag that
    /// changed nothing leaves none.
    pub(crate) fn tracked(&mut self, response: &Response, change: impl FnOnce(&mut T)) {
        if response.gained_focus() || response.drag_started() {
            self.begin();
        }
        if response.changed() {
            // a slider still takes the pointer on the frame its drag ends
            let ongoing = response.has_focus() || response.dragged() || response.drag_stopped();
            if !ongoing {
                self.begin();
            }
            self.continue_change(change);
        }
        if response.lost_focus() || response.drag_stopped() {
            self.end();
        }
    }

    /// Puts the value back to how it was before the last edit. Returns
    /// whether there was one.
    pub(crate) fn undo(&mut self) -> bool {
        let done = self.undo.undo(self.value);
        self.changed |= done;
        done
    }

    /// Makes the last undone edit again. Returns whether there was one.
    pub(crate) fn redo(&mut self) -> bool {
        let done = self.undo.redo(self.value);
        self.changed |= done;
        done
    }

    pub(crate) fn can_undo(&self) -> bool {
        self.undo.can_undo()
    }

    pub(crate) fn can_redo(&self) -> bool {
        self.undo.can_redo()
    }

    /// Whether anything changed the value through this edit.
    pub(crate) fn changed(&self) -> bool {
        self.changed
    }
}

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

    #[test]
    fn a_change_is_one_undo_step() {
        let mut value = 1;
        let mut undo = UndoStack::default();
        let mut edit = Edit::new(&mut value, &mut undo);

        edit.apply(|value| *value = 2);
        assert!(edit.changed());
        assert!(edit.undo());
        assert_eq!(*edit.value, 1);
        assert!(edit.redo());
        assert_eq!(*edit.value, 2);
    }

    #[test]
    fn a_change_that_changes_nothing_leaves_no_step() {
        let mut value = 1;
        let mut undo = UndoStack::default();
        let mut edit = Edit::new(&mut value, &mut undo);

        edit.apply(|value| *value = 1);
        assert!(!edit.changed());
        assert!(!edit.can_undo());
    }

    #[test]
    fn a_drag_over_many_frames_is_one_step() {
        let mut value = 0;
        let mut undo = UndoStack::default();
        let mut edit = Edit::new(&mut value, &mut undo);

        edit.begin();
        for frame in 1..=10 {
            edit.continue_change(|value| *value = frame);
        }
        edit.end();

        assert!(edit.undo());
        assert_eq!(*edit.value, 0);
        assert!(!edit.can_undo());
    }

    #[test]
    fn a_drag_that_moved_nothing_leaves_no_step() {
        let mut value = 0;
        let mut undo = UndoStack::default();
        let mut edit = Edit::new(&mut value, &mut undo);

        edit.begin();
        edit.end();

        assert!(!edit.can_undo());
    }

    /// Clicking into a field and out again is no edit, so what was undone
    /// can still be redone.
    #[test]
    fn an_edit_that_changed_nothing_keeps_redo() {
        let mut value = 0;
        let mut undo = UndoStack::default();
        let mut edit = Edit::new(&mut value, &mut undo);
        edit.apply(|value| *value = 1);
        edit.undo();

        edit.begin();
        edit.end();

        assert!(edit.redo());
        assert_eq!(*edit.value, 1);
    }
}