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#![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#[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 pub static UNDO_ENTRY_FN_VAR: WidgetFn<UndoEntryArgs> = WidgetFn::new(default_undo_entry_fn);
49
50 pub static UNDO_STACK_FN_VAR: WidgetFn<UndoStackArgs> = WidgetFn::new(default_undo_stack_fn);
52
53 pub static UNDO_PANEL_FN_VAR: WidgetFn<UndoPanelArgs> = WidgetFn::new(default_undo_panel_fn);
57
58 pub static GROUP_BY_UNDO_INTERVAL_VAR: bool = true;
64}
65
66#[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#[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#[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#[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#[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
113pub 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
159pub 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 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
239pub 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#[derive(Clone)]
264#[non_exhaustive]
265pub struct UndoEntryArgs {
266 pub info: Vec<(DInstant, Arc<dyn UndoInfo>)>,
272
273 pub op: UndoOp,
275
276 pub cmd: Command,
278}
279impl UndoEntryArgs {
280 pub fn new(info: Vec<(DInstant, Arc<dyn UndoInfo>)>, op: UndoOp, cmd: Command) -> Self {
282 Self { info, op, cmd }
283 }
284
285 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#[derive(Clone)]
316#[non_exhaustive]
317pub struct UndoStackArgs {
318 pub stack: UndoStackInfo,
320 pub op: UndoOp,
322 pub cmd: Command,
324}
325impl UndoStackArgs {
326 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#[derive(Debug, Clone, PartialEq)]
358#[non_exhaustive]
359pub struct UndoPanelArgs {
360 pub op: UndoOp,
362}
363impl UndoPanelArgs {
364 pub fn new(op: UndoOp) -> Self {
366 Self { op }
367 }
368}
369
370#[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 static HOVERED_TIMESTAMP_VAR: Option<DInstant> = None;
401
402 pub static UNDO_ENTRY_VAR: Option<UndoEntryArgs> = None;
404
405 pub static UNDO_STACK_VAR: Option<UndoOp> = None;
407}
408
409#[property(CONTEXT-1)]
413pub fn undo_entry(child: impl IntoUiNode, entry: impl IntoValue<UndoEntryArgs>) -> UiNode {
414 let entry = entry.into();
415
416 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#[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#[property(CONTEXT)]
456pub fn is_cap_hovered_timestamp(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
457 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}