graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/edit_history.rs (23.8K)
1 //! Undo for edits to the node tree: parameters, and the graph itself.
2 //!
3 //! The app has no project-wide history: a text box undoes its own typing
4 //! and a viewer state its own handles. This is the third. It holds two
5 //! kinds of step, on ONE stack so that undo takes them back in the order
6 //! they were made:
7 //!
8 //! - **Parameters** — parameters of one node as they stood before
9 //! something changed them: a row of the params pane, the row menu,
10 //! `set_param`, or a command that rewrites the lot (Reset Parameters).
11 //! Recorded by the writers.
12 //! - **Structure** — nodes added, nodes removed, nodes moved, nodes
13 //! renamed, wires made and broken, the display and bypass flags. Recorded by NOTICING: the
14 //! tree is compared with how it stood at the last look
15 //! (`State::structure_base`) once an event or an action has been
16 //! applied, and what differs is the step. There are a dozen writers of
17 //! the graph — the widget's own drag read back a frame later, the
18 //! keyboard families, paste, the palette, MCP, the image commands,
19 //! Arrange — and a recording call in each is one that the thirteenth
20 //! would not make.
21 //!
22 //! **A step holds what changed and nothing else.** A parameter step names
23 //! its parameters; a structure step names its nodes, and of a node that
24 //! stayed only its position, its two flags and its wires. Restoring a
25 //! node's whole parameter list would take back what is written to it
26 //! without passing here — a camera node's Rotation under an orbit, a
27 //! curve's Points under its handles.
28 //!
29 //! **One step per gesture.** The pane writes back on every motion of a
30 //! drag, so records that share a group are one step until the group is
31 //! broken: by a press or a release, by Enter, Tab or Escape, or by
32 //! [`GROUP_IDLE`] without a record, which is what ends a run of wheel
33 //! notches or of alt+hjkl. The graph is not looked at while a drag is
34 //! held, so a dragged node is one step, from where it was picked up.
35 //!
36 //! It is not `cce_ui::history::History` because a step has to be looked at
37 //! before it is taken: what is filed for redo is the CURRENT state of what
38 //! the step names, and what that is is in the step.
39
40 use crate::app::{FsNode, State};
41 use crate::param::{ParamDef, ParamKind};
42 use std::time::{Duration, Instant};
43
44 /// Parameters of a node as they stood, and what changed them.
45 #[derive(Clone)]
46 pub struct ParamSnapshot {
47 pub node_id: String,
48 pub params: Vec<ParamDef>,
49 /// What the step was, for the status line: "Reset Parameters",
50 /// "Radius".
51 pub what: String,
52 }
53
54 impl ParamSnapshot {
55 /// The group an edit of these parameters belongs to.
56 pub fn group(&self) -> String {
57 let names: Vec<&str> = self.params.iter().map(|p| p.name.as_str()).collect();
58 format!("{}\u{0}{}", self.node_id, names.join("\u{0}"))
59 }
60 }
61
62 /// Whether two states of a parameter are one: what a step would restore.
63 pub fn same(a: &ParamDef, b: &ParamDef) -> bool {
64 a.text() == b.text() && a.is_expr() == b.is_expr() && a.view == b.view
65 }
66
67 pub const LIMIT: usize = 256;
68 /// How long a group stands with nothing recorded into it.
69 pub const GROUP_IDLE: Duration = Duration::from_millis(1000);
70
71 /// What a node of a level was, before a structure step.
72 #[derive(Clone)]
73 pub enum NodeBefore {
74 /// Not there: the step added it.
75 Absent { id: String },
76 /// There, whole, at this place among its siblings: the step removed it.
77 Whole { index: usize, node: FsNode },
78 /// There, and these of its fields were otherwise. `wires` holds the
79 /// wire parameters that differed, as they were.
80 Fields { id: String, position: (f32, f32), geometry_visible: bool, bypassed: bool, wires: Vec<ParamDef> },
81 }
82
83 /// The nodes of one level a structure step changed. `dir_id` is the id of
84 /// the node whose children they are.
85 #[derive(Clone)]
86 pub struct LevelBefore {
87 pub dir_id: String,
88 pub nodes: Vec<NodeBefore>,
89 }
90
91 #[derive(Clone)]
92 pub struct StructureStep {
93 pub levels: Vec<LevelBefore>,
94 /// Nodes that had another name: (id, the name it was). Put back by
95 /// renaming, not by writing the name, so that what names the node —
96 /// wires, expression paths anywhere in the tree — follows it back.
97 pub renames: Vec<(String, String)>,
98 pub what: String,
99 }
100
101 #[derive(Clone)]
102 pub enum Step {
103 Params(ParamSnapshot),
104 Structure(StructureStep),
105 }
106
107 impl Step {
108 pub fn what(&self) -> &str {
109 match self {
110 Step::Params(p) => &p.what,
111 Step::Structure(s) => &s.what,
112 }
113 }
114 }
115
116 #[derive(Default)]
117 pub struct EditHistory {
118 undo: Vec<Step>,
119 redo: Vec<Step>,
120 /// The group of the last record and when it was made.
121 group: Option<(String, Instant)>,
122 }
123
124 impl EditHistory {
125 /// File `before` as what the next undo returns to. A new edit forks:
126 /// what had been undone cannot be redone over it.
127 pub fn record(&mut self, before: Step) {
128 self.group = None;
129 self.push(before);
130 }
131
132 /// [`Self::record`], unless the last record was of the same group and
133 /// the group still stands: the earlier step already holds what this
134 /// gesture began from.
135 pub fn record_grouped(&mut self, before: Step, group: String) {
136 let now = Instant::now();
137 let standing = matches!(&self.group, Some((g, at)) if *g == group && now.duration_since(*at) < GROUP_IDLE);
138 if !(standing && !self.undo.is_empty()) {
139 self.push(before);
140 }
141 self.group = Some((group, now));
142 }
143
144 /// The gesture is over: the next grouped record is a step of its own.
145 pub fn break_group(&mut self) {
146 self.group = None;
147 }
148
149 fn push(&mut self, before: Step) {
150 self.redo.clear();
151 self.undo.push(before);
152 if self.undo.len() > LIMIT {
153 self.undo.remove(0);
154 }
155 }
156
157 /// Take a step off one stack. The caller restores it and files what
158 /// the restore replaced with [`Self::file`].
159 pub fn take(&mut self, undo: bool) -> Option<Step> {
160 self.group = None;
161 if undo { self.undo.pop() } else { self.redo.pop() }
162 }
163
164 /// File the state a taken step replaced, on the OTHER stack.
165 pub fn file(&mut self, undo: bool, replaced: Step) {
166 if undo { self.redo.push(replaced) } else { self.undo.push(replaced) }
167 }
168
169 pub fn can_undo(&self) -> bool {
170 !self.undo.is_empty()
171 }
172
173 pub fn can_redo(&self) -> bool {
174 !self.redo.is_empty()
175 }
176
177 pub fn undo_len(&self) -> usize {
178 self.undo.len()
179 }
180
181 /// Another document: its nodes are not these.
182 pub fn clear(&mut self) {
183 self.undo.clear();
184 self.redo.clear();
185 self.group = None;
186 }
187 }
188
189 /// A node's wires: its parameters of the `node` kind.
190 fn wires(node: &FsNode) -> impl Iterator<Item = &ParamDef> {
191 node.params.iter().filter(|p| p.kind() == ParamKind::Node)
192 }
193
194 /// Whether a level's children can be told apart by id. A hand-built tree
195 /// may carry empty ids or one id twice; such a level is not followed.
196 fn ids_tell_apart(nodes: &[FsNode]) -> bool {
197 let mut seen = std::collections::HashSet::new();
198 nodes.iter().all(|n| !n.id.is_empty() && seen.insert(n.id.as_str()))
199 }
200
201 /// What differs between how a tree stood and how it stands.
202 #[derive(Default)]
203 pub struct Difference {
204 /// The structure that changed, as it WAS: a step's content.
205 pub levels: Vec<LevelBefore>,
206 /// The nodes renamed: (id, the name it was).
207 pub renames: Vec<(String, String)>,
208 /// Whether anything differs at all, a parameter's value included —
209 /// which is no step, and is when the base has to be taken again.
210 pub any: bool,
211 added: Vec<String>,
212 removed: Vec<String>,
213 moved: Vec<String>,
214 wired: usize,
215 flagged: usize,
216 }
217
218 impl Difference {
219 /// The step's name, for the status line.
220 pub fn what(&self) -> String {
221 let list = |names: &[String]| match names {
222 [one] => one.clone(),
223 many => format!("{} nodes", many.len()),
224 };
225 if !self.renames.is_empty() && self.added.is_empty() && self.removed.is_empty() {
226 // The wires that named the node changed with it, and are part
227 // of the rename.
228 let names: Vec<String> = self.renames.iter().map(|(_, was)| was.clone()).collect();
229 format!("Rename {}", list(&names))
230 } else if !self.added.is_empty() && self.removed.is_empty() {
231 format!("Add {}", list(&self.added))
232 } else if !self.removed.is_empty() && self.added.is_empty() {
233 format!("Delete {}", list(&self.removed))
234 } else if !self.added.is_empty() {
235 "Edit Nodes".to_string()
236 } else if self.wired > 0 {
237 "Wire".to_string()
238 } else if !self.moved.is_empty() {
239 format!("Move {}", list(&self.moved))
240 } else {
241 "Node Flag".to_string()
242 }
243 }
244
245 /// The group a run of these belongs to, when it is a move and nothing
246 /// else: a run of alt+hjkl is one step.
247 pub fn move_group(&self) -> Option<String> {
248 let only_moves = self.added.is_empty() && self.removed.is_empty() && self.wired == 0 && self.flagged == 0;
249 (only_moves && self.renames.is_empty() && !self.moved.is_empty()).then(|| format!("move\u{0}{}", self.moved.join("\u{0}")))
250 }
251 }
252
253 /// Compare a level, and the levels under the nodes that stayed.
254 pub fn difference(base: &FsNode, now: &FsNode, out: &mut Difference) {
255 if !ids_tell_apart(&base.children) || !ids_tell_apart(&now.children) {
256 out.any |= base.children.len() != now.children.len();
257 return;
258 }
259 let mut nodes = Vec::new();
260 for (index, was) in base.children.iter().enumerate() {
261 let Some(is) = now.children.iter().find(|n| n.id == was.id) else {
262 out.removed.push(was.name.clone());
263 nodes.push(NodeBefore::Whole { index, node: was.clone() });
264 continue;
265 };
266 let changed_wires: Vec<ParamDef> = wires(was)
267 .filter(|w| is.params.iter().find(|p| p.name == w.name).is_some_and(|p| !same(p, w)))
268 .cloned()
269 .collect();
270 let moved = was.position != is.position;
271 let flagged = was.geometry_visible != is.geometry_visible || was.bypassed != is.bypassed;
272 if moved || flagged || !changed_wires.is_empty() {
273 if moved {
274 out.moved.push(is.name.clone());
275 }
276 out.flagged += flagged as usize;
277 out.wired += changed_wires.len();
278 nodes.push(NodeBefore::Fields {
279 id: was.id.clone(),
280 position: was.position,
281 geometry_visible: was.geometry_visible,
282 bypassed: was.bypassed,
283 wires: changed_wires,
284 });
285 }
286 if was.name != is.name {
287 out.renames.push((was.id.clone(), was.name.clone()));
288 }
289 out.any |= was.name != is.name
290 || was.params.len() != is.params.len()
291 || was.params.iter().zip(is.params.iter()).any(|(a, b)| a.name != b.name || !same(a, b));
292 difference(was, is, out);
293 }
294 for is in &now.children {
295 if !base.children.iter().any(|n| n.id == is.id) {
296 out.added.push(is.name.clone());
297 nodes.push(NodeBefore::Absent { id: is.id.clone() });
298 }
299 }
300 if !nodes.is_empty() {
301 out.any = true;
302 out.levels.push(LevelBefore { dir_id: base.id.clone(), nodes });
303 }
304 }
305
306 /// Put a tree back as a step says it was, and return the step that would
307 /// put it back as it is: undo's is redo's and redo's undo's.
308 pub fn restore(root: &mut FsNode, step: StructureStep) -> StructureStep {
309 let mut levels = Vec::new();
310 for level in step.levels {
311 let Some(dir) = crate::viewer_state::find_node_by_id_mut(root, &level.dir_id) else { continue };
312 let mut nodes: Vec<NodeBefore> = Vec::new();
313 let mut going: Vec<String> = Vec::new();
314 let mut back: Vec<(usize, FsNode)> = Vec::new();
315 for node in level.nodes {
316 match node {
317 NodeBefore::Absent { id } => going.push(id),
318 NodeBefore::Whole { index, node } => back.push((index, node)),
319 NodeBefore::Fields { id, position, geometry_visible, bypassed, wires } => {
320 let Some(n) = dir.children.iter_mut().find(|n| n.id == id) else { continue };
321 let mut were = Vec::new();
322 for w in wires {
323 if let Some(p) = n.params.iter_mut().find(|p| p.name == w.name) {
324 were.push(std::mem::replace(p, w));
325 }
326 }
327 nodes.push(NodeBefore::Fields {
328 id,
329 position: std::mem::replace(&mut n.position, position),
330 geometry_visible: std::mem::replace(&mut n.geometry_visible, geometry_visible),
331 bypassed: std::mem::replace(&mut n.bypassed, bypassed),
332 wires: were,
333 });
334 }
335 }
336 }
337 // What the step added goes, as it stands NOW and from where it
338 // stands — the places read before any of them is taken out.
339 let mut gone: Vec<(usize, FsNode)> = dir
340 .children
341 .iter()
342 .enumerate()
343 .filter(|(_, n)| going.contains(&n.id))
344 .map(|(i, n)| (i, n.clone()))
345 .collect();
346 dir.children.retain(|n| !going.contains(&n.id));
347 nodes.extend(gone.drain(..).map(|(index, node)| NodeBefore::Whole { index, node }));
348 // What it removed comes back where it was, lowest first, so each
349 // lands among the siblings it had.
350 back.sort_by_key(|(index, _)| *index);
351 for (index, node) in back {
352 nodes.push(NodeBefore::Absent { id: node.id.clone() });
353 let at = index.min(dir.children.len());
354 dir.children.insert(at, node);
355 }
356 levels.push(LevelBefore { dir_id: level.dir_id, nodes });
357 }
358 // The names last, and by renaming: every wire and every expression
359 // path that names the node is written back with it. After the wires
360 // the step holds, or what is filed for redo would be those wires as
361 // the rename had just left them. Last renamed, first put back.
362 let mut renames = Vec::new();
363 for (id, was) in step.renames.into_iter().rev() {
364 let Some(is) = crate::viewer_state::find_node_by_id(root, &id).map(|n| n.name.clone()) else { continue };
365 // A sibling has taken the name since: two nodes of one name would
366 // leave every wire to either naming both. The node keeps the name
367 // it has.
368 let taken = crate::geometry::find_parent_node(root, &id)
369 .is_some_and(|p| p.children.iter().any(|c| c.id != id && c.name == was));
370 if !taken && crate::geometry::rename_node_in_tree(root, &id, &was) {
371 renames.push((id, is));
372 }
373 }
374 renames.reverse();
375 StructureStep { levels, renames, what: step.what }
376 }
377
378 impl State {
379 /// Take the tree as it stands for what the next look compares with.
380 /// After anything that replaces the tree or that has recorded its own
381 /// step, so that it is not noticed as an edit.
382 pub fn rebase_structure(&mut self) {
383 self.structure_base = Some(self.fs_root.clone());
384 }
385
386 /// Whether a drag is held. The graph is not looked at until it is let
387 /// go: a dragged node is one step, from where it was picked up.
388 fn gesture_held(&self) -> bool {
389 self.drag_widget.is_some()
390 || self.app_drag.is_some()
391 || self.node_drag_group.is_some()
392 // A camera orbit or a pan is not an edit, but an orbit with a
393 // camera node active writes its rotation every motion — and a
394 // changed parameter is a full-tree clone here (`rebase_structure`).
395 || self.orbit_drag.is_some()
396 || self.pan_drag.is_some()
397 || self.is_panning
398 }
399
400 /// Look at the tree, and record what of its structure has changed
401 /// since the last look. Run once an event or an action has been
402 /// applied.
403 pub fn record_structure_changes(&mut self) {
404 if !self.gesture_held() {
405 self.note_structure_changes();
406 }
407 }
408
409 fn note_structure_changes(&mut self) {
410 let Some(base) = &self.structure_base else {
411 self.rebase_structure();
412 return;
413 };
414 let mut diff = Difference::default();
415 difference(base, &self.fs_root, &mut diff);
416 if !diff.any {
417 return;
418 }
419 if !diff.levels.is_empty() || !diff.renames.is_empty() {
420 let group = diff.move_group();
421 let step =
422 Step::Structure(StructureStep { what: diff.what(), levels: diff.levels, renames: diff.renames });
423 match group {
424 Some(group) => self.edit_history.record_grouped(step, group),
425 None => self.edit_history.record(step),
426 }
427 }
428 self.rebase_structure();
429 }
430
431 /// Record parameters of a node as they were. For the writers of
432 /// parameters, which know what they changed, and call this once they
433 /// have changed it.
434 pub fn record_params(&mut self, before: ParamSnapshot, grouped: bool) {
435 // A wire is structure too. The base is told of these parameters
436 // as they are now, so that the look below does not take this edit
437 // for one of its own and record it a second time.
438 let now: Vec<ParamDef> = crate::viewer_state::find_node_by_id(&self.fs_root, &before.node_id)
439 .map(|n| {
440 n.params.iter().filter(|p| before.params.iter().any(|b| b.name == p.name)).cloned().collect()
441 })
442 .unwrap_or_default();
443 if let Some(node) = self
444 .structure_base
445 .as_mut()
446 .and_then(|base| crate::viewer_state::find_node_by_id_mut(base, &before.node_id))
447 {
448 for p in now {
449 if let Some(was) = node.params.iter_mut().find(|w| w.name == p.name) {
450 *was = p;
451 }
452 }
453 }
454 // What the graph did before this edit is its own step, under it.
455 self.note_structure_changes();
456 if grouped {
457 let group = before.group();
458 self.edit_history.record_grouped(Step::Params(before), group);
459 } else {
460 self.edit_history.record(Step::Params(before));
461 }
462 }
463
464 /// Record that `pname` of a node was `before` until just now, if it is
465 /// not still. For the writers that change one parameter in one go: the
466 /// row menu and `set_param`.
467 pub fn record_param_edit(&mut self, node_id: &str, before: ParamDef) {
468 let now = crate::viewer_state::find_node_by_id(&self.fs_root, node_id)
469 .and_then(|n| n.params.iter().find(|p| p.name == before.name));
470 if now.is_some_and(|p| !same(p, &before)) {
471 self.record_params(
472 ParamSnapshot { node_id: node_id.to_string(), what: before.name.clone(), params: vec![before] },
473 false,
474 );
475 }
476 }
477
478 /// Undo (or redo) the last edit to the tree. False when there is none,
479 /// so the caller can say nothing was taken.
480 pub fn history_step(&mut self, undo: bool) -> bool {
481 // What has been done and not yet looked at is the step to take.
482 self.note_structure_changes();
483 let Some(step) = self.edit_history.take(undo) else { return false };
484 let verb = if undo { "Undo" } else { "Redo" };
485 let what = step.what().to_string();
486 match step {
487 Step::Params(step) => {
488 let Some(node) = crate::viewer_state::find_node_by_id_mut(&mut self.fs_root, &step.node_id) else {
489 // Deleted since. The step names nothing, and is dropped.
490 self.update_status_text(&format!("{verb} {what}: the node is gone"));
491 return false;
492 };
493 // By name: the step holds the parameters that changed, and
494 // the rest of the node is as whatever wrote it last left it.
495 let mut replaced = Vec::new();
496 for was in step.params {
497 if let Some(p) = node.params.iter_mut().find(|p| p.name == was.name) {
498 replaced.push(std::mem::replace(p, was));
499 }
500 }
501 let name = node.name.clone();
502 self.edit_history.file(
503 undo,
504 Step::Params(ParamSnapshot { node_id: step.node_id, params: replaced, what: step.what }),
505 );
506 self.sync_grid_settings();
507 self.sync_nodes();
508 self.rebuild_scene_geometry();
509 self.sync_parameters_pane();
510 self.update_status_text(&format!("{verb} {what}: {name}"));
511 }
512 Step::Structure(step) => {
513 // Where the editor is and what is selected are slots, which
514 // a node coming or going moves: held by id across it.
515 let path = self.ids_along(&self.current_path.clone());
516 let selected = self
517 .graph()
518 .selected_node()
519 .and_then(|i| self.current_dir().children.get(i))
520 .map(|n| n.id.clone());
521 // The active camera is a name, which a rename changes.
522 let camera = self
523 .camera_level()
524 .children
525 .iter()
526 .find(|c| c.node_type == "camera" && c.name == self.active_camera)
527 .map(|c| c.id.clone());
528 let inverse = restore(&mut self.fs_root, step);
529 self.edit_history.file(undo, Step::Structure(inverse));
530 self.current_path = self.slots_along(&path);
531 let camera = camera
532 .and_then(|id| crate::viewer_state::find_node_by_id(&self.fs_root, &id))
533 .map(|n| n.name.clone());
534 if let Some(name) = camera {
535 self.set_active_camera(name);
536 }
537 let slot = selected.and_then(|id| self.current_dir().children.iter().position(|n| n.id == id));
538 self.graph_mut().set_selected_node(slot);
539 self.grid_cursor_expanse = None;
540 let at = slot.and_then(|i| self.current_dir().children.get(i)).map(|n| n.position);
541 if let Some((col, row)) = at {
542 self.grid_cursor_col = col as i32;
543 self.grid_cursor_row = row as i32;
544 }
545 self.sync_nodes();
546 self.rebuild_positions();
547 self.apply_layout();
548 self.update_panel_bounds();
549 self.rebuild_scene_geometry();
550 self.sync_parameters_pane();
551 self.viewport_dirty = true;
552 self.update_status_text(&format!("{verb} {what}"));
553 }
554 }
555 self.rebase_structure();
556 if self.syncing_windows() {
557 self.needs_autosave = true;
558 }
559 true
560 }
561
562 /// The ids of the nodes a path of slots goes down through.
563 fn ids_along(&self, path: &[usize]) -> Vec<String> {
564 let mut node = &self.fs_root;
565 let mut ids = Vec::new();
566 for &slot in path {
567 let Some(child) = node.children.get(slot) else { break };
568 ids.push(child.id.clone());
569 node = child;
570 }
571 ids
572 }
573
574 /// The path of slots that goes down through those nodes now, as far as
575 /// they are still there.
576 fn slots_along(&self, ids: &[String]) -> Vec<usize> {
577 let mut node = &self.fs_root;
578 let mut path = Vec::new();
579 for id in ids {
580 let Some(slot) = node.children.iter().position(|n| n.id == *id) else { break };
581 path.push(slot);
582 node = &node.children[slot];
583 }
584 path
585 }
586 }