init
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "hcie-history"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# Generic - no app-specific dependencies
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
@@ -0,0 +1,100 @@
|
||||
#![allow(dead_code)]
|
||||
//! Type-independent, generic undo/redo engine.
|
||||
//! No app-specific dependencies. Pure generic Rust state-management.
|
||||
|
||||
/// Trait implemented by all generic undoable actions.
|
||||
pub trait UndoableAction<S>: Send {
|
||||
fn undo(&mut self, state: &mut S);
|
||||
fn redo(&mut self, state: &mut S);
|
||||
fn description(&self) -> String;
|
||||
}
|
||||
|
||||
/// Generic history manager — parameterized over state type `S`.
|
||||
pub struct HistoryManager<S> {
|
||||
entries: Vec<Box<dyn UndoableAction<S>>>,
|
||||
current_index: i32,
|
||||
max_steps: usize,
|
||||
}
|
||||
|
||||
impl<S> HistoryManager<S> {
|
||||
pub fn new(max_steps: usize) -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
current_index: -1,
|
||||
max_steps,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, action: Box<dyn UndoableAction<S>>) {
|
||||
let keep = (self.current_index + 1) as usize;
|
||||
if keep < self.entries.len() {
|
||||
self.entries.truncate(keep);
|
||||
}
|
||||
self.entries.push(action);
|
||||
self.current_index = self.entries.len() as i32 - 1;
|
||||
if self.entries.len() > self.max_steps {
|
||||
self.entries.remove(0);
|
||||
self.current_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, state: &mut S) {
|
||||
if self.current_index >= 0 {
|
||||
let idx = self.current_index as usize;
|
||||
self.entries[idx].undo(state);
|
||||
self.current_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, state: &mut S) {
|
||||
if self.current_index >= 0 && (self.current_index as usize) + 1 < self.entries.len() {
|
||||
let idx = (self.current_index + 1) as usize;
|
||||
self.entries[idx].redo(state);
|
||||
self.current_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.current_index >= 0
|
||||
}
|
||||
|
||||
pub fn can_redo(&self) -> bool {
|
||||
self.current_index >= 0 && (self.current_index as usize + 1) < self.entries.len()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn current_index(&self) -> i32 {
|
||||
self.current_index
|
||||
}
|
||||
|
||||
pub fn entry_description(&self, idx: usize) -> Option<String> {
|
||||
self.entries.get(idx).map(|e| e.description())
|
||||
}
|
||||
|
||||
/// Clears all recorded undo/redo entries and resets the current index.
|
||||
///
|
||||
/// **Purpose:** Drops the entire action stack, typically used when
|
||||
/// replacing the underlying state (e.g., loading a new document).
|
||||
///
|
||||
/// **Side Effects:** Removes all entries and resets `current_index` to -1.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.current_index = -1;
|
||||
}
|
||||
|
||||
pub fn jump_to(&mut self, target: i32, state: &mut S) {
|
||||
let current = self.current_index;
|
||||
if target < current {
|
||||
for _ in 0..(current - target) {
|
||||
self.undo(state);
|
||||
}
|
||||
} else if target > current {
|
||||
for _ in 0..(target - current) {
|
||||
self.redo(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use hcie_history::{HistoryManager, UndoableAction};
|
||||
use hcie_protocol::Layer;
|
||||
|
||||
struct SetPixelAction {
|
||||
layer_idx: usize,
|
||||
old_color: [u8; 4],
|
||||
new_color: [u8; 4],
|
||||
}
|
||||
|
||||
impl UndoableAction<Vec<Layer>> for SetPixelAction {
|
||||
fn undo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if let Some(layer) = layers.get_mut(self.layer_idx) {
|
||||
layer.set_pixel(0, 0, self.old_color);
|
||||
}
|
||||
}
|
||||
fn redo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if let Some(layer) = layers.get_mut(self.layer_idx) {
|
||||
layer.set_pixel(0, 0, self.new_color);
|
||||
}
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
"SetPixel".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_layer() -> Layer {
|
||||
Layer::new_transparent("test", 10, 10)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_new_is_empty() {
|
||||
let hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
assert_eq!(hist.len(), 0);
|
||||
assert!(!hist.can_undo());
|
||||
assert!(!hist.can_redo());
|
||||
assert_eq!(hist.current_index(), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_push_undo_redo() {
|
||||
let mut layers = vec![make_layer()];
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [0;4], new_color: [255, 0, 0, 255],
|
||||
}));
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [255, 0, 0, 255], new_color: [0, 255, 0, 255],
|
||||
}));
|
||||
assert!(hist.can_undo());
|
||||
assert!(!hist.can_redo());
|
||||
|
||||
hist.undo(&mut layers);
|
||||
assert_eq!(layers[0].get_pixel(0, 0), [255, 0, 0, 255]);
|
||||
assert!(hist.can_redo());
|
||||
|
||||
hist.redo(&mut layers);
|
||||
assert_eq!(layers[0].get_pixel(0, 0), [0, 255, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_undo_twice_is_stable() {
|
||||
let mut layers = vec![make_layer()];
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [0; 4], new_color: [255; 4],
|
||||
}));
|
||||
hist.undo(&mut layers);
|
||||
hist.undo(&mut layers);
|
||||
assert!(!hist.can_undo(), "should reach bottom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_redo_twice_is_stable() {
|
||||
let mut layers = vec![make_layer()];
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [0; 4], new_color: [255; 4],
|
||||
}));
|
||||
hist.undo(&mut layers);
|
||||
hist.redo(&mut layers);
|
||||
hist.redo(&mut layers);
|
||||
assert!(!hist.can_redo(), "should reach top");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_max_steps() {
|
||||
let _layers = vec![make_layer()];
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(3);
|
||||
for i in 0..10 {
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0,
|
||||
old_color: [0; 4],
|
||||
new_color: [i; 4],
|
||||
}));
|
||||
}
|
||||
assert_eq!(hist.len(), 3, "should cap at max_steps=3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_new_action_truncates_redo() {
|
||||
let mut layers = vec![make_layer()];
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [0; 4], new_color: [1; 4],
|
||||
}));
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [1; 4], new_color: [2; 4],
|
||||
}));
|
||||
hist.undo(&mut layers);
|
||||
assert!(hist.can_redo());
|
||||
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [1; 4], new_color: [3; 4],
|
||||
}));
|
||||
assert!(!hist.can_redo(), "new action should truncate redo stack");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history_jump_to() {
|
||||
let mut layers = vec![make_layer()];
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [0; 4], new_color: [1; 4],
|
||||
}));
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [1; 4], new_color: [2; 4],
|
||||
}));
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [2; 4], new_color: [3; 4],
|
||||
}));
|
||||
hist.jump_to(-1, &mut layers);
|
||||
assert!(!hist.can_undo(), "jump to -1 should be at bottom");
|
||||
assert!(!hist.can_redo(), "jump to -1: no redo at initial state");
|
||||
assert_eq!(layers[0].get_pixel(0, 0), [0; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entry_description() {
|
||||
let mut hist = HistoryManager::<Vec<Layer>>::new(10);
|
||||
hist.push(Box::new(SetPixelAction {
|
||||
layer_idx: 0, old_color: [0; 4], new_color: [255; 4],
|
||||
}));
|
||||
assert_eq!(hist.entry_description(0), Some("SetPixel".to_string()));
|
||||
assert_eq!(hist.entry_description(99), None);
|
||||
}
|
||||
Reference in New Issue
Block a user