Files
cadcraft/crates/engine/src/undoctl.rs
T
2026-10-11 11:36:30 -04:00

171 lines
5.8 KiB
Rust

//! UNDO control: marks, BEgin/End groups and the Control/Auto settings of one drawing, kept on top
//! of its undo stack ([`DocState::undo`]). Every command records its step through
//! [`DocState::push_undo`], so marks, groups, Control and the stack limit apply to all of them
//! alike; a command never needs to know about them.
use crate::{DocState, Snapshot};
/// The most undo steps a drawing keeps; the oldest are dropped first.
pub const MAX_UNDO: usize = 2000;
/// The most marks a drawing keeps; the oldest are dropped first.
const MAX_MARKS: usize = 1000;
/// UNDO Control: how much undo information is kept.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Control {
/// Full undo (the default).
#[default]
All,
/// Undo is off: nothing is recorded.
None,
/// Only the last step can be undone.
One,
}
/// The UNDO settings and markers of one drawing.
#[derive(Clone, Debug)]
pub struct UndoCtl {
pub control: Control,
/// UNDO Auto. Every command already records one undo step for all it does, so this is kept and
/// reported (UNDOCTL) only.
pub auto: bool,
/// UNDO Control Combine: consecutive zooms and pans as one step. View changes record no undo
/// steps, so this is kept and reported only.
pub combine_view: bool,
/// UNDO Control Layer: layer property changes as one step. Kept and reported only.
pub combine_layer: bool,
/// Undo-stack depths at each UNDO Mark, oldest first.
pub marks: Vec<usize>,
/// Undo-stack depth when the open BEgin group started.
pub group: Option<usize>,
}
impl Default for UndoCtl {
fn default() -> Self {
UndoCtl { control: Control::All, auto: true, combine_view: true, combine_layer: true, marks: Vec::new(), group: None }
}
}
impl UndoCtl {
/// The UNDOCTL bit code: 1 undo on, 2 only one step, 4 Auto, 8 a group is open, 16 combine
/// zooms and pans, 32 combine layer changes.
pub fn bits(&self) -> i64 {
let mut b = 0;
if self.control != Control::None {
b |= 1;
}
if self.control == Control::One {
b |= 2;
}
if self.auto {
b |= 4;
}
if self.group.is_some() {
b |= 8;
}
if self.combine_view {
b |= 16;
}
if self.combine_layer {
b |= 32;
}
b
}
}
impl DocState {
/// Record an undo step (`snap` holds the drawing as it was before the change) and clear redo.
/// UNDO Control None records nothing and One keeps just this step; the stack holds at most
/// [`MAX_UNDO`] steps.
pub fn push_undo(&mut self, snap: Snapshot) {
self.redo.clear();
match self.undo_ctl.control {
Control::None => {
self.undo.clear();
return;
}
Control::One => {
self.undo.clear();
self.undo.push(snap);
return;
}
Control::All => {}
}
// Marks above the current depth were undone; a new step makes them unreachable for good.
let depth = self.undo.len();
self.undo_ctl.marks.retain(|m| *m <= depth);
if let Some(g) = self.undo_ctl.group
&& g > depth
{
self.undo_ctl.group = Some(depth);
}
self.undo.push(snap);
if let Some(extra) = self.undo.len().checked_sub(MAX_UNDO).filter(|e| *e > 0) {
self.undo.drain(..extra);
for m in &mut self.undo_ctl.marks {
*m = m.saturating_sub(extra);
}
if let Some(g) = &mut self.undo_ctl.group {
*g = g.saturating_sub(extra);
}
}
}
/// UNDO Mark at the current depth. Returns the number of marks.
pub fn undo_mark(&mut self) -> usize {
let depth = self.undo.len();
let marks = &mut self.undo_ctl.marks;
marks.retain(|m| *m <= depth);
marks.push(depth);
if marks.len() > MAX_MARKS {
marks.remove(0);
}
marks.len()
}
/// The marks still reachable from the current depth.
pub fn undo_marks(&self) -> usize {
let depth = self.undo.len();
self.undo_ctl.marks.iter().filter(|m| **m <= depth).count()
}
/// UNDO BEgin: the steps from here to UNDO End become one. BEgin while a group is open ends that
/// group first (returns true then).
pub fn begin_undo_group(&mut self) -> bool {
let ended = self.end_undo_group();
self.undo_ctl.group = Some(self.undo.len());
ended
}
/// UNDO End (also on save): collapse the steps since BEgin into one undo step. Returns whether
/// a group was open; End with no open group does nothing.
pub fn end_undo_group(&mut self) -> bool {
let Some(start) = self.undo_ctl.group.take() else { return false };
// The first step of the group holds the drawing as it was at BEgin.
if self.undo.len() > start {
self.undo.truncate(start + 1);
if let Some(first) = self.undo.get_mut(start) {
first.label = "Group".into();
}
}
// States inside the group no longer exist.
self.undo_ctl.marks.retain(|m| *m <= start);
true
}
/// UNDO Control: All, None (drops all undo information) or One (keeps only the last step).
/// Any open group and all marks are dropped when leaving All.
pub fn set_undo_control(&mut self, c: Control) {
self.undo_ctl.control = c;
if c == Control::All {
return;
}
self.undo_ctl.group = None;
self.undo_ctl.marks.clear();
self.redo.clear();
let keep = if c == Control::One { 1 } else { 0 };
let drop = self.undo.len().saturating_sub(keep);
self.undo.drain(..drop);
}
}