Skip to main content

dioxus_mind_elixir/
handle.rs

1use crate::plugins::{Capability, Command, Plugin, PluginRegistry};
2use crate::{
3    CoreEvent, MindMapData, MindMapError, MindMapState, Operation, OperationOptions,
4    OperationResult,
5};
6use dioxus::prelude::*;
7use std::{collections::BTreeMap, rc::Rc};
8
9type Before = Rc<dyn Fn(&Operation) -> bool>;
10type Listener = Rc<dyn Fn(CoreEvent)>;
11
12/// Copyable shared document handle. Mutation always passes through checked core
13/// transactions. Individual views own their viewport, editor and mounted nodes.
14#[derive(Clone, Copy, PartialEq)]
15pub struct MindMapHandle {
16    state: Signal<MindMapState>,
17    before: Signal<Option<Before>>,
18    listener: Signal<Option<Listener>>,
19    editor_owner: Signal<Option<usize>>,
20    pending: Signal<Option<(usize, PendingEdit)>>,
21    subscribers: Signal<BTreeMap<u64, Listener>>,
22    next_subscription: Signal<u64>,
23    plugins: Signal<PluginRegistry>,
24}
25
26/// A topic draft that is being edited in one of the views. Hosts read it to
27/// protect unsaved text (before switching documents or closing a window) and
28/// decide with [`MindMapHandle::commit_pending`] or
29/// [`MindMapHandle::cancel_pending`].
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct PendingEdit {
32    /// Edited node.
33    pub id: String,
34    /// Topic when editing started.
35    pub original: String,
36    /// Current draft text.
37    pub draft: String,
38}
39impl PendingEdit {
40    /// Whether committing would change the document.
41    pub fn is_changed(&self) -> bool {
42        !self.draft.trim().is_empty() && self.draft != self.original
43    }
44}
45
46/// Create one shared document for any number of views in the current scope.
47/// The initial value is consumed only when the component first mounts.
48pub fn use_mind_map(data: MindMapData) -> Result<MindMapHandle, MindMapError> {
49    use_hook(|| {
50        MindMapState::new(data).map(|state| MindMapHandle {
51            state: Signal::new(state),
52            before: Signal::new(None),
53            listener: Signal::new(None),
54            editor_owner: Signal::new(None),
55            pending: Signal::new(None),
56            subscribers: Signal::new(BTreeMap::new()),
57            next_subscription: Signal::new(0),
58            plugins: Signal::new(PluginRegistry::default()),
59        })
60    })
61}
62impl MindMapHandle {
63    /// Read a reactive persistent snapshot. No internal mutable signal is exposed.
64    pub fn document(self) -> Result<MindMapData, MindMapError> {
65        self.state
66            .try_read()
67            .map(|state| state.data().clone())
68            .map_err(|_| MindMapError::StaleOperation)
69    }
70    /// Current selected IDs, reactively tracked.
71    pub fn selection(self) -> Result<Vec<String>, MindMapError> {
72        self.state
73            .try_read()
74            .map(|state| state.selection().to_vec())
75            .map_err(|_| MindMapError::StaleOperation)
76    }
77    /// Current generation, reactively tracked.
78    pub fn revision(self) -> Result<u64, MindMapError> {
79        self.state
80            .try_read()
81            .map(|state| state.revision())
82            .map_err(|_| MindMapError::StaleOperation)
83    }
84    /// Whether mutation is disabled.
85    pub fn is_read_only(self) -> Result<bool, MindMapError> {
86        self.state
87            .try_read()
88            .map(|state| state.is_read_only())
89            .map_err(|_| MindMapError::StaleOperation)
90    }
91    /// Set a synchronous veto hook. Invoked without a state borrow; reentrant
92    /// mutations cause the outer operation to fail with StaleOperation.
93    pub fn set_before(
94        mut self,
95        hook: impl Fn(&Operation) -> bool + 'static,
96    ) -> Result<(), MindMapError> {
97        *self
98            .before
99            .try_write()
100            .map_err(|_| MindMapError::StaleOperation)? = Some(Rc::new(hook));
101        Ok(())
102    }
103    /// Clear the veto hook.
104    pub fn clear_before(mut self) -> Result<(), MindMapError> {
105        *self
106            .before
107            .try_write()
108            .map_err(|_| MindMapError::StaleOperation)? = None;
109        Ok(())
110    }
111    /// Install a host event listener. Events are delivered after releasing state.
112    pub fn set_listener(
113        mut self,
114        listener: impl Fn(CoreEvent) + 'static,
115    ) -> Result<(), MindMapError> {
116        *self
117            .listener
118            .try_write()
119            .map_err(|_| MindMapError::StaleOperation)? = Some(Rc::new(listener));
120        Ok(())
121    }
122    /// Execute a checked mutation using the current before hook.
123    pub fn apply(self, operation: Operation) -> Result<OperationResult, MindMapError> {
124        self.apply_with(operation, OperationOptions::default())
125    }
126    /// Execute a checked mutation with explicit recording options.
127    pub fn apply_with(
128        mut self,
129        operation: Operation,
130        options: OperationOptions,
131    ) -> Result<OperationResult, MindMapError> {
132        let revision = self.revision()?;
133        if self.is_read_only()? {
134            return Err(MindMapError::ReadOnly);
135        }
136        let before = self
137            .before
138            .try_read()
139            .map_err(|_| MindMapError::StaleOperation)?
140            .clone();
141        if let Some(before) = before {
142            if !before(&operation) {
143                return Err(MindMapError::Rejected);
144            }
145        }
146        let result = {
147            let mut state = self
148                .state
149                .try_write()
150                .map_err(|_| MindMapError::StaleOperation)?;
151            if state.revision() != revision {
152                return Err(MindMapError::StaleOperation);
153            }
154            state.apply_with(operation, options)?
155        };
156        self.emit(&result.events)?;
157        Ok(result)
158    }
159    /// Select existing IDs, including collapsed descendants.
160    pub fn select(mut self, ids: &[String]) -> Result<(), MindMapError> {
161        let events = self
162            .state
163            .try_write()
164            .map_err(|_| MindMapError::StaleOperation)?
165            .select(ids)?;
166        self.emit(&events)
167    }
168    /// Enable/disable all document mutation entry points.
169    pub fn set_read_only(mut self, value: bool) -> Result<(), MindMapError> {
170        self.state
171            .try_write()
172            .map_err(|_| MindMapError::StaleOperation)?
173            .set_read_only(value);
174        Ok(())
175    }
176    /// Undo and restore selection.
177    pub fn undo(mut self) -> Result<(), MindMapError> {
178        let events = self
179            .state
180            .try_write()
181            .map_err(|_| MindMapError::StaleOperation)?
182            .undo()?;
183        self.emit(&events)
184    }
185    /// Redo and restore selection.
186    pub fn redo(mut self) -> Result<(), MindMapError> {
187        let events = self
188            .state
189            .try_write()
190            .map_err(|_| MindMapError::StaleOperation)?
191            .redo()?;
192        self.emit(&events)
193    }
194    /// Validate and replace the document, clearing selection and history.
195    pub fn refresh(mut self, data: MindMapData) -> Result<(), MindMapError> {
196        let events = self
197            .state
198            .try_write()
199            .map_err(|_| MindMapError::StaleOperation)?
200            .refresh(data)?;
201        self.emit(&events)
202    }
203    /// The topic draft currently being edited, reactively tracked.
204    pub fn pending_edit(self) -> Result<Option<PendingEdit>, MindMapError> {
205        self.pending
206            .try_read()
207            .map(|pending| pending.as_ref().map(|(_, edit)| edit.clone()))
208            .map_err(|_| MindMapError::StaleOperation)
209    }
210    /// Apply the pending draft as a normal undoable topic change and close the
211    /// editor. Blank or unchanged drafts only close the editor. Returns whether
212    /// the document changed. On failure the draft stays pending.
213    pub fn commit_pending(mut self) -> Result<bool, MindMapError> {
214        let pending = self
215            .pending
216            .try_read()
217            .map_err(|_| MindMapError::StaleOperation)?
218            .clone();
219        let Some((_, edit)) = pending else {
220            return Ok(false);
221        };
222        let changed = if edit.is_changed() {
223            self.apply(Operation::SetTopic {
224                id: edit.id.clone(),
225                topic: edit.draft.clone(),
226            })?
227            .changed
228        } else {
229            false
230        };
231        *self
232            .pending
233            .try_write()
234            .map_err(|_| MindMapError::StaleOperation)? = None;
235        Ok(changed)
236    }
237    /// Discard the pending draft and close the editor.
238    pub fn cancel_pending(mut self) -> Result<(), MindMapError> {
239        *self
240            .pending
241            .try_write()
242            .map_err(|_| MindMapError::StaleOperation)? = None;
243        Ok(())
244    }
245    pub(crate) fn begin_edit(
246        mut self,
247        owner: usize,
248        id: &str,
249        original: &str,
250    ) -> Result<(), MindMapError> {
251        self.claim_editor(owner)?;
252        *self
253            .pending
254            .try_write()
255            .map_err(|_| MindMapError::StaleOperation)? = Some((
256            owner,
257            PendingEdit {
258                id: id.into(),
259                original: original.into(),
260                draft: original.into(),
261            },
262        ));
263        Ok(())
264    }
265    pub(crate) fn update_draft(mut self, owner: usize, draft: String) {
266        if let Ok(mut pending) = self.pending.try_write() {
267            if let Some((current, edit)) = pending.as_mut() {
268                if *current == owner {
269                    edit.draft = draft;
270                }
271            }
272        }
273    }
274    /// Start a draft without a view (tests and host-driven editors).
275    #[doc(hidden)]
276    pub fn begin_edit_for_test(self, id: &str, original: &str) -> Result<(), MindMapError> {
277        self.begin_edit(usize::MAX, id, original)
278    }
279    /// Update a draft started by [`Self::begin_edit_for_test`].
280    #[doc(hidden)]
281    pub fn update_draft_for_test(self, draft: &str) {
282        self.update_draft(usize::MAX, draft.into());
283    }
284    /// Whether `owner` still edits node `id` (the host may have closed it).
285    pub(crate) fn edits(self, owner: usize, id: &str) -> bool {
286        self.pending
287            .try_read()
288            .is_ok_and(|p| p.as_ref().is_some_and(|(o, e)| *o == owner && e.id == id))
289    }
290    pub(crate) fn claim_editor(mut self, owner: usize) -> Result<(), MindMapError> {
291        *self
292            .editor_owner
293            .try_write()
294            .map_err(|_| MindMapError::StaleOperation)? = Some(owner);
295        Ok(())
296    }
297    pub(crate) fn owns_editor(self, owner: usize) -> Result<bool, MindMapError> {
298        Ok(*self
299            .editor_owner
300            .try_read()
301            .map_err(|_| MindMapError::StaleOperation)?
302            == Some(owner))
303    }
304    /// Add an independent listener. Dropping the returned guard unsubscribes it.
305    pub fn subscribe(
306        mut self,
307        listener: impl Fn(CoreEvent) + 'static,
308    ) -> Result<Subscription, MindMapError> {
309        let id = *self
310            .next_subscription
311            .try_read()
312            .map_err(|_| MindMapError::StaleOperation)?;
313        let next = id
314            .checked_add(1)
315            .ok_or_else(|| MindMapError::InvalidOperation("Subscription ID exhausted".into()))?;
316        *self
317            .next_subscription
318            .try_write()
319            .map_err(|_| MindMapError::StaleOperation)? = next;
320        self.subscribers
321            .try_write()
322            .map_err(|_| MindMapError::StaleOperation)?
323            .insert(id, Rc::new(listener));
324        Ok(Subscription { handle: self, id })
325    }
326    /// Register a plugin for the lifetime of a guard. No document/history mutation.
327    pub fn register_plugin(mut self, plugin: Plugin) -> Result<PluginGuard, MindMapError> {
328        let id = plugin.id.clone();
329        self.plugins
330            .try_write()
331            .map_err(|_| MindMapError::StaleOperation)?
332            .register(plugin)?;
333        Ok(PluginGuard {
334            handle: self,
335            id,
336            subscriptions: Vec::new(),
337        })
338    }
339    /// Toggle an existing registration. Disabled capabilities disappear reactively.
340    pub fn set_plugin_enabled(mut self, id: &str, enabled: bool) -> Result<(), MindMapError> {
341        self.plugins
342            .try_write()
343            .map_err(|_| MindMapError::StaleOperation)?
344            .set_enabled(id, enabled)
345    }
346    /// Test an enabled capability for this document only.
347    pub fn has_plugin(self, capability: Capability) -> bool {
348        self.plugins
349            .try_read()
350            .is_ok_and(|plugins| plugins.provides(capability))
351    }
352    /// Commands supplied by enabled plugins.
353    pub fn plugin_commands(self) -> Result<Vec<Command>, MindMapError> {
354        Ok(self
355            .plugins
356            .try_read()
357            .map_err(|_| MindMapError::StaleOperation)?
358            .commands())
359    }
360    fn emit(self, events: &[CoreEvent]) -> Result<(), MindMapError> {
361        let listener = self
362            .listener
363            .try_read()
364            .map_err(|_| MindMapError::StaleOperation)?
365            .clone();
366        let subscribers = self
367            .subscribers
368            .try_read()
369            .map_err(|_| MindMapError::StaleOperation)?
370            .iter()
371            .map(|(id, listener)| (*id, listener.clone()))
372            .collect::<Vec<_>>();
373        for event in events {
374            if let Some(listener) = &listener {
375                listener(event.clone());
376            }
377            for (id, callback) in &subscribers {
378                if self
379                    .subscribers
380                    .try_read()
381                    .is_ok_and(|current| current.contains_key(id))
382                {
383                    callback(event.clone());
384                }
385            }
386        }
387        Ok(())
388    }
389}
390
391/// An event subscription; dropping it releases the callback, including on unmount.
392pub struct Subscription {
393    handle: MindMapHandle,
394    id: u64,
395}
396impl Drop for Subscription {
397    fn drop(&mut self) {
398        if let Ok(mut entries) = self.handle.subscribers.try_write() {
399            entries.remove(&self.id);
400        }
401    }
402}
403/// Registration lifetime and owned event subscriptions for one plugin instance.
404pub struct PluginGuard {
405    handle: MindMapHandle,
406    id: String,
407    subscriptions: Vec<Subscription>,
408}
409impl PluginGuard {
410    /// Attach a listener that is removed together with this plugin.
411    pub fn subscribe(
412        &mut self,
413        listener: impl Fn(CoreEvent) + 'static,
414    ) -> Result<(), MindMapError> {
415        let handle = self.handle;
416        let id = self.id.clone();
417        self.subscriptions.push(handle.subscribe(move |event| {
418            let enabled = handle
419                .plugins
420                .try_read()
421                .is_ok_and(|registry| registry.is_enabled(&id));
422            if enabled {
423                listener(event);
424            }
425        })?);
426        Ok(())
427    }
428    /// Toggle this registration without releasing it.
429    pub fn set_enabled(&self, enabled: bool) -> Result<(), MindMapError> {
430        self.handle.set_plugin_enabled(&self.id, enabled)
431    }
432}
433impl Drop for PluginGuard {
434    fn drop(&mut self) {
435        if let Ok(mut registry) = self.handle.plugins.try_write() {
436            let _ = registry.unregister(&self.id);
437        }
438    }
439}
440/// Install plugins for the current component's lifetime. Returns errors explicitly.
441pub fn use_plugins(
442    handle: MindMapHandle,
443    plugins: Vec<Plugin>,
444) -> Result<Rc<Vec<PluginGuard>>, MindMapError> {
445    use_hook(|| {
446        plugins
447            .into_iter()
448            .map(|plugin| handle.register_plugin(plugin))
449            .collect::<Result<Vec<_>, _>>()
450            .map(Rc::new)
451    })
452}