1pub mod export;
3pub mod gesture;
4use crate::{MindMapData, MindMapError, MindMapHandle, Node, Operation, Placement};
5use std::collections::BTreeMap;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9pub enum Capability {
10 Keyboard,
12 MultiSelect,
14 DragDrop,
16 EdgePan,
18 ContextMenu,
20 Toolbar,
22 Outline,
24 Export,
26}
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Command {
30 Child,
32 SiblingAfter,
34 SiblingBefore,
36 Parent,
38 Delete,
40 Duplicate,
42 Fold,
44 Indent,
46 Outdent,
48 Undo,
50 Redo,
52}
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct Plugin {
56 pub id: String,
58 pub capabilities: Vec<Capability>,
60 pub commands: Vec<Command>,
62}
63impl Plugin {
64 pub fn new(id: impl Into<String>, capabilities: Vec<Capability>) -> Self {
66 Self {
67 id: id.into(),
68 capabilities,
69 commands: Vec::new(),
70 }
71 }
72 pub fn builtins() -> Vec<Self> {
74 [
75 Capability::Keyboard,
76 Capability::MultiSelect,
77 Capability::DragDrop,
78 Capability::EdgePan,
79 Capability::ContextMenu,
80 Capability::Toolbar,
81 Capability::Outline,
82 Capability::Export,
83 ]
84 .into_iter()
85 .map(|capability| {
86 let mut plugin = Self::new(format!("builtin.{capability:?}"), vec![capability]);
87 if matches!(capability, Capability::Toolbar | Capability::ContextMenu) {
88 plugin.commands = vec![
89 Command::Child,
90 Command::SiblingAfter,
91 Command::Parent,
92 Command::Duplicate,
93 Command::Fold,
94 Command::Delete,
95 Command::Undo,
96 Command::Redo,
97 ];
98 }
99 plugin
100 })
101 .collect()
102 }
103}
104#[derive(Clone, Debug, Default)]
106pub struct PluginRegistry(BTreeMap<String, (Plugin, bool)>);
107impl PluginRegistry {
108 pub fn register(&mut self, plugin: Plugin) -> Result<(), MindMapError> {
110 if plugin.id.trim().is_empty() || self.0.contains_key(&plugin.id) {
111 return Err(MindMapError::InvalidOperation(
112 "Empty or duplicate plugin ID".into(),
113 ));
114 }
115 self.0.insert(plugin.id.clone(), (plugin, true));
116 Ok(())
117 }
118 pub fn set_enabled(&mut self, id: &str, enabled: bool) -> Result<(), MindMapError> {
120 self.0
121 .get_mut(id)
122 .ok_or_else(|| MindMapError::InvalidOperation(format!("Unknown plugin: {id}")))?
123 .1 = enabled;
124 Ok(())
125 }
126 pub fn unregister(&mut self, id: &str) -> Result<(), MindMapError> {
128 self.0
129 .remove(id)
130 .ok_or_else(|| MindMapError::InvalidOperation(format!("Unknown plugin: {id}")))?;
131 Ok(())
132 }
133 pub fn is_enabled(&self, id: &str) -> bool {
135 self.0.get(id).is_some_and(|(_, enabled)| *enabled)
136 }
137 pub fn provides(&self, capability: Capability) -> bool {
139 self.0
140 .values()
141 .any(|(plugin, enabled)| *enabled && plugin.capabilities.contains(&capability))
142 }
143 pub fn commands(&self) -> Vec<Command> {
145 let mut commands = Vec::new();
146 for (plugin, enabled) in self.0.values() {
147 if *enabled {
148 for command in &plugin.commands {
149 if !commands.contains(command) {
150 commands.push(*command);
151 }
152 }
153 }
154 }
155 commands
156 }
157}
158#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct Labels {
162 pub new_topic: String,
164 pub add_child: String,
166 pub add_sibling: String,
168 pub insert_before: String,
170 pub insert_parent: String,
172 pub delete: String,
174 pub duplicate: String,
176 pub fold: String,
178 pub indent: String,
180 pub outdent: String,
182 pub undo: String,
184 pub redo: String,
186 pub map: String,
188 pub outline: String,
190 pub breadcrumbs: String,
192 pub toolbar: String,
194 pub edit_topic: String,
196 pub collapse: String,
198 pub expand: String,
200 pub toggle_children: String,
202 pub focus_branch: String,
204 pub has_note: String,
206 pub has_image: String,
208 pub image_unavailable: String,
210 pub moving: String,
212 pub drop_here: String,
214}
215impl Default for Labels {
216 fn default() -> Self {
217 let s = String::from;
218 Self {
219 new_topic: s("New node"),
220 add_child: s("Add child"),
221 add_sibling: s("Add sibling"),
222 insert_before: s("Insert before"),
223 insert_parent: s("Insert parent"),
224 delete: s("Delete"),
225 duplicate: s("Duplicate"),
226 fold: s("Expand / collapse"),
227 indent: s("Indent"),
228 outdent: s("Outdent"),
229 undo: s("Undo"),
230 redo: s("Redo"),
231 map: s("Mind map"),
232 outline: s("Outline"),
233 breadcrumbs: s("Breadcrumbs"),
234 toolbar: s("Mind map toolbar"),
235 edit_topic: s("Edit node"),
236 collapse: s("Collapse"),
237 expand: s("Expand"),
238 toggle_children: s("Toggle children"),
239 focus_branch: s("Focus branch"),
240 has_note: s("Has note"),
241 has_image: s("Has image"),
242 image_unavailable: s("Image unavailable"),
243 moving: s("Moving branches ยท Esc to cancel"),
244 drop_here: s("Drop to move"),
245 }
246 }
247}
248impl Labels {
249 pub fn command(&self, command: Command) -> &str {
251 match command {
252 Command::Child => &self.add_child,
253 Command::SiblingAfter => &self.add_sibling,
254 Command::SiblingBefore => &self.insert_before,
255 Command::Parent => &self.insert_parent,
256 Command::Delete => &self.delete,
257 Command::Duplicate => &self.duplicate,
258 Command::Fold => &self.fold,
259 Command::Indent => &self.indent,
260 Command::Outdent => &self.outdent,
261 Command::Undo => &self.undo,
262 Command::Redo => &self.redo,
263 }
264 }
265}
266pub fn parent<'a>(root: &'a Node, id: &str) -> Option<&'a Node> {
268 if root.children.iter().any(|child| child.id == id) {
269 Some(root)
270 } else {
271 root.children.iter().find_map(|child| parent(child, id))
272 }
273}
274pub fn command_operation(
276 data: &MindMapData,
277 ids: &[String],
278 command: Command,
279 new_topic: &str,
280) -> Result<Operation, MindMapError> {
281 let id = ids
282 .first()
283 .ok_or_else(|| MindMapError::InvalidOperation("Select a node first".into()))?;
284 let node = data
285 .node_data
286 .find(id)
287 .ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?;
288 let fresh = || -> Result<Node, MindMapError> {
289 let mut n = 1u64;
290 loop {
291 let candidate = format!("plugin-{n}");
292 if data.node_data.find(&candidate).is_none() {
293 return Ok(Node::new(candidate, new_topic));
294 }
295 n = n
296 .checked_add(1)
297 .ok_or_else(|| MindMapError::InvalidOperation("Node ID space exhausted".into()))?;
298 }
299 };
300 let operation = match command {
301 Command::Child => Operation::Insert {
302 node: fresh()?,
303 placement: Placement::In(id.clone()),
304 },
305 Command::SiblingAfter => Operation::Insert {
306 node: fresh()?,
307 placement: Placement::After(id.clone()),
308 },
309 Command::SiblingBefore => Operation::Insert {
310 node: fresh()?,
311 placement: Placement::Before(id.clone()),
312 },
313 Command::Parent => Operation::InsertParent {
314 target: id.clone(),
315 parent: fresh()?,
316 },
317 Command::Delete => Operation::Remove(ids.to_vec()),
318 Command::Duplicate => Operation::Copy {
319 ids: ids.to_vec(),
320 placement: Placement::After(id.clone()),
321 },
322 Command::Fold => Operation::SetExpanded {
323 id: id.clone(),
324 expanded: !node.expanded,
325 },
326 Command::Indent => {
327 let p = parent(&data.node_data, id).ok_or(MindMapError::RootOperation)?;
328 let index = p
329 .children
330 .iter()
331 .position(|n| n.id == *id)
332 .ok_or(MindMapError::StaleOperation)?;
333 let previous = index
334 .checked_sub(1)
335 .and_then(|i| p.children.get(i))
336 .ok_or_else(|| MindMapError::InvalidOperation("No previous sibling".into()))?;
337 Operation::Move {
338 ids: ids.to_vec(),
339 placement: Placement::In(previous.id.clone()),
340 }
341 }
342 Command::Outdent => {
343 let p = parent(&data.node_data, id).ok_or(MindMapError::RootOperation)?;
344 if p.id == data.node_data.id {
345 return Err(MindMapError::RootOperation);
346 }
347 Operation::Move {
348 ids: ids.to_vec(),
349 placement: Placement::After(p.id.clone()),
350 }
351 }
352 Command::Undo | Command::Redo => {
353 return Err(MindMapError::InvalidOperation(
354 "History command has no tree operation".into(),
355 ));
356 }
357 };
358 Ok(operation)
359}
360pub fn execute(
362 handle: MindMapHandle,
363 command: Command,
364 new_topic: &str,
365) -> Result<(), MindMapError> {
366 match command {
367 Command::Undo => handle.undo(),
368 Command::Redo => handle.redo(),
369 _ => handle
370 .apply(command_operation(
371 &handle.document()?,
372 &handle.selection()?,
373 command,
374 new_topic,
375 )?)
376 .map(|_| ()),
377 }
378}