use crate::params::ParamState;
#[derive(Debug, Clone)]
pub struct HistoryEntry {
pub params: ParamState,
pub label: Option<String>,
}
impl HistoryEntry {
pub fn new(params: ParamState) -> Self {
Self {
params,
label: None,
}
}
pub fn with_label(params: ParamState, label: impl Into<String>) -> Self {
Self {
params,
label: Some(label.into()),
}
}
}
pub struct History {
entries: Vec<HistoryEntry>,
current: usize,
max_entries: usize,
}
impl History {
pub fn new(max_entries: usize) -> Self {
Self {
entries: Vec::new(),
current: 0,
max_entries,
}
}
pub fn with_default_capacity() -> Self {
Self::new(50)
}
pub fn push(&mut self, entry: HistoryEntry) {
if !self.entries.is_empty() && self.current < self.entries.len() - 1 {
self.entries.truncate(self.current + 1);
}
self.entries.push(entry);
if self.entries.len() > self.max_entries {
self.entries.remove(0);
}
self.current = self.entries.len() - 1;
}
pub fn undo(&mut self) -> Option<&ParamState> {
if self.entries.is_empty() || self.current == 0 {
return None;
}
self.current -= 1;
Some(&self.entries[self.current].params)
}
pub fn redo(&mut self) -> Option<&ParamState> {
if self.entries.is_empty() || self.current >= self.entries.len() - 1 {
return None;
}
self.current += 1;
Some(&self.entries[self.current].params)
}
pub fn current(&self) -> Option<&ParamState> {
if self.entries.is_empty() {
None
} else {
Some(&self.entries[self.current].params)
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn can_undo(&self) -> bool {
!self.entries.is_empty() && self.current > 0
}
pub fn can_redo(&self) -> bool {
!self.entries.is_empty() && self.current < self.entries.len() - 1
}
pub fn clear(&mut self) {
self.entries.clear();
self.current = 0;
}
pub fn labels(&self) -> Vec<Option<&str>> {
self.entries.iter().map(|e| e.label.as_deref()).collect()
}
pub fn jump_to(&mut self, index: usize) -> Option<&ParamState> {
if index >= self.entries.len() {
return None;
}
self.current = index;
Some(&self.entries[self.current].params)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(height: f32) -> HistoryEntry {
HistoryEntry::new(ParamState::new(height, 0.5, 0.5, 0.5))
}
fn make_labeled(height: f32, label: &str) -> HistoryEntry {
HistoryEntry::with_label(ParamState::new(height, 0.5, 0.5, 0.5), label)
}
#[test]
fn push_and_current() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.7));
let state = h.current().expect("should have current");
assert!((state.height - 0.7).abs() < 1e-6);
}
#[test]
fn undo_returns_previous() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.3)); h.push(make_entry(0.8)); let prev = h.undo().expect("should undo");
assert!((prev.height - 0.3).abs() < 1e-6);
}
#[test]
fn redo_after_undo() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.3)); h.push(make_entry(0.8)); h.undo();
let next = h.redo().expect("should redo");
assert!((next.height - 0.8).abs() < 1e-6);
}
#[test]
fn undo_at_start_returns_none() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.5));
assert!(h.undo().is_none());
}
#[test]
fn redo_at_end_returns_none() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.5));
assert!(h.redo().is_none());
}
#[test]
fn push_clears_redo() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.1)); h.push(make_entry(0.2)); h.undo(); h.push(make_entry(0.9)); assert!(h.redo().is_none());
}
#[test]
fn capacity_limit_respected() {
let mut h = History::new(50);
for i in 0..55 {
h.push(make_entry(i as f32 / 55.0));
}
assert_eq!(h.len(), 50);
}
#[test]
fn can_undo_false_when_empty() {
let h = History::with_default_capacity();
assert!(!h.can_undo());
}
#[test]
fn can_redo_true_after_undo() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.1)); h.push(make_entry(0.2)); h.undo();
assert!(h.can_redo());
}
#[test]
fn jump_to_valid_index() {
let mut h = History::with_default_capacity();
h.push(make_entry(0.1)); h.push(make_entry(0.5)); h.push(make_entry(0.9)); let state = h.jump_to(0).expect("should jump");
assert!((state.height - 0.1).abs() < 1e-6);
}
#[test]
fn labels_all_entries() {
let mut h = History::with_default_capacity();
h.push(make_labeled(0.1, "first"));
h.push(make_labeled(0.5, "second"));
h.push(make_labeled(0.9, "third"));
assert_eq!(h.labels().len(), 3);
}
}