undoredo 0.10.21

Undo-redo for Rust using deltas, snapshots, or commands.
Documentation
// SPDX-FileCopyrightText: 2026 undoredo contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::ops::{Add, AddAssign};

use undoredo::{Delta, Recorder, UndoRedo};

// No need for `#[derive(Delta)]` for types stored in containers, only the
// containers themselves need this.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vector2<T> {
    x: T,
    y: T,
}

impl<T: Add<T, Output = T>> Add for Vector2<T> {
    type Output = Self;

    fn add(self, rhs: Self) -> Self {
        Self {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

impl<T: AddAssign<T>> AddAssign for Vector2<T> {
    fn add_assign(&mut self, rhs: Self) {
        self.x += rhs.x;
        self.y += rhs.y;
    }
}

// `#[derive(Delta)]` generates the `EntitiesDelta` type, which is needed to
// store deltas as edits in an `UndoRedo` bistack.
#[derive(Delta)]
// You can choose the name for the generated delta edit type through the
// `undoredo` attribute with key `delta`. If you omit this, the default is
// the name of the input type followed by the word `Delta`. The name chosen in
// this example, `EntitiesDelta`, happens to be the same as the default.
#[undoredo(delta = EntitiesDelta)]
// Each delta is made of two collections of elements called "half-deltas".
// `#[derive(Delta)]` generates a type for them as well, named similarly to full
// deltas. If that name does not suit you, you can analogously use the
// `undoredo` attribute with key `half_delta` to rename them.,
#[undoredo(half_delta = EntitiesHalfDelta)]
pub struct Entities<T> {
    positions: Recorder<Vec<Vector2<T>>>,
    velocities: Recorder<Vec<Vector2<T>>>,
    healths: Recorder<Vec<i64>>,
    // You can record changes to primitive types too, not just collections.
    turn_counter: Recorder<u64>,
    // You can make fields not be subject to undo-redo by marking them with
    // `#[undoredo(skip)]`.
    // Note that this skipping only works for delta-based undo-redo because
    // snapshots and commands rely on different mechanisms.
    #[undoredo(skip)]
    #[allow(unused)]
    not_in_delta: String,
}

impl<T: Add<T, Output = T> + AddAssign<T> + Copy + PartialOrd> Entities<T> {
    pub fn new_entity(&mut self, position: Vector2<T>, velocity: Vector2<T>, health: i64) -> usize {
        self.positions.push(position);
        self.velocities.push(velocity);
        self.healths.push(health)
    }

    pub fn update(&mut self) {
        self.turn_counter.assign(*self.turn_counter.container() + 1);

        for i in 0..self.positions.container().len() {
            self.update_entity(i);
        }
    }

    fn update_entity(&mut self, index: usize) {
        // Add velocities to positions on every update.

        let velocity = self.velocities[index];
        self.positions
            .modify(index, |position| *position += velocity);

        // Alternatively, you could update them more verbosely this way:

        /*self.positions
            .set(index, self.positions[index] + self.velocities[index]);*/

        // Decrease health by 1 on every update.
        self.healths.set(index, self.healths[index] - 1);
    }
}

fn assert_entity(entities: &Entities<f64>, index: usize, pos: Vector2<f64>, health: i64) {
    assert_eq!(entities.positions[index], pos);
    assert_eq!(entities.healths[index], health);
}

fn main() {
    let mut entities = Entities::<f64> {
        positions: Recorder::new(Vec::new()),
        velocities: Recorder::new(Vec::new()),
        healths: Recorder::new(Vec::new()),
        turn_counter: Recorder::new(0),
        not_in_delta: "not_in_delta".to_string(),
    };

    // `EntitiesDelta` was generated by `#[derive(Delta)]` on `Entities`.
    let mut undoredo: UndoRedo<EntitiesDelta<f64>> = UndoRedo::new();

    entities.new_entity(Vector2 { x: 0.0, y: 0.0 }, Vector2 { x: 1.0, y: 0.0 }, 100);
    entities.new_entity(Vector2 { x: 0.0, y: 0.0 }, Vector2 { x: 0.0, y: 2.0 }, 100);
    undoredo.commit(&mut entities);

    entities.new_entity(
        Vector2 { x: 100.0, y: 100.0 },
        Vector2 { x: -1.0, y: -1.0 },
        50,
    );
    undoredo.commit(&mut entities);

    assert_eq!(entities.positions.container().len(), 3);

    // Perform three simulation updates, committing after each tick.
    for _ in 0..3 {
        entities.update();
        undoredo.commit(&mut entities);
    }

    // Perform five more simulation updates, this time committing only once afterwards.
    for _ in 0..5 {
        entities.update();
    }
    undoredo.commit(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 8.0, y: 0.0 }, 92);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 16.0 }, 92);
    assert_entity(&entities, 2, Vector2 { x: 92.0, y: 92.0 }, 42);
    assert_eq!(*entities.turn_counter.container(), 8);

    undoredo.undo(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 3.0, y: 0.0 }, 97);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 6.0 }, 97);
    assert_entity(&entities, 2, Vector2 { x: 97.0, y: 97.0 }, 47);
    assert_eq!(*entities.turn_counter.container(), 3);

    undoredo.undo(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 2.0, y: 0.0 }, 98);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 4.0 }, 98);
    assert_entity(&entities, 2, Vector2 { x: 98.0, y: 98.0 }, 48);

    undoredo.undo(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 1.0, y: 0.0 }, 99);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 2.0 }, 99);
    assert_entity(&entities, 2, Vector2 { x: 99.0, y: 99.0 }, 49);

    undoredo.undo(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 0.0, y: 0.0 }, 100);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 0.0 }, 100);
    assert_entity(&entities, 2, Vector2 { x: 100.0, y: 100.0 }, 50);
    assert_eq!(*entities.turn_counter.container(), 0);

    undoredo.undo(&mut entities);

    assert!(entities.positions.container().len() == 2);
    assert_entity(&entities, 0, Vector2 { x: 0.0, y: 0.0 }, 100);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 0.0 }, 100);

    undoredo.redo(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 0.0, y: 0.0 }, 100);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 0.0 }, 100);
    assert_entity(&entities, 2, Vector2 { x: 100.0, y: 100.0 }, 50);

    undoredo.redo(&mut entities);

    assert_entity(&entities, 0, Vector2 { x: 1.0, y: 0.0 }, 99);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 2.0 }, 99);
    assert_entity(&entities, 2, Vector2 { x: 99.0, y: 99.0 }, 49);

    undoredo.redo(&mut entities);

    assert_entity(&entities, 0, Vector2 { x: 2.0, y: 0.0 }, 98);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 4.0 }, 98);
    assert_entity(&entities, 2, Vector2 { x: 98.0, y: 98.0 }, 48);

    undoredo.redo(&mut entities);

    assert_entity(&entities, 0, Vector2 { x: 3.0, y: 0.0 }, 97);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 6.0 }, 97);
    assert_entity(&entities, 2, Vector2 { x: 97.0, y: 97.0 }, 47);

    undoredo.redo(&mut entities);

    assert!(entities.positions.container().len() == 3);
    assert_entity(&entities, 0, Vector2 { x: 8.0, y: 0.0 }, 92);
    assert_entity(&entities, 1, Vector2 { x: 0.0, y: 16.0 }, 92);
    assert_entity(&entities, 2, Vector2 { x: 92.0, y: 92.0 }, 42);
    assert_eq!(*entities.turn_counter.container(), 8);
}

#[test]
fn test() {
    main();
}