use crate::plugins::{Capability, Command, Plugin, PluginRegistry};
use crate::{
CoreEvent, MindMapData, MindMapError, MindMapState, Operation, OperationOptions,
OperationResult,
};
use dioxus::prelude::*;
use std::{collections::BTreeMap, rc::Rc};
type Before = Rc<dyn Fn(&Operation) -> bool>;
type Listener = Rc<dyn Fn(CoreEvent)>;
#[derive(Clone, Copy, PartialEq)]
pub struct MindMapHandle {
state: Signal<MindMapState>,
before: Signal<Option<Before>>,
listener: Signal<Option<Listener>>,
editor_owner: Signal<Option<usize>>,
pending: Signal<Option<(usize, PendingEdit)>>,
subscribers: Signal<BTreeMap<u64, Listener>>,
next_subscription: Signal<u64>,
plugins: Signal<PluginRegistry>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingEdit {
pub id: String,
pub original: String,
pub draft: String,
}
impl PendingEdit {
pub fn is_changed(&self) -> bool {
!self.draft.trim().is_empty() && self.draft != self.original
}
}
pub fn use_mind_map(data: MindMapData) -> Result<MindMapHandle, MindMapError> {
use_hook(|| {
MindMapState::new(data).map(|state| MindMapHandle {
state: Signal::new(state),
before: Signal::new(None),
listener: Signal::new(None),
editor_owner: Signal::new(None),
pending: Signal::new(None),
subscribers: Signal::new(BTreeMap::new()),
next_subscription: Signal::new(0),
plugins: Signal::new(PluginRegistry::default()),
})
})
}
impl MindMapHandle {
pub fn document(self) -> Result<MindMapData, MindMapError> {
self.state
.try_read()
.map(|state| state.data().clone())
.map_err(|_| MindMapError::StaleOperation)
}
pub fn selection(self) -> Result<Vec<String>, MindMapError> {
self.state
.try_read()
.map(|state| state.selection().to_vec())
.map_err(|_| MindMapError::StaleOperation)
}
pub fn revision(self) -> Result<u64, MindMapError> {
self.state
.try_read()
.map(|state| state.revision())
.map_err(|_| MindMapError::StaleOperation)
}
pub fn is_read_only(self) -> Result<bool, MindMapError> {
self.state
.try_read()
.map(|state| state.is_read_only())
.map_err(|_| MindMapError::StaleOperation)
}
pub fn set_before(
mut self,
hook: impl Fn(&Operation) -> bool + 'static,
) -> Result<(), MindMapError> {
*self
.before
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = Some(Rc::new(hook));
Ok(())
}
pub fn clear_before(mut self) -> Result<(), MindMapError> {
*self
.before
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = None;
Ok(())
}
pub fn set_listener(
mut self,
listener: impl Fn(CoreEvent) + 'static,
) -> Result<(), MindMapError> {
*self
.listener
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = Some(Rc::new(listener));
Ok(())
}
pub fn apply(self, operation: Operation) -> Result<OperationResult, MindMapError> {
self.apply_with(operation, OperationOptions::default())
}
pub fn apply_with(
mut self,
operation: Operation,
options: OperationOptions,
) -> Result<OperationResult, MindMapError> {
let revision = self.revision()?;
if self.is_read_only()? {
return Err(MindMapError::ReadOnly);
}
let before = self
.before
.try_read()
.map_err(|_| MindMapError::StaleOperation)?
.clone();
if let Some(before) = before {
if !before(&operation) {
return Err(MindMapError::Rejected);
}
}
let result = {
let mut state = self
.state
.try_write()
.map_err(|_| MindMapError::StaleOperation)?;
if state.revision() != revision {
return Err(MindMapError::StaleOperation);
}
state.apply_with(operation, options)?
};
self.emit(&result.events)?;
Ok(result)
}
pub fn select(mut self, ids: &[String]) -> Result<(), MindMapError> {
let events = self
.state
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.select(ids)?;
self.emit(&events)
}
pub fn set_read_only(mut self, value: bool) -> Result<(), MindMapError> {
self.state
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.set_read_only(value);
Ok(())
}
pub fn undo(mut self) -> Result<(), MindMapError> {
let events = self
.state
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.undo()?;
self.emit(&events)
}
pub fn redo(mut self) -> Result<(), MindMapError> {
let events = self
.state
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.redo()?;
self.emit(&events)
}
pub fn refresh(mut self, data: MindMapData) -> Result<(), MindMapError> {
let events = self
.state
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.refresh(data)?;
self.emit(&events)
}
pub fn pending_edit(self) -> Result<Option<PendingEdit>, MindMapError> {
self.pending
.try_read()
.map(|pending| pending.as_ref().map(|(_, edit)| edit.clone()))
.map_err(|_| MindMapError::StaleOperation)
}
pub fn commit_pending(mut self) -> Result<bool, MindMapError> {
let pending = self
.pending
.try_read()
.map_err(|_| MindMapError::StaleOperation)?
.clone();
let Some((_, edit)) = pending else {
return Ok(false);
};
let changed = if edit.is_changed() {
self.apply(Operation::SetTopic {
id: edit.id.clone(),
topic: edit.draft.clone(),
})?
.changed
} else {
false
};
*self
.pending
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = None;
Ok(changed)
}
pub fn cancel_pending(mut self) -> Result<(), MindMapError> {
*self
.pending
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = None;
Ok(())
}
pub(crate) fn begin_edit(
mut self,
owner: usize,
id: &str,
original: &str,
) -> Result<(), MindMapError> {
self.claim_editor(owner)?;
*self
.pending
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = Some((
owner,
PendingEdit {
id: id.into(),
original: original.into(),
draft: original.into(),
},
));
Ok(())
}
pub(crate) fn update_draft(mut self, owner: usize, draft: String) {
if let Ok(mut pending) = self.pending.try_write() {
if let Some((current, edit)) = pending.as_mut() {
if *current == owner {
edit.draft = draft;
}
}
}
}
#[doc(hidden)]
pub fn begin_edit_for_test(self, id: &str, original: &str) -> Result<(), MindMapError> {
self.begin_edit(usize::MAX, id, original)
}
#[doc(hidden)]
pub fn update_draft_for_test(self, draft: &str) {
self.update_draft(usize::MAX, draft.into());
}
pub(crate) fn edits(self, owner: usize, id: &str) -> bool {
self.pending
.try_read()
.is_ok_and(|p| p.as_ref().is_some_and(|(o, e)| *o == owner && e.id == id))
}
pub(crate) fn claim_editor(mut self, owner: usize) -> Result<(), MindMapError> {
*self
.editor_owner
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = Some(owner);
Ok(())
}
pub(crate) fn owns_editor(self, owner: usize) -> Result<bool, MindMapError> {
Ok(*self
.editor_owner
.try_read()
.map_err(|_| MindMapError::StaleOperation)?
== Some(owner))
}
pub fn subscribe(
mut self,
listener: impl Fn(CoreEvent) + 'static,
) -> Result<Subscription, MindMapError> {
let id = *self
.next_subscription
.try_read()
.map_err(|_| MindMapError::StaleOperation)?;
let next = id
.checked_add(1)
.ok_or_else(|| MindMapError::InvalidOperation("Subscription ID exhausted".into()))?;
*self
.next_subscription
.try_write()
.map_err(|_| MindMapError::StaleOperation)? = next;
self.subscribers
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.insert(id, Rc::new(listener));
Ok(Subscription { handle: self, id })
}
pub fn register_plugin(mut self, plugin: Plugin) -> Result<PluginGuard, MindMapError> {
let id = plugin.id.clone();
self.plugins
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.register(plugin)?;
Ok(PluginGuard {
handle: self,
id,
subscriptions: Vec::new(),
})
}
pub fn set_plugin_enabled(mut self, id: &str, enabled: bool) -> Result<(), MindMapError> {
self.plugins
.try_write()
.map_err(|_| MindMapError::StaleOperation)?
.set_enabled(id, enabled)
}
pub fn has_plugin(self, capability: Capability) -> bool {
self.plugins
.try_read()
.is_ok_and(|plugins| plugins.provides(capability))
}
pub fn plugin_commands(self) -> Result<Vec<Command>, MindMapError> {
Ok(self
.plugins
.try_read()
.map_err(|_| MindMapError::StaleOperation)?
.commands())
}
fn emit(self, events: &[CoreEvent]) -> Result<(), MindMapError> {
let listener = self
.listener
.try_read()
.map_err(|_| MindMapError::StaleOperation)?
.clone();
let subscribers = self
.subscribers
.try_read()
.map_err(|_| MindMapError::StaleOperation)?
.iter()
.map(|(id, listener)| (*id, listener.clone()))
.collect::<Vec<_>>();
for event in events {
if let Some(listener) = &listener {
listener(event.clone());
}
for (id, callback) in &subscribers {
if self
.subscribers
.try_read()
.is_ok_and(|current| current.contains_key(id))
{
callback(event.clone());
}
}
}
Ok(())
}
}
pub struct Subscription {
handle: MindMapHandle,
id: u64,
}
impl Drop for Subscription {
fn drop(&mut self) {
if let Ok(mut entries) = self.handle.subscribers.try_write() {
entries.remove(&self.id);
}
}
}
pub struct PluginGuard {
handle: MindMapHandle,
id: String,
subscriptions: Vec<Subscription>,
}
impl PluginGuard {
pub fn subscribe(
&mut self,
listener: impl Fn(CoreEvent) + 'static,
) -> Result<(), MindMapError> {
let handle = self.handle;
let id = self.id.clone();
self.subscriptions.push(handle.subscribe(move |event| {
let enabled = handle
.plugins
.try_read()
.is_ok_and(|registry| registry.is_enabled(&id));
if enabled {
listener(event);
}
})?);
Ok(())
}
pub fn set_enabled(&self, enabled: bool) -> Result<(), MindMapError> {
self.handle.set_plugin_enabled(&self.id, enabled)
}
}
impl Drop for PluginGuard {
fn drop(&mut self) {
if let Ok(mut registry) = self.handle.plugins.try_write() {
let _ = registry.unregister(&self.id);
}
}
}
pub fn use_plugins(
handle: MindMapHandle,
plugins: Vec<Plugin>,
) -> Result<Rc<Vec<PluginGuard>>, MindMapError> {
use_hook(|| {
plugins
.into_iter()
.map(|plugin| handle.register_plugin(plugin))
.collect::<Result<Vec<_>, _>>()
.map(Rc::new)
})
}