Skip to content

Implement IndexedDB document auto-save #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e1aaee1
removed all use of document indicies
mfish33 Dec 18, 2021
d3e3ab6
-add u64 support for wasm bridge
mfish33 Dec 19, 2021
51c1056
fixed rust formating
mfish33 Dec 19, 2021
f73a631
Merge branch 'master' into only-document-ids
mfish33 Dec 20, 2021
b720ef9
Merge branch 'master' into only-document-ids
Keavon Dec 20, 2021
9483fae
Cleaned up FrontendDocumentState in js-messages
mfish33 Dec 20, 2021
833f1f9
Merge branch 'only-document-ids' of https://github.com/GraphiteEditor…
mfish33 Dec 20, 2021
f9e2cbb
Merge branch 'master' into only-document-ids
Keavon Dec 20, 2021
8574683
Tiny tweaks from code review
Keavon Dec 21, 2021
e1a0308
- moved more of closeDocumentWithConfirmation to rust
mfish33 Dec 22, 2021
fb4edec
Merge branch 'only-document-ids' of https://github.com/GraphiteEditor…
mfish33 Dec 22, 2021
fe55503
Merge branch 'master' into only-document-ids
mfish33 Dec 22, 2021
161c2bf
Merge branch 'master' into only-document-ids
mfish33 Dec 23, 2021
0599861
working initial auto save impl
mfish33 Dec 24, 2021
65afd61
auto save is a lifetime file
mfish33 Dec 26, 2021
6feaf74
Merge branch 'master' into indexed-db-auto-save
mfish33 Dec 26, 2021
82e146f
- cargo fmt
mfish33 Dec 26, 2021
9a9ac45
Merge branch 'master' into indexed-db-auto-save
Keavon Dec 26, 2021
3400200
code review round 1
mfish33 Dec 27, 2021
b9a6100
Merge branch 'indexed-db-auto-save' of https://github.com/GraphiteEdi…
mfish33 Dec 27, 2021
d81acdd
generate seed for uuid in js when wasm is initialized
mfish33 Dec 27, 2021
45b0355
Resolve PR feedback
otdavies Dec 27, 2021
b70bea8
Further address PR feedback
otdavies Dec 27, 2021
e135a74
Fix failing test
Keavon Dec 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion editor/src/communication/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl Dispatcher {

#[cfg(test)]
mod test {
use crate::{document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
use crate::{communication::set_uuid_seed, document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
use graphene::{color::Color, Operation};

fn init_logger() {
Expand All @@ -108,6 +108,7 @@ mod test {
/// 2. A blue shape
/// 3. A green ellipse
fn create_editor_with_three_layers() -> Editor {
set_uuid_seed(0);
let mut editor = Editor::new();

editor.select_primary_color(Color::RED);
Expand Down
15 changes: 13 additions & 2 deletions editor/src/communication/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use rand_chacha::{
use spin::Mutex;

pub use crate::input::InputPreprocessor;
use std::collections::VecDeque;
use std::{cell::Cell, collections::VecDeque};

pub type ActionList = Vec<Vec<MessageDiscriminant>>;

Expand All @@ -29,10 +29,21 @@ where
fn actions(&self) -> ActionList;
}

thread_local! {
pub static UUID_SEED: Cell<Option<u64>> = Cell::new(None);
}

pub fn set_uuid_seed(random_seed: u64) {
UUID_SEED.with(|seed| seed.set(Some(random_seed)))
}

pub fn generate_uuid() -> u64 {
let mut lock = RNG.lock();
if lock.is_none() {
*lock = Some(ChaCha20Rng::seed_from_u64(0));
UUID_SEED.with(|seed| {
let random_seed = seed.get().expect("random seed not set before editor was initialized");
*lock = Some(ChaCha20Rng::seed_from_u64(random_seed));
})
}
lock.as_mut().map(ChaCha20Rng::next_u64).unwrap()
}
19 changes: 18 additions & 1 deletion editor/src/document/document_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub use super::layer_panel::*;
use super::movement_handler::{MovementMessage, MovementMessageHandler};
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};

use crate::consts::DEFAULT_DOCUMENT_NAME;
use crate::consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
Expand Down Expand Up @@ -186,6 +187,13 @@ impl DocumentMessageHandler {
}
}

pub fn is_unmodified_default(&self) -> bool {
self.serialize_root().len() == Self::default().serialize_root().len()
&& self.document_undo_history.len() == 0
&& self.document_redo_history.len() == 0
&& self.name.starts_with(DEFAULT_DOCUMENT_NAME)
}

fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
if self.graphene_document.layer(path).ok()?.overlay {
return None;
Expand Down Expand Up @@ -413,6 +421,14 @@ impl DocumentMessageHandler {
self.current_identifier() == self.saved_document_identifier
}

pub fn set_save_state(&mut self, is_saved: bool) {
if is_saved {
self.saved_document_identifier = self.current_identifier();
} else {
self.saved_document_identifier = generate_uuid();
}
}

pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
let data: LayerData = *layer_data(&mut self.layer_data, &path);
let layer = self.graphene_document.layer(&path)?;
Expand Down Expand Up @@ -493,7 +509,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
)
}
SaveDocument => {
self.saved_document_identifier = self.current_identifier();
self.set_save_state(true);
responses.push_back(DocumentsMessage::AutoSaveActiveDocument.into());
// Update the save status of the just saved document
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());

Expand Down
88 changes: 70 additions & 18 deletions editor/src/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ pub enum DocumentsMessage {
RequestAboutGraphiteDialog,
NewDocument,
OpenDocument,
OpenDocumentFileWithId {
document: String,
document_name: String,
document_id: u64,
document_is_saved: bool,
},
OpenDocumentFile(String, String),
UpdateOpenDocumentsList,
AutoSaveDocument(u64),
AutoSaveActiveDocument,
NextDocument,
PrevDocument,
}
Expand Down Expand Up @@ -76,11 +84,23 @@ impl DocumentsMessageHandler {
name
}

fn load_document(&mut self, new_document: DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let new_id = generate_uuid();
self.active_document_id = new_id;
self.document_ids.push(new_id);
self.documents.insert(new_id, new_document);
// TODO Fix how this doesn't preserve tab order upon loading new document from file>load
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, replace_first_empty: bool, responses: &mut VecDeque<Message>) {
// Special case when loading a document on an empty page
if replace_first_empty && self.active_document().is_unmodified_default() {
responses.push_back(DocumentsMessage::CloseDocument(self.active_document_id).into());

let active_document_index = self
.document_ids
.iter()
.position(|id| self.active_document_id == *id)
.expect("Did not find matching active document id");
self.document_ids.insert(active_document_index + 1, document_id);
} else {
self.document_ids.push(document_id);
}

self.documents.insert(document_id, new_document);

// Send the new list of document tab names
let open_documents = self
Expand All @@ -97,12 +117,7 @@ impl DocumentsMessageHandler {

responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.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() {
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
}
responses.push_back(DocumentsMessage::SelectDocument(document_id).into());
}

// Returns an iterator over the open documents in order
Expand Down Expand Up @@ -140,6 +155,10 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
}
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
SelectDocument(id) => {
let active_document = self.active_document();
if !active_document.is_saved() {
responses.push_back(DocumentsMessage::AutoSaveDocument(self.active_document_id).into());
}
self.active_document_id = id;
responses.push_back(FrontendMessage::SetActiveDocument { document_id: id }.into());
responses.push_back(RenderDocument.into());
Expand Down Expand Up @@ -210,6 +229,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
// 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_id: self.active_document_id }.into());
responses.push_back(FrontendMessage::RemoveAutoSaveDocument { document_id: id }.into());
responses.push_back(RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.active_document().layer_data.keys() {
Expand All @@ -219,16 +239,33 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
NewDocument => {
let name = self.generate_new_document_name();
let new_document = DocumentMessageHandler::with_name(name, ipp);
self.load_document(new_document, responses);
self.load_document(new_document, generate_uuid(), false, responses);
}
OpenDocument => {
responses.push_back(FrontendMessage::OpenDocumentBrowse.into());
}
OpenDocumentFile(name, serialized_contents) => {
let document = DocumentMessageHandler::with_name_and_content(name, serialized_contents, ipp);
OpenDocumentFile(document_name, document) => {
responses.push_back(
DocumentsMessage::OpenDocumentFileWithId {
document,
document_name,
document_id: generate_uuid(),
document_is_saved: true,
}
.into(),
);
}
OpenDocumentFileWithId {
document_name,
document_id,
document,
document_is_saved,
} => {
let document = DocumentMessageHandler::with_name_and_content(document_name, document, ipp);
match document {
Ok(document) => {
self.load_document(document, responses);
Ok(mut document) => {
document.set_save_state(document_is_saved);
self.load_document(document, document_id, true, responses);
}
Err(e) => responses.push_back(
FrontendMessage::DisplayError {
Expand All @@ -254,18 +291,33 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
.collect::<Vec<_>>();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
}
AutoSaveDocument(id) => {
let document = self.documents.get(&id).unwrap();
responses.push_back(
FrontendMessage::AutoSaveDocument {
document: document.graphene_document.serialize_document(),
details: FrontendDocumentDetails {
is_saved: document.is_saved(),
id,
name: document.name.clone(),
},
}
.into(),
)
}
AutoSaveActiveDocument => responses.push_back(DocumentsMessage::AutoSaveDocument(self.active_document_id).into()),
NextDocument => {
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());
responses.push_back(DocumentsMessage::SelectDocument(next_id).into());
}
PrevDocument => {
let len = self.document_ids.len();
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());
responses.push_back(DocumentsMessage::SelectDocument(prev_id).into());
}
Copy => {
let paths = self.active_document().selected_layers_sorted();
Expand Down
1 change: 0 additions & 1 deletion editor/src/document/movement_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use crate::{
input::{mouse::ViewportBounds, mouse::ViewportPosition, InputPreprocessor},
};
use graphene::document::Document;
use graphene::layers::style::ViewMode;
use graphene::Operation as DocumentOperation;

use glam::DVec2;
Expand Down
2 changes: 2 additions & 0 deletions editor/src/frontend/frontend_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub enum FrontendMessage {
UpdateRulers { origin: (f64, f64), spacing: f64, interval: f64 },
ExportDocument { document: String, name: String },
SaveDocument { document: String, name: String },
AutoSaveDocument { document: String, details: FrontendDocumentDetails },
RemoveAutoSaveDocument { document_id: u64 },
OpenDocumentBrowse,
UpdateWorkingColors { primary: Color, secondary: Color },
SetCanvasZoom { new_zoom: f64 },
Expand Down
4 changes: 3 additions & 1 deletion editor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

extern crate graphite_proc_macros;

mod communication;
pub mod communication;
#[macro_use]
pub mod misc;
pub mod consts;
Expand Down Expand Up @@ -31,6 +31,8 @@ pub struct Editor {
}

impl Editor {
/// Construct a new editor instance.
/// Remember to provide a random seed with `editor::communication::set_uuid_seed(seed)` before any editors can be used.
pub fn new() -> Self {
Self { dispatcher: Dispatcher::new() }
}
Expand Down
4 changes: 2 additions & 2 deletions editor/src/tool/tools/eyedropper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ impl Fsm for EyedropperToolFsmState {
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
Expand Down
2 changes: 1 addition & 1 deletion editor/src/tool/tools/fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl Fsm for FillToolFsmState {
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
import { createEditorState, EditorState } from "@/state/wasm-loader";
import { createInputManager, InputManager } from "@/lifetime/input";
import { initErrorHandling } from "@/lifetime/errors";
import { createAutoSaveManager } from "@/lifetime/auto-save";

// Vue injects don't play well with TypeScript, and all injects will show up as `any`. As a workaround, we can define these types.
declare module "@vue/runtime-core" {
Expand Down Expand Up @@ -259,6 +260,7 @@ export default defineComponent({
const documents = createDocumentsState(editor, dialog);
const fullscreen = createFullscreenState();
initErrorHandling(editor, dialog);
createAutoSaveManager(editor, documents);

return {
editor,
Expand Down
32 changes: 30 additions & 2 deletions frontend/src/dispatcher/js-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,26 @@ export class JsMessage {
// for details about how to transform the JSON from wasm-bindgen into classes.
// ============================================================================

export class FrontendDocumentDetails {
// Allows the auto save system to use a string for the id rather than a BigInt.
// IndexedDb does not allow for BigInts as primary keys. TypeScript does not allow
// subclasses to change the type of class variables in subclasses. It is an abstract
// class to point out that it should not be instantiated directly.
export abstract class DocumentDetails {
readonly name!: string;

readonly is_saved!: boolean;

readonly id!: BigInt;
readonly id!: BigInt | string;

get displayName() {
return `${this.name}${this.is_saved ? "" : "*"}`;
}
}

export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: BigInt;
}

export class UpdateOpenDocumentsList extends JsMessage {
@Type(() => FrontendDocumentDetails)
readonly open_documents!: FrontendDocumentDetails[];
Expand Down Expand Up @@ -296,6 +304,24 @@ export const LayerTypeOptions = {

export type LayerType = typeof LayerTypeOptions[keyof typeof LayerTypeOptions];

export class IndexedDbDocumentDetails extends DocumentDetails {
@Transform(({ value }: { value: BigInt }) => value.toString())
id!: string;
}

export class AutoSaveDocument extends JsMessage {
document!: string;

@Type(() => IndexedDbDocumentDetails)
details!: IndexedDbDocumentDetails;
}

export class RemoveAutoSaveDocument extends JsMessage {
// Use a string since IndexedDB can not use BigInts for keys
@Transform(({ value }: { value: BigInt }) => value.toString())
document_id!: string;
}

// Any is used since the type of the object should be known from the rust side
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
Expand All @@ -322,5 +348,7 @@ export const messageConstructors: Record<string, MessageMaker> = {
DisplayConfirmationToCloseDocument,
DisplayConfirmationToCloseAllDocuments,
DisplayAboutGraphiteDialog,
AutoSaveDocument,
RemoveAutoSaveDocument,
} as const;
export type JsMessageType = keyof typeof messageConstructors;
Loading