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, Copy, Debug, Default, PartialEq, Eq)]
160pub enum Locale {
161 #[default]
163 En,
164 Zh,
166}
167impl Locale {
168 pub fn label(self, command: Command) -> &'static str {
170 match (self, command) {
171 (Self::En, Command::Child) => "Add child",
172 (Self::Zh, Command::Child) => "添加子节点",
173 (Self::En, Command::SiblingAfter) => "Add sibling",
174 (Self::Zh, Command::SiblingAfter) => "添加同级",
175 (Self::En, Command::SiblingBefore) => "Insert before",
176 (Self::Zh, Command::SiblingBefore) => "前插同级",
177 (Self::En, Command::Parent) => "Insert parent",
178 (Self::Zh, Command::Parent) => "插入父节点",
179 (Self::En, Command::Delete) => "Delete",
180 (Self::Zh, Command::Delete) => "删除",
181 (Self::En, Command::Duplicate) => "Duplicate",
182 (Self::Zh, Command::Duplicate) => "复制",
183 (Self::En, Command::Fold) => "Expand / collapse",
184 (Self::Zh, Command::Fold) => "展开 / 折叠",
185 (Self::En, Command::Indent) => "Indent",
186 (Self::Zh, Command::Indent) => "缩进",
187 (Self::En, Command::Outdent) => "Outdent",
188 (Self::Zh, Command::Outdent) => "提升层级",
189 (Self::En, Command::Undo) => "Undo",
190 (Self::Zh, Command::Undo) => "撤销",
191 (Self::En, Command::Redo) => "Redo",
192 (Self::Zh, Command::Redo) => "重做",
193 }
194 }
195 pub(crate) fn topic(self) -> &'static str {
196 if self == Self::Zh {
197 "新节点"
198 } else {
199 "New node"
200 }
201 }
202}
203pub fn parent<'a>(root: &'a Node, id: &str) -> Option<&'a Node> {
205 if root.children.iter().any(|child| child.id == id) {
206 Some(root)
207 } else {
208 root.children.iter().find_map(|child| parent(child, id))
209 }
210}
211pub fn command_operation(
213 data: &MindMapData,
214 ids: &[String],
215 command: Command,
216 locale: Locale,
217) -> Result<Operation, MindMapError> {
218 let id = ids
219 .first()
220 .ok_or_else(|| MindMapError::InvalidOperation("Select a node first".into()))?;
221 let node = data
222 .node_data
223 .find(id)
224 .ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?;
225 let fresh = || -> Result<Node, MindMapError> {
226 let mut n = 1u64;
227 loop {
228 let candidate = format!("plugin-{n}");
229 if data.node_data.find(&candidate).is_none() {
230 return Ok(Node::new(candidate, locale.topic()));
231 }
232 n = n
233 .checked_add(1)
234 .ok_or_else(|| MindMapError::InvalidOperation("Node ID space exhausted".into()))?;
235 }
236 };
237 let operation = match command {
238 Command::Child => Operation::Insert {
239 node: fresh()?,
240 placement: Placement::In(id.clone()),
241 },
242 Command::SiblingAfter => Operation::Insert {
243 node: fresh()?,
244 placement: Placement::After(id.clone()),
245 },
246 Command::SiblingBefore => Operation::Insert {
247 node: fresh()?,
248 placement: Placement::Before(id.clone()),
249 },
250 Command::Parent => Operation::InsertParent {
251 target: id.clone(),
252 parent: fresh()?,
253 },
254 Command::Delete => Operation::Remove(ids.to_vec()),
255 Command::Duplicate => Operation::Copy {
256 ids: ids.to_vec(),
257 placement: Placement::After(id.clone()),
258 },
259 Command::Fold => Operation::SetExpanded {
260 id: id.clone(),
261 expanded: !node.expanded,
262 },
263 Command::Indent => {
264 let p = parent(&data.node_data, id).ok_or(MindMapError::RootOperation)?;
265 let index = p
266 .children
267 .iter()
268 .position(|n| n.id == *id)
269 .ok_or(MindMapError::StaleOperation)?;
270 let previous = index
271 .checked_sub(1)
272 .and_then(|i| p.children.get(i))
273 .ok_or_else(|| MindMapError::InvalidOperation("No previous sibling".into()))?;
274 Operation::Move {
275 ids: ids.to_vec(),
276 placement: Placement::In(previous.id.clone()),
277 }
278 }
279 Command::Outdent => {
280 let p = parent(&data.node_data, id).ok_or(MindMapError::RootOperation)?;
281 if p.id == data.node_data.id {
282 return Err(MindMapError::RootOperation);
283 }
284 Operation::Move {
285 ids: ids.to_vec(),
286 placement: Placement::After(p.id.clone()),
287 }
288 }
289 Command::Undo | Command::Redo => {
290 return Err(MindMapError::InvalidOperation(
291 "History command has no tree operation".into(),
292 ));
293 }
294 };
295 Ok(operation)
296}
297pub fn execute(
299 handle: MindMapHandle,
300 command: Command,
301 locale: Locale,
302) -> Result<(), MindMapError> {
303 match command {
304 Command::Undo => handle.undo(),
305 Command::Redo => handle.redo(),
306 _ => handle
307 .apply(command_operation(
308 &handle.document()?,
309 &handle.selection()?,
310 command,
311 locale,
312 )?)
313 .map(|_| ()),
314 }
315}