Remove all use of document indices (#406)

* removed all use of document indicies

* -add u64 support for wasm bridge

* fixed rust formating

* Cleaned up FrontendDocumentState in js-messages

* Tiny tweaks from code review

* - moved more of closeDocumentWithConfirmation to rust
- updated serde_wasm_bindgen to add feature flag

* changed to upsteam version of serde_wasm_bindgen

* cargo fmt

* -fix event propigation on delete
- Js message change class extention to typedef

* changed another typedef

* cargo fmt

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mfish33 2021-12-24 19:07:19 -05:00 committed by Keavon Chambers
parent 1594b9c61d
commit 04c1b2ed03
13 changed files with 213 additions and 184 deletions

21
Cargo.lock generated
View File

@ -69,6 +69,12 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "glam" name = "glam"
version = "0.17.3" version = "0.17.3"
@ -129,6 +135,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"serde", "serde",
"serde-wasm-bindgen",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-test", "wasm-bindgen-test",
] ]
@ -292,6 +299,18 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.130" version = "1.0.130"
@ -376,8 +395,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"serde",
"serde_json",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]

View File

@ -17,11 +17,15 @@ pub struct Dispatcher {
pub responses: Vec<FrontendMessage>, pub responses: Vec<FrontendMessage>,
} }
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::RenderDocument)),
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayFolderTreeStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayFolderTreeStructure),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList),
MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged), MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged),
]; ];
@ -31,7 +35,8 @@ impl Dispatcher {
use Message::*; use Message::*;
while let Some(message) = self.messages.pop_front() { 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; continue;
} }
self.log_message(&message); self.log_message(&message);

View File

@ -1,3 +1,4 @@
use crate::frontend::frontend_message_handler::FrontendDocumentDetails;
use crate::input::InputPreprocessor; use crate::input::InputPreprocessor;
use crate::message_prelude::*; use crate::message_prelude::*;
use graphene::layers::Layer; use graphene::layers::Layer;
@ -18,11 +19,12 @@ pub enum DocumentsMessage {
insert_index: isize, insert_index: isize,
}, },
Paste, Paste,
SelectDocument(usize), SelectDocument(u64),
CloseDocument(usize), CloseDocument(u64),
#[child] #[child]
Document(DocumentMessage), Document(DocumentMessage),
CloseActiveDocumentWithConfirmation, CloseActiveDocumentWithConfirmation,
CloseDocumentWithConfirmation(u64),
CloseAllDocumentsWithConfirmation, CloseAllDocumentsWithConfirmation,
CloseAllDocuments, CloseAllDocuments,
RequestAboutGraphiteDialog, RequestAboutGraphiteDialog,
@ -38,20 +40,17 @@ pub enum DocumentsMessage {
pub struct DocumentsMessageHandler { pub struct DocumentsMessageHandler {
documents: HashMap<u64, DocumentMessageHandler>, documents: HashMap<u64, DocumentMessageHandler>,
document_ids: Vec<u64>, document_ids: Vec<u64>,
document_id_counter: u64, active_document_id: u64,
active_document_index: usize,
copy_buffer: Vec<Layer>, copy_buffer: Vec<Layer>,
} }
impl DocumentsMessageHandler { impl DocumentsMessageHandler {
pub fn active_document(&self) -> &DocumentMessageHandler { pub fn active_document(&self) -> &DocumentMessageHandler {
let id = self.document_ids[self.active_document_index]; self.documents.get(&self.active_document_id).unwrap()
self.documents.get(&id).unwrap()
} }
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler { pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
let id = self.document_ids[self.active_document_index]; self.documents.get_mut(&self.active_document_id).unwrap()
self.documents.get_mut(&id).unwrap()
} }
fn generate_new_document_name(&self) -> String { fn generate_new_document_name(&self) -> String {
@ -78,21 +77,27 @@ impl DocumentsMessageHandler {
} }
fn load_document(&mut self, new_document: DocumentMessageHandler, responses: &mut VecDeque<Message>) { fn load_document(&mut self, new_document: DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.document_id_counter += 1; let new_id = generate_uuid();
self.active_document_index = self.document_ids.len(); self.active_document_id = new_id;
self.document_ids.push(self.document_id_counter); self.document_ids.push(new_id);
self.documents.insert(self.document_id_counter, new_document); self.documents.insert(new_id, new_document);
// Send the new list of document tab names // Send the new list of document tab names
let open_documents = self let open_documents = self
.document_ids .document_ids
.iter() .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::<Vec<_>>(); .collect::<Vec<_>>();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); 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::RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.active_document().layer_data.keys() { for layer in self.active_document().layer_data.keys() {
@ -104,18 +109,23 @@ impl DocumentsMessageHandler {
pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> { pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> {
self.document_ids.iter().map(|id| self.documents.get(id).expect("document id was not found in the document hashmap")) 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 { impl Default for DocumentsMessageHandler {
fn default() -> Self { fn default() -> Self {
let mut documents_map: HashMap<u64, DocumentMessageHandler> = HashMap::with_capacity(1); let mut documents_map: HashMap<u64, DocumentMessageHandler> = HashMap::with_capacity(1);
documents_map.insert(0, DocumentMessageHandler::default()); let starting_key = generate_uuid();
documents_map.insert(starting_key, DocumentMessageHandler::default());
Self { Self {
documents: documents_map, documents: documents_map,
document_ids: vec![0], document_ids: vec![starting_key],
copy_buffer: vec![], copy_buffer: vec![],
active_document_index: 0, active_document_id: starting_key,
document_id_counter: 0,
} }
} }
} }
@ -129,11 +139,9 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
responses.push_back(FrontendMessage::DisplayAboutGraphiteDialog.into()); responses.push_back(FrontendMessage::DisplayAboutGraphiteDialog.into());
} }
Document(message) => self.active_document_mut().process_action(message, ipp, responses), Document(message) => self.active_document_mut().process_action(message, ipp, responses),
SelectDocument(index) => { SelectDocument(id) => {
// NOTE: Potentially this will break if we ever exceed 56 bit values due to how the message parsing system works. self.active_document_id = id;
assert!(index < self.documents.len(), "Tried to select a document that was not initialized"); responses.push_back(FrontendMessage::SetActiveDocument { document_id: id }.into());
self.active_document_index = index;
responses.push_back(FrontendMessage::SetActiveDocument { document_index: index }.into());
responses.push_back(RenderDocument.into()); responses.push_back(RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.active_document().layer_data.keys() { for layer in self.active_document().layer_data.keys() {
@ -141,12 +149,17 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
} }
} }
CloseActiveDocumentWithConfirmation => { CloseActiveDocumentWithConfirmation => {
responses.push_back( responses.push_back(DocumentsMessage::CloseDocumentWithConfirmation(self.active_document_id).into());
FrontendMessage::DisplayConfirmationToCloseDocument { }
document_index: self.active_document_index, CloseDocumentWithConfirmation(id) => {
} let target_document = self.documents.get(&id).unwrap();
.into(), 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 => { CloseAllDocumentsWithConfirmation => {
responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into()); responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into());
@ -159,38 +172,44 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
// Create a new blank document // Create a new blank document
responses.push_back(NewDocument.into()); responses.push_back(NewDocument.into());
} }
CloseDocument(index) => { CloseDocument(id) => {
assert!(index < self.documents.len(), "Tried to close a document that was not initialized"); let document_index = self.document_index(id);
// 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
self.documents.remove(&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 // Last tab was closed, so create a new blank tab
if self.document_ids.is_empty() { if self.document_ids.is_empty() {
self.document_id_counter += 1; let new_id = generate_uuid();
self.document_ids.push(self.document_id_counter); self.document_ids.push(new_id);
self.documents.insert(self.document_id_counter, DocumentMessageHandler::default()); self.documents.insert(new_id, DocumentMessageHandler::default());
} }
self.active_document_index = if self.active_document_index >= self.document_ids.len() { self.active_document_id = if id != self.active_document_id {
self.document_ids.len() - 1 // 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 { } else {
index // Move to the next tab
self.document_ids[document_index]
}; };
// Send the new list of document tab names // 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::<Vec<_>>();
// Update the list of new documents on the front end, active tab, and ensure that document renders // 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::UpdateOpenDocumentsList { open_documents }.into());
responses.push_back( responses.push_back(FrontendMessage::SetActiveDocument { document_id: self.active_document_id }.into());
FrontendMessage::SetActiveDocument {
document_index: self.active_document_index,
}
.into(),
);
responses.push_back(RenderDocument.into()); responses.push_back(RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.active_document().layer_data.keys() { for layer in self.active_document().layer_data.keys() {
@ -222,17 +241,31 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
} }
UpdateOpenDocumentsList => { UpdateOpenDocumentsList => {
// Send the list of document tab names // 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::<Vec<_>>();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
} }
NextDocument => { NextDocument => {
let next = (self.active_document_index + 1) % self.document_ids.len(); let current_index = self.document_index(self.active_document_id);
responses.push_back(SelectDocument(next).into()); 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 => { PrevDocument => {
let len = self.document_ids.len(); let len = self.document_ids.len();
let prev = (self.active_document_index + len - 1) % len; let current_index = self.document_index(self.active_document_id);
responses.push_back(SelectDocument(prev).into()); let prev_index = (current_index + len - 1) % len;
let prev_id = self.document_ids[prev_index];
responses.push_back(SelectDocument(prev_id).into());
} }
Copy => { Copy => {
let paths = self.active_document().selected_layers_sorted(); let paths = self.active_document().selected_layers_sorted();

View File

@ -2,10 +2,7 @@ use crate::consts::VIEWPORT_ROTATE_SNAP_INTERVAL;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene::layers::{style::ViewMode, BlendMode, Layer, LayerData as DocumentLayerData, LayerDataType}; use graphene::layers::{style::ViewMode, BlendMode, Layer, LayerData as DocumentLayerData, LayerDataType};
use graphene::LayerId; use graphene::LayerId;
use serde::{ use serde::{ser::SerializeStruct, Deserialize, Serialize};
ser::{SerializeSeq, SerializeStruct},
Deserialize, Serialize,
};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -84,39 +81,11 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La
opacity: layer.opacity, opacity: layer.opacity,
layer_type: (&layer.data).into(), layer_type: (&layer.data).into(),
layer_data: *layer_data, layer_data: *layer_data,
path: path.into(), path,
thumbnail, thumbnail,
} }
} }
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Path(Vec<LayerId>);
impl From<Vec<LayerId>> for Path {
fn from(iter: Vec<LayerId>) -> Self {
Self(iter)
}
}
impl Serialize for Path {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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)] #[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct RawBuffer(Vec<u8>); pub struct RawBuffer(Vec<u8>);
@ -152,7 +121,7 @@ pub struct LayerPanelEntry {
pub opacity: f64, pub opacity: f64,
pub layer_type: LayerType, pub layer_type: LayerType,
pub layer_data: LayerData, pub layer_data: LayerData,
pub path: crate::document::layer_panel::Path, pub path: Vec<LayerId>,
pub thumbnail: String, pub thumbnail: String,
} }

View File

@ -5,17 +5,24 @@ use crate::tool::tool_options::ToolOptions;
use crate::Color; use crate::Color;
use serde::{Deserialize, Serialize}; 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)] #[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
pub enum FrontendMessage { pub enum FrontendMessage {
DisplayFolderTreeStructure { data_buffer: RawBuffer }, DisplayFolderTreeStructure { data_buffer: RawBuffer },
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> }, SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
SetActiveDocument { document_index: usize }, SetActiveDocument { document_id: u64 },
UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> }, UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
UpdateInputHints { hint_data: HintData }, UpdateInputHints { hint_data: HintData },
DisplayError { title: String, description: String }, DisplayError { title: String, description: String },
DisplayPanic { panic_info: String, title: String, description: String }, DisplayPanic { panic_info: String, title: String, description: String },
DisplayConfirmationToCloseDocument { document_index: usize }, DisplayConfirmationToCloseDocument { document_id: u64 },
DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseAllDocuments,
DisplayAboutGraphiteDialog, DisplayAboutGraphiteDialog,
UpdateLayer { data: LayerPanelEntry }, UpdateLayer { data: LayerPanelEntry },

View File

@ -1,5 +1,5 @@
<template> <template>
<button class="icon-button" :class="`size-${String(size)}`" @click="action"> <button class="icon-button" :class="`size-${String(size)}`" @click="(e) => action(e)">
<IconLabel :icon="icon" /> <IconLabel :icon="icon" />
</button> </button>
</template> </template>

View File

@ -7,26 +7,11 @@
:class="{ active: tabIndex === tabActiveIndex }" :class="{ active: tabIndex === tabActiveIndex }"
v-for="(tabLabel, tabIndex) in tabLabels" v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex" :key="tabIndex"
@click.middle=" @click="(e) => e.stopPropagation() || (clickAction && clickAction(tabIndex))"
(e) => { @click.middle="(e) => e.stopPropagation() || (closeAction && closeAction(tabIndex))"
e.stopPropagation();
documents.closeDocumentWithConfirmation(tabIndex);
}
"
@click="panelType === 'Document' && documents.selectDocument(tabIndex)"
> >
<span>{{ tabLabel }}</span> <span>{{ tabLabel }}</span>
<IconButton <IconButton :action="(e) => e.stopPropagation() || (closeAction && closeAction(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
:action="
(e) => {
e.stopPropagation();
documents.closeDocumentWithConfirmation(tabIndex);
}
"
:icon="'CloseX'"
:size="16"
v-if="tabCloseButtons"
/>
</div> </div>
</div> </div>
<PopoverButton :icon="PopoverButtonIcon.VerticalEllipsis"> <PopoverButton :icon="PopoverButtonIcon.VerticalEllipsis">
@ -193,6 +178,8 @@ export default defineComponent({
tabLabels: { type: Array as PropType<string[]>, required: true }, tabLabels: { type: Array as PropType<string[]>, required: true },
tabActiveIndex: { type: Number, required: true }, tabActiveIndex: { type: Number, required: true },
panelType: { type: String, 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() { data() {
return { return {

View File

@ -6,6 +6,18 @@
:tabCloseButtons="true" :tabCloseButtons="true"
:tabMinWidths="true" :tabMinWidths="true"
:tabLabels="documents.state.documents.map((doc) => doc.displayName)" :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" :tabActiveIndex="documents.state.activeDocumentIndex"
ref="documentsPanel" ref="documentsPanel"
/> />
@ -61,16 +73,13 @@ import LayoutCol from "@/components/layout/LayoutCol.vue";
import DialogModal from "@/components/widgets/floating-menus/DialogModal.vue"; import DialogModal from "@/components/widgets/floating-menus/DialogModal.vue";
export default defineComponent({ export default defineComponent({
inject: ["documents", "dialog"], inject: ["documents", "dialog", "editor"],
components: { components: {
LayoutRow, LayoutRow,
LayoutCol, LayoutCol,
Panel, Panel,
DialogModal, DialogModal,
}, },
data() {
return {};
},
computed: { computed: {
activeDocumentIndex() { activeDocumentIndex() {
return this.documents.state.activeDocumentIndex; return this.documents.state.activeDocumentIndex;

View File

@ -19,19 +19,31 @@ export class JsMessage {
// for details about how to transform the JSON from wasm-bindgen into classes. // for details about how to transform the JSON from wasm-bindgen into classes.
// ============================================================================ // ============================================================================
export class UpdateOpenDocumentsList extends JsMessage { export class FrontendDocumentDetails {
@Transform(({ value }) => value.map((tuple: [string, boolean]) => ({ name: tuple[0], isSaved: tuple[1] }))) readonly name!: string;
readonly open_documents!: { name: string; isSaved: boolean }[];
readonly is_saved!: boolean;
readonly id!: BigInt;
get displayName() {
return `${this.name}${this.is_saved ? "" : "*"}`;
}
} }
export class UpdateOpenDocumentsList extends JsMessage {
@Type(() => FrontendDocumentDetails)
readonly open_documents!: FrontendDocumentDetails[];
}
export type HintData = HintInfo[][];
export class UpdateInputHints extends JsMessage { export class UpdateInputHints extends JsMessage {
@Type(() => HintInfo) @Type(() => HintInfo)
readonly hint_data!: HintData; readonly hint_data!: HintData;
} }
export class HintGroup extends Array<HintInfo> {} export type KeysGroup = string[];
export class HintData extends Array<HintGroup> {}
export class HintInfo { export class HintInfo {
readonly keys!: string[]; readonly keys!: string[];
@ -43,8 +55,6 @@ export class HintInfo {
readonly plus!: boolean; readonly plus!: boolean;
} }
export class KeysGroup extends Array<string> {}
const To255Scale = Transform(({ value }) => value * 255); const To255Scale = Transform(({ value }) => value * 255);
export class Color { export class Color {
@To255Scale @To255Scale
@ -83,7 +93,7 @@ export class SetActiveTool extends JsMessage {
} }
export class SetActiveDocument extends JsMessage { export class SetActiveDocument extends JsMessage {
readonly document_index!: number; readonly document_id!: BigInt;
} }
export class DisplayError extends JsMessage { export class DisplayError extends JsMessage {
@ -101,7 +111,7 @@ export class DisplayPanic extends JsMessage {
} }
export class DisplayConfirmationToCloseDocument extends JsMessage { export class DisplayConfirmationToCloseDocument extends JsMessage {
readonly document_index!: number; readonly document_id!: BigInt;
} }
export class DisplayConfirmationToCloseAllDocuments extends JsMessage {} export class DisplayConfirmationToCloseAllDocuments extends JsMessage {}
@ -157,23 +167,25 @@ export class DisplayFolderTreeStructure extends JsMessage {
} }
interface DataBuffer { interface DataBuffer {
pointer: number; pointer: BigInt;
length: number; length: BigInt;
} }
export function newDisplayFolderTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayFolderTreeStructure { export function newDisplayFolderTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayFolderTreeStructure {
const { pointer, length } = input.data_buffer; const { pointer, length } = input.data_buffer;
const pointerNum = Number(pointer);
const lengthNum = Number(length);
const wasmMemoryBuffer = wasm.wasm_memory().buffer; const wasmMemoryBuffer = wasm.wasm_memory().buffer;
// Decode the folder structure encoding // 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 // 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 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 // 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 layersEncountered = 0;
let currentFolder = new DisplayFolderTreeStructure(BigInt(-1), []); let currentFolder = new DisplayFolderTreeStructure(BigInt(-1), []);
@ -226,12 +238,6 @@ export class SetCanvasRotation extends JsMessage {
readonly new_radians!: number; 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 = export type BlendMode =
| "Normal" | "Normal"
| "Multiply" | "Multiply"
@ -263,7 +269,7 @@ export class LayerPanelEntry {
layer_type!: LayerType; layer_type!: LayerType;
@Transform(({ value }) => newPath(value)) @Transform(({ value }) => new BigUint64Array(value))
path!: BigUint64Array; path!: BigUint64Array;
@Type(() => LayerData) @Type(() => LayerData)

View File

@ -175,7 +175,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
// Skip the message during development, since it's annoying when testing // Skip the message during development, since it's annoying when testing
if (process.env.NODE_ENV === "development") return; 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) { if (!allDocumentsSaved) {
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?"; e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
e.preventDefault(); e.preventDefault();

View File

@ -8,47 +8,30 @@ import {
DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseAllDocuments,
DisplayConfirmationToCloseDocument, DisplayConfirmationToCloseDocument,
ExportDocument, ExportDocument,
FrontendDocumentDetails,
OpenDocumentBrowse, OpenDocumentBrowse,
SaveDocument, SaveDocument,
SetActiveDocument, SetActiveDocument,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
} from "@/dispatcher/js-messages"; } 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) { export function createDocumentsState(editor: EditorState, dialogState: DialogState) {
const state = reactive({ const state = reactive({
unsaved: false, unsaved: false,
documents: [] as DocumentSaveState[], documents: [] as FrontendDocumentDetails[],
activeDocumentIndex: 0, activeDocumentIndex: 0,
}); });
const selectDocument = (tabIndex: number) => { const closeDocumentWithConfirmation = async (documentId: BigInt) => {
editor.instance.select_document(tabIndex); // Assume we receive a correct document_id
}; const targetDocument = state.documents.find((doc) => doc.id === documentId) as FrontendDocumentDetails;
const tabLabel = targetDocument.displayName;
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);
// Show the close confirmation prompt // Show the close confirmation prompt
dialogState.createDialog("File", "Save changes before closing?", targetDocument.displayName, [ dialogState.createDialog("File", "Save changes before closing?", tabLabel, [
{ {
kind: "TextButton", kind: "TextButton",
callback: () => { callback: async () => {
editor.instance.save_document(); editor.instance.save_document();
dialogState.dismissDialog(); dialogState.dismissDialog();
}, },
@ -56,15 +39,15 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
}, },
{ {
kind: "TextButton", kind: "TextButton",
callback: () => { callback: async () => {
editor.instance.close_document(tabIndex); editor.instance.close_document(targetDocument.id);
dialogState.dismissDialog(); dialogState.dismissDialog();
}, },
props: { label: "Discard", minWidth: 96 }, props: { label: "Discard", minWidth: 96 },
}, },
{ {
kind: "TextButton", kind: "TextButton",
callback: () => { callback: async () => {
dialogState.dismissDialog(); dialogState.dismissDialog();
}, },
props: { label: "Cancel", minWidth: 96 }, props: { label: "Cancel", minWidth: 96 },
@ -94,15 +77,17 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
// Set up message subscriptions on creation // Set up message subscriptions on creation
editor.dispatcher.subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => { 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) => { 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) => { editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseDocument, (displayConfirmationToCloseDocument) => {
closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_index); closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_id);
}); });
editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseAllDocuments, () => { editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseAllDocuments, () => {
@ -128,8 +113,6 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
return { return {
state: readonly(state), state: readonly(state),
selectDocument,
closeDocumentWithConfirmation,
closeAllDocumentsWithConfirmation, closeAllDocumentsWithConfirmation,
}; };
} }

View File

@ -18,7 +18,8 @@ editor = { path = "../../editor", package = "graphite-editor" }
graphene = { path = "../../graphene", package = "graphite-graphene" } graphene = { path = "../../graphene", package = "graphite-graphene" }
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } 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" js-sys = "0.3.55"
[dev-dependencies] [dev-dependencies]

View File

@ -13,7 +13,12 @@ use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::message_prelude::*; use editor::message_prelude::*;
use editor::misc::EditorError; use editor::misc::EditorError;
use editor::tool::{tool_options::ToolOptions, tools, ToolType}; 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::*; use wasm_bindgen::prelude::*;
// To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell // To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell
@ -29,7 +34,7 @@ pub struct JsEditorHandle {
#[wasm_bindgen] #[wasm_bindgen]
impl JsEditorHandle { impl JsEditorHandle {
#[wasm_bindgen(constructor)] #[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_id = generate_uuid();
let editor = Editor::new(); let editor = Editor::new();
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().insert(editor_id, editor)); EDITOR_INSTANCES.with(|instances| instances.borrow_mut().insert(editor_id, editor));
@ -68,7 +73,9 @@ impl JsEditorHandle {
// Sends a FrontendMessage to JavaScript // Sends a FrontendMessage to JavaScript
fn handle_response(&self, message: FrontendMessage) { fn handle_response(&self, message: FrontendMessage) {
let message_type = message.to_discriminant().local_name(); 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); 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 /// Update the options for a given tool
pub fn set_tool_options(&self, tool: String, options: &JsValue) -> Result<(), JsValue> { pub fn set_tool_options(&self, tool: String, options: &JsValue) -> Result<(), JsValue> {
match options.into_serde::<ToolOptions>() { match serde_wasm_bindgen::from_value::<ToolOptions>(options.clone()) {
Ok(options) => match translate_tool_type(&tool) { Ok(options) => match translate_tool_type(&tool) {
Some(tool) => { Some(tool) => {
let message = ToolMessage::SetToolOptions(tool, options); let message = ToolMessage::SetToolOptions(tool, options);
@ -124,7 +131,7 @@ impl JsEditorHandle {
pub fn send_tool_message(&self, tool: String, message: &JsValue) -> Result<(), JsValue> { pub fn send_tool_message(&self, tool: String, message: &JsValue) -> Result<(), JsValue> {
let tool_message = match translate_tool_type(&tool) { let tool_message = match translate_tool_type(&tool) {
Some(tool) => match tool { Some(tool) => match tool {
ToolType::Select => match message.into_serde::<tools::select::SelectMessage>() { ToolType::Select => match serde_wasm_bindgen::from_value::<tools::select::SelectMessage>(message.clone()) {
Ok(select_message) => Ok(ToolMessage::Select(select_message)), Ok(select_message) => Ok(ToolMessage::Select(select_message)),
Err(err) => Err(Error::new(&format!("Invalid message for {}: {}", tool, err)).into()), 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) { pub fn select_document(&self, document_id: u64) {
let message = DocumentsMessage::SelectDocument(document); let message = DocumentsMessage::SelectDocument(document_id);
self.dispatch(message); self.dispatch(message);
} }
@ -173,8 +180,8 @@ impl JsEditorHandle {
self.dispatch(message); self.dispatch(message);
} }
pub fn close_document(&self, document: usize) { pub fn close_document(&self, document_id: u64) {
let message = DocumentsMessage::CloseDocument(document); let message = DocumentsMessage::CloseDocument(document_id);
self.dispatch(message); self.dispatch(message);
} }
@ -188,6 +195,11 @@ impl JsEditorHandle {
self.dispatch(message); 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) { pub fn close_all_documents_with_confirmation(&self) {
let message = DocumentsMessage::CloseAllDocumentsWithConfirmation; let message = DocumentsMessage::CloseAllDocumentsWithConfirmation;
self.dispatch(message); self.dispatch(message);