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