This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+16
View File
@@ -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" }
+100
View File
@@ -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);
}
}
}
}
+145
View File
@@ -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);
}