use alloc::{collections::VecDeque, vec::Vec};
use azul_core::{
dom::NodeId,
geom::LogicalPosition,
selection::{
CursorAffinity, GraphemeClusterId, OptionSelectionRange, OptionTextCursor, SelectionRange,
TextCursor,
},
task::Instant,
window::CursorPosition,
};
use azul_css::{impl_option, impl_option_inner, AzString};
use super::changeset::{TextChangeset, TextOperation};
pub const MAX_UNDO_HISTORY: usize = 10;
pub const MAX_REDO_HISTORY: usize = 10;
#[derive(Debug, Clone)]
#[repr(C)]
pub struct NodeStateSnapshot {
pub node_id: NodeId,
pub text_content: AzString,
pub cursor_position: OptionTextCursor,
pub selection_range: OptionSelectionRange,
pub timestamp: Instant,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct UndoableOperation {
pub changeset: TextChangeset,
pub pre_state: NodeStateSnapshot,
}
impl_option!(
UndoableOperation,
OptionUndoableOperation,
copy = false,
[Debug, Clone]
);
#[derive(Debug, Clone)]
pub struct NodeUndoRedoStack {
pub node_id: NodeId,
pub undo_stack: VecDeque<UndoableOperation>,
pub redo_stack: VecDeque<UndoableOperation>,
}
impl NodeUndoRedoStack {
pub fn new(node_id: NodeId) -> Self {
Self {
node_id,
undo_stack: VecDeque::with_capacity(MAX_UNDO_HISTORY),
redo_stack: VecDeque::with_capacity(MAX_REDO_HISTORY),
}
}
pub fn push_undo(&mut self, operation: UndoableOperation) {
self.redo_stack.clear();
self.undo_stack.push_back(operation);
if self.undo_stack.len() > MAX_UNDO_HISTORY {
self.undo_stack.pop_front();
}
}
pub fn pop_undo(&mut self) -> Option<UndoableOperation> {
self.undo_stack.pop_back()
}
pub fn push_redo(&mut self, operation: UndoableOperation) {
self.redo_stack.push_back(operation);
if self.redo_stack.len() > MAX_REDO_HISTORY {
self.redo_stack.pop_front();
}
}
pub fn pop_redo(&mut self) -> Option<UndoableOperation> {
self.redo_stack.pop_back()
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn peek_undo(&self) -> Option<&UndoableOperation> {
self.undo_stack.back()
}
pub fn peek_redo(&self) -> Option<&UndoableOperation> {
self.redo_stack.back()
}
}
#[derive(Debug, Clone, Default)]
pub struct UndoRedoManager {
pub node_stacks: Vec<NodeUndoRedoStack>,
}
impl UndoRedoManager {
pub fn new() -> Self {
Self {
node_stacks: Vec::new(),
}
}
pub fn get_or_create_stack_mut(&mut self, node_id: NodeId) -> &mut NodeUndoRedoStack {
let stack_exists = self.node_stacks.iter().any(|s| s.node_id == node_id);
if !stack_exists {
let stack = NodeUndoRedoStack::new(node_id);
self.node_stacks.push(stack);
}
self.node_stacks
.iter_mut()
.find(|s| s.node_id == node_id)
.unwrap()
}
pub fn get_stack(&self, node_id: NodeId) -> Option<&NodeUndoRedoStack> {
self.node_stacks.iter().find(|s| s.node_id == node_id)
}
fn get_stack_mut(&mut self, node_id: NodeId) -> Option<&mut NodeUndoRedoStack> {
self.node_stacks.iter_mut().find(|s| s.node_id == node_id)
}
pub fn record_operation(&mut self, changeset: TextChangeset, pre_state: NodeStateSnapshot) {
let node_id = changeset
.target
.node
.into_crate_internal()
.expect("TextChangeset target node should not be None");
let stack = self.get_or_create_stack_mut(node_id);
let operation = UndoableOperation {
changeset,
pre_state,
};
stack.push_undo(operation);
}
pub fn can_undo(&self, node_id: NodeId) -> bool {
self.get_stack(node_id)
.map(|s| s.can_undo())
.unwrap_or(false)
}
pub fn can_redo(&self, node_id: NodeId) -> bool {
self.get_stack(node_id)
.map(|s| s.can_redo())
.unwrap_or(false)
}
pub fn peek_undo(&self, node_id: NodeId) -> Option<&UndoableOperation> {
self.get_stack(node_id).and_then(|s| s.peek_undo())
}
pub fn peek_redo(&self, node_id: NodeId) -> Option<&UndoableOperation> {
self.get_stack(node_id).and_then(|s| s.peek_redo())
}
pub fn pop_undo(&mut self, node_id: NodeId) -> Option<UndoableOperation> {
self.get_stack_mut(node_id)?.pop_undo()
}
pub fn pop_redo(&mut self, node_id: NodeId) -> Option<UndoableOperation> {
self.get_stack_mut(node_id)?.pop_redo()
}
pub fn push_redo(&mut self, operation: UndoableOperation) {
let node_id = operation
.changeset
.target
.node
.into_crate_internal()
.expect("TextChangeset target node should not be None");
let stack = self.get_or_create_stack_mut(node_id);
stack.push_redo(operation);
}
pub fn push_undo(&mut self, operation: UndoableOperation) {
let node_id = operation
.changeset
.target
.node
.into_crate_internal()
.expect("TextChangeset target node should not be None");
let stack = self.get_or_create_stack_mut(node_id);
stack.push_undo(operation);
}
pub fn clear_node(&mut self, node_id: NodeId) {
if let Some(stack) = self.get_stack_mut(node_id) {
stack.undo_stack.clear();
stack.redo_stack.clear();
}
}
pub fn clear_all(&mut self) {
self.node_stacks.clear();
}
pub fn undo_depth(&self, node_id: NodeId) -> usize {
self.get_stack(node_id)
.map(|s| s.undo_stack.len())
.unwrap_or(0)
}
pub fn redo_depth(&self, node_id: NodeId) -> usize {
self.get_stack(node_id)
.map(|s| s.redo_stack.len())
.unwrap_or(0)
}
}
pub fn create_revert_changeset(operation: &UndoableOperation, timestamp: Instant) -> TextChangeset {
use crate::managers::changeset::{
TextOpClearSelection, TextOpCopy, TextOpCut, TextOpDeleteText, TextOpExtendSelection,
TextOpInsertText, TextOpMoveCursor, TextOpPaste, TextOpReplaceText, TextOpSelectAll,
TextOpSetSelection,
};
let revert_operation = match &operation.changeset.operation {
TextOperation::InsertText(op) => {
let dummy_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
let end_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: operation.pre_state.text_content.len() as u32,
},
affinity: CursorAffinity::Leading,
};
TextOperation::ReplaceText(TextOpReplaceText {
range: SelectionRange {
start: dummy_cursor,
end: end_cursor,
},
old_text: op.text.clone(), new_text: operation.pre_state.text_content.clone(), new_cursor: operation
.pre_state
.cursor_position
.as_ref()
.map(|_| {
CursorPosition::InWindow(azul_core::geom::LogicalPosition::new(0.0, 0.0))
})
.unwrap_or(CursorPosition::Uninitialized),
})
}
TextOperation::DeleteText(op) => {
let dummy_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
TextOperation::ReplaceText(TextOpReplaceText {
range: SelectionRange {
start: dummy_cursor,
end: dummy_cursor,
},
old_text: AzString::from(""),
new_text: operation.pre_state.text_content.clone(),
new_cursor: operation
.pre_state
.cursor_position
.as_ref()
.map(|_| {
CursorPosition::InWindow(azul_core::geom::LogicalPosition::new(0.0, 0.0))
})
.unwrap_or(CursorPosition::Uninitialized),
})
}
TextOperation::ReplaceText(op) => {
let end_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: op.new_text.len() as u32,
},
affinity: CursorAffinity::Leading,
};
TextOperation::ReplaceText(TextOpReplaceText {
range: SelectionRange {
start: TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
},
end: end_cursor,
},
old_text: op.new_text.clone(),
new_text: operation.pre_state.text_content.clone(),
new_cursor: operation
.pre_state
.cursor_position
.as_ref()
.map(|_| CursorPosition::InWindow(LogicalPosition::new(0.0, 0.0)))
.unwrap_or(CursorPosition::Uninitialized),
})
}
TextOperation::SetSelection(op) => TextOperation::SetSelection(TextOpSetSelection {
old_range: OptionSelectionRange::Some(op.new_range),
new_range: op.old_range.into_option().unwrap_or(op.new_range),
}),
TextOperation::ExtendSelection(op) => TextOperation::SetSelection(TextOpSetSelection {
old_range: OptionSelectionRange::Some(op.new_range),
new_range: op.old_range,
}),
TextOperation::ClearSelection(op) => TextOperation::SetSelection(TextOpSetSelection {
old_range: OptionSelectionRange::None,
new_range: op.old_range,
}),
TextOperation::MoveCursor(op) => {
TextOperation::MoveCursor(TextOpMoveCursor {
old_position: op.new_position,
new_position: op.old_position,
movement: op.movement, })
}
TextOperation::SelectAll(op) => {
if let OptionSelectionRange::Some(old_sel) = op.old_range {
TextOperation::SetSelection(TextOpSetSelection {
old_range: OptionSelectionRange::Some(op.new_range),
new_range: old_sel,
})
} else {
let dummy_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
TextOperation::SetSelection(TextOpSetSelection {
old_range: OptionSelectionRange::Some(op.new_range),
new_range: SelectionRange {
start: dummy_cursor,
end: dummy_cursor,
},
})
}
}
TextOperation::Copy(_) | TextOperation::Cut(_) | TextOperation::Paste(_) => {
operation.changeset.operation.clone()
}
};
TextChangeset::new(operation.changeset.target, revert_operation, timestamp)
}