use super::error::{EngineError, Result}; use super::history::{History, HistoryEntry}; use super::interaction::{Interaction, LastTransform, UndoGroup}; use super::*; /// Per-document editing state. #[derive(Clone, Debug)] pub struct DocState { pub doc: Arc, pub selection: Selection, pub history: History, pub path: Option, /// Increments on every change (also selection and view-only changes); UIs re-render when it /// moves. Not a dirty flag: see [`DocState::is_dirty`]. pub revision: u64, /// The document as last saved (or opened). Structural sharing makes "unchanged" a pointer /// comparison, and undoing back to the saved state counts as clean. pub(crate) saved_doc: Arc, /// The documents that changes that are not edits to save replaced ([`DocState::same_content`]). unsaved_changes: SameContent, /// The layer new art goes into (the "current layer" in the Layers panel). pub active_layer: Option, /// The rows highlighted in the Layers panel (layers, sublayers, groups or objects, in the order /// they were clicked): what the panel's Duplicate, Delete, Merge, Options… and similar act on. /// Panel state, not art selection: not saved, not undoable (`layer.setCurrent`, /// `layer.highlight`). pub layer_rows: Vec, /// The variable highlighted in the Variables panel: what its Delete, Options… and Select /// Bound Object act on. Panel state, not art selection: not saved, not undoable /// (`variable.highlight`). A name, so renaming a variable moves the highlight with it. pub variables_highlight: Option, /// Isolation mode container. pub isolation: Option, /// Select › Start Global Edit: the objects edited together until Stop Global Edit or Escape /// ([`cmd::globaledit`]). Editing state: not saved, not undoable. pub global_edit: Option, pub interaction: Option, /// Edits made while this is open are one undo step (a scrubbed numeric field). pub undo_group: Option, /// For Object → Transform → Transform Again (⌘D): what the last transformation was. A /// transform command keeps the `copy`, `typeAreas` and `patterns` it used, and `strokes` and /// `corners` when it scales; a drag keeps the params of its last preview with the values that /// preview noted ([`Session::note_journal`]). pub last_transform: Option, /// What Select → Reselect repeats: the last selection command, its params and the objects /// selected when it ran (a Same command's reference objects). pub last_selection_cmd: Option<(String, Value, Vec)>, /// The saved selection last recalled (`select.recall`), which Select → Update Selection /// changes. It is not saved with the file, and a recall is not an undo step. pub recalled_selection: RecalledSelection, /// Process-unique id of this open document (tab indices shift when tabs close). pub uid: u64, /// View Opacity Mask (Alt-click the mask thumbnail): the masked object whose mask the canvas /// shows alone, in greyscale, while that mask is edited (see [`DocState::shown_mask`]). pub mask_view: Option, /// View → Show Transparency Grid, per document (saved view state, not undoable). pub transparency_grid: bool, /// The format Save writes ([`cmd::fileio::SAVE_FORMATS`]): the one the document was opened /// from or last saved as. pub format: &'static str, /// That format's options as last saved (SVG options for SVG, the Save PDF settings for PDF; /// empty for native files): Save reuses them and `file.formatOptions` reads them back. pub save_options: serde_json::Map, /// The ids of the document's artboards, in order, when `save_options` were stored: the /// artboards their `artboard`, `artboards` and `range` named by number then /// ([`cmd::fileio::remembered_save_options`]). pub save_artboards: Vec, /// Opened from an older native file (former name or format version): the title says /// "[Converted]" and Save asks for a new name instead of overwriting it. pub converted: bool, /// The view saved into native files (`Document::last_view`); the UI keeps it current before a /// save and restores it when the document opens. pub view: Option, /// Restored by Data Recovery: the title says "[Recovered]" and Save asks where to save it /// (suggesting `path`, the file it was copied from) instead of overwriting that file. pub recovered: bool, /// The document's Data Recovery copy, once one was written ([`cmd::recovery`]). pub recovery: Option, /// View → Show Print Tiling, per document (view state: not saved, not undoable). pub print_tiling: bool, /// The rows open in the Layers panel (view state: not undoable; native files keep it). pub layers_open: OpenRows, /// The file this document was read from, and what reading it left out (hidden text, art or /// layers it could not read): writing the document over that file would lose those for good, /// so an export or save to it asks first ([`cmd::fileio::check_not_lossy_overwrite`]). Not saved. pub imported_from: Option, pub import_losses: Vec, /// Transform Again after a perspective move or scale (Perspective Selection tool): the /// `perspective.transform` params it repeats. `None` once an ordinary transform follows. pub last_perspective: Option, /// The folders a confined (automation) caller that opened or made the document may read: the /// paths the document links to are looked at within them later too, when the Links panel /// asks about a link outside any caller's scope. pub opened_by: Option>, } static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); /// The rows open in the Layers panel: the layers, sublayers and groups that show what they hold. /// A document opens with the ones it was saved with ([`Document::layers_open`]), else with only /// its top-level layers open (a `.ai` another app wrote opens with none open). #[derive(Clone, Debug, Default)] pub struct OpenRows { ids: std::collections::HashSet, /// Counts the changes, so views can keep what they work out from the open rows. generation: u64, } impl OpenRows { /// The rows `saved` in a file, else `doc`'s top-level layers. fn new(doc: &Document, saved: Option>) -> Self { let ids = match saved { Some(ids) => ids.into_iter().collect(), None => doc.layers.iter().map(|l| l.id).collect(), }; Self { ids, generation: 0 } } pub fn contains(&self, id: NodeId) -> bool { self.ids.contains(&id) } /// Open or close row `id`. pub fn set(&mut self, id: NodeId, open: bool) { let changed = if open { self.ids.insert(id) } else { self.ids.remove(&id) }; self.generation += u64::from(changed); } /// Changes so far: the same number means the same open rows. pub fn generation(&self) -> u64 { self.generation } /// What a native file keeps of the open rows: those of `doc` that hold others, in id order; /// `None` when they are the default (only the top-level layers). pub fn saved(&self, doc: &Document) -> Option> { let mut ids = vec![]; doc.walk(|n| { if n.children().is_some() && self.ids.contains(&n.id) { ids.push(n.id); } }); ids.sort_unstable(); let mut layers: Vec = doc.layers.iter().map(|l| l.id).collect(); layers.sort_unstable(); (ids != layers).then_some(ids) } } /// The saved selection last recalled, which Select → Update Selection changes. The Select commands /// give each saved selection an id for the session and keep the ids for every version of the /// document's saved selections they make or read, so the recalled one is found again after Edit /// Selection…, Undo and Redo. A version that nothing holds any more (neither the document nor its /// undo history) is dropped the next time a version is added. #[derive(Clone, Debug, Default)] pub struct RecalledSelection { /// The recalled saved selection's id. id: Option, /// Each version of the saved selections (identified by its allocation), with the id of each saved /// selection in it. versions: Vec<(std::sync::Weak>, Vec)>, /// The last id given. last: u64, } impl RecalledSelection { /// The ids of `list`'s saved selections; a version not met before gets new ones. pub(crate) fn ids(&mut self, list: &vectorcraft_doc::Shared>) -> Vec { let version = vectorcraft_doc::Shared::downgrade(list); if let Some((_, ids)) = self.versions.iter().find(|(v, _)| v.ptr_eq(&version)) { return ids.clone(); } let ids: Vec = (0..list.len()).map(|_| self.new_id()).collect(); self.remember(list, ids.clone()); ids } /// An id no saved selection has had. pub(crate) fn new_id(&mut self) -> u64 { self.last += 1; self.last } /// Keep `ids` as those of `list`'s saved selections, dropping versions nothing holds any more. pub(crate) fn remember(&mut self, list: &vectorcraft_doc::Shared>, ids: Vec) { let version = vectorcraft_doc::Shared::downgrade(list); self.versions.retain(|(v, _)| v.strong_count() > 0 && !v.ptr_eq(&version)); self.versions.push((version, ids)); } /// Recall the saved selection at `index` in `list`. pub(crate) fn recall(&mut self, list: &vectorcraft_doc::Shared>, index: usize) { self.id = self.ids(list).get(index).copied(); } /// Where the recalled saved selection is in `list`, if `list` holds it. pub fn index_in(&self, list: &vectorcraft_doc::Shared>) -> Option { let id = self.id?; let version = vectorcraft_doc::Shared::downgrade(list); let (_, ids) = self.versions.iter().find(|(v, _)| v.ptr_eq(&version))?; ids.iter().position(|&x| x == id).filter(|&i| i < list.len()) } } /// The documents that changes that are not edits to save (Puppet Warp pins dropped, Edit Text ended, /// type laid out again after a font scan) replaced, each counted as having the content of the /// document it was replaced by ([`DocState::change_unsaved`]). A document that nothing holds any /// more is dropped the next time one is added. #[derive(Clone, Debug, Default)] struct SameContent { /// The document the last such change made. now: std::sync::Weak, /// The documents with its content that such changes replaced, oldest first. before: Vec>, } impl SameContent { /// Note that `now` replaced `before` without a change to its content. fn replaced(&mut self, before: std::sync::Weak, now: &Arc) { if !self.now.ptr_eq(&before) { self.before.clear(); } self.before.retain(|d| d.strong_count() > 0); if before.strong_count() > 0 { self.before.push(before); } self.now = Arc::downgrade(now); } /// Whether `doc` is one of the documents with the content of `now`. fn holds(&self, now: &Arc, doc: &Arc) -> bool { std::ptr::eq(self.now.as_ptr(), Arc::as_ptr(now)) && self.before.iter().any(|d| std::ptr::eq(d.as_ptr(), Arc::as_ptr(doc))) } } impl DocState { pub fn new(mut doc: Document, path: Option) -> Self { let active_layer = doc.default_layer(); // Saved view state lives here while the document is open (saves write it back). let view = doc.last_view.take(); let transparency_grid = std::mem::take(&mut doc.transparency_grid); let saved_open = doc.layers_open.take(); let layers_open = OpenRows::new(&doc, saved_open); let doc = Arc::new(doc); Self { saved_doc: doc.clone(), unsaved_changes: SameContent::default(), doc, selection: Selection::default(), history: History { limit: 500, ..Default::default() }, path, revision: 1, active_layer, layer_rows: vec![], variables_highlight: None, isolation: None, global_edit: None, interaction: None, undo_group: None, last_transform: None, last_selection_cmd: None, recalled_selection: RecalledSelection::default(), uid: NEXT_DOC_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), mask_view: None, transparency_grid, format: "vectorcraft", save_options: Default::default(), save_artboards: vec![], converted: false, view, recovered: false, recovery: None, print_tiling: false, layers_open, imported_from: None, import_losses: vec![], last_perspective: None, opened_by: file_access::current(), } } /// Unsaved changes: the document differs from the saved one (selection changes and changes that /// are not edits to save don't count, [`DocState::same_content`]). pub fn is_dirty(&self) -> bool { !self.same_content(&self.saved_doc) } /// Record the current document as saved. pub fn mark_saved(&mut self) { self.saved_doc = self.doc.clone(); } /// Record `snapshot` (the document as a background save took it) as saved: edits made since /// keep the document modified. pub fn mark_saved_as(&mut self, snapshot: &Arc) { self.saved_doc = snapshot.clone(); } /// Whether `doc` counts as having the content of the document now: it is the document, or one /// that changes that are not edits to save replaced since ([`DocState::change_unsaved`]). pub fn same_content(&self, doc: &Arc) -> bool { Arc::ptr_eq(&self.doc, doc) || self.unsaved_changes.holds(&self.doc, doc) } /// Make a change that is not an edit to save with `f`, without an undo step: editing state that /// is never saved, or type laid out again after a font scan (which can also refit the frame of /// Auto Size area type). A saved document stays saved, and a save or recovery copy taken of the /// document before counts as having its content ([`DocState::same_content`]). pub(crate) fn change_unsaved(&mut self, f: impl FnOnce(&mut Arc)) { let saved = !self.is_dirty(); let before = Arc::downgrade(&self.doc); f(&mut self.doc); if !std::ptr::eq(before.as_ptr(), Arc::as_ptr(&self.doc)) { self.unsaved_changes.replaced(before, &self.doc); } if saved { self.mark_saved(); } } /// Keep what interaction `it` (taken from this document) changed, as one undo step. pub(crate) fn keep_interaction(&mut self, it: Interaction) { if !Arc::ptr_eq(&it.doc, &self.doc) { let selection = match self.history.undo.last() { Some(l) if l.selection == it.selection => l.selection.clone(), _ => it.selection, }; self.push_undo(it.label, it.doc, selection); self.revision += 1; } } /// Record an undo step: `doc` and `selection` as they were before an edit (the document now is /// the one after it). In an undo group only the group's first edit records one: the edits /// after it extend that step. pub(crate) fn push_undo(&mut self, mut label: String, doc: Arc, selection: Arc) { if let Some(g) = &mut self.undo_group { let recorded = |first: &Arc| self.history.undo.last().is_some_and(|l| Arc::ptr_eq(&l.doc, first)); if g.first.as_ref().is_some_and(recorded) { self.history.recost_last(&self.doc); self.history.trim(); return; } g.first = Some(doc.clone()); if let Some(l) = &g.label { label = l.clone(); } } let e = HistoryEntry::kept(label, doc, selection, &self.doc); self.history.undo.push(e); self.history.redo.clear(); self.history.trim(); } /// End the interaction in progress, undoing what it changed. pub(crate) fn undo_interaction(&mut self) { if let Some(it) = self.interaction.take() { self.doc = it.doc; self.selection = Arc::unwrap_or_clone(it.selection); self.sync_new_art(); cmd::globaledit::follow_history(self); self.active_layer = it.active_layer; self.layer_rows = it.layer_rows; self.variables_highlight = it.variables_highlight; self.isolation = it.isolation; self.revision += 1; } } /// Count the document as modified, as if never saved (a document restored by Data Recovery). pub fn mark_unsaved(&mut self) { self.saved_doc = Arc::new(Document::new(1.0, 1.0)); } /// Deselect the art on a locked artboard, except new art ([`Document::new_art`]). New art /// becomes the objects of layers on a locked artboard that hold selected objects (a group for /// its selected members) and that an edit made (those not in `before`, the document before /// it), or that held objects selected in `was` that were new art (`old`, the new art of that /// selection), or were inside new art in `before` (the members of an ungrouped group). New art /// that a selection leaves out, or that leaves the locked artboards, is protected from then /// on. The selection and the document get the new art; the document's changes in place, so a /// selection change doesn't replace the document. pub(crate) fn protect_locked_artboards(&mut self, was: &[NodeId], old: &std::collections::HashSet, before: Option<&Document>) { let new_art: std::collections::HashSet = { let (doc, locks) = (&self.doc, self.doc.locked_artboards()); if locks.is_empty() { Default::default() } else { // `was` and `old` name the objects of the document they were selected in. let then = before.unwrap_or(doc); let was: std::collections::HashSet = was.iter().filter_map(|o| then.layer_object(*o)).map(|n| n.id).filter(|o| old.contains(o)).collect(); let selected: Vec<(NodeId, &vectorcraft_doc::Node)> = self.selection.objects.iter().filter_map(|o| doc.layer_object(*o).map(|n| (*o, n))).collect(); // One walk of the document before the edit finds where the selected objects were; // the objects the edit made aren't in it. let paths = before.filter(|_| !selected.is_empty()).map(|b| b.index_paths(selected.iter().flat_map(|(o, n)| [*o, n.id]))); let new = |o: NodeId, object: NodeId| { was.contains(&object) || before.zip(paths.as_ref()).is_some_and(|(b, p)| { !p.contains_key(&object) || p.get(&o).and_then(|p| b.layer_object_at(p)).is_some_and(|n| was.contains(&n.id)) }) }; selected.into_iter().filter(|(o, n)| new(*o, n.id) && locks.overlaps(n)).map(|(_, n)| n.id).collect() } }; if new_art != *self.selection.new_art { self.selection.new_art = Arc::new(new_art); } self.sync_new_art(); self.selection.deselect_on_locked_artboards(&self.doc); } /// Give the document the selection's new art ([`Selection::new_art`]), less the objects the /// selection no longer holds: after Undo, Redo or a failed batch brings back a document and /// a selection, and after a command that changed the selection directly. pub(crate) fn sync_new_art(&mut self) { if !self.selection.new_art.is_empty() { let held: std::collections::HashSet = self.selection.objects.iter().filter_map(|o| self.doc.layer_object(*o)).map(|n| n.id).collect(); if !self.selection.new_art.is_subset(&held) { self.selection.new_art = Arc::new(self.selection.new_art.intersection(&held).copied().collect()); } } let doc = self.doc.new_art.get(); if !Arc::ptr_eq(&doc, &self.selection.new_art) && *doc != *self.selection.new_art { self.doc.new_art.set(self.selection.new_art.clone()); } } pub fn title(&self) -> String { let name = self .path .as_deref() .and_then(|p| std::path::Path::new(p).file_name()) .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| self.doc.title.clone()); if self.converted { format!("{} [Converted]", cmd::fileio::file_stem(&name)) } else if self.recovered { format!("{} [Recovered]", cmd::fileio::file_stem(&name)) } else { name } } /// Where new art is inserted: the isolation container, else the active layer. /// The current layer, if the remembered id still names a layer (ids are reused after undo). pub fn current_layer(&self) -> Option { self.active_layer.filter(|l| self.doc.node(*l).is_some_and(|n| n.is_layer())) } pub fn insertion_parent(&self) -> Option { // Ids are reused after undo (the id counter is part of the document), so a remembered // layer or isolated group must still be one: a pasted group must never land in a path. if let Some(i) = self.isolation && self.doc.node(i).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. })) { return Some(i); } // A single object isolated: new art goes beside it, in its group or layer. if let Some(p) = self.isolation.filter(|i| self.doc.node(*i).is_some_and(|n| !n.is_container())).and_then(|i| self.doc.parent_of(i)) && self.doc.node(p).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. })) { return Some(p); } // A sublayer takes new art only while it and the layers around it are shown and unlocked. self.active_layer.filter(|l| self.doc.node(*l).is_some_and(|n| n.is_layer()) && self.doc.is_editable(*l)).or_else(|| self.doc.default_layer()) } /// [`Self::insertion_parent`] for new art, which a locked or hidden layer never takes: an error /// when no layer is shown and unlocked, so the art is refused rather than hidden or locked away. pub fn target_parent(&self) -> Result> { match self.insertion_parent() { Some(p) if !self.doc.is_editable(p) => Err(EngineError::Other("the target layer is locked or hidden".into())), parent => Ok(parent), } } /// The highlighted Layers panel rows that still exist (ids are reused after undo, so a /// remembered row must still be in the document). pub fn highlighted_rows(&self) -> Vec { self.layer_rows.iter().copied().filter(|id| self.doc.node(*id).is_some()).collect() } /// The object whose opacity mask View Opacity Mask shows ([`DocState::mask_view`]): only while /// its mask is being edited, so leaving editing by any route (undo, deleting the object) ends it. pub fn shown_mask(&self) -> Option { self.mask_view.filter(|id| self.doc.mask_edit.is_some_and(|m| m.object == *id)) } }