zng_wgt_undo/
lib.rs

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