zng_wgt_undo/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Undo properties.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use std::time::Duration;
13
14use zng_ext_undo::*;
15use zng_wgt::prelude::*;
16
17/// Sets if the widget is an undo scope.
18///
19/// If `true` the widget will handle [`UNDO_CMD`] and [`REDO_CMD`] for all undo actions
20/// that happen inside it.
21///
22/// [`UNDO_CMD`]: static@zng_ext_undo::UNDO_CMD
23/// [`REDO_CMD`]: static@zng_ext_undo::REDO_CMD
24#[property(CONTEXT - 10, default(false))]
25pub fn undo_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
26    let mut scope = WidgetUndoScope::new();
27    let mut undo_cmd = CommandHandle::dummy();
28    let mut redo_cmd = CommandHandle::dummy();
29    let mut clear_cmd = CommandHandle::dummy();
30    let is_scope = is_scope.into_var();
31    let mut can_undo = false;
32    let mut can_redo = false;
33    match_node(child, move |c, mut op| {
34        match &mut op {
35            UiNodeOp::Init => {
36                WIDGET.sub_var(&is_scope);
37
38                if !is_scope.get() {
39                    return; // default handling without scope context.
40                }
41
42                scope.init();
43
44                let id = WIDGET.id();
45                undo_cmd = UNDO_CMD.scoped(id).subscribe(false);
46                redo_cmd = REDO_CMD.scoped(id).subscribe(false);
47                clear_cmd = CLEAR_HISTORY_CMD.scoped(id).subscribe(false);
48                can_undo = false;
49                can_redo = false;
50            }
51            UiNodeOp::Deinit => {
52                if !is_scope.get() {
53                    return;
54                }
55
56                UNDO.with_scope(&mut scope, || c.deinit());
57                scope.deinit();
58                undo_cmd = CommandHandle::dummy();
59                redo_cmd = CommandHandle::dummy();
60                return;
61            }
62            UiNodeOp::Info { info } => {
63                if !is_scope.get() {
64                    return;
65                }
66                scope.info(info);
67            }
68            UiNodeOp::Update { .. } => {
69                if let Some(is_scope) = is_scope.get_new() {
70                    WIDGET.update_info();
71
72                    if is_scope {
73                        if !scope.is_inited() {
74                            scope.init();
75
76                            let id = WIDGET.id();
77                            undo_cmd = UNDO_CMD.scoped(id).subscribe(false);
78                            redo_cmd = REDO_CMD.scoped(id).subscribe(false);
79                        }
80                    } else if scope.is_inited() {
81                        scope.deinit();
82                        undo_cmd = CommandHandle::dummy();
83                        redo_cmd = CommandHandle::dummy();
84                    }
85                }
86                if !is_scope.get() {
87                    return;
88                }
89
90                let id = WIDGET.id();
91                UNDO_CMD.scoped(id).each_update(false, false, |args| {
92                    args.propagation.stop();
93                    UNDO.with_scope(&mut scope, || {
94                        if let Some(&n) = args.param::<u32>() {
95                            UNDO.undo_select(n);
96                        } else if let Some(&i) = args.param::<Duration>() {
97                            UNDO.undo_select(i);
98                        } else if let Some(&t) = args.param::<DInstant>() {
99                            UNDO.undo_select(t);
100                        } else {
101                            UNDO.undo();
102                        }
103                    });
104                });
105                REDO_CMD.scoped(id).each_update(false, false, |args| {
106                    args.propagation.stop();
107                    UNDO.with_scope(&mut scope, || {
108                        if let Some(&n) = args.param::<u32>() {
109                            UNDO.redo_select(n);
110                        } else if let Some(&i) = args.param::<Duration>() {
111                            UNDO.redo_select(i);
112                        } else if let Some(&t) = args.param::<DInstant>() {
113                            UNDO.redo_select(t);
114                        } else {
115                            UNDO.redo();
116                        }
117                    });
118                });
119                CLEAR_HISTORY_CMD.scoped(id).each_update(false, false, |args| {
120                    args.propagation.stop();
121                    UNDO.with_scope(&mut scope, || {
122                        UNDO.clear();
123                    });
124                });
125            }
126            _ => {
127                if !is_scope.get() {
128                    return;
129                }
130            }
131        }
132
133        UNDO.with_scope(&mut scope, || c.op(op));
134
135        let new_can_undo = scope.can_undo();
136        let new_can_redo = scope.can_redo();
137
138        if can_undo != new_can_undo || can_redo != new_can_redo {
139            tracing::trace!("undo_scope={:?}, undo={new_can_undo:?}, redo={new_can_redo:?}", WIDGET.id());
140            can_undo = new_can_undo;
141            can_redo = new_can_redo;
142            undo_cmd.enabled().set(can_undo);
143            redo_cmd.enabled().set(can_redo);
144            clear_cmd.enabled().set(can_undo || can_redo);
145        }
146    })
147}
148
149/// Enable or disable undo inside the widget.
150#[property(CONTEXT, default(true))]
151pub fn undo_enabled(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
152    let enabled = enabled.into_var();
153    match_node(child, move |c, op| {
154        if !enabled.get() {
155            UNDO.with_disabled(|| c.op(op))
156        }
157    })
158}
159
160/// Sets the maximum length for undo/redo stacks in the widget and descendants.
161///
162/// This property sets the [`UNDO_LIMIT_VAR`].
163///
164/// [`UNDO_LIMIT_VAR`]: zng_ext_undo::UNDO_LIMIT_VAR
165#[property(CONTEXT - 11, default(UNDO_LIMIT_VAR))]
166pub fn undo_limit(child: impl IntoUiNode, max: impl IntoVar<u32>) -> UiNode {
167    with_context_var(child, UNDO_LIMIT_VAR, max)
168}
169
170/// Sets the time interval that undo and redo cover each call for undo handlers in the widget and descendants.
171///
172/// When undo is requested inside the context all actions after the latest that are within `interval` of the
173/// previous are undone.
174///
175/// This property sets the [`UNDO_INTERVAL_VAR`].
176///
177/// [`UNDO_INTERVAL_VAR`]: zng_ext_undo::UNDO_INTERVAL_VAR
178#[property(CONTEXT - 11, default(UNDO_INTERVAL_VAR))]
179pub fn undo_interval(child: impl IntoUiNode, interval: impl IntoVar<Duration>) -> UiNode {
180    with_context_var(child, UNDO_INTERVAL_VAR, interval)
181}
182
183/// Undo scope widget mixin.
184///
185/// Widget is an undo/redo scope, it tracks changes and handles undo/redo commands.
186///
187/// You can force the widget to use a parent undo scope by setting [`undo_scope`] to `false`, this will cause the widget
188/// to start registering undo/redo actions in the parent, note that the widget will continue behaving as if it
189/// owns the scope, so it may clear it.
190///
191/// [`undo_scope`]: fn@undo_scope
192#[widget_mixin]
193pub struct UndoMix<P>(P);
194
195impl<P: WidgetImpl> UndoMix<P> {
196    fn widget_intrinsic(&mut self) {
197        widget_set! {
198            self;
199            crate::undo_scope = true;
200        }
201    }
202
203    widget_impl! {
204        /// If the widget can register undo actions.
205        ///
206        /// Is `true` by default in this widget, if set to `false` disables undo in the widget.
207        pub undo_enabled(enabled: impl IntoVar<bool>);
208
209        /// Sets the maximum number of undo/redo actions that are retained in the widget.
210        pub undo_limit(limit: impl IntoVar<u32>);
211
212        /// Sets the time interval that undo and redo cover each call for undo handlers in the widget and descendants.
213        ///
214        /// When undo is requested inside the context all actions after the latest that are within `interval` of the
215        /// previous are undone.
216        pub undo_interval(interval: impl IntoVar<Duration>);
217    }
218}