#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
#![recursion_limit = "256"]
#![expect(clippy::type_complexity)]
use std::{
any::Any,
fmt, mem,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use atomic::Atomic;
use parking_lot::Mutex;
use zng_app::{
DInstant, INSTANT,
event::{Command, CommandNameExt, CommandScope, command},
hn,
shortcut::{CommandShortcutExt, shortcut},
widget::{
WIDGET, WidgetId,
info::{WidgetInfo, WidgetInfoBuilder},
},
};
use zng_app_context::{RunOnDrop, app_local, context_local};
use zng_clone_move::clmv;
use zng_ext_input::{focus::cmd::CommandFocusExt, keyboard::KEYBOARD};
use zng_state_map::{StateId, StateMapRef, static_id};
use zng_txt::Txt;
use zng_var::{Var, VarHandle, VarValue, context_var, var};
use zng_wgt::{CommandIconExt as _, ICONS, wgt_fn};
mod private {
pub trait Sealed {}
}
context_var! {
pub static UNDO_LIMIT_VAR: u32 = UNDO.undo_limit();
pub static UNDO_INTERVAL_VAR: Duration = UNDO.undo_interval();
}
pub struct UNDO;
impl UNDO {
pub fn undo_limit(&self) -> Var<u32> {
UNDO_SV.read().undo_limit.clone()
}
pub fn undo_interval(&self) -> Var<Duration> {
UNDO_SV.read().undo_interval.clone()
}
pub fn is_enabled(&self) -> bool {
UNDO_SCOPE_CTX.get().enabled.load(Ordering::Relaxed) && UNDO_SV.read().undo_limit.get() > 0
}
pub fn undo_select(&self, selector: impl UndoSelector) {
UNDO_SCOPE_CTX.get().undo_select(selector);
}
pub fn redo_select(&self, selector: impl UndoSelector) {
UNDO_SCOPE_CTX.get().redo_select(selector);
}
pub fn undo(&self) {
self.undo_select(UNDO_INTERVAL_VAR.get());
}
pub fn redo(&self) {
self.redo_select(UNDO_INTERVAL_VAR.get());
}
pub fn scope(&self) -> Option<WidgetId> {
UNDO_SCOPE_CTX.get().id()
}
pub fn register(&self, action: impl UndoAction) {
UNDO_SCOPE_CTX.get().register(Box::new(action))
}
pub fn register_op(&self, info: impl UndoInfo, op: impl FnMut(UndoOp) + Send + 'static) {
self.register(UndoRedoOp {
info: info.into_dyn(),
op: Box::new(op),
})
}
pub fn register_full_op<D>(&self, data: D, mut op: impl FnMut(&mut D, UndoFullOp) + Send + 'static)
where
D: Any + Send + 'static,
{
self.register(UndoRedoFullOp {
data: Box::new(data),
op: Box::new(move |d, o| {
op(d.downcast_mut::<D>().unwrap(), o);
}),
})
}
pub fn run(&self, action: impl RedoAction) {
UNDO_SCOPE_CTX.get().register(Box::new(action).redo())
}
pub fn run_op(&self, info: impl UndoInfo, op: impl FnMut(UndoOp) + Send + 'static) {
self.run(UndoRedoOp {
info: info.into_dyn(),
op: Box::new(op),
})
}
pub fn run_full_op<D>(&self, mut data: D, mut op: impl FnMut(&mut D, UndoFullOp) + Send + 'static)
where
D: Any + Send + 'static,
{
let mut redo = true;
op(&mut data, UndoFullOp::Init { redo: &mut redo });
if redo {
self.run(UndoRedoFullOp {
data: Box::new(data),
op: Box::new(move |d, o| {
op(d.downcast_mut::<D>().unwrap(), o);
}),
})
}
}
pub fn group(&self, info: impl UndoInfo, actions: impl FnOnce()) -> bool {
let t = self.transaction(actions);
let any = !t.is_empty();
if any {
t.commit_group(info);
}
any
}
pub fn transaction(&self, actions: impl FnOnce()) -> UndoTransaction {
let mut scope = UndoScope::default();
let parent_scope = UNDO_SCOPE_CTX.get();
*scope.enabled.get_mut() = parent_scope.enabled.load(Ordering::Relaxed);
*scope.id.get_mut() = parent_scope.id.load(Ordering::Relaxed);
let t_scope = Arc::new(scope);
let _panic_undo = RunOnDrop::new(clmv!(t_scope, || {
for undo in mem::take(&mut *t_scope.undo.lock()).into_iter().rev() {
let _ = undo.action.undo();
}
}));
let mut scope = Some(t_scope);
UNDO_SCOPE_CTX.with_context(&mut scope, actions);
let scope = scope.unwrap();
let undo = mem::take(&mut *scope.undo.lock());
UndoTransaction { undo }
}
pub fn try_group<O, E>(&self, info: impl UndoInfo, actions: impl FnOnce() -> Result<O, E>) -> Result<O, E> {
let mut r = None;
let t = self.transaction(|| r = Some(actions()));
let r = r.unwrap();
if !t.is_empty() {
if r.is_ok() {
t.commit_group(info);
} else {
t.undo();
}
}
r
}
pub fn try_commit<O, E>(&self, actions: impl FnOnce() -> Result<O, E>) -> Result<O, E> {
let mut r = None;
let t = self.transaction(|| r = Some(actions()));
let r = r.unwrap();
if !t.is_empty() {
if r.is_ok() {
t.commit();
} else {
t.undo();
}
}
r
}
pub fn with_scope<R>(&self, scope: &mut WidgetUndoScope, f: impl FnOnce() -> R) -> R {
UNDO_SCOPE_CTX.with_context(&mut scope.0, f)
}
pub fn with_disabled<R>(&self, f: impl FnOnce() -> R) -> R {
let mut scope = UndoScope::default();
let parent_scope = UNDO_SCOPE_CTX.get();
*scope.enabled.get_mut() = false;
*scope.id.get_mut() = parent_scope.id.load(Ordering::Relaxed);
UNDO_SCOPE_CTX.with_context(&mut Some(Arc::new(scope)), f)
}
pub fn watch_var<T: VarValue>(&self, info: impl UndoInfo, var: Var<T>) -> VarHandle {
if var.capabilities().is_always_read_only() {
return VarHandle::dummy();
}
let var = var.current_context();
let wk_var = var.downgrade();
let mut prev_value = Some(var.get());
let info = info.into_dyn();
var.trace_value(move |args| {
if args.downcast_tags::<UndoVarModifyTag>().next().is_none() {
let prev = prev_value.take().unwrap();
let new = args.value();
if &prev == new {
prev_value = Some(prev);
return;
}
prev_value = Some(new.clone());
UNDO.register_op(
info.clone(),
clmv!(wk_var, new, |op| if let Some(var) = wk_var.upgrade() {
match op {
UndoOp::Undo => var.modify(clmv!(prev, |args| {
args.set(prev);
args.push_tag(UndoVarModifyTag);
})),
UndoOp::Redo => var.modify(clmv!(new, |args| {
args.set(new);
args.push_tag(UndoVarModifyTag);
})),
};
}),
);
}
})
}
pub fn clear_redo(&self) {
UNDO_SCOPE_CTX.get().redo.lock().clear();
}
pub fn clear(&self) {
let ctx = UNDO_SCOPE_CTX.get();
let mut u = ctx.undo.lock();
u.clear();
ctx.redo.lock().clear();
}
pub fn can_undo(&self) -> bool {
!UNDO_SCOPE_CTX.get().undo.lock().is_empty()
}
pub fn can_redo(&self) -> bool {
!UNDO_SCOPE_CTX.get().redo.lock().is_empty()
}
pub fn undo_stack(&self) -> UndoStackInfo {
UndoStackInfo::undo(&UNDO_SCOPE_CTX.get(), UNDO_INTERVAL_VAR.get())
}
pub fn redo_stack(&self) -> UndoStackInfo {
UndoStackInfo::redo(&UNDO_SCOPE_CTX.get(), UNDO_INTERVAL_VAR.get())
}
}
#[derive(Clone)]
pub struct UndoStackInfo {
pub stack: Vec<(DInstant, Arc<dyn UndoInfo>)>,
pub undo_interval: Duration,
}
impl UndoStackInfo {
fn undo(ctx: &UndoScope, undo_interval: Duration) -> Self {
Self {
stack: ctx.undo.lock().iter_mut().map(|e| (e.timestamp, e.action.info())).collect(),
undo_interval,
}
}
fn redo(ctx: &UndoScope, undo_interval: Duration) -> Self {
Self {
stack: ctx.redo.lock().iter_mut().map(|e| (e.timestamp, e.action.info())).collect(),
undo_interval,
}
}
pub fn iter_groups(&self) -> impl DoubleEndedIterator<Item = &[(DInstant, Arc<dyn UndoInfo>)]> {
struct Iter<'a> {
stack: &'a [(DInstant, Arc<dyn UndoInfo>)],
interval: Duration,
ts_inverted: bool,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a [(DInstant, Arc<dyn UndoInfo>)];
fn next(&mut self) -> Option<Self::Item> {
if self.stack.is_empty() {
None
} else {
let mut older = self.stack[0].0;
let mut r = self.stack;
if let Some(i) = self.stack.iter().position(|(newer, _)| {
let (a, b) = if self.ts_inverted { (older, *newer) } else { (*newer, older) };
let break_ = a.saturating_duration_since(b) > self.interval;
older = *newer;
break_
}) {
r = &self.stack[..i];
self.stack = &self.stack[i..];
} else {
self.stack = &[];
}
Some(r)
}
}
}
impl DoubleEndedIterator for Iter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.stack.is_empty() {
None
} else {
let mut newer = self.stack[self.stack.len() - 1].0;
let mut r = self.stack;
if let Some(i) = self.stack.iter().rposition(|(older, _)| {
let (a, b) = if self.ts_inverted { (*older, newer) } else { (newer, *older) };
let break_ = a.saturating_duration_since(b) > self.interval;
newer = *older;
break_
}) {
let i = i + 1;
r = &self.stack[i..];
self.stack = &self.stack[..i];
} else {
self.stack = &[];
}
Some(r)
}
}
}
Iter {
stack: &self.stack,
interval: self.undo_interval,
ts_inverted: self.stack.len() > 1 && self.stack[0].0 > self.stack[self.stack.len() - 1].0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UndoVarModifyTag;
pub trait UndoInfo: Send + Sync + Any {
fn description(&self) -> Txt;
fn meta(&self) -> StateMapRef<'_, UNDO> {
StateMapRef::empty()
}
fn into_dyn(self) -> Arc<dyn UndoInfo>
where
Self: Sized,
{
Arc::new(self)
}
}
impl UndoInfo for Txt {
fn description(&self) -> Txt {
self.clone()
}
}
impl UndoInfo for Var<Txt> {
fn description(&self) -> Txt {
self.get()
}
}
impl UndoInfo for &'static str {
fn description(&self) -> Txt {
Txt::from_static(self)
}
}
impl UndoInfo for Arc<dyn UndoInfo> {
fn description(&self) -> Txt {
self.as_ref().description()
}
fn meta(&self) -> StateMapRef<'_, UNDO> {
self.as_ref().meta()
}
fn into_dyn(self) -> Arc<dyn UndoInfo>
where
Self: Sized,
{
self
}
}
pub trait UndoAction: Send + Any {
fn info(&mut self) -> Arc<dyn UndoInfo>;
fn undo(self: Box<Self>) -> Box<dyn RedoAction>;
fn as_any(&mut self) -> &mut dyn Any;
fn merge(self: Box<Self>, args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)>;
}
pub struct UndoActionMergeArgs {
pub next: Box<dyn UndoAction>,
pub prev_timestamp: DInstant,
pub within_undo_interval: bool,
}
pub trait RedoAction: Send + Any {
fn info(&mut self) -> Arc<dyn UndoInfo>;
fn redo(self: Box<Self>) -> Box<dyn UndoAction>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UndoOp {
Undo,
Redo,
}
impl UndoOp {
pub fn cmd(self) -> Command {
match self {
UndoOp::Undo => UNDO_CMD,
UndoOp::Redo => REDO_CMD,
}
}
}
pub enum UndoFullOp<'r> {
Init {
redo: &'r mut bool,
},
Op(UndoOp),
Info {
info: &'r mut Option<Arc<dyn UndoInfo>>,
},
Merge {
next_data: &'r mut dyn Any,
prev_timestamp: DInstant,
within_undo_interval: bool,
merged: &'r mut bool,
},
}
impl fmt::Debug for UndoFullOp<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Init { .. } => f.debug_struct("Init").finish_non_exhaustive(),
Self::Op(arg0) => f.debug_tuple("Op").field(arg0).finish(),
Self::Info { .. } => f.debug_struct("Info").finish_non_exhaustive(),
Self::Merge { .. } => f.debug_struct("Merge").finish_non_exhaustive(),
}
}
}
#[must_use = "dropping the transaction undoes all captured actions"]
pub struct UndoTransaction {
undo: Vec<UndoEntry>,
}
impl UndoTransaction {
pub fn is_empty(&self) -> bool {
self.undo.is_empty()
}
pub fn commit(mut self) {
let mut undo = mem::take(&mut self.undo);
let now = INSTANT.now();
for u in &mut undo {
u.timestamp = now;
}
let ctx = UNDO_SCOPE_CTX.get();
let mut ctx_undo = ctx.undo.lock();
if ctx_undo.is_empty() {
*ctx_undo = undo;
} else {
ctx_undo.extend(undo);
}
}
pub fn commit_group(mut self, info: impl UndoInfo) {
UNDO.register(UndoGroup {
info: info.into_dyn(),
undo: mem::take(&mut self.undo),
})
}
pub fn undo(self) {
let _ = self;
}
}
impl Drop for UndoTransaction {
fn drop(&mut self) {
for undo in self.undo.drain(..).rev() {
let _ = undo.action.undo();
}
}
}
command! {
pub static UNDO_CMD {
l10n!: true,
name: "Undo",
shortcut: [shortcut!(CTRL + 'Z')],
icon: wgt_fn!(|_| ICONS.get("undo")),
init: |_| {
let _ = UNDO_SV.read();
},
};
pub static REDO_CMD {
l10n!: true,
name: "Redo",
shortcut: [shortcut!(CTRL + 'Y')],
icon: wgt_fn!(|_| ICONS.get("redo")),
init: |_| {
let _ = UNDO_SV.read();
},
};
pub static CLEAR_HISTORY_CMD {
l10n!: true,
name: "Clear History",
init: |_| {
let _ = UNDO_SV.read();
},
};
}
pub struct WidgetUndoScope(Option<Arc<UndoScope>>);
impl WidgetUndoScope {
pub const fn new() -> Self {
Self(None)
}
pub fn is_inited(&self) -> bool {
self.0.is_some()
}
pub fn init(&mut self) {
let mut scope = UndoScope::default();
let id = WIDGET.id();
*scope.id.get_mut() = Some(id);
let scope = Arc::new(scope);
let wk_scope = Arc::downgrade(&scope);
let interval = UNDO_INTERVAL_VAR.current_context();
UNDO_CMD
.scoped(id)
.with_meta(|m| m.set(*WEAK_UNDO_SCOPE_ID, (wk_scope.clone(), interval.clone())));
REDO_CMD.scoped(id).with_meta(|m| m.set(*WEAK_UNDO_SCOPE_ID, (wk_scope, interval)));
self.0 = Some(scope);
}
pub fn info(&mut self, info: &mut WidgetInfoBuilder) {
info.flag_meta(*FOCUS_SCOPE_ID);
}
pub fn deinit(&mut self) {
self.0 = None;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.0.as_ref().unwrap().enabled.store(enabled, Ordering::Relaxed);
}
pub fn can_undo(&self) -> bool {
!self.0.as_ref().unwrap().undo.lock().is_empty()
}
pub fn can_redo(&self) -> bool {
!self.0.as_ref().unwrap().redo.lock().is_empty()
}
}
impl Default for WidgetUndoScope {
fn default() -> Self {
Self::new()
}
}
struct UndoScope {
id: Atomic<Option<WidgetId>>,
undo: Mutex<Vec<UndoEntry>>,
redo: Mutex<Vec<RedoEntry>>,
enabled: AtomicBool,
}
impl Default for UndoScope {
fn default() -> Self {
Self {
id: Default::default(),
undo: Default::default(),
redo: Default::default(),
enabled: AtomicBool::new(true),
}
}
}
impl UndoScope {
fn with_enabled_undo_redo(&self, f: impl FnOnce(&mut Vec<UndoEntry>, &mut Vec<RedoEntry>)) {
let mut undo = self.undo.lock();
let mut redo = self.redo.lock();
let max_undo = if self.enabled.load(Ordering::Relaxed) {
UNDO_LIMIT_VAR.get() as usize
} else {
tracing::debug!("not enabled, will cleanup");
0
};
if undo.len() > max_undo {
undo.reverse();
while undo.len() > max_undo {
undo.pop();
}
undo.reverse();
}
if redo.len() > max_undo {
redo.reverse();
while redo.len() > max_undo {
redo.pop();
}
redo.reverse();
}
if max_undo > 0 {
f(&mut undo, &mut redo);
}
}
fn register(&self, mut action: Box<dyn UndoAction>) {
tracing::trace!("register '{}'", action.info().description());
self.with_enabled_undo_redo(|undo, redo| {
let now = INSTANT.now();
if let Some(prev) = undo.pop() {
match prev.action.merge(UndoActionMergeArgs {
next: action,
prev_timestamp: prev.timestamp,
within_undo_interval: now.duration_since(prev.timestamp) <= UNDO_SV.read().undo_interval.get(),
}) {
Ok(merged) => undo.push(UndoEntry {
timestamp: now,
action: merged,
}),
Err((p, action)) => {
undo.push(UndoEntry {
timestamp: prev.timestamp,
action: p,
});
undo.push(UndoEntry { timestamp: now, action });
}
}
} else {
undo.push(UndoEntry { timestamp: now, action });
}
redo.clear();
});
}
fn undo_select(&self, selector: impl UndoSelector) {
let _s = tracing::trace_span!("undo").entered();
let mut actions = vec![];
self.with_enabled_undo_redo(|undo, _| {
let mut select = selector.select(UndoOp::Undo);
while let Some(entry) = undo.last() {
if select.include(entry.timestamp) {
actions.push(undo.pop().unwrap());
} else {
break;
}
}
});
for mut undo in actions {
tracing::trace!("undo '{}'", undo.action.info().description());
let redo = undo.action.undo();
self.redo.lock().push(RedoEntry {
timestamp: undo.timestamp,
action: redo,
});
}
}
fn redo_select(&self, selector: impl UndoSelector) {
let _s = tracing::trace_span!("redo").entered();
let mut actions = vec![];
self.with_enabled_undo_redo(|_, redo| {
let mut select = selector.select(UndoOp::Redo);
while let Some(entry) = redo.last() {
if select.include(entry.timestamp) {
actions.push(redo.pop().unwrap());
} else {
break;
}
}
});
for mut redo in actions {
tracing::trace!("redo '{}'", redo.action.info().description());
let undo = redo.action.redo();
self.undo.lock().push(UndoEntry {
timestamp: redo.timestamp,
action: undo,
});
}
}
fn id(&self) -> Option<WidgetId> {
self.id.load(Ordering::Relaxed)
}
}
struct UndoEntry {
timestamp: DInstant,
action: Box<dyn UndoAction>,
}
struct RedoEntry {
pub timestamp: DInstant,
pub action: Box<dyn RedoAction>,
}
struct UndoGroup {
info: Arc<dyn UndoInfo>,
undo: Vec<UndoEntry>,
}
impl UndoAction for UndoGroup {
fn undo(self: Box<Self>) -> Box<dyn RedoAction> {
let mut redo = Vec::with_capacity(self.undo.len());
for undo in self.undo.into_iter().rev() {
redo.push(RedoEntry {
timestamp: undo.timestamp,
action: undo.action.undo(),
});
}
Box::new(RedoGroup { info: self.info, redo })
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
self.info.clone()
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
fn merge(self: Box<Self>, args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)> {
Err((self, args.next))
}
}
struct RedoGroup {
info: Arc<dyn UndoInfo>,
redo: Vec<RedoEntry>,
}
impl RedoAction for RedoGroup {
fn redo(self: Box<Self>) -> Box<dyn UndoAction> {
let mut undo = Vec::with_capacity(self.redo.len());
for redo in self.redo.into_iter().rev() {
undo.push(UndoEntry {
timestamp: redo.timestamp,
action: redo.action.redo(),
});
}
Box::new(UndoGroup { info: self.info, undo })
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
self.info.clone()
}
}
struct UndoRedoOp {
info: Arc<dyn UndoInfo>,
op: Box<dyn FnMut(UndoOp) + Send>,
}
impl UndoAction for UndoRedoOp {
fn undo(mut self: Box<Self>) -> Box<dyn RedoAction> {
(self.op)(UndoOp::Undo);
self
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
self.info.clone()
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
fn merge(self: Box<Self>, args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)> {
Err((self, args.next))
}
}
impl RedoAction for UndoRedoOp {
fn redo(mut self: Box<Self>) -> Box<dyn UndoAction> {
(self.op)(UndoOp::Redo);
self
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
self.info.clone()
}
}
struct UndoRedoFullOp {
data: Box<dyn Any + Send>,
op: Box<dyn FnMut(&mut dyn Any, UndoFullOp) + Send>,
}
impl UndoAction for UndoRedoFullOp {
fn info(&mut self) -> Arc<dyn UndoInfo> {
let mut info = None;
(self.op)(&mut self.data, UndoFullOp::Info { info: &mut info });
info.unwrap_or_else(|| Arc::new("action"))
}
fn undo(mut self: Box<Self>) -> Box<dyn RedoAction> {
(self.op)(&mut self.data, UndoFullOp::Op(UndoOp::Undo));
self
}
fn merge(mut self: Box<Self>, mut args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)>
where
Self: Sized,
{
if let Some(u) = args.next.as_any().downcast_mut::<Self>() {
let mut merged = false;
(self.op)(
&mut self.data,
UndoFullOp::Merge {
next_data: &mut u.data,
prev_timestamp: args.prev_timestamp,
within_undo_interval: args.within_undo_interval,
merged: &mut merged,
},
);
if merged { Ok(self) } else { Err((self, args.next)) }
} else {
Err((self, args.next))
}
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
}
impl RedoAction for UndoRedoFullOp {
fn info(&mut self) -> Arc<dyn UndoInfo> {
let mut info = None;
(self.op)(&mut self.data, UndoFullOp::Info { info: &mut info });
info.unwrap_or_else(|| Arc::new("action"))
}
fn redo(mut self: Box<Self>) -> Box<dyn UndoAction> {
(self.op)(&mut self.data, UndoFullOp::Op(UndoOp::Redo));
self
}
}
struct UndoService {
undo_limit: Var<u32>,
undo_interval: Var<Duration>,
}
impl Default for UndoService {
fn default() -> Self {
Self {
undo_limit: var(u32::MAX),
undo_interval: KEYBOARD.repeat_config().map(|c| c.start_delay + c.interval).cow(),
}
}
}
context_local! {
static UNDO_SCOPE_CTX: UndoScope = UndoScope::default();
}
app_local! {
static UNDO_SV: UndoService = {
hooks();
UndoService::default()
};
}
fn hooks() {
UNDO_CMD
.on_event(
true,
true,
false,
hn!(|args| {
args.propagation.stop();
if let Some(c) = args.param::<u32>() {
UNDO.undo_select(*c);
} else if let Some(i) = args.param::<Duration>() {
UNDO.undo_select(*i);
} else if let Some(t) = args.param::<DInstant>() {
UNDO.undo_select(*t);
} else {
UNDO.undo();
}
}),
)
.perm();
REDO_CMD
.on_event(
true,
true,
false,
hn!(|args| {
args.propagation.stop();
if let Some(c) = args.param::<u32>() {
UNDO.redo_select(*c);
} else if let Some(i) = args.param::<Duration>() {
UNDO.redo_select(*i);
} else if let Some(t) = args.param::<DInstant>() {
UNDO.redo_select(*t);
} else {
UNDO.redo();
}
}),
)
.perm();
}
pub trait WidgetInfoUndoExt {
fn is_undo_scope(&self) -> bool;
fn undo_scope(&self) -> Option<WidgetInfo>;
}
impl WidgetInfoUndoExt for WidgetInfo {
fn is_undo_scope(&self) -> bool {
self.meta().flagged(*FOCUS_SCOPE_ID)
}
fn undo_scope(&self) -> Option<WidgetInfo> {
self.ancestors().find(WidgetInfoUndoExt::is_undo_scope)
}
}
static_id! {
static ref FOCUS_SCOPE_ID: StateId<()>;
}
pub trait CommandUndoExt {
fn undo_scoped(self) -> Var<Command>;
fn undo_stack(self) -> UndoStackInfo;
fn redo_stack(self) -> UndoStackInfo;
}
impl CommandUndoExt for Command {
fn undo_scoped(self) -> Var<Command> {
self.focus_scoped_with(|w| match w {
Some(w) => {
if w.is_undo_scope() {
CommandScope::Widget(w.id())
} else if let Some(scope) = w.undo_scope() {
CommandScope::Widget(scope.id())
} else {
CommandScope::App
}
}
None => CommandScope::App,
})
}
fn undo_stack(self) -> UndoStackInfo {
let scope = self.with_meta(|m| m.get(*WEAK_UNDO_SCOPE_ID));
if let Some(scope) = scope
&& let Some(s) = scope.0.upgrade()
{
return UndoStackInfo::undo(&s, scope.1.get());
}
if let CommandScope::App = self.scope() {
let mut r = UNDO_SCOPE_CTX.with_default(|| UNDO.undo_stack());
r.undo_interval = UNDO.undo_interval().get();
return r;
}
UndoStackInfo {
stack: vec![],
undo_interval: Duration::ZERO,
}
}
fn redo_stack(self) -> UndoStackInfo {
let scope = self.with_meta(|m| m.get(*WEAK_UNDO_SCOPE_ID));
if let Some(scope) = scope
&& let Some(s) = scope.0.upgrade()
{
return UndoStackInfo::redo(&s, scope.1.get());
}
if let CommandScope::App = self.scope() {
let mut r = UNDO_SCOPE_CTX.with_default(|| UNDO.redo_stack());
r.undo_interval = UNDO.undo_interval().get();
return r;
}
UndoStackInfo {
stack: vec![],
undo_interval: Duration::ZERO,
}
}
}
static_id! {
static ref WEAK_UNDO_SCOPE_ID: StateId<(std::sync::Weak<UndoScope>, Var<Duration>)>;
}
pub trait UndoSelector: crate::private::Sealed {
type Select: UndoSelect;
fn select(self, op: UndoOp) -> Self::Select;
}
pub trait UndoSelect {
fn include(&mut self, timestamp: DInstant) -> bool;
}
impl crate::private::Sealed for u32 {}
impl UndoSelector for u32 {
type Select = u32;
fn select(self, op: UndoOp) -> Self::Select {
let _ = op;
self
}
}
impl UndoSelect for u32 {
fn include(&mut self, _: DInstant) -> bool {
let i = *self > 0;
if i {
*self -= 1;
}
i
}
}
impl crate::private::Sealed for Duration {}
impl UndoSelector for Duration {
type Select = UndoSelectInterval;
fn select(self, op: UndoOp) -> Self::Select {
UndoSelectInterval {
prev: None,
interval: self,
op,
}
}
}
#[doc(hidden)]
pub struct UndoSelectInterval {
prev: Option<DInstant>,
interval: Duration,
op: UndoOp,
}
impl UndoSelect for UndoSelectInterval {
fn include(&mut self, timestamp: DInstant) -> bool {
if let Some(prev) = &mut self.prev {
let (older, newer) = match self.op {
UndoOp::Undo => (timestamp, *prev),
UndoOp::Redo => (*prev, timestamp),
};
if newer.saturating_duration_since(older) <= self.interval {
*prev = timestamp;
true
} else {
false
}
} else {
self.prev = Some(timestamp);
true
}
}
}
impl crate::private::Sealed for DInstant {}
impl UndoSelector for DInstant {
type Select = UndoSelectLtEq;
fn select(self, op: UndoOp) -> Self::Select {
UndoSelectLtEq { instant: self, op }
}
}
#[doc(hidden)]
pub struct UndoSelectLtEq {
instant: DInstant,
op: UndoOp,
}
impl UndoSelect for UndoSelectLtEq {
fn include(&mut self, timestamp: DInstant) -> bool {
match self.op {
UndoOp::Undo => timestamp >= self.instant,
UndoOp::Redo => timestamp <= self.instant,
}
}
}
#[cfg(test)]
mod tests {
use zng_app::APP;
use super::*;
#[test]
fn register() {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![1, 2]));
UNDO.register(PushAction {
data: data.clone(),
item: 1,
});
UNDO.register(PushAction {
data: data.clone(),
item: 2,
});
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[1], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[] as &[u8], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
}
fn push_1_2(data: &Arc<Mutex<Vec<u8>>>) {
UNDO.run_op(
"push 1",
clmv!(data, |op| match op {
UndoOp::Undo => assert_eq!(data.lock().pop(), Some(1)),
UndoOp::Redo => data.lock().push(1),
}),
);
UNDO.run_op(
"push 2",
clmv!(data, |op| match op {
UndoOp::Undo => assert_eq!(data.lock().pop(), Some(2)),
UndoOp::Redo => data.lock().push(2),
}),
);
}
#[test]
fn run_op() {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![]));
push_1_2(&data);
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[1], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[] as &[u8], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
}
#[test]
fn transaction_undo() {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![]));
let t = UNDO.transaction(|| {
push_1_2(&data);
});
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
t.undo();
assert_eq!(&[] as &[u8], &data.lock()[..]);
}
#[test]
fn transaction_commit() {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![]));
let t = UNDO.transaction(|| {
push_1_2(&data);
});
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
t.commit();
UNDO.undo_select(1);
assert_eq!(&[1], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[] as &[u8], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
}
#[test]
fn transaction_group() {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![]));
let t = UNDO.transaction(|| {
push_1_2(&data);
});
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
t.commit_group("push 1, 2");
UNDO.undo_select(1);
assert_eq!(&[] as &[u8], &data.lock()[..]);
UNDO.redo_select(1);
assert_eq!(&[1, 2], &data.lock()[..]);
}
fn push_1_sleep_2(data: &Arc<Mutex<Vec<u8>>>) {
UNDO.run_op(
"push 1",
clmv!(data, |op| match op {
UndoOp::Undo => assert_eq!(data.lock().pop(), Some(1)),
UndoOp::Redo => data.lock().push(1),
}),
);
std::thread::sleep(Duration::from_millis(100));
UNDO.run_op(
"push 2",
clmv!(data, |op| match op {
UndoOp::Undo => assert_eq!(data.lock().pop(), Some(2)),
UndoOp::Redo => data.lock().push(2),
}),
);
}
#[test]
fn undo_redo_t_zero() {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![]));
push_1_sleep_2(&data);
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(Duration::ZERO);
assert_eq!(&[1], &data.lock()[..]);
UNDO.undo_select(Duration::ZERO);
assert_eq!(&[] as &[u8], &data.lock()[..]);
UNDO.redo_select(Duration::ZERO);
assert_eq!(&[1], &data.lock()[..]);
UNDO.redo_select(Duration::ZERO);
assert_eq!(&[1, 2], &data.lock()[..]);
}
#[test]
fn undo_redo_t_max() {
undo_redo_t_large(Duration::MAX);
}
#[test]
fn undo_redo_t_10s() {
undo_redo_t_large(Duration::from_secs(10));
}
fn undo_redo_t_large(t: Duration) {
let _a = APP.minimal().run_headless(false);
let data = Arc::new(Mutex::new(vec![]));
push_1_sleep_2(&data);
assert_eq!(&[1, 2], &data.lock()[..]);
UNDO.undo_select(t);
assert_eq!(&[] as &[u8], &data.lock()[..]);
UNDO.redo_select(t);
assert_eq!(&[1, 2], &data.lock()[..]);
}
#[test]
fn watch_var() {
let mut app = APP.minimal().run_headless(false);
let test_var = var(0);
UNDO.watch_var("set test var", test_var.clone()).perm();
test_var.set(10);
app.update(false).assert_wait();
test_var.set(20);
app.update(false).assert_wait();
assert_eq!(20, test_var.get());
UNDO.undo_select(1);
app.update(false).assert_wait();
assert_eq!(10, test_var.get());
UNDO.undo_select(1);
app.update(false).assert_wait();
assert_eq!(0, test_var.get());
UNDO.redo_select(1);
app.update(false).assert_wait();
assert_eq!(10, test_var.get());
UNDO.redo_select(1);
app.update(false).assert_wait();
assert_eq!(20, test_var.get());
}
struct PushAction {
data: Arc<Mutex<Vec<u8>>>,
item: u8,
}
impl UndoAction for PushAction {
fn undo(self: Box<Self>) -> Box<dyn RedoAction> {
assert_eq!(self.data.lock().pop(), Some(self.item));
self
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
Arc::new("push")
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
fn merge(self: Box<Self>, args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)> {
Err((self, args.next))
}
}
impl RedoAction for PushAction {
fn redo(self: Box<Self>) -> Box<dyn UndoAction> {
self.data.lock().push(self.item);
self
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
Arc::new("push")
}
}
}