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