use crate::event::{Event, EventHub, Origin, UndoRedoEvent};
use crate::types::EntityId;
use anyhow::{Result, anyhow};
use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
pub const UNTRACKED_STACK_ID: u64 = u64::MAX;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UndoLabel {
pub subject: &'static str,
pub action: &'static str,
}
impl UndoLabel {
pub const fn new(subject: &'static str, action: &'static str) -> Self {
Self { subject, action }
}
pub const fn act(subject: &'static str) -> Self {
Self {
subject,
action: "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UndoStatus {
Undone,
Superseded,
Empty,
}
pub trait UndoRedoCommand: Send {
fn undo(&mut self) -> Result<()>;
fn redo(&mut self) -> Result<()>;
fn can_merge(&self, _other: &dyn UndoRedoCommand) -> bool {
false
}
fn merge(&mut self, _other: &dyn UndoRedoCommand) -> bool {
false
}
fn as_any(&self) -> &dyn Any;
fn label(&self) -> Option<UndoLabel> {
None
}
}
pub struct CompositeCommand {
label: Option<UndoLabel>,
commands: Vec<Box<dyn UndoRedoCommand>>,
pub stack_id: u64,
}
impl fmt::Debug for CompositeCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompositeCommand")
.field("commands_len", &self.commands.len())
.field("stack_id", &self.stack_id)
.finish()
}
}
impl CompositeCommand {
pub fn new(stack_id: Option<u64>) -> Self {
Self::labeled(stack_id, None)
}
pub fn labeled(stack_id: Option<u64>, label: Option<UndoLabel>) -> Self {
CompositeCommand {
label,
commands: Vec::new(),
stack_id: stack_id.unwrap_or(0),
}
}
pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
self.commands.push(command);
}
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
pub fn set_label(&mut self, label: Option<UndoLabel>) {
self.label = label;
}
}
impl UndoRedoCommand for CompositeCommand {
fn undo(&mut self) -> Result<()> {
for command in self.commands.iter_mut().rev() {
command.undo()?;
}
Ok(())
}
fn redo(&mut self) -> Result<()> {
for command in self.commands.iter_mut() {
command.redo()?;
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn label(&self) -> Option<UndoLabel> {
self.label
}
}
pub trait AsyncUndoRedoCommand: UndoRedoCommand {
fn start_undo(&mut self) -> Result<()>;
fn start_redo(&mut self) -> Result<()>;
fn check_progress(&self) -> f32;
fn cancel(&mut self) -> Result<()>;
fn is_complete(&self) -> bool;
}
struct UndoEntry {
command: Box<dyn UndoRedoCommand>,
seq: u64,
sealed: bool,
}
#[derive(Default)]
struct StackData {
undo_stack: Vec<UndoEntry>,
redo_stack: Vec<UndoEntry>,
}
impl fmt::Debug for StackData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StackData")
.field("undo_len", &self.undo_stack.len())
.field("redo_len", &self.redo_stack.len())
.finish()
}
}
#[derive(Debug)]
pub struct UndoRedoManager {
stacks: HashMap<u64, StackData>,
next_stack_id: u64,
in_progress_composite: Option<CompositeCommand>,
composite_nesting_level: usize,
composite_stack_id: Option<u64>,
composite_label: Option<UndoLabel>,
event_hub: Option<Arc<EventHub>>,
next_seq: u64,
last_pushed_seq: Option<u64>,
undo_limit: Option<usize>,
}
impl Default for UndoRedoManager {
fn default() -> Self {
Self::new()
}
}
impl UndoRedoManager {
pub fn new() -> Self {
let mut stacks = HashMap::new();
stacks.insert(0, StackData::default());
UndoRedoManager {
stacks,
next_stack_id: 1,
in_progress_composite: None,
composite_nesting_level: 0,
composite_stack_id: None,
composite_label: None,
event_hub: None,
next_seq: 1,
last_pushed_seq: None,
undo_limit: None,
}
}
fn emit(&self, event: UndoRedoEvent, stack_id: u64) {
if let Some(event_hub) = &self.event_hub {
event_hub.send_event(Event {
origin: Origin::UndoRedo(event),
ids: Vec::<EntityId>::new(),
data: Some(stack_id.to_string()),
});
}
}
pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
self.event_hub = Some(Arc::clone(event_hub));
}
pub fn undo(&mut self, stack_id: Option<u64>) -> Result<()> {
let target_stack_id = stack_id.unwrap_or(0);
let stack = self
.stacks
.get_mut(&target_stack_id)
.ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
let stepped = if let Some(mut entry) = stack.undo_stack.pop() {
if let Err(e) = entry.command.undo() {
log::error!("Undo failed, re-pushing command to undo stack: {e}");
stack.undo_stack.push(entry);
return Err(e);
}
stack.redo_stack.push(entry);
true
} else {
false
};
if stepped {
self.emit(UndoRedoEvent::Undone, target_stack_id);
}
Ok(())
}
pub fn redo(&mut self, stack_id: Option<u64>) -> Result<()> {
let target_stack_id = stack_id.unwrap_or(0);
let stack = self
.stacks
.get_mut(&target_stack_id)
.ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
let stepped = if let Some(mut entry) = stack.redo_stack.pop() {
if let Err(e) = entry.command.redo() {
log::error!("Redo failed, re-pushing command to redo stack: {e}");
stack.redo_stack.push(entry);
return Err(e);
}
stack.undo_stack.push(entry);
true
} else {
false
};
if stepped {
self.emit(UndoRedoEvent::Redone, target_stack_id);
}
Ok(())
}
pub fn undo_if_head(&mut self, stack_id: Option<u64>, seq: u64) -> Result<UndoStatus> {
let target_stack_id = stack_id.unwrap_or(0);
let stack = self
.stacks
.get(&target_stack_id)
.ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
match stack.undo_stack.last() {
None => Ok(UndoStatus::Empty),
Some(entry) if entry.seq != seq => Ok(UndoStatus::Superseded),
Some(_) => {
self.undo(stack_id)?;
Ok(UndoStatus::Undone)
}
}
}
pub fn begin_composite(&mut self, stack_id: Option<u64>) -> Result<()> {
self.begin_composite_labeled(stack_id, None)
}
pub fn begin_composite_labeled(
&mut self,
stack_id: Option<u64>,
label: Option<UndoLabel>,
) -> Result<()> {
if stack_id == Some(UNTRACKED_STACK_ID) {
return Err(anyhow!(
"Cannot open a composite on the untracked stack: its commands are \
dropped, so the group could never be undone"
));
}
if self.composite_stack_id.is_some() && self.composite_stack_id != stack_id {
return Err(anyhow!(
"Cannot begin a composite on a different stack while another composite is in progress"
));
}
self.composite_stack_id = stack_id;
self.composite_nesting_level += 1;
if self.in_progress_composite.is_none() {
self.in_progress_composite = Some(CompositeCommand::labeled(stack_id, label));
self.composite_label = label;
} else if self.composite_label.is_none() {
self.composite_label = label;
}
self.emit(UndoRedoEvent::BeginComposite, stack_id.unwrap_or(0));
Ok(())
}
pub fn end_composite(&mut self) {
if self.composite_nesting_level > 0 {
self.composite_nesting_level -= 1;
}
if self.composite_nesting_level == 0 {
if let Some(composite) = self.in_progress_composite.take()
&& !composite.is_empty()
{
let target_stack_id = self.composite_stack_id.unwrap_or(0);
if self.stacks.contains_key(&target_stack_id) {
let mut composite = composite;
composite.set_label(self.composite_label);
self.push_entry(target_stack_id, Box::new(composite));
} else {
self.last_pushed_seq = None;
debug_assert!(
false,
"end_composite: undo stack {} does not exist — a composite was \
opened on a stack that has since been removed",
target_stack_id
);
}
} else {
self.last_pushed_seq = None;
}
let ended_on = self.composite_stack_id.unwrap_or(0);
self.composite_label = None;
self.composite_stack_id = None;
self.emit(UndoRedoEvent::EndComposite, ended_on);
}
}
pub fn cancel_composite(&mut self) {
if self.composite_nesting_level > 0 {
self.composite_nesting_level -= 1;
}
if let Some(ref mut composite) = self.in_progress_composite {
let _ = composite.undo();
}
let cancelled_on = self.composite_stack_id.unwrap_or(0);
self.in_progress_composite = None;
self.composite_stack_id = None;
self.composite_label = None;
self.last_pushed_seq = None;
self.emit(UndoRedoEvent::CancelComposite, cancelled_on);
}
pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
let _ = self.add_command_to_stack(command, None);
}
pub fn add_command_to_stack(
&mut self,
command: Box<dyn UndoRedoCommand>,
stack_id: Option<u64>,
) -> Result<()> {
if stack_id == Some(UNTRACKED_STACK_ID) {
self.last_pushed_seq = None;
return Ok(());
}
if let Some(composite) = &mut self.in_progress_composite {
if composite.stack_id != stack_id.unwrap_or(0) {
return Err(anyhow!(
"Cannot add command to composite with different stack ID"
));
}
composite.add_command(command);
self.last_pushed_seq = None;
return Ok(());
}
let target_stack_id = stack_id.unwrap_or(0);
let stack = self
.stacks
.get_mut(&target_stack_id)
.ok_or_else(|| anyhow!("Stack with ID {} does not exist", target_stack_id))?;
if let Some(last) = stack.undo_stack.last_mut()
&& !last.sealed
&& last.command.can_merge(&*command)
&& last.command.merge(&*command)
{
let seq = last.seq;
stack.redo_stack.clear();
self.last_pushed_seq = Some(seq);
self.emit(UndoRedoEvent::StackChanged, target_stack_id);
return Ok(());
}
self.push_entry(target_stack_id, command);
Ok(())
}
fn push_entry(&mut self, target_stack_id: u64, command: Box<dyn UndoRedoCommand>) {
let seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
let limit = self.undo_limit;
let Some(stack) = self.stacks.get_mut(&target_stack_id) else {
self.last_pushed_seq = None;
return;
};
stack.undo_stack.push(UndoEntry {
command,
seq,
sealed: false,
});
stack.redo_stack.clear();
if let Some(limit) = limit
&& stack.undo_stack.len() > limit
{
let excess = stack.undo_stack.len() - limit;
stack.undo_stack.drain(0..excess);
}
self.last_pushed_seq = Some(seq);
self.emit(UndoRedoEvent::StackChanged, target_stack_id);
}
pub fn can_undo(&self, stack_id: Option<u64>) -> bool {
let target_stack_id = stack_id.unwrap_or(0);
self.stacks
.get(&target_stack_id)
.map(|s| !s.undo_stack.is_empty())
.unwrap_or(false)
}
pub fn can_redo(&self, stack_id: Option<u64>) -> bool {
let target_stack_id = stack_id.unwrap_or(0);
self.stacks
.get(&target_stack_id)
.map(|s| !s.redo_stack.is_empty())
.unwrap_or(false)
}
pub fn clear_stack(&mut self, stack_id: u64) {
let cleared = if let Some(stack) = self.stacks.get_mut(&stack_id) {
stack.undo_stack.clear();
stack.redo_stack.clear();
true
} else {
false
};
if cleared {
self.emit(UndoRedoEvent::StackChanged, stack_id);
}
}
pub fn clear_all_stacks(&mut self) {
for stack in self.stacks.values_mut() {
stack.undo_stack.clear();
stack.redo_stack.clear();
}
self.in_progress_composite = None;
self.composite_nesting_level = 0;
self.composite_label = None;
self.composite_stack_id = None;
self.last_pushed_seq = None;
let ids: Vec<u64> = self.stacks.keys().copied().collect();
for id in ids {
self.emit(UndoRedoEvent::StackChanged, id);
}
}
pub fn create_new_stack(&mut self) -> u64 {
let id = self.next_stack_id;
self.stacks.insert(id, StackData::default());
self.next_stack_id += 1;
id
}
pub fn delete_stack(&mut self, stack_id: u64) -> Result<()> {
if stack_id == 0 {
return Err(anyhow!("Cannot delete the default stack"));
}
if self.stacks.remove(&stack_id).is_some() {
Ok(())
} else {
Err(anyhow!("Stack with ID {} does not exist", stack_id))
}
}
pub fn get_stack_size(&self, stack_id: u64) -> usize {
self.stacks
.get(&stack_id)
.map(|s| s.undo_stack.len())
.unwrap_or(0)
}
pub fn get_redo_stack_size(&self, stack_id: u64) -> usize {
self.stacks
.get(&stack_id)
.map(|s| s.redo_stack.len())
.unwrap_or(0)
}
pub fn last_pushed_seq(&self) -> Option<u64> {
self.last_pushed_seq
}
pub fn head_seq(&self, stack_id: Option<u64>) -> Option<u64> {
self.stacks
.get(&stack_id.unwrap_or(0))?
.undo_stack
.last()
.map(|e| e.seq)
}
pub fn undo_label(&self, stack_id: Option<u64>) -> Option<UndoLabel> {
self.stacks
.get(&stack_id.unwrap_or(0))?
.undo_stack
.last()?
.command
.label()
}
pub fn redo_label(&self, stack_id: Option<u64>) -> Option<UndoLabel> {
self.stacks
.get(&stack_id.unwrap_or(0))?
.redo_stack
.last()?
.command
.label()
}
pub fn seal_head(&mut self, stack_id: Option<u64>) {
if let Some(stack) = self.stacks.get_mut(&stack_id.unwrap_or(0))
&& let Some(last) = stack.undo_stack.last_mut()
{
last.sealed = true;
}
}
pub fn set_undo_limit(&mut self, limit: Option<usize>) {
self.undo_limit = limit;
}
pub fn undo_limit(&self) -> Option<usize> {
self.undo_limit
}
}