diff --git a/editor/src/application.rs b/editor/src/application.rs index 3a1fb47d4b..19d4aad674 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -75,6 +75,7 @@ mod test { document_is_auto_saved: true, document_is_saved: true, document_serialized_content: r#" [removed until test is reenabled] "#.into(), + to_front: false, } .into(), InputPreprocessorMessage::BoundsOfViewports { diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 7dabc354e7..f6a922a6ae 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -141,7 +141,7 @@ impl Dispatcher { Message::NoOp => {} Message::Init => { // Load persistent data from the browser database - queue.add(FrontendMessage::TriggerLoadAutoSaveDocuments); + queue.add(FrontendMessage::TriggerLoadFirstAutoSaveDocument); queue.add(FrontendMessage::TriggerLoadPreferences); // Display the menu bar at the top of the window @@ -153,6 +153,9 @@ impl Dispatcher { node_descriptions: document_node_definitions::collect_node_descriptions(), node_types: document_node_definitions::collect_node_types(), }); + + // Finish loading persistent data from the browser database + queue.add(FrontendMessage::TriggerLoadRestAutoSaveDocuments); } Message::Batched(messages) => { messages.iter().for_each(|message| self.handle_message(message.to_owned(), false)); diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 00830e93ee..3bbf2791b5 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -85,13 +85,18 @@ pub enum FrontendMessage { document: String, details: FrontendDocumentDetails, }, - TriggerLoadAutoSaveDocuments, + TriggerLoadFirstAutoSaveDocument, + TriggerLoadRestAutoSaveDocuments, TriggerLoadPreferences, TriggerOpenDocument, TriggerPaste, TriggerSavePreferences { preferences: PreferencesMessageHandler, }, + TriggerSaveActiveDocument { + #[serde(rename = "documentId")] + document_id: DocumentId, + }, TriggerTextCommit, TriggerTextCopy { #[serde(rename = "copyText")] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index feabfa0536..0c45330ba1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -117,6 +117,9 @@ pub struct DocumentMessageHandler { /// If the user clicks or Ctrl-clicks one layer, it becomes the start of the range selection and then Shift-clicking another layer selects all layers between the start and end. #[serde(skip)] layer_range_selection_reference: Option, + /// Whether or not the editor has executed the network to render the document yet. If this is opened as an inactive tab, it won't be loaded initially because the active tab is prioritized. + #[serde(skip)] + pub is_loaded: bool, } impl Default for DocumentMessageHandler { @@ -154,6 +157,7 @@ impl Default for DocumentMessageHandler { saved_hash: None, auto_saved_hash: None, layer_range_selection_reference: None, + is_loaded: false, } } } diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 2674ec2f13..6fe3df5964 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -78,6 +78,7 @@ pub enum PortfolioMessage { document_is_auto_saved: bool, document_is_saved: bool, document_serialized_content: String, + to_front: bool, }, PasteIntoFolder { clipboard: Clipboard, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index fe4c94da79..a2a3e2d5f6 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -32,7 +32,7 @@ pub struct PortfolioMessageData<'a> { pub struct PortfolioMessageHandler { menu_bar_message_handler: MenuBarMessageHandler, pub documents: HashMap, - document_ids: Vec, + document_ids: VecDeque, active_panel: PanelType, pub(crate) active_document_id: Option, copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], @@ -258,7 +258,7 @@ impl MessageHandler> for PortfolioMes } else if self.active_document_id.is_some() { let document_id = if document_index == self.document_ids.len() { // If we closed the last document take the one previous (same as last) - *self.document_ids.last().unwrap() + *self.document_ids.back().unwrap() } else { // Move to the next tab self.document_ids[document_index] @@ -349,7 +349,8 @@ impl MessageHandler> for PortfolioMes responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); } - self.load_document(new_document, document_id, responses); + self.load_document(new_document, document_id, responses, false); + responses.add(PortfolioMessage::SelectDocument { document_id }); } PortfolioMessage::NextDocument => { if let Some(active_document_id) = self.active_document_id { @@ -368,13 +369,16 @@ impl MessageHandler> for PortfolioMes document_name, document_serialized_content, } => { + let document_id = DocumentId(generate_uuid()); responses.add(PortfolioMessage::OpenDocumentFileWithId { - document_id: DocumentId(generate_uuid()), + document_id, document_name, document_is_auto_saved: false, document_is_saved: true, document_serialized_content, + to_front: false, }); + responses.add(PortfolioMessage::SelectDocument { document_id }); } PortfolioMessage::OpenDocumentFileWithId { document_id, @@ -382,6 +386,7 @@ impl MessageHandler> for PortfolioMes document_is_auto_saved, document_is_saved, document_serialized_content, + to_front, } => { // TODO: Eventually remove this document upgrade code // This big code block contains lots of hacky code for upgrading old documents to the new format @@ -756,7 +761,7 @@ impl MessageHandler> for PortfolioMes document.set_auto_save_state(document_is_auto_saved); document.set_save_state(document_is_saved); - self.load_document(document, document_id, responses); + self.load_document(document, document_id, responses, to_front); } PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| { @@ -896,6 +901,7 @@ impl MessageHandler> for PortfolioMes responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(FrontendMessage::UpdateActiveDocument { document_id }); + responses.add(FrontendMessage::TriggerSaveActiveDocument { document_id }); responses.add(ToolMessage::InitTools); responses.add(NodeGraphMessage::Init); responses.add(OverlaysMessage::Draw); @@ -909,6 +915,17 @@ impl MessageHandler> for PortfolioMes } else { responses.add(PortfolioMessage::UpdateDocumentWidgets); } + + let Some(document) = self.documents.get_mut(&document_id) else { + warn!("Tried to read non existant document"); + return; + }; + if !document.is_loaded { + document.is_loaded = true; + responses.add(PortfolioMessage::LoadDocumentResources { document_id }); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + responses.add(PropertiesPanelMessage::Clear); + } } PortfolioMessage::SubmitDocumentExport { file_name, @@ -1065,10 +1082,12 @@ impl PortfolioMessageHandler { } } - // TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Open* - fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque) { - let new_document = new_document; - self.document_ids.push(document_id); + fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque, to_front: bool) { + if to_front { + self.document_ids.push_front(document_id); + } else { + self.document_ids.push_back(document_id); + } new_document.update_layers_panel_control_bar_widgets(responses); self.documents.insert(document_id, new_document); @@ -1085,14 +1104,6 @@ impl PortfolioMessageHandler { // TODO: Remove this and find a way to fix the issue where creating a new document when the node graph is open causes the transform in the new document to be incorrect responses.add(DocumentMessage::GraphViewOverlay { open: false }); responses.add(PortfolioMessage::UpdateOpenDocumentsList); - responses.add(PortfolioMessage::SelectDocument { document_id }); - responses.add(PortfolioMessage::LoadDocumentResources { document_id }); - responses.add(PortfolioMessage::UpdateDocumentWidgets); - responses.add(ToolMessage::InitTools); - responses.add(NodeGraphMessage::Init); - responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); - responses.add(PropertiesPanelMessage::Clear); - responses.add(NodeGraphMessage::UpdateNewNodeGraph); } /// Returns an iterator over the open documents in order. diff --git a/frontend/src/io-managers/persistence.ts b/frontend/src/io-managers/persistence.ts index e641bbb5a4..a23fb079a9 100644 --- a/frontend/src/io-managers/persistence.ts +++ b/frontend/src/io-managers/persistence.ts @@ -2,7 +2,15 @@ import { createStore, del, get, set, update } from "idb-keyval"; import { get as getFromStore } from "svelte/store"; import { type Editor } from "@graphite/editor"; -import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument, TriggerSavePreferences, TriggerLoadAutoSaveDocuments, TriggerLoadPreferences } from "@graphite/messages"; +import { + TriggerIndexedDbWriteDocument, + TriggerIndexedDbRemoveDocument, + TriggerSavePreferences, + TriggerLoadPreferences, + TriggerLoadFirstAutoSaveDocument, + TriggerLoadRestAutoSaveDocuments, + TriggerSaveActiveDocument, +} from "@graphite/messages"; import { type PortfolioState } from "@graphite/state-providers/portfolio"; const graphiteStore = createStore("graphite", "store"); @@ -12,10 +20,13 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta async function storeDocumentOrder() { const documentOrder = getFromStore(portfolio).documents.map((doc) => String(doc.id)); - await set("documents_tab_order", documentOrder, graphiteStore); } + async function storeCurrentDocumentId(documentId: string) { + await set("current_document_id", String(documentId), graphiteStore); + } + async function storeDocument(autoSaveDocument: TriggerIndexedDbWriteDocument) { await update>( "documents", @@ -28,6 +39,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta ); await storeDocumentOrder(); + await storeCurrentDocumentId(autoSaveDocument.details.id); } async function removeDocument(id: string) { @@ -41,19 +53,80 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta graphiteStore, ); + const documentCount = getFromStore(portfolio).documents.length; + if (documentCount > 0) { + const documentIndex = getFromStore(portfolio).activeDocumentIndex; + const documentId = getFromStore(portfolio).documents[documentIndex].id; + + await storeCurrentDocumentId(String(documentId)); + } else { + await del("current_document_id", graphiteStore); + } + await storeDocumentOrder(); } - async function loadDocuments() { + async function loadFirstDocument() { + const previouslySavedDocuments = await get>("documents", graphiteStore); + const documentOrder = await get("documents_tab_order", graphiteStore); + const currentDocumentId = await get("current_document_id", graphiteStore); + if (!previouslySavedDocuments || !documentOrder) return; + + const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); + + if (currentDocumentId) { + const doc = previouslySavedDocuments[currentDocumentId]; + editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false); + editor.handle.selectDocument(BigInt(currentDocumentId)); + } else { + const len = orderedSavedDocuments.length; + if (len > 0) { + const doc = orderedSavedDocuments[len - 1]; + editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false); + editor.handle.selectDocument(BigInt(doc.details.id)); + } + } + } + + async function loadRestDocuments() { const previouslySavedDocuments = await get>("documents", graphiteStore); const documentOrder = await get("documents_tab_order", graphiteStore); + const currentDocumentId = await get("current_document_id", graphiteStore); if (!previouslySavedDocuments || !documentOrder) return; const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); - orderedSavedDocuments?.forEach(async (doc: TriggerIndexedDbWriteDocument) => { - editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document); - }); + if (currentDocumentId) { + const currentIndex = orderedSavedDocuments.findIndex((doc) => doc.details.id === currentDocumentId); + const beforeCurrentIndex = currentIndex - 1; + const afterCurrentIndex = currentIndex + 1; + + for (let i = beforeCurrentIndex; i >= 0; i--) { + const { document, details } = orderedSavedDocuments[i]; + const { id, name, isSaved } = details; + editor.handle.openAutoSavedDocument(BigInt(id), name, isSaved, document, true); + } + for (let i = afterCurrentIndex; i < orderedSavedDocuments.length; i++) { + const { document, details } = orderedSavedDocuments[i]; + const { id, name, isSaved } = details; + editor.handle.openAutoSavedDocument(BigInt(id), name, isSaved, document, false); + } + + editor.handle.selectDocument(BigInt(currentDocumentId)); + } else { + const length = orderedSavedDocuments.length; + + for (let i = length - 2; i >= 0; i--) { + const { document, details } = orderedSavedDocuments[i]; + const { id, name, isSaved } = details; + editor.handle.openAutoSavedDocument(BigInt(id), name, isSaved, document, true); + } + + if (length > 0) { + const id = orderedSavedDocuments[length - 1].details.id; + editor.handle.selectDocument(BigInt(id)); + } + } } // PREFERENCES @@ -84,12 +157,24 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => { await removeDocument(removeAutoSaveDocument.documentId); }); - editor.subscriptions.subscribeJsMessage(TriggerLoadAutoSaveDocuments, async () => { - await loadDocuments(); + editor.subscriptions.subscribeJsMessage(TriggerLoadFirstAutoSaveDocument, async () => { + await loadFirstDocument(); + }); + editor.subscriptions.subscribeJsMessage(TriggerLoadRestAutoSaveDocuments, async () => { + await loadRestDocuments(); + }); + editor.subscriptions.subscribeJsMessage(TriggerSaveActiveDocument, async (triggerSaveActiveDocument) => { + const documentId = String(triggerSaveActiveDocument.documentId); + const previouslySavedDocuments = await get>("documents", graphiteStore); + if (!previouslySavedDocuments) return; + if (documentId in previouslySavedDocuments) { + await storeCurrentDocumentId(documentId); + } }); } export async function wipeDocuments() { await del("documents_tab_order", graphiteStore); + await del("current_document_id", graphiteStore); await del("documents", graphiteStore); } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 71588ee31f..7a32752f0e 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -768,7 +768,8 @@ export class UpdateMouseCursor extends JsMessage { readonly cursor!: MouseCursorIcon; } -export class TriggerLoadAutoSaveDocuments extends JsMessage {} +export class TriggerLoadFirstAutoSaveDocument extends JsMessage {} +export class TriggerLoadRestAutoSaveDocuments extends JsMessage {} export class TriggerLoadPreferences extends JsMessage {} @@ -807,6 +808,10 @@ export class TriggerSavePreferences extends JsMessage { readonly preferences!: Record; } +export class TriggerSaveActiveDocument extends JsMessage { + readonly documentId!: bigint; +} + export class DocumentChanged extends JsMessage {} export type DataBuffer = { @@ -1574,10 +1579,12 @@ export const messageMakers: Record = { TriggerImport, TriggerIndexedDbRemoveDocument, TriggerIndexedDbWriteDocument, - TriggerLoadAutoSaveDocuments, + TriggerLoadFirstAutoSaveDocument, TriggerLoadPreferences, + TriggerLoadRestAutoSaveDocuments, TriggerOpenDocument, TriggerPaste, + TriggerSaveActiveDocument, TriggerSavePreferences, TriggerTextCommit, TriggerTextCopy, @@ -1597,14 +1604,14 @@ export const messageMakers: Record = { UpdateDocumentModeLayout, UpdateDocumentRulers, UpdateDocumentScrollbars, + UpdateExportReorderIndex, UpdateEyedropperSamplingState, UpdateGraphFadeArtwork, UpdateGraphViewOverlay, + UpdateImportReorderIndex, UpdateImportsExports, UpdateInputHints, UpdateInSelectedNetwork, - UpdateExportReorderIndex, - UpdateImportReorderIndex, UpdateLayersPanelControlBarLayout, UpdateLayerWidths, UpdateMenuBarLayout, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 5468f8acc6..425fa4837b 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -299,7 +299,7 @@ impl EditorHandle { } #[wasm_bindgen(js_name = openAutoSavedDocument)] - pub fn open_auto_saved_document(&self, document_id: u64, document_name: String, document_is_saved: bool, document_serialized_content: String) { + pub fn open_auto_saved_document(&self, document_id: u64, document_name: String, document_is_saved: bool, document_serialized_content: String, to_front: bool) { let document_id = DocumentId(document_id); let message = PortfolioMessage::OpenDocumentFileWithId { document_id, @@ -307,6 +307,7 @@ impl EditorHandle { document_is_auto_saved: true, document_is_saved, document_serialized_content, + to_front, }; self.dispatch(message); } @@ -753,6 +754,7 @@ impl EditorHandle { document_is_auto_saved, document_is_saved, document_serialized_content: document_serialized_content.clone(), + to_front: false, }); let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap(); @@ -821,6 +823,7 @@ impl EditorHandle { document_is_auto_saved, document_is_saved, document_serialized_content, + to_front: false, }); return; } @@ -925,6 +928,7 @@ impl EditorHandle { document_is_auto_saved, document_is_saved, document_serialized_content, + to_front: false, }); } }