mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 14:58:05 +08:00
Implement IndexedDB document auto-save (#422)
* 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 * working initial auto save impl * auto save is a lifetime file * - cargo fmt - fixc error message - move document version constant * code review round 1 * generate seed for uuid in js when wasm is initialized * Resolve PR feedback * Further address PR feedback * Fix failing test Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: otdavies <oliver@psyfer.io>
This commit is contained in:
@@ -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() {
|
||||
@@ -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);
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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;
|
||||
@@ -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)?;
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
@@ -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() {
|
||||
@@ -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 {
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
extern crate graphite_proc_macros;
|
||||
|
||||
mod communication;
|
||||
pub mod communication;
|
||||
#[macro_use]
|
||||
pub mod misc;
|
||||
pub mod consts;
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user