mod canvas_geometry;
mod canvas_resize;
mod compound;
mod filter;
mod gpu_region;
mod layer;
pub mod property;
mod selection;
mod tombstones;
pub use canvas_geometry::CanvasGeometryAction;
pub use canvas_resize::CanvasResizeAction;
pub use compound::CompoundAction;
pub use filter::{FilterAddAction, FilterRemoveAction, NodeLockedAction, NodeVisibleAction};
pub use gpu_region::GpuRegionAction;
pub use layer::{
BakeLayersAction, BakeSourceSlot, DuplicateAction, LayerAddAction, LayerMoveAction,
LayerRemoveAction,
};
pub use property::PropertyAction;
pub use selection::SelectionAction;
use crate::document::Document;
use crate::gpu::compositor::Compositor;
use crate::gpu::region_store::UndoRegionEntry;
use crate::layer::LayerId;
use std::collections::{HashMap, HashSet};
pub trait UndoAction {
fn undo(&mut self, doc: &mut Document) -> HashMap<LayerId, HashSet<(i32, i32)>>;
fn redo(&mut self, doc: &mut Document) -> HashMap<LayerId, HashSet<(i32, i32)>>;
fn try_coalesce_property(&mut self, _other: &PropertyAction) -> bool {
false
}
fn gpu_region_entry_mut(&mut self) -> Option<&mut UndoRegionEntry> {
None
}
fn gpu_region_entries_mut(&mut self) -> Vec<&mut UndoRegionEntry> {
self.gpu_region_entry_mut().into_iter().collect()
}
fn selection_region_entry_mut(&mut self) -> Option<&mut UndoRegionEntry> {
None
}
fn swap_selection_active(&mut self, _current_active: bool) -> Option<bool> {
None
}
fn on_evict(&mut self, _compositor: &mut Compositor) {}
fn byte_cost(&self) -> u64 {
0
}
}
#[cfg(target_arch = "wasm32")]
const DEFAULT_MEMORY_CAP: u64 = 128 << 20;
#[cfg(not(target_arch = "wasm32"))]
const DEFAULT_MEMORY_CAP: u64 = 512 << 20;
pub struct UndoStack {
undo_steps: Vec<Box<dyn UndoAction>>,
redo_steps: Vec<Box<dyn UndoAction>>,
max_steps: usize,
memory_cap: u64,
total_bytes: u64,
}
impl UndoStack {
pub fn new(max_steps: usize) -> Self {
UndoStack {
undo_steps: Vec::new(),
redo_steps: Vec::new(),
max_steps,
memory_cap: DEFAULT_MEMORY_CAP,
total_bytes: 0,
}
}
#[must_use = "evicted actions must have on_evict called to release tombstones"]
pub fn push(
&mut self,
doc: &mut Document,
action: Box<dyn UndoAction>,
) -> Vec<Box<dyn UndoAction>> {
doc.dirty = true;
let mut evicted: Vec<Box<dyn UndoAction>> = self.redo_steps.drain(..).collect();
for a in &evicted {
self.total_bytes = self.total_bytes.saturating_sub(a.byte_cost());
}
self.total_bytes = self.total_bytes.saturating_add(action.byte_cost());
self.undo_steps.push(action);
if self.undo_steps.len() > self.max_steps {
let remove = self.undo_steps.len() - self.max_steps;
let drained: Vec<_> = self.undo_steps.drain(0..remove).collect();
for a in &drained {
self.total_bytes = self.total_bytes.saturating_sub(a.byte_cost());
}
evicted.extend(drained);
}
while self.total_bytes > self.memory_cap && !self.undo_steps.is_empty() {
let a = self.undo_steps.remove(0);
self.total_bytes = self.total_bytes.saturating_sub(a.byte_cost());
evicted.push(a);
}
evicted
}
#[must_use = "evicted actions must have on_evict called to release tombstones"]
pub fn coalesce_property(
&mut self,
doc: &mut Document,
action: PropertyAction,
) -> Vec<Box<dyn UndoAction>> {
doc.dirty = true;
if let Some(top) = self.undo_steps.last_mut() {
if top.try_coalesce_property(&action) {
return Vec::new();
}
}
self.push(doc, Box::new(action))
}
#[must_use = "drained actions must have on_evict called to release tombstones"]
pub fn drain_all(&mut self) -> Vec<Box<dyn UndoAction>> {
let mut all: Vec<Box<dyn UndoAction>> = self.undo_steps.drain(..).collect();
all.append(&mut self.redo_steps);
self.total_bytes = 0;
all
}
pub fn undo(&mut self, doc: &mut Document) -> Option<HashMap<LayerId, HashSet<(i32, i32)>>> {
let mut action = self.undo_steps.pop()?;
let affected = action.undo(doc);
self.redo_steps.push(action);
Some(affected)
}
pub fn redo(&mut self, doc: &mut Document) -> Option<HashMap<LayerId, HashSet<(i32, i32)>>> {
let mut action = self.redo_steps.pop()?;
let affected = action.redo(doc);
self.undo_steps.push(action);
Some(affected)
}
pub fn pop_for_undo(&mut self) -> Option<Box<dyn UndoAction>> {
self.undo_steps.pop()
}
pub fn complete_undo(&mut self, action: Box<dyn UndoAction>) {
self.redo_steps.push(action);
}
pub fn pop_for_redo(&mut self) -> Option<Box<dyn UndoAction>> {
self.redo_steps.pop()
}
pub fn complete_redo(&mut self, action: Box<dyn UndoAction>) {
self.undo_steps.push(action);
}
pub fn can_undo(&self) -> bool {
!self.undo_steps.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_steps.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layer::Layer;
#[test]
fn undo_layer_add_remove() {
let mut doc = Document::new(128, 128);
let mut undo = UndoStack::new(100);
let id = doc.add_raster_layer(None);
let parent = doc.parent_of(id);
let pos = doc.position_in_parent(id).unwrap();
let _ = undo.push(&mut doc, Box::new(LayerAddAction::new(id, parent, pos)));
assert_eq!(doc.flat_layers().len(), 1);
undo.undo(&mut doc);
assert_eq!(doc.flat_layers().len(), 0);
undo.redo(&mut doc);
assert_eq!(doc.flat_layers().len(), 1);
}
#[test]
fn undo_layer_remove() {
let mut doc = Document::new(128, 128);
let mut undo = UndoStack::new(100);
let id = doc.add_raster_layer(None);
let parent = doc.parent_of(id);
let pos = doc.position_in_parent(id).unwrap();
let node = doc.detach_for_undo(id).unwrap();
let _ = undo.push(
&mut doc,
Box::new(LayerRemoveAction::new(node, parent, pos, Vec::new())),
);
assert_eq!(doc.flat_layers().len(), 0);
undo.undo(&mut doc);
assert_eq!(doc.flat_layers().len(), 1);
undo.redo(&mut doc);
assert_eq!(doc.flat_layers().len(), 0);
}
#[test]
fn undo_layer_move() {
let mut doc = Document::new(128, 128);
let mut undo = UndoStack::new(100);
let l1 = doc.add_raster_layer(None);
let l2 = doc.add_raster_layer(None);
let l3 = doc.add_raster_layer(None);
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l1, l2, l3]);
let old_parent = doc.parent_of(l1);
let old_pos = doc.position_in_parent(l1).unwrap();
doc.move_layer(l1, crate::document::MoveTarget::After(l3));
let new_parent = doc.parent_of(l1);
let new_pos = doc.position_in_parent(l1).unwrap();
let _ = undo.push(
&mut doc,
Box::new(LayerMoveAction::new(
l1, old_parent, old_pos, new_parent, new_pos,
)),
);
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l2, l3, l1]);
undo.undo(&mut doc);
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l1, l2, l3]);
undo.redo(&mut doc);
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l2, l3, l1]);
}
#[test]
fn undo_property_change() {
use super::property::Property;
let mut doc = Document::new(128, 128);
let mut undo = UndoStack::new(100);
let id = doc.add_raster_layer(None);
let _ = undo.push(
&mut doc,
Box::new(PropertyAction::new(
id,
Property::Opacity(1.0),
Property::Opacity(0.5),
)),
);
if let Some(Layer::Raster(r)) = doc.layer_mut(id) {
r.blend.opacity = 0.5;
}
undo.undo(&mut doc);
if let Some(Layer::Raster(r)) = doc.layer(id) {
assert!((r.blend.opacity - 1.0).abs() < f32::EPSILON);
}
undo.redo(&mut doc);
if let Some(Layer::Raster(r)) = doc.layer(id) {
assert!((r.blend.opacity - 0.5).abs() < f32::EPSILON);
}
}
#[test]
fn coalesce_opacity_slider_drag() {
use super::property::Property;
let mut doc = Document::new(128, 128);
let mut undo = UndoStack::new(100);
let id = doc.add_raster_layer(None);
let steps = [0.9_f32, 0.7, 0.5, 0.3];
for &new_val in &steps {
let old_val = match doc.layer(id) {
Some(Layer::Raster(r)) => r.blend.opacity,
_ => unreachable!(),
};
if let Some(Layer::Raster(r)) = doc.layer_mut(id) {
r.blend.opacity = new_val;
}
let _ = undo.coalesce_property(
&mut doc,
PropertyAction::new(id, Property::Opacity(old_val), Property::Opacity(new_val)),
);
}
assert!(undo.can_undo());
assert_eq!(doc.layer(id).map(|l| l.blend().opacity), Some(0.3),);
undo.undo(&mut doc);
let after_undo = match doc.layer(id) {
Some(Layer::Raster(r)) => r.blend.opacity,
_ => unreachable!(),
};
assert!(
(after_undo - 1.0).abs() < f32::EPSILON,
"undo should restore original opacity 1.0, got {after_undo}"
);
assert!(!undo.can_undo());
undo.redo(&mut doc);
let after_redo = match doc.layer(id) {
Some(Layer::Raster(r)) => r.blend.opacity,
_ => unreachable!(),
};
assert!(
(after_redo - 0.3).abs() < f32::EPSILON,
"redo should restore final opacity 0.3, got {after_redo}"
);
}
#[test]
fn dirty_flag_set_by_undo_push() {
let mut doc = Document::new(64, 64);
let mut undo = UndoStack::new(50);
assert!(!doc.dirty, "fresh doc starts clean");
let id = doc.add_raster_layer(None);
let parent = doc.parent_of(id);
let pos = doc.position_in_parent(id).unwrap();
let _ = undo.push(&mut doc, Box::new(LayerAddAction::new(id, parent, pos)));
assert!(doc.dirty, "push must flip dirty");
}
#[test]
fn dirty_flag_set_by_coalesce_property() {
use super::property::Property;
let mut doc = Document::new(64, 64);
let mut undo = UndoStack::new(50);
let id = doc.add_raster_layer(None);
doc.dirty = false;
let _ = undo.coalesce_property(
&mut doc,
PropertyAction::new(id, Property::Opacity(1.0), Property::Opacity(0.5)),
);
assert!(doc.dirty, "first coalesce push flips dirty");
doc.dirty = false;
let _ = undo.coalesce_property(
&mut doc,
PropertyAction::new(id, Property::Opacity(0.5), Property::Opacity(0.3)),
);
assert!(
doc.dirty,
"subsequent coalesce (merging into existing step) still flips dirty"
);
}
#[test]
fn dirty_flag_sticky_through_undo_redo() {
let mut doc = Document::new(64, 64);
let mut undo = UndoStack::new(50);
let id = doc.add_raster_layer(None);
let parent = doc.parent_of(id);
let pos = doc.position_in_parent(id).unwrap();
let _ = undo.push(&mut doc, Box::new(LayerAddAction::new(id, parent, pos)));
assert!(doc.dirty);
undo.undo(&mut doc);
assert!(
doc.dirty,
"undo back to original state must NOT clear dirty"
);
undo.redo(&mut doc);
assert!(doc.dirty, "redo also leaves dirty set");
}
#[test]
fn undo_add_raster_layer_with_anchor_restores_position() {
let mut doc = Document::new(128, 128);
let mut undo = UndoStack::new(100);
let l1 = doc.add_raster_layer(None);
let l2 = doc.add_raster_layer(None);
let new_id = doc.add_raster_layer(Some(l1));
let parent = doc.parent_of(new_id);
let pos = doc.position_in_parent(new_id).unwrap();
let _ = undo.push(&mut doc, Box::new(LayerAddAction::new(new_id, parent, pos)));
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l1, new_id, l2]);
undo.undo(&mut doc);
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l1, l2]);
undo.redo(&mut doc);
let flat: Vec<_> = doc.flat_layers().iter().map(|l| l.id()).collect();
assert_eq!(flat, vec![l1, new_id, l2]);
}
}