Skip to main content

dioxus_mind_elixir/
state.rs

1//! Transactional, renderer-independent document operations and snapshot history.
2use std::collections::{BTreeMap, BTreeSet};
3
4use crate::{LayoutDirection, MindMapData, MindMapError, Node, NodeDirection, NodeImage};
5
6/// Where an ordered group is inserted.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum Placement {
9    /// Append as children, expanding the target in the same transaction.
10    In(String),
11    /// Insert immediately before a sibling.
12    Before(String),
13    /// Insert immediately after a sibling.
14    After(String),
15}
16impl Placement {
17    fn target(&self) -> &str {
18        match self {
19            Self::In(id) | Self::Before(id) | Self::After(id) => id,
20        }
21    }
22}
23
24/// A mutation performed atomically through the core state.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum Operation {
27    /// Insert a new subtree.
28    Insert {
29        /// Subtree, including caller-provided IDs.
30        node: Node,
31        /// Destination.
32        placement: Placement,
33    },
34    /// Wrap a non-root node in a newly supplied leaf.
35    InsertParent {
36        /// Existing child.
37        target: String,
38        /// New parent (must have no children).
39        parent: Node,
40    },
41    /// Remove nodes; selected descendants of selected ancestors are deduplicated.
42    Remove(Vec<String>),
43    /// Move an ordered group, retaining all IDs.
44    Move {
45        /// Source IDs, ordered by document traversal.
46        ids: Vec<String>,
47        /// Destination.
48        placement: Placement,
49    },
50    /// Copy an ordered group, generating new IDs throughout every subtree.
51    Copy {
52        /// Source IDs.
53        ids: Vec<String>,
54        /// Destination.
55        placement: Placement,
56    },
57    /// Replace a plain-text topic. Empty/whitespace-only edits are rejected.
58    SetTopic {
59        /// Node ID.
60        id: String,
61        /// New text.
62        topic: String,
63    },
64    /// Change visibility of a node's descendants.
65    SetExpanded {
66        /// Node ID.
67        id: String,
68        /// New expansion state.
69        expanded: bool,
70    },
71    /// Replace or clear a plain-text note. Blank notes are stored as `None`.
72    SetNote {
73        /// Node ID.
74        id: String,
75        /// New note.
76        note: Option<String>,
77    },
78    /// Replace or clear a node picture.
79    SetImage {
80        /// Node ID.
81        id: String,
82        /// New picture.
83        image: Option<NodeImage>,
84    },
85    /// Change document layout.
86    SetLayout(LayoutDirection),
87    /// Change compact spacing.
88    SetCompact(bool),
89}
90
91/// Recording options for a single operation.
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93pub struct OperationOptions {
94    /// Apply without an operation event or independent undo entry. Existing undo
95    /// history is retained; redo is invalidated. Prefer one transaction for
96    /// auto-expansion plus insertion (already handled by Insert/Move/Copy).
97    pub silent: bool,
98}
99
100/// Observable core events. Operation is delivered before selection changes.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum CoreEvent {
103    /// A successful, changed, non-silent document transaction.
104    Operation(Box<Operation>),
105    /// Selection now contains these existing IDs, in document order.
106    Selection(Vec<String>),
107    /// An undo or redo restored a snapshot.
108    History {
109        /// True for undo, false for redo.
110        undo: bool,
111    },
112    /// A replacement document reset the history baseline.
113    Refresh,
114}
115
116/// A transaction result; no-op operations return `changed = false`.
117#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub struct OperationResult {
119    /// Whether the persistent document changed.
120    pub changed: bool,
121    /// Newly inserted top-level IDs, or moved IDs.
122    pub ids: Vec<String>,
123    /// Events to deliver after releasing all state borrows.
124    pub events: Vec<CoreEvent>,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
128struct Snapshot {
129    data: MindMapData,
130    selection: Vec<String>,
131}
132#[derive(Clone, Debug)]
133struct HistoryEntry {
134    before: Snapshot,
135    after: Snapshot,
136}
137
138/// Validated document with private tree/index/selection and bounded history.
139///
140/// Every mutation is built on a candidate snapshot, validated, then committed.
141/// A failed operation cannot change data, selection, index, history or generation.
142#[derive(Clone, Debug)]
143pub struct MindMapState {
144    snapshot: Snapshot,
145    parents: BTreeMap<String, Option<String>>,
146    undo: Vec<HistoryEntry>,
147    redo: Vec<HistoryEntry>,
148    history_limit: usize,
149    read_only: bool,
150    revision: u64,
151    next_id: u64,
152}
153impl MindMapState {
154    /// Validate and establish an initial history baseline.
155    pub fn new(data: MindMapData) -> Result<Self, MindMapError> {
156        data.validate()?;
157        let mut state = Self {
158            snapshot: Snapshot {
159                data,
160                selection: Vec::new(),
161            },
162            parents: BTreeMap::new(),
163            undo: Vec::new(),
164            redo: Vec::new(),
165            history_limit: 100,
166            read_only: false,
167            revision: 0,
168            next_id: 0,
169        };
170        state.normalize_sides();
171        state.reindex();
172        Ok(state)
173    }
174    /// Persistent document, without runtime state.
175    pub fn data(&self) -> &MindMapData {
176        &self.snapshot.data
177    }
178    /// Current selected IDs.
179    pub fn selection(&self) -> &[String] {
180        &self.snapshot.selection
181    }
182    /// Node lookup includes collapsed descendants.
183    pub fn node(&self, id: &str) -> Option<&Node> {
184        self.data().node_data.find(id)
185    }
186    /// Parent ID. Root and unknown IDs return None; use `node` to distinguish.
187    pub fn parent_id(&self, id: &str) -> Option<&str> {
188        self.parents.get(id).and_then(Option::as_deref)
189    }
190    /// Monotonic generation for stale operation/measurement checks.
191    pub fn revision(&self) -> u64 {
192        self.revision
193    }
194    /// Whether document mutations are disabled.
195    pub fn is_read_only(&self) -> bool {
196        self.read_only
197    }
198    /// Disable/enable every mutation entry, including refresh and history.
199    pub fn set_read_only(&mut self, value: bool) {
200        if self.read_only != value {
201            self.read_only = value;
202            self.bump();
203        }
204    }
205    /// Maximum retained undo transactions; zero disables recording.
206    pub fn set_history_limit(&mut self, limit: usize) {
207        self.history_limit = limit;
208        if self.undo.len() > limit {
209            self.undo.drain(..self.undo.len() - limit);
210        }
211        if self.redo.len() > limit {
212            self.redo.drain(..self.redo.len() - limit);
213        }
214    }
215    /// Number of available undo and redo entries.
216    pub fn history_len(&self) -> (usize, usize) {
217        (self.undo.len(), self.redo.len())
218    }
219    /// Replace selection atomically, preserving traversal order and removing duplicates.
220    pub fn select(&mut self, ids: &[String]) -> Result<Vec<CoreEvent>, MindMapError> {
221        for id in ids {
222            self.require(id)?;
223        }
224        let wanted: BTreeSet<_> = ids.iter().collect();
225        let ordered = ordered_nodes(&self.data().node_data)
226            .into_iter()
227            .filter(|node| wanted.contains(&node.id))
228            .map(|node| node.id.clone())
229            .collect::<Vec<_>>();
230        if ordered == self.snapshot.selection {
231            return Ok(Vec::new());
232        }
233        self.snapshot.selection = ordered.clone();
234        self.bump();
235        Ok(vec![CoreEvent::Selection(ordered)])
236    }
237    /// Execute one checked transaction.
238    pub fn apply(&mut self, operation: Operation) -> Result<OperationResult, MindMapError> {
239        self.apply_with(operation, OperationOptions::default())
240    }
241    /// Execute with explicit history/event recording options.
242    pub fn apply_with(
243        &mut self,
244        operation: Operation,
245        options: OperationOptions,
246    ) -> Result<OperationResult, MindMapError> {
247        self.writable()?;
248        match &operation {
249            Operation::Insert { node, .. } | Operation::InsertParent { parent: node, .. } => {
250                node.validate()?
251            }
252            _ => {}
253        }
254        let before = self.snapshot.clone();
255        let mut candidate = before.clone();
256        let mut next_id = self.next_id;
257        let ids = self.mutate(&mut candidate, &operation, &mut next_id)?;
258        candidate.data.validate()?;
259        normalize_sides(&mut candidate.data);
260        if candidate.data == before.data {
261            return Ok(OperationResult::default());
262        }
263        candidate
264            .selection
265            .retain(|id| candidate.data.node_data.find(id).is_some());
266        let selection_changed = before.selection != candidate.selection;
267        self.snapshot = candidate;
268        self.next_id = next_id;
269        self.reindex();
270        self.bump();
271        self.redo.clear();
272        let mut events = Vec::new();
273        if !options.silent {
274            if self.history_limit > 0 {
275                self.undo.push(HistoryEntry {
276                    before,
277                    after: self.snapshot.clone(),
278                });
279                if self.undo.len() > self.history_limit {
280                    self.undo.remove(0);
281                }
282            }
283            events.push(CoreEvent::Operation(Box::new(operation)));
284        }
285        if selection_changed {
286            events.push(CoreEvent::Selection(self.selection().to_vec()));
287        }
288        Ok(OperationResult {
289            changed: true,
290            ids,
291            events,
292        })
293    }
294    /// Restore the previous document and selection snapshot.
295    pub fn undo(&mut self) -> Result<Vec<CoreEvent>, MindMapError> {
296        self.writable()?;
297        let entry = self.undo.pop().ok_or(MindMapError::NoHistory)?;
298        self.snapshot = entry.before.clone();
299        self.redo.push(entry);
300        self.reindex();
301        self.bump();
302        Ok(vec![
303            CoreEvent::History { undo: true },
304            CoreEvent::Selection(self.selection().to_vec()),
305        ])
306    }
307    /// Restore the next snapshot.
308    pub fn redo(&mut self) -> Result<Vec<CoreEvent>, MindMapError> {
309        self.writable()?;
310        let entry = self.redo.pop().ok_or(MindMapError::NoHistory)?;
311        self.snapshot = entry.after.clone();
312        self.undo.push(entry);
313        self.reindex();
314        self.bump();
315        Ok(vec![
316            CoreEvent::History { undo: false },
317            CoreEvent::Selection(self.selection().to_vec()),
318        ])
319    }
320    /// Replace the document and reset selection/history. Validation happens first.
321    pub fn refresh(&mut self, mut data: MindMapData) -> Result<Vec<CoreEvent>, MindMapError> {
322        self.writable()?;
323        data.validate()?;
324        normalize_sides(&mut data);
325        self.snapshot = Snapshot {
326            data,
327            selection: Vec::new(),
328        };
329        self.undo.clear();
330        self.redo.clear();
331        self.reindex();
332        self.bump();
333        Ok(vec![CoreEvent::Refresh, CoreEvent::Selection(Vec::new())])
334    }
335    fn require(&self, id: &str) -> Result<&Node, MindMapError> {
336        self.node(id)
337            .ok_or_else(|| MindMapError::NodeNotFound(id.into()))
338    }
339    fn writable(&self) -> Result<(), MindMapError> {
340        if self.read_only {
341            Err(MindMapError::ReadOnly)
342        } else {
343            Ok(())
344        }
345    }
346    fn bump(&mut self) {
347        self.revision = self.revision.saturating_add(1);
348    }
349    fn normalize_sides(&mut self) {
350        normalize_sides(&mut self.snapshot.data);
351    }
352    fn reindex(&mut self) {
353        self.parents.clear();
354        let mut pending = vec![(&self.snapshot.data.node_data, None)];
355        while let Some((node, parent)) = pending.pop() {
356            self.parents.insert(node.id.clone(), parent);
357            pending.extend(
358                node.children
359                    .iter()
360                    .map(|child| (child, Some(node.id.clone()))),
361            );
362        }
363    }
364    fn sources(&self, ids: &[String]) -> Result<Vec<String>, MindMapError> {
365        let wanted: BTreeSet<_> = ids.iter().collect();
366        for id in ids {
367            self.require(id)?;
368            if id == &self.data().node_data.id {
369                return Err(MindMapError::RootOperation);
370            }
371        }
372        let mut result = Vec::new();
373        for node in ordered_nodes(&self.data().node_data) {
374            if !wanted.contains(&node.id) {
375                continue;
376            }
377            let mut parent = self.parent_id(&node.id);
378            let mut covered = false;
379            while let Some(id) = parent {
380                if wanted.iter().any(|wanted| wanted.as_str() == id) {
381                    covered = true;
382                    break;
383                }
384                parent = self.parent_id(id);
385            }
386            if !covered {
387                result.push(node.id.clone());
388            }
389        }
390        Ok(result)
391    }
392    fn mutate(
393        &self,
394        candidate: &mut Snapshot,
395        operation: &Operation,
396        next_id: &mut u64,
397    ) -> Result<Vec<String>, MindMapError> {
398        let root = &mut candidate.data.node_data;
399        match operation {
400            Operation::SetTopic { id, topic } => {
401                if topic.trim().is_empty() {
402                    return Err(MindMapError::InvalidOperation(
403                        "Topic cannot be empty".into(),
404                    ));
405                }
406                root.find_mut(id)
407                    .ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?
408                    .topic = topic.clone();
409                Ok(Vec::new())
410            }
411            Operation::SetExpanded { id, expanded } => {
412                root.find_mut(id)
413                    .ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?
414                    .expanded = *expanded;
415                Ok(Vec::new())
416            }
417            Operation::SetNote { id, note } => {
418                root.find_mut(id)
419                    .ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?
420                    .note = note.clone().filter(|note| !note.trim().is_empty());
421                Ok(Vec::new())
422            }
423            Operation::SetImage { id, image } => {
424                if let Some(image) = image {
425                    image.validate()?;
426                }
427                root.find_mut(id)
428                    .ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?
429                    .image = image.clone();
430                Ok(Vec::new())
431            }
432            Operation::SetLayout(direction) => {
433                candidate.data.direction = *direction;
434                Ok(Vec::new())
435            }
436            Operation::SetCompact(compact) => {
437                candidate.data.compact = *compact;
438                Ok(Vec::new())
439            }
440            Operation::Insert { node, placement } => {
441                let ids = vec![node.id.clone()];
442                insert(root, vec![node.clone()], placement)?;
443                candidate.selection = ids.clone();
444                Ok(ids)
445            }
446            Operation::InsertParent { target, parent } => {
447                self.require(target)?;
448                if !parent.children.is_empty() {
449                    return Err(MindMapError::InvalidOperation(
450                        "Inserted parent must be a leaf".into(),
451                    ));
452                }
453                let parent_id = self.parent_id(target).ok_or(MindMapError::RootOperation)?;
454                let siblings = &mut root
455                    .find_mut(parent_id)
456                    .ok_or_else(|| MindMapError::NodeNotFound(parent_id.into()))?
457                    .children;
458                let index = siblings
459                    .iter()
460                    .position(|node| &node.id == target)
461                    .ok_or_else(|| MindMapError::NodeNotFound(target.clone()))?;
462                let mut wrapper = parent.clone();
463                let original = siblings.remove(index);
464                wrapper.direction = original.direction;
465                wrapper.children.push(original);
466                siblings.insert(index, wrapper);
467                candidate.selection = vec![parent.id.clone()];
468                Ok(candidate.selection.clone())
469            }
470            Operation::Remove(ids) => {
471                let sources = self.sources(ids)?;
472                let fallback = sources
473                    .first()
474                    .and_then(|id| self.parent_id(id))
475                    .map(str::to_owned);
476                for id in &sources {
477                    take(root, id).ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?;
478                }
479                if !sources.is_empty() {
480                    candidate.selection = fallback.into_iter().collect();
481                }
482                Ok(sources)
483            }
484            Operation::Move { ids, placement } | Operation::Copy { ids, placement } => {
485                let sources = self.sources(ids)?;
486                self.require(placement.target())?;
487                let copying = matches!(operation, Operation::Copy { .. });
488                if !copying {
489                    for id in &sources {
490                        if self.require(id)?.find(placement.target()).is_some() {
491                            return Err(MindMapError::AncestorMove);
492                        }
493                    }
494                }
495                let mut nodes = Vec::new();
496                let mut used: BTreeSet<String> = ordered_nodes(root)
497                    .iter()
498                    .map(|node| node.id.clone())
499                    .collect();
500                for id in &sources {
501                    let mut node = if copying {
502                        self.require(id)?.clone()
503                    } else {
504                        take(root, id).ok_or_else(|| MindMapError::NodeNotFound(id.clone()))?
505                    };
506                    if copying {
507                        regenerate_ids(&mut node, &mut used, next_id)?;
508                    }
509                    nodes.push(node);
510                }
511                let inserted = nodes.iter().map(|node| node.id.clone()).collect::<Vec<_>>();
512                if !nodes.is_empty() {
513                    insert(root, nodes, placement)?;
514                    candidate.selection = inserted.clone();
515                }
516                Ok(inserted)
517            }
518        }
519    }
520}
521
522fn ordered_nodes(root: &Node) -> Vec<&Node> {
523    let mut result = Vec::new();
524    let mut pending = vec![root];
525    while let Some(node) = pending.pop() {
526        result.push(node);
527        pending.extend(node.children.iter().rev());
528    }
529    result
530}
531fn take(root: &mut Node, id: &str) -> Option<Node> {
532    if let Some(index) = root.children.iter().position(|node| node.id == id) {
533        return Some(root.children.remove(index));
534    }
535    root.children.iter_mut().find_map(|node| take(node, id))
536}
537fn find_parent_mut<'a>(root: &'a mut Node, id: &str) -> Option<&'a mut Node> {
538    if root.children.iter().any(|node| node.id == id) {
539        return Some(root);
540    }
541    root.children
542        .iter_mut()
543        .find_map(|node| find_parent_mut(node, id))
544}
545fn insert(
546    root: &mut Node,
547    mut nodes: Vec<Node>,
548    placement: &Placement,
549) -> Result<(), MindMapError> {
550    let target = placement.target();
551    match placement {
552        Placement::In(_) => {
553            let parent = root
554                .find_mut(target)
555                .ok_or_else(|| MindMapError::NodeNotFound(target.into()))?;
556            parent.expanded = true;
557            parent.children.append(&mut nodes);
558        }
559        Placement::Before(_) | Placement::After(_) => {
560            if root.id == target {
561                return Err(MindMapError::RootOperation);
562            }
563            let parent = find_parent_mut(root, target)
564                .ok_or_else(|| MindMapError::NodeNotFound(target.into()))?;
565            let index = parent
566                .children
567                .iter()
568                .position(|node| node.id == target)
569                .ok_or_else(|| MindMapError::NodeNotFound(target.into()))?;
570            let side = parent.children.get(index).and_then(|node| node.direction);
571            for node in &mut nodes {
572                node.direction = side;
573            }
574            let offset = usize::from(matches!(placement, Placement::After(_)));
575            parent
576                .children
577                .splice(index + offset..index + offset, nodes);
578        }
579    }
580    Ok(())
581}
582fn regenerate_ids(
583    node: &mut Node,
584    used: &mut BTreeSet<String>,
585    next: &mut u64,
586) -> Result<(), MindMapError> {
587    loop {
588        *next = next
589            .checked_add(1)
590            .ok_or_else(|| MindMapError::InvalidOperation("Generated ID space exhausted".into()))?;
591        let id = format!("dme-{next}");
592        if used.insert(id.clone()) {
593            node.id = id;
594            break;
595        }
596    }
597    for child in &mut node.children {
598        regenerate_ids(child, used, next)?;
599    }
600    Ok(())
601}
602fn normalize_sides(data: &mut MindMapData) {
603    if data.direction != LayoutDirection::Side {
604        return;
605    }
606    let (mut left, mut right) = (0usize, 0usize);
607    for child in &mut data.node_data.children {
608        let side = match child.direction {
609            Some(side) => side,
610            None if left <= right => NodeDirection::Left,
611            None => NodeDirection::Right,
612        };
613        child.direction = Some(side);
614        match side {
615            NodeDirection::Left => left += 1,
616            NodeDirection::Right => right += 1,
617        }
618    }
619}