zng_wgt_undo_history/
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 history widget.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use colors::BASE_COLOR_VAR;
15use zng_ext_l10n::{L10nArgument, l10n};
16use zng_ext_undo::*;
17use zng_wgt::{base_color, margin, prelude::*};
18use zng_wgt_button::Button;
19use zng_wgt_container::{Container, child_align, padding};
20use zng_wgt_fill::background_color;
21use zng_wgt_input::{is_cap_hovered, is_pressed};
22use zng_wgt_scroll::{Scroll, ScrollMode};
23use zng_wgt_size_offset::max_height;
24use zng_wgt_stack::{Stack, StackDirection};
25use zng_wgt_style::{Style, impl_named_style_fn};
26use zng_wgt_text::Text;
27
28use std::fmt;
29use std::sync::Arc;
30
31/// Undo/redo stack view.
32///
33/// This widget shows a snapshot of the undo/redo stacks of the focused undo scope when the history widget is open.
34/// Note that the stack is not live, this widget is designed to work as a menu popup content.
35#[widget($crate::UndoHistory { ($op:expr) => { op = $op; } })]
36pub struct UndoHistory(WidgetBase);
37impl UndoHistory {
38    fn widget_intrinsic(&mut self) {
39        self.widget_builder().push_build_action(|wgt| {
40            let op = wgt.capture_value::<UndoOp>(property_id!(Self::op)).unwrap_or(UndoOp::Undo);
41            wgt.set_child(UNDO_PANEL_FN_VAR.present_data(UndoPanelArgs { op }));
42        });
43    }
44}
45
46context_var! {
47    /// Widget function for a single undo or redo entry.
48    pub static UNDO_ENTRY_FN_VAR: WidgetFn<UndoEntryArgs> = WidgetFn::new(default_undo_entry_fn);
49
50    /// Widget function for an undo or redo stack.
51    pub static UNDO_STACK_FN_VAR: WidgetFn<UndoStackArgs> = WidgetFn::new(default_undo_stack_fn);
52
53    /// Widget function for the [`UndoHistory!`] child.
54    ///
55    /// [`UndoHistory!`]: struct@UndoHistory
56    pub static UNDO_PANEL_FN_VAR: WidgetFn<UndoPanelArgs> = WidgetFn::new(default_undo_panel_fn);
57
58    /// If undo entries are grouped by the [`UNDO.undo_interval`].
59    ///
60    /// Enabled by default.
61    ///
62    /// [`UNDO.undo_interval`]: zng_ext_undo::UNDO::undo_interval
63    pub static GROUP_BY_UNDO_INTERVAL_VAR: bool = true;
64}
65
66/// Widget function that converts [`UndoEntryArgs`] to widgets.
67///
68/// Try [`undo_redo_button_style_fn`] for making only visual changes.
69///
70/// Sets the [`UNDO_ENTRY_FN_VAR`].
71///
72/// [`undo_redo_button_style_fn`]: fn@undo_redo_button_style_fn
73#[property(CONTEXT+1, default(UNDO_ENTRY_FN_VAR), widget_impl(UndoHistory))]
74pub fn undo_entry_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<UndoEntryArgs>>) -> UiNode {
75    with_context_var(child, UNDO_ENTRY_FN_VAR, wgt_fn)
76}
77
78/// Widget function that converts [`UndoStackArgs`] to widgets.
79///
80/// Sets the [`UNDO_STACK_FN_VAR`].
81#[property(CONTEXT+1, default(UNDO_STACK_FN_VAR), widget_impl(UndoHistory))]
82pub fn undo_stack_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<UndoStackArgs>>) -> UiNode {
83    with_context_var(child, UNDO_STACK_FN_VAR, wgt_fn)
84}
85
86/// Widget function that converts [`UndoPanelArgs`] to widgets.
87///
88/// Sets the [`UNDO_PANEL_FN_VAR`].
89#[property(CONTEXT+1, default(UNDO_PANEL_FN_VAR), widget_impl(UndoHistory))]
90pub fn undo_panel_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<UndoPanelArgs>>) -> UiNode {
91    with_context_var(child, UNDO_PANEL_FN_VAR, wgt_fn)
92}
93
94/// If undo entries are grouped by the [`UNDO.undo_interval`].
95///
96/// Enabled by default.
97///
98/// Sets the [`GROUP_BY_UNDO_INTERVAL_VAR`].
99///
100/// [`UNDO.undo_interval`]: UNDO::undo_interval
101#[property(CONTEXT+1, default(GROUP_BY_UNDO_INTERVAL_VAR), widget_impl(UndoHistory))]
102pub fn group_by_undo_interval(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
103    with_context_var(child, GROUP_BY_UNDO_INTERVAL_VAR, enabled)
104}
105
106/// Identifies what stack history is shown by the widget.
107#[property(CONTEXT, default(UndoOp::Undo), widget_impl(UndoHistory))]
108pub fn op(wgt: &mut WidgetBuilding, op: impl IntoValue<UndoOp>) {
109    let _ = op;
110    wgt.expect_property_capture();
111}
112
113/// Default [`UNDO_ENTRY_FN_VAR`].
114///
115/// Returns a `Button!` with the [`UNDO_REDO_BUTTON_STYLE_FN_VAR`] and the entry displayed in a `Text!` child.
116/// The button notifies [`UNDO_CMD`] or [`REDO_CMD`] with the entry timestamp, the command is scoped on the
117/// undo parent of the caller not of the button.
118///
119/// [`UndoRedoButtonStyle!`]: struct@UndoRedoButtonStyle
120/// [`UNDO_CMD`]: zng_ext_undo::UNDO_CMD
121/// [`REDO_CMD`]: zng_ext_undo::REDO_CMD
122pub fn default_undo_entry_fn(args: UndoEntryArgs) -> UiNode {
123    let ts = args.timestamp();
124    let cmd = args.cmd;
125
126    let label = if args.info.len() == 1 {
127        args.info[0].1.description()
128    } else {
129        let mut txt = Txt::from_static("");
130        let mut sep = "";
131        let mut info_iter = args.info.iter();
132        for (_, info) in &mut info_iter {
133            use std::fmt::Write;
134
135            if txt.chars().take(10).count() == 10 {
136                let count = 1 + info_iter.count();
137                let _ = write!(&mut txt, "{sep}{count}");
138                break;
139            }
140
141            let _ = write!(&mut txt, "{sep}{}", info.description());
142            sep = "₊";
143        }
144        txt.end_mut();
145        txt
146    };
147
148    Button! {
149        child = Text!(label);
150        undo_entry = args;
151        style_fn = UndoRedoButtonStyle!();
152        on_click = hn!(|args| {
153            args.propagation().stop();
154            cmd.notify_param(ts);
155        });
156    }
157}
158
159/// Default [`UNDO_STACK_FN_VAR`].
160///
161/// Returns top-to-bottom `Stack!` of [`UNDO_ENTRY_FN_VAR`], latest first.
162pub fn default_undo_stack_fn(args: UndoStackArgs) -> UiNode {
163    let entry = UNDO_ENTRY_FN_VAR.get();
164
165    let timestamps;
166    let children: UiVec;
167
168    if GROUP_BY_UNDO_INTERVAL_VAR.get() {
169        let mut ts = vec![];
170
171        children = args
172            .stack
173            .iter_groups()
174            .rev()
175            .map(|g| {
176                let e = UndoEntryArgs {
177                    info: g.to_vec(),
178                    op: args.op,
179                    cmd: args.cmd,
180                };
181                ts.push(e.timestamp());
182                entry(e)
183            })
184            .collect();
185
186        timestamps = ts;
187    } else {
188        timestamps = args.stack.stack.iter().rev().map(|(i, _)| *i).collect();
189        children = args
190            .stack
191            .stack
192            .into_iter()
193            .rev()
194            .map(|info| {
195                entry(UndoEntryArgs {
196                    info: vec![info],
197                    op: args.op,
198                    cmd: args.cmd,
199                })
200            })
201            .collect();
202    };
203
204    let op = args.op;
205    let count = HOVERED_TIMESTAMP_VAR.map(move |t| {
206        let c = match t {
207            Some(t) => match op {
208                UndoOp::Undo => timestamps.iter().take_while(|ts| *ts >= t).count(),
209                UndoOp::Redo => timestamps.iter().take_while(|ts| *ts <= t).count(),
210            },
211            None => 0,
212        };
213        L10nArgument::from(c)
214    });
215    // l10n-# Number of undo/redo actions that are selected to run
216    let count = l10n!("UndoHistory.count_actions", "{$n} actions", n = count);
217
218    Container! {
219        undo_stack = args.op;
220
221        child = Scroll! {
222            child = Stack! {
223                direction = StackDirection::top_to_bottom();
224                children;
225            };
226            child_align = Align::FILL_TOP;
227            mode = ScrollMode::VERTICAL;
228            max_height = 200.dip().min(80.pct());
229        };
230
231        child_bottom = Text! {
232            margin = 2;
233            txt = count;
234            txt_align = Align::CENTER;
235        };
236    }
237}
238
239/// Default [`UNDO_PANEL_FN_VAR`].
240pub fn default_undo_panel_fn(args: UndoPanelArgs) -> UiNode {
241    let stack = UNDO_STACK_FN_VAR.get();
242    match args.op {
243        UndoOp::Undo => {
244            let cmd = UNDO_CMD.undo_scoped().get();
245            stack(UndoStackArgs {
246                stack: cmd.undo_stack(),
247                op: UndoOp::Undo,
248                cmd,
249            })
250        }
251        UndoOp::Redo => {
252            let cmd = REDO_CMD.undo_scoped().get();
253            stack(UndoStackArgs {
254                stack: cmd.redo_stack(),
255                op: UndoOp::Redo,
256                cmd,
257            })
258        }
259    }
260}
261
262/// Represents an action in the undo or redo stack.
263#[derive(Clone)]
264#[non_exhaustive]
265pub struct UndoEntryArgs {
266    /// Info about the action.
267    ///
268    /// Is at least one item, can be more if [`GROUP_BY_UNDO_INTERVAL_VAR`] is enabled.
269    ///
270    /// The latest undo action is the last entry in the list.
271    pub info: Vec<(DInstant, Arc<dyn UndoInfo>)>,
272
273    /// What stack this entry is at.
274    pub op: UndoOp,
275
276    /// The undo or redo command in the correct scope.
277    pub cmd: Command,
278}
279impl UndoEntryArgs {
280    /// New args.
281    pub fn new(info: Vec<(DInstant, Arc<dyn UndoInfo>)>, op: UndoOp, cmd: Command) -> Self {
282        Self { info, op, cmd }
283    }
284
285    /// Moment the undo action was first registered.
286    ///
287    /// This does not change after redo and undo, it is always the register time.
288    ///
289    /// This is the first timestamp in `info`.
290    pub fn timestamp(&self) -> DInstant {
291        self.info[0].0
292    }
293}
294impl PartialEq for UndoEntryArgs {
295    fn eq(&self, other: &Self) -> bool {
296        self.op == other.op
297            && self.info.len() == other.info.len()
298            && self
299                .info
300                .iter()
301                .zip(&other.info)
302                .all(|(a, b)| a.0 == b.0 && Arc::ptr_eq(&a.1, &b.1))
303    }
304}
305impl fmt::Debug for UndoEntryArgs {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        f.debug_struct("UndoEntryArgs")
308            .field("info[0]", &self.info[0].1.description())
309            .field("op", &self.op)
310            .finish()
311    }
312}
313
314/// Represents an undo or redo stack.
315#[derive(Clone)]
316#[non_exhaustive]
317pub struct UndoStackArgs {
318    /// Stack, latest at the end.
319    pub stack: UndoStackInfo,
320    /// What stack this is.
321    pub op: UndoOp,
322    /// The undo or redo command, scoped.
323    pub cmd: Command,
324}
325impl UndoStackArgs {
326    /// New args.
327    pub fn new(stack: UndoStackInfo, op: UndoOp, cmd: Command) -> Self {
328        Self { stack, op, cmd }
329    }
330}
331
332impl PartialEq for UndoStackArgs {
333    fn eq(&self, other: &Self) -> bool {
334        self.op == other.op
335            && self.stack.stack.len() == other.stack.stack.len()
336            && self
337                .stack
338                .stack
339                .iter()
340                .zip(&other.stack.stack)
341                .all(|((t0, a0), (t1, a1))| t0 == t1 && Arc::ptr_eq(a0, a1))
342    }
343}
344
345impl fmt::Debug for UndoStackArgs {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        f.debug_struct("UndoStackArgs")
348            .field("stack.len()", &self.stack.stack.len())
349            .field("op", &self.op)
350            .finish()
351    }
352}
353
354/// Args to present the child of [`UndoHistory!`].
355///
356/// [`UndoHistory!`]: struct@UndoHistory
357#[derive(Debug, Clone, PartialEq)]
358#[non_exhaustive]
359pub struct UndoPanelArgs {
360    /// What stack history must be shown.
361    pub op: UndoOp,
362}
363impl UndoPanelArgs {
364    /// New args.
365    pub fn new(op: UndoOp) -> Self {
366        Self { op }
367    }
368}
369
370/// Menu style button for an entry in a undo/redo stack.
371#[widget($crate::UndoRedoButtonStyle)]
372pub struct UndoRedoButtonStyle(Style);
373impl_named_style_fn!(undo_redo_button, UndoRedoButtonStyle);
374impl UndoRedoButtonStyle {
375    fn widget_intrinsic(&mut self) {
376        widget_set! {
377            self;
378            named_style_fn = UNDO_REDO_BUTTON_STYLE_FN_VAR;
379            padding = 4;
380            child_align = Align::START;
381
382            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
383            background_color = BASE_COLOR_VAR.rgba();
384
385            when *#is_cap_hovered_timestamp {
386                background_color = BASE_COLOR_VAR.shade(1);
387            }
388
389            when *#is_pressed {
390                #[easing(0.ms())]
391                background_color = BASE_COLOR_VAR.shade(2);
392            }
393        }
394    }
395}
396
397context_var! {
398    /// Variable set by the parent undo/redo stack widget, can be used to highlight items
399    /// that will be included in the undo/redo operation.
400    static HOVERED_TIMESTAMP_VAR: Option<DInstant> = None;
401
402    /// Variable set in each undo/redo entry widget.
403    pub static UNDO_ENTRY_VAR: Option<UndoEntryArgs> = None;
404
405    /// Variable set in each undo/redo stack widget.
406    pub static UNDO_STACK_VAR: Option<UndoOp> = None;
407}
408
409/// Sets the undo/redo entry widget context.
410///
411/// In the widget style the [`UNDO_ENTRY_VAR`] can be used to access the [`UndoEntryArgs`].
412#[property(CONTEXT-1)]
413pub fn undo_entry(child: impl IntoUiNode, entry: impl IntoValue<UndoEntryArgs>) -> UiNode {
414    let entry = entry.into();
415
416    // set the hovered timestamp
417    let timestamp = entry.timestamp();
418    let is_hovered = var(false);
419    let child = is_cap_hovered(child, is_hovered.clone());
420    let child = match_node(child, move |_, op| {
421        if let UiNodeOp::Init = op {
422            let actual = HOVERED_TIMESTAMP_VAR.current_context();
423            is_hovered
424                .hook(move |a| {
425                    let is_hovered = *a.value();
426                    actual.modify(move |a| {
427                        if is_hovered {
428                            a.set(Some(timestamp));
429                        } else if a.value() == &Some(timestamp) {
430                            a.set(None);
431                        }
432                    });
433                    true
434                })
435                .perm();
436        }
437    });
438
439    with_context_var(child, UNDO_ENTRY_VAR, Some(entry))
440}
441
442/// Setups the context in an undo/redo stack widget.
443///
444/// If this is not set in the stack widget the entry widgets may not work properly.
445#[property(CONTEXT-1)]
446pub fn undo_stack(child: impl IntoUiNode, op: impl IntoValue<UndoOp>) -> UiNode {
447    let child = with_context_var(child, HOVERED_TIMESTAMP_VAR, var(None));
448    with_context_var(child, UNDO_STACK_VAR, Some(op.into()))
449}
450
451/// State is true when the widget is an [`undo_entry`] and it is hovered, has captured the mouse
452/// or a sibling with higher timestamp is hovered/has cap.
453///
454/// [`undo_entry`]: fn@undo_entry
455#[property(CONTEXT)]
456pub fn is_cap_hovered_timestamp(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
457    // check the hovered timestamp
458    bind_state(
459        child,
460        merge_var!(HOVERED_TIMESTAMP_VAR, UNDO_ENTRY_VAR, UNDO_STACK_VAR, |&ts, entry, &op| {
461            match (ts, entry) {
462                (Some(ts), Some(entry)) => match op {
463                    Some(UndoOp::Undo) => entry.timestamp() >= ts,
464                    Some(UndoOp::Redo) => entry.timestamp() <= ts,
465                    None => false,
466                },
467                _ => false,
468            }
469        }),
470        state,
471    )
472}