nightshade_api/web/
undo_tree.rs1use leptos::prelude::*;
4
5#[derive(Clone)]
6struct Node<T> {
7 value: T,
8 parent: Option<usize>,
9 label: String,
10}
11
12pub struct UndoHistory<T: Send + Sync + 'static> {
17 nodes: RwSignal<Vec<Node<T>>>,
18 current: RwSignal<usize>,
19}
20
21impl<T: Send + Sync + 'static> Clone for UndoHistory<T> {
22 fn clone(&self) -> Self {
23 *self
24 }
25}
26
27impl<T: Send + Sync + 'static> Copy for UndoHistory<T> {}
28
29#[derive(Clone)]
32pub struct HistoryNode {
33 pub id: usize,
35 pub label: String,
37 pub depth: usize,
39 pub current: bool,
41}
42
43impl<T: Clone + Send + Sync + 'static> UndoHistory<T> {
44 pub fn new(initial: T) -> Self {
46 Self {
47 nodes: RwSignal::new(vec![Node {
48 value: initial,
49 parent: None,
50 label: "initial".to_string(),
51 }]),
52 current: RwSignal::new(0),
53 }
54 }
55
56 pub fn value(&self) -> T {
58 let current = self.current.get();
59 self.nodes.with(|nodes| nodes[current].value.clone())
60 }
61
62 pub fn push(&self, value: T, label: impl Into<String>) {
64 let parent = self.current.get_untracked();
65 let id = self.nodes.with_untracked(Vec::len);
66 self.nodes.update(|nodes| {
67 nodes.push(Node {
68 value,
69 parent: Some(parent),
70 label: label.into(),
71 });
72 });
73 self.current.set(id);
74 }
75
76 pub fn undo(&self) {
78 let parent = self
79 .nodes
80 .with_untracked(|nodes| nodes[self.current.get_untracked()].parent);
81 if let Some(parent) = parent {
82 self.current.set(parent);
83 }
84 }
85
86 pub fn redo(&self) {
88 let current = self.current.get_untracked();
89 let child = self.nodes.with_untracked(|nodes| {
90 nodes
91 .iter()
92 .enumerate()
93 .rev()
94 .find(|(_, node)| node.parent == Some(current))
95 .map(|(index, _)| index)
96 });
97 if let Some(child) = child {
98 self.current.set(child);
99 }
100 }
101
102 pub fn restore(&self, id: usize) {
104 if self.nodes.with_untracked(|nodes| id < nodes.len()) {
105 self.current.set(id);
106 }
107 }
108
109 pub fn rows(&self) -> Vec<HistoryNode> {
111 let current = self.current.get();
112 self.nodes.with(|nodes| {
113 nodes
114 .iter()
115 .enumerate()
116 .map(|(id, node)| {
117 let mut depth = 0;
118 let mut cursor = node.parent;
119 while let Some(parent) = cursor {
120 depth += 1;
121 cursor = nodes[parent].parent;
122 }
123 HistoryNode {
124 id,
125 label: node.label.clone(),
126 depth,
127 current: id == current,
128 }
129 })
130 .collect()
131 })
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::UndoHistory;
138 use leptos::prelude::*;
139
140 #[test]
141 fn undo_redo_and_branching_navigate_the_tree() {
142 let _owner = Owner::new();
143 _owner.set();
144 let history = UndoHistory::new("a".to_string());
145 history.push("b".to_string(), "b");
146 history.push("c".to_string(), "c");
147 assert_eq!(history.value(), "c");
148 history.undo();
149 assert_eq!(history.value(), "b");
150 history.undo();
151 assert_eq!(history.value(), "a");
152 history.redo();
153 assert_eq!(history.value(), "b");
154 history.push("d".to_string(), "d");
155 assert_eq!(history.value(), "d");
156 assert_eq!(history.rows().len(), 4);
157 }
158}
159
160#[component]
164pub fn UndoTree(
165 #[prop(into)] nodes: Signal<Vec<HistoryNode>>,
166 on_restore: Callback<usize>,
167) -> impl IntoView {
168 view! {
169 <div class="nightshade-undo-tree">
170 {move || {
171 nodes
172 .get()
173 .into_iter()
174 .rev()
175 .map(|node| {
176 let id = node.id;
177 let indent = format!("padding-left:{}px", 8 + node.depth * 14);
178 view! {
179 <button
180 class="nightshade-undo-node"
181 class:current=node.current
182 style=indent
183 on:click=move |_| on_restore.run(id)
184 >
185 <span class="nightshade-undo-dot"></span>
186 <span class="nightshade-undo-label">{node.label}</span>
187 </button>
188 }
189 })
190 .collect_view()
191 }}
192 </div>
193 }
194}