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#![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#[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 pub static UNDO_ENTRY_FN_VAR: WidgetFn<UndoEntryArgs> = WidgetFn::new(default_undo_entry_fn);
54
55 pub static UNDO_STACK_FN_VAR: WidgetFn<UndoStackArgs> = WidgetFn::new(default_undo_stack_fn);
57
58 pub static UNDO_PANEL_FN_VAR: WidgetFn<UndoPanelArgs> = WidgetFn::new(default_undo_panel_fn);
62
63 pub static GROUP_BY_UNDO_INTERVAL_VAR: bool = true;
69}
70
71#[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#[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#[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#[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#[property(CONTEXT, capture, default(UndoOp::Undo), widget_impl(UndoHistory))]
113pub fn op(op: impl IntoValue<UndoOp>) {}
114
115pub 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
161pub 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 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
243pub 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#[derive(Clone)]
268pub struct UndoEntryArgs {
269 pub info: Vec<(DInstant, Arc<dyn UndoInfo>)>,
275
276 pub op: UndoOp,
278
279 pub cmd: Command,
281}
282impl UndoEntryArgs {
283 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#[derive(Clone)]
314pub struct UndoStackArgs {
315 pub stack: UndoStackInfo,
317 pub op: UndoOp,
319 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#[derive(Debug, Clone, PartialEq)]
349pub struct UndoPanelArgs {
350 pub op: UndoOp,
352}
353
354#[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 static HOVERED_TIMESTAMP_VAR: Option<DInstant> = None;
383
384 pub static UNDO_ENTRY_VAR: Option<UndoEntryArgs> = None;
386
387 pub static UNDO_STACK_VAR: Option<UndoOp> = None;
389
390 pub static UNDO_BUTTON_STYLE_FN_VAR: StyleFn = style_fn!(|_| UndoRedoButtonStyle!());
397}
398
399#[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#[property(CONTEXT-1)]
409pub fn undo_entry(child: impl UiNode, entry: impl IntoValue<UndoEntryArgs>) -> impl UiNode {
410 let entry = entry.into();
411
412 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#[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#[property(CONTEXT)]
452pub fn is_cap_hovered_timestamp(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
453 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}