diff --git a/Cargo.lock b/Cargo.lock index 4cfd4331cf..c1934e355d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "glam" version = "0.17.3" @@ -129,6 +135,7 @@ dependencies = [ "js-sys", "log", "serde", + "serde-wasm-bindgen", "wasm-bindgen", "wasm-bindgen-test", ] @@ -292,6 +299,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5cefe81e058ce25d1acbd79160e110d2eb4b9459024d46818d7553e4be6ff7e" +dependencies = [ + "fnv", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.130" @@ -376,8 +395,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if", - "serde", - "serde_json", "wasm-bindgen-macro", ] diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 8043ece860..242a897226 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -17,11 +17,15 @@ pub struct Dispatcher { pub responses: Vec, } -const GROUP_MESSAGES: &[MessageDiscriminant] = &[ +// For optimization, these are messages guaranteed to be redundant when repeated +// The last occurrence of the message in the message queue is sufficient to ensure correctness +// In addition, these messages do not change any state in the backend (aside from caches) +const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)), MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayFolderTreeStructure), + MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList), MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged), ]; @@ -31,7 +35,8 @@ impl Dispatcher { use Message::*; while let Some(message) = self.messages.pop_front() { - if GROUP_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) { + // Skip processing of this message if it will be processed later + if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) { continue; } self.log_message(&message); diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index b915682762..e3cf95c6ec 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,3 +1,4 @@ +use crate::frontend::frontend_message_handler::FrontendDocumentDetails; use crate::input::InputPreprocessor; use crate::message_prelude::*; use graphene::layers::Layer; @@ -18,11 +19,12 @@ pub enum DocumentsMessage { insert_index: isize, }, Paste, - SelectDocument(usize), - CloseDocument(usize), + SelectDocument(u64), + CloseDocument(u64), #[child] Document(DocumentMessage), CloseActiveDocumentWithConfirmation, + CloseDocumentWithConfirmation(u64), CloseAllDocumentsWithConfirmation, CloseAllDocuments, RequestAboutGraphiteDialog, @@ -38,20 +40,17 @@ pub enum DocumentsMessage { pub struct DocumentsMessageHandler { documents: HashMap, document_ids: Vec, - document_id_counter: u64, - active_document_index: usize, + active_document_id: u64, copy_buffer: Vec, } impl DocumentsMessageHandler { pub fn active_document(&self) -> &DocumentMessageHandler { - let id = self.document_ids[self.active_document_index]; - self.documents.get(&id).unwrap() + self.documents.get(&self.active_document_id).unwrap() } pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler { - let id = self.document_ids[self.active_document_index]; - self.documents.get_mut(&id).unwrap() + self.documents.get_mut(&self.active_document_id).unwrap() } fn generate_new_document_name(&self) -> String { @@ -78,21 +77,27 @@ impl DocumentsMessageHandler { } fn load_document(&mut self, new_document: DocumentMessageHandler, responses: &mut VecDeque) { - self.document_id_counter += 1; - self.active_document_index = self.document_ids.len(); - self.document_ids.push(self.document_id_counter); - self.documents.insert(self.document_id_counter, new_document); + let new_id = generate_uuid(); + self.active_document_id = new_id; + self.document_ids.push(new_id); + self.documents.insert(new_id, new_document); // Send the new list of document tab names let open_documents = self .document_ids .iter() - .filter_map(|id| self.documents.get(&id).map(|doc| (doc.name.clone(), doc.is_saved()))) + .filter_map(|id| { + self.documents.get(&id).map(|doc| FrontendDocumentDetails { + is_saved: doc.is_saved(), + id: *id, + name: doc.name.clone(), + }) + }) .collect::>(); responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); - responses.push_back(DocumentsMessage::SelectDocument(self.active_document_index).into()); + responses.push_back(DocumentsMessage::SelectDocument(self.active_document_id).into()); responses.push_back(DocumentMessage::RenderDocument.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into()); for layer in self.active_document().layer_data.keys() { @@ -104,18 +109,23 @@ impl DocumentsMessageHandler { pub fn ordered_document_iterator(&self) -> impl Iterator { self.document_ids.iter().map(|id| self.documents.get(id).expect("document id was not found in the document hashmap")) } + + fn document_index(&self, document_id: u64) -> usize { + self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") + } } impl Default for DocumentsMessageHandler { fn default() -> Self { let mut documents_map: HashMap = HashMap::with_capacity(1); - documents_map.insert(0, DocumentMessageHandler::default()); + let starting_key = generate_uuid(); + documents_map.insert(starting_key, DocumentMessageHandler::default()); + Self { documents: documents_map, - document_ids: vec![0], + document_ids: vec![starting_key], copy_buffer: vec![], - active_document_index: 0, - document_id_counter: 0, + active_document_id: starting_key, } } } @@ -129,11 +139,9 @@ impl MessageHandler for DocumentsMessageHa responses.push_back(FrontendMessage::DisplayAboutGraphiteDialog.into()); } Document(message) => self.active_document_mut().process_action(message, ipp, responses), - SelectDocument(index) => { - // NOTE: Potentially this will break if we ever exceed 56 bit values due to how the message parsing system works. - assert!(index < self.documents.len(), "Tried to select a document that was not initialized"); - self.active_document_index = index; - responses.push_back(FrontendMessage::SetActiveDocument { document_index: index }.into()); + SelectDocument(id) => { + self.active_document_id = id; + responses.push_back(FrontendMessage::SetActiveDocument { document_id: id }.into()); responses.push_back(RenderDocument.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into()); for layer in self.active_document().layer_data.keys() { @@ -141,12 +149,17 @@ impl MessageHandler for DocumentsMessageHa } } CloseActiveDocumentWithConfirmation => { - responses.push_back( - FrontendMessage::DisplayConfirmationToCloseDocument { - document_index: self.active_document_index, - } - .into(), - ); + responses.push_back(DocumentsMessage::CloseDocumentWithConfirmation(self.active_document_id).into()); + } + CloseDocumentWithConfirmation(id) => { + let target_document = self.documents.get(&id).unwrap(); + if target_document.is_saved() { + responses.push_back(DocumentsMessage::CloseDocument(id).into()); + } else { + responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id: id }.into()); + // Select the document being closed + responses.push_back(DocumentsMessage::SelectDocument(id).into()); + } } CloseAllDocumentsWithConfirmation => { responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into()); @@ -159,38 +172,44 @@ impl MessageHandler for DocumentsMessageHa // Create a new blank document responses.push_back(NewDocument.into()); } - CloseDocument(index) => { - assert!(index < self.documents.len(), "Tried to close a document that was not initialized"); - // Get the ID based on the current collection of the documents. - let id = self.document_ids[index]; - // Map the ID to an index and remove the document + CloseDocument(id) => { + let document_index = self.document_index(id); self.documents.remove(&id); - self.document_ids.remove(index); + self.document_ids.remove(document_index); // Last tab was closed, so create a new blank tab if self.document_ids.is_empty() { - self.document_id_counter += 1; - self.document_ids.push(self.document_id_counter); - self.documents.insert(self.document_id_counter, DocumentMessageHandler::default()); + let new_id = generate_uuid(); + self.document_ids.push(new_id); + self.documents.insert(new_id, DocumentMessageHandler::default()); } - self.active_document_index = if self.active_document_index >= self.document_ids.len() { - self.document_ids.len() - 1 + self.active_document_id = if id != self.active_document_id { + // If we are not closing the active document, stay on it + self.active_document_id + } else 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() } else { - index + // Move to the next tab + self.document_ids[document_index] }; // Send the new list of document tab names - let open_documents = self.ordered_document_iterator().map(|doc| (doc.name.clone(), doc.is_saved())).collect(); - + let open_documents = self + .document_ids + .iter() + .filter_map(|id| { + self.documents.get(&id).map(|doc| FrontendDocumentDetails { + is_saved: doc.is_saved(), + id: *id, + name: doc.name.clone(), + }) + }) + .collect::>(); // Update the list of new documents on the front end, active tab, and ensure that document renders responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); - responses.push_back( - FrontendMessage::SetActiveDocument { - document_index: self.active_document_index, - } - .into(), - ); + responses.push_back(FrontendMessage::SetActiveDocument { document_id: self.active_document_id }.into()); responses.push_back(RenderDocument.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into()); for layer in self.active_document().layer_data.keys() { @@ -222,17 +241,31 @@ impl MessageHandler for DocumentsMessageHa } UpdateOpenDocumentsList => { // Send the list of document tab names - let open_documents = self.ordered_document_iterator().map(|doc| (doc.name.clone(), doc.is_saved())).collect(); + let open_documents = self + .document_ids + .iter() + .filter_map(|id| { + self.documents.get(&id).map(|doc| FrontendDocumentDetails { + is_saved: doc.is_saved(), + id: *id, + name: doc.name.clone(), + }) + }) + .collect::>(); responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); } NextDocument => { - let next = (self.active_document_index + 1) % self.document_ids.len(); - responses.push_back(SelectDocument(next).into()); + let current_index = self.document_index(self.active_document_id); + let next_index = (current_index + 1) % self.document_ids.len(); + let next_id = self.document_ids[next_index]; + responses.push_back(SelectDocument(next_id).into()); } PrevDocument => { let len = self.document_ids.len(); - let prev = (self.active_document_index + len - 1) % len; - responses.push_back(SelectDocument(prev).into()); + let current_index = self.document_index(self.active_document_id); + let prev_index = (current_index + len - 1) % len; + let prev_id = self.document_ids[prev_index]; + responses.push_back(SelectDocument(prev_id).into()); } Copy => { let paths = self.active_document().selected_layers_sorted(); diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index 1d2cee8cac..21f72559a1 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -2,10 +2,7 @@ use crate::consts::VIEWPORT_ROTATE_SNAP_INTERVAL; use glam::{DAffine2, DVec2}; use graphene::layers::{style::ViewMode, BlendMode, Layer, LayerData as DocumentLayerData, LayerDataType}; use graphene::LayerId; -use serde::{ - ser::{SerializeSeq, SerializeStruct}, - Deserialize, Serialize, -}; +use serde::{ser::SerializeStruct, Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; @@ -84,39 +81,11 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La opacity: layer.opacity, layer_type: (&layer.data).into(), layer_data: *layer_data, - path: path.into(), + path, thumbnail, } } -#[derive(Debug, Clone, Deserialize, PartialEq)] -pub struct Path(Vec); - -impl From> for Path { - fn from(iter: Vec) -> Self { - Self(iter) - } -} -impl Serialize for Path { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut seq = serializer.serialize_seq(Some(self.0.len()))?; - for e in self.0.iter() { - #[cfg(target_arch = "wasm32")] - { - // LayerIds are sent as (u32, u32) because json does not support u64s - let id = ((e >> 32) as u32, (e << 32 >> 32) as u32); - seq.serialize_element(&id)?; - } - #[cfg(not(target_arch = "wasm32"))] - seq.serialize_element(e)?; - } - seq.end() - } -} - #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct RawBuffer(Vec); @@ -152,7 +121,7 @@ pub struct LayerPanelEntry { pub opacity: f64, pub layer_type: LayerType, pub layer_data: LayerData, - pub path: crate::document::layer_panel::Path, + pub path: Vec, pub thumbnail: String, } diff --git a/editor/src/frontend/frontend_message_handler.rs b/editor/src/frontend/frontend_message_handler.rs index 24f556292b..b9e50ce87e 100644 --- a/editor/src/frontend/frontend_message_handler.rs +++ b/editor/src/frontend/frontend_message_handler.rs @@ -5,17 +5,24 @@ use crate::tool::tool_options::ToolOptions; use crate::Color; use serde::{Deserialize, Serialize}; +#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] +pub struct FrontendDocumentDetails { + pub is_saved: bool, + pub name: String, + pub id: u64, +} + #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] pub enum FrontendMessage { DisplayFolderTreeStructure { data_buffer: RawBuffer }, SetActiveTool { tool_name: String, tool_options: Option }, - SetActiveDocument { document_index: usize }, - UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> }, + SetActiveDocument { document_id: u64 }, + UpdateOpenDocumentsList { open_documents: Vec }, UpdateInputHints { hint_data: HintData }, DisplayError { title: String, description: String }, DisplayPanic { panic_info: String, title: String, description: String }, - DisplayConfirmationToCloseDocument { document_index: usize }, + DisplayConfirmationToCloseDocument { document_id: u64 }, DisplayConfirmationToCloseAllDocuments, DisplayAboutGraphiteDialog, UpdateLayer { data: LayerPanelEntry }, diff --git a/frontend/src/components/widgets/buttons/IconButton.vue b/frontend/src/components/widgets/buttons/IconButton.vue index c186e59f96..d5f174f743 100644 --- a/frontend/src/components/widgets/buttons/IconButton.vue +++ b/frontend/src/components/widgets/buttons/IconButton.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/components/workspace/Panel.vue b/frontend/src/components/workspace/Panel.vue index fd4bf79d14..ae4d1a2055 100644 --- a/frontend/src/components/workspace/Panel.vue +++ b/frontend/src/components/workspace/Panel.vue @@ -7,26 +7,11 @@ :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabIndex" - @click.middle=" - (e) => { - e.stopPropagation(); - documents.closeDocumentWithConfirmation(tabIndex); - } - " - @click="panelType === 'Document' && documents.selectDocument(tabIndex)" + @click="(e) => e.stopPropagation() || (clickAction && clickAction(tabIndex))" + @click.middle="(e) => e.stopPropagation() || (closeAction && closeAction(tabIndex))" > {{ tabLabel }} - + @@ -193,6 +178,8 @@ export default defineComponent({ tabLabels: { type: Array as PropType, required: true }, tabActiveIndex: { type: Number, required: true }, panelType: { type: String, required: true }, + clickAction: { type: Function as PropType<(index: number) => void>, required: false }, + closeAction: { type: Function as PropType<(index: number) => void>, required: false }, }, data() { return { diff --git a/frontend/src/components/workspace/Workspace.vue b/frontend/src/components/workspace/Workspace.vue index a2391f58fa..0406b106e0 100644 --- a/frontend/src/components/workspace/Workspace.vue +++ b/frontend/src/components/workspace/Workspace.vue @@ -6,6 +6,18 @@ :tabCloseButtons="true" :tabMinWidths="true" :tabLabels="documents.state.documents.map((doc) => doc.displayName)" + :clickAction=" + (tabIndex) => { + const targetId = documents.state.documents[tabIndex].id; + editor.instance.select_document(targetId); + } + " + :closeAction=" + (tabIndex) => { + const targetId = documents.state.documents[tabIndex].id; + editor.instance.close_document_with_confirmation(targetId); + } + " :tabActiveIndex="documents.state.activeDocumentIndex" ref="documentsPanel" /> @@ -61,16 +73,13 @@ import LayoutCol from "@/components/layout/LayoutCol.vue"; import DialogModal from "@/components/widgets/floating-menus/DialogModal.vue"; export default defineComponent({ - inject: ["documents", "dialog"], + inject: ["documents", "dialog", "editor"], components: { LayoutRow, LayoutCol, Panel, DialogModal, }, - data() { - return {}; - }, computed: { activeDocumentIndex() { return this.documents.state.activeDocumentIndex; diff --git a/frontend/src/dispatcher/js-messages.ts b/frontend/src/dispatcher/js-messages.ts index 2a9147a4ef..8b7f75e547 100644 --- a/frontend/src/dispatcher/js-messages.ts +++ b/frontend/src/dispatcher/js-messages.ts @@ -19,19 +19,31 @@ export class JsMessage { // for details about how to transform the JSON from wasm-bindgen into classes. // ============================================================================ +export class FrontendDocumentDetails { + readonly name!: string; + + readonly is_saved!: boolean; + + readonly id!: BigInt; + + get displayName() { + return `${this.name}${this.is_saved ? "" : "*"}`; + } +} + export class UpdateOpenDocumentsList extends JsMessage { - @Transform(({ value }) => value.map((tuple: [string, boolean]) => ({ name: tuple[0], isSaved: tuple[1] }))) - readonly open_documents!: { name: string; isSaved: boolean }[]; + @Type(() => FrontendDocumentDetails) + readonly open_documents!: FrontendDocumentDetails[]; } +export type HintData = HintInfo[][]; + export class UpdateInputHints extends JsMessage { @Type(() => HintInfo) readonly hint_data!: HintData; } -export class HintGroup extends Array {} - -export class HintData extends Array {} +export type KeysGroup = string[]; export class HintInfo { readonly keys!: string[]; @@ -43,8 +55,6 @@ export class HintInfo { readonly plus!: boolean; } -export class KeysGroup extends Array {} - const To255Scale = Transform(({ value }) => value * 255); export class Color { @To255Scale @@ -83,7 +93,7 @@ export class SetActiveTool extends JsMessage { } export class SetActiveDocument extends JsMessage { - readonly document_index!: number; + readonly document_id!: BigInt; } export class DisplayError extends JsMessage { @@ -101,7 +111,7 @@ export class DisplayPanic extends JsMessage { } export class DisplayConfirmationToCloseDocument extends JsMessage { - readonly document_index!: number; + readonly document_id!: BigInt; } export class DisplayConfirmationToCloseAllDocuments extends JsMessage {} @@ -157,23 +167,25 @@ export class DisplayFolderTreeStructure extends JsMessage { } interface DataBuffer { - pointer: number; - length: number; + pointer: BigInt; + length: BigInt; } export function newDisplayFolderTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayFolderTreeStructure { const { pointer, length } = input.data_buffer; + const pointerNum = Number(pointer); + const lengthNum = Number(length); const wasmMemoryBuffer = wasm.wasm_memory().buffer; // Decode the folder structure encoding - const encoding = new DataView(wasmMemoryBuffer, pointer, length); + const encoding = new DataView(wasmMemoryBuffer, pointerNum, lengthNum); // The structure section indicates how to read through the upcoming layer list and assign depths to each layer const structureSectionLength = Number(encoding.getBigUint64(0, true)); - const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, pointer + 8, structureSectionLength * 8); + const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, pointerNum + 8, structureSectionLength * 8); // The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel - const layerIdsSection = new DataView(wasmMemoryBuffer, pointer + 8 + structureSectionLength * 8); + const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8); let layersEncountered = 0; let currentFolder = new DisplayFolderTreeStructure(BigInt(-1), []); @@ -226,12 +238,6 @@ export class SetCanvasRotation extends JsMessage { readonly new_radians!: number; } -function newPath(input: number[][]): BigUint64Array { - // eslint-disable-next-line - const u32CombinedPairs = input.map((n: number[]) => BigInt((BigInt(n[0]) << BigInt(32)) | BigInt(n[1]))); - return new BigUint64Array(u32CombinedPairs); -} - export type BlendMode = | "Normal" | "Multiply" @@ -263,7 +269,7 @@ export class LayerPanelEntry { layer_type!: LayerType; - @Transform(({ value }) => newPath(value)) + @Transform(({ value }) => new BigUint64Array(value)) path!: BigUint64Array; @Type(() => LayerData) diff --git a/frontend/src/lifetime/input.ts b/frontend/src/lifetime/input.ts index cdaee766a2..5798ac4b7a 100644 --- a/frontend/src/lifetime/input.ts +++ b/frontend/src/lifetime/input.ts @@ -175,7 +175,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement, // Skip the message during development, since it's annoying when testing if (process.env.NODE_ENV === "development") return; - const allDocumentsSaved = document.state.documents.reduce((acc, doc) => acc && doc.isSaved, true); + const allDocumentsSaved = document.state.documents.reduce((acc, doc) => acc && doc.is_saved, true); if (!allDocumentsSaved) { e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?"; e.preventDefault(); diff --git a/frontend/src/state/documents.ts b/frontend/src/state/documents.ts index 7bcbaabe20..b48dd30811 100644 --- a/frontend/src/state/documents.ts +++ b/frontend/src/state/documents.ts @@ -8,47 +8,30 @@ import { DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseDocument, ExportDocument, + FrontendDocumentDetails, OpenDocumentBrowse, SaveDocument, SetActiveDocument, UpdateOpenDocumentsList, } from "@/dispatcher/js-messages"; -class DocumentSaveState { - readonly displayName: string; - - constructor(readonly name: string, readonly isSaved: boolean) { - this.displayName = `${name}${isSaved ? "" : "*"}`; - } -} - export function createDocumentsState(editor: EditorState, dialogState: DialogState) { const state = reactive({ unsaved: false, - documents: [] as DocumentSaveState[], + documents: [] as FrontendDocumentDetails[], activeDocumentIndex: 0, }); - const selectDocument = (tabIndex: number) => { - editor.instance.select_document(tabIndex); - }; - - const closeDocumentWithConfirmation = (tabIndex: number) => { - // Close automatically if it's already saved, no confirmation is needed - const targetDocument = state.documents[tabIndex]; - if (targetDocument.isSaved) { - editor.instance.close_document(tabIndex); - return; - } - - // Switch to the document that's being prompted to close - selectDocument(tabIndex); + const closeDocumentWithConfirmation = async (documentId: BigInt) => { + // Assume we receive a correct document_id + const targetDocument = state.documents.find((doc) => doc.id === documentId) as FrontendDocumentDetails; + const tabLabel = targetDocument.displayName; // Show the close confirmation prompt - dialogState.createDialog("File", "Save changes before closing?", targetDocument.displayName, [ + dialogState.createDialog("File", "Save changes before closing?", tabLabel, [ { kind: "TextButton", - callback: () => { + callback: async () => { editor.instance.save_document(); dialogState.dismissDialog(); }, @@ -56,15 +39,15 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta }, { kind: "TextButton", - callback: () => { - editor.instance.close_document(tabIndex); + callback: async () => { + editor.instance.close_document(targetDocument.id); dialogState.dismissDialog(); }, props: { label: "Discard", minWidth: 96 }, }, { kind: "TextButton", - callback: () => { + callback: async () => { dialogState.dismissDialog(); }, props: { label: "Cancel", minWidth: 96 }, @@ -94,15 +77,17 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta // Set up message subscriptions on creation editor.dispatcher.subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => { - state.documents = updateOpenDocumentList.open_documents.map(({ name, isSaved }) => new DocumentSaveState(name, isSaved)); + state.documents = updateOpenDocumentList.open_documents; }); editor.dispatcher.subscribeJsMessage(SetActiveDocument, (setActiveDocument) => { - state.activeDocumentIndex = setActiveDocument.document_index; + // Assume we receive a correct document id + const activeId = state.documents.findIndex((doc) => doc.id === setActiveDocument.document_id); + state.activeDocumentIndex = activeId; }); editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseDocument, (displayConfirmationToCloseDocument) => { - closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_index); + closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_id); }); editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseAllDocuments, () => { @@ -128,8 +113,6 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta return { state: readonly(state), - selectDocument, - closeDocumentWithConfirmation, closeAllDocumentsWithConfirmation, }; } diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index abb61abbdc..c87bd3fb79 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -18,7 +18,8 @@ editor = { path = "../../editor", package = "graphite-editor" } graphene = { path = "../../graphene", package = "graphite-graphene" } log = "0.4" serde = { version = "1.0", features = ["derive"] } -wasm-bindgen = { version = "0.2.73", features = ["serde-serialize"] } +wasm-bindgen = { version = "0.2.73" } +serde-wasm-bindgen = "0.4.1" js-sys = "0.3.55" [dev-dependencies] diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index f85c0688d3..25ba2295a5 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -13,7 +13,12 @@ use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::message_prelude::*; use editor::misc::EditorError; use editor::tool::{tool_options::ToolOptions, tools, ToolType}; -use editor::{Color, Editor, LayerId}; +use editor::Color; +use editor::LayerId; + +use editor::Editor; +use serde::Serialize; +use serde_wasm_bindgen; use wasm_bindgen::prelude::*; // To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell @@ -29,7 +34,7 @@ pub struct JsEditorHandle { #[wasm_bindgen] impl JsEditorHandle { #[wasm_bindgen(constructor)] - pub fn new(handle_response: js_sys::Function) -> JsEditorHandle { + pub fn new(handle_response: js_sys::Function) -> Self { let editor_id = generate_uuid(); let editor = Editor::new(); EDITOR_INSTANCES.with(|instances| instances.borrow_mut().insert(editor_id, editor)); @@ -68,7 +73,9 @@ impl JsEditorHandle { // Sends a FrontendMessage to JavaScript fn handle_response(&self, message: FrontendMessage) { let message_type = message.to_discriminant().local_name(); - let message_data = JsValue::from_serde(&message).expect("Failed to serialize FrontendMessage"); + + let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + let message_data = message.serialize(&serializer).expect("Failed to serialize FrontendMessage"); let js_return_value = self.handle_response.call2(&JsValue::null(), &JsValue::from(message_type), &message_data); @@ -106,7 +113,7 @@ impl JsEditorHandle { /// Update the options for a given tool pub fn set_tool_options(&self, tool: String, options: &JsValue) -> Result<(), JsValue> { - match options.into_serde::() { + match serde_wasm_bindgen::from_value::(options.clone()) { Ok(options) => match translate_tool_type(&tool) { Some(tool) => { let message = ToolMessage::SetToolOptions(tool, options); @@ -124,7 +131,7 @@ impl JsEditorHandle { pub fn send_tool_message(&self, tool: String, message: &JsValue) -> Result<(), JsValue> { let tool_message = match translate_tool_type(&tool) { Some(tool) => match tool { - ToolType::Select => match message.into_serde::() { + ToolType::Select => match serde_wasm_bindgen::from_value::(message.clone()) { Ok(select_message) => Ok(ToolMessage::Select(select_message)), Err(err) => Err(Error::new(&format!("Invalid message for {}: {}", tool, err)).into()), }, @@ -143,8 +150,8 @@ impl JsEditorHandle { } } - pub fn select_document(&self, document: usize) { - let message = DocumentsMessage::SelectDocument(document); + pub fn select_document(&self, document_id: u64) { + let message = DocumentsMessage::SelectDocument(document_id); self.dispatch(message); } @@ -173,8 +180,8 @@ impl JsEditorHandle { self.dispatch(message); } - pub fn close_document(&self, document: usize) { - let message = DocumentsMessage::CloseDocument(document); + pub fn close_document(&self, document_id: u64) { + let message = DocumentsMessage::CloseDocument(document_id); self.dispatch(message); } @@ -188,6 +195,11 @@ impl JsEditorHandle { self.dispatch(message); } + pub fn close_document_with_confirmation(&self, document_id: u64) { + let message = DocumentsMessage::CloseDocumentWithConfirmation(document_id); + self.dispatch(message); + } + pub fn close_all_documents_with_confirmation(&self) { let message = DocumentsMessage::CloseAllDocumentsWithConfirmation; self.dispatch(message);