Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
44 changes: 15 additions & 29 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,39 +314,25 @@ impl App {
responses.push(message);
}
}
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
if let Some((id, document)) = self.persistent_data.current_document() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: true,
};
responses.push(message);
}
}
DesktopFrontendMessage::PersistenceLoadRemainingDocuments => {
for (id, document) in self.persistent_data.documents_before_current().into_iter().rev() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: true,
select_after_open: false,
};
responses.push(message);
}
for (id, document) in self.persistent_data.documents_after_current() {
let message = DesktopWrapperMessage::LoadDocument {
DesktopFrontendMessage::PersistenceLoadDocuments => {
let current_id = self.persistent_data.current_document_id();
let mut seen_current = false;

for (id, document) in self.persistent_data.all_documents() {
let is_current = current_id == Some(id);
let to_front = !seen_current && !is_current;
seen_current |= is_current;

responses.push(DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
to_front,
select_after_open: false,
};
responses.push(message);
});
}
if let Some(id) = self.persistent_data.current_document_id() {
let message = DesktopWrapperMessage::SelectDocument { id };
responses.push(message);

if let Some(id) = current_id {
responses.push(DesktopWrapperMessage::SelectDocument { id });
}
}
DesktopFrontendMessage::OpenLaunchDocuments => {
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/cef/dirs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use crate::dirs::{app_data_dir, ensure_dir_exists};

static CEF_DIR_NAME: &str = "browser";
static CEF_DIR_NAME: &str = "cef";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still no, If you are really that annoyed by it I will instead move this to a tmp dir outside of the graphite folder. On linux/unix I could even use a ram disk (even /dev/shm might be ok).


pub(crate) fn delete_instance_dirs() {
let cef_dir = app_data_dir().join(CEF_DIR_NAME);
Expand Down
182 changes: 81 additions & 101 deletions desktop/src/persist.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
use crate::wrapper::messages::{Document, DocumentId};
use crate::wrapper::messages::{Document, DocumentId, PersistedDocumentInfo};

// Wraps PersistedState (shared with the web frontend) and adds desktop-specific behavior like file I/O
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct PersistentData {
documents: DocumentStore,
documents: Vec<PersistedDocumentInfo>,
current_document: Option<DocumentId>,
#[serde(skip)]
document_order: Option<Vec<DocumentId>>,
}

impl PersistentData {
pub(crate) fn write_document(&mut self, id: DocumentId, document: Document) {
self.documents.write(id, document);
// Update or add the document metadata
let info = PersistedDocumentInfo {
id,
name: document.name.clone(),
path: document.path.clone(),
is_saved: document.is_saved,
};
if let Some(existing) = self.documents.iter_mut().find(|doc| doc.id == id) {
*existing = info;
} else {
self.documents.push(info);
}

// Write the document content to a separate file
if let Err(e) = std::fs::write(Self::document_content_path(&id), document.content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}

if let Some(order) = &self.document_order {
self.documents.force_order(order);
self.force_order(order.clone());
}
self.flush();
}
Expand All @@ -21,45 +39,24 @@ impl PersistentData {
if Some(*id) == self.current_document {
self.current_document = None;
}
self.documents.delete(id);

self.documents.retain(|doc| doc.id != *id);
if let Err(e) = std::fs::remove_file(Self::document_content_path(id)) {
tracing::error!("Failed to delete document {id:?} from disk: {e}");
}

self.flush();
}

pub(crate) fn current_document_id(&self) -> Option<DocumentId> {
match self.current_document {
Some(id) => Some(id),
None => Some(*self.documents.document_ids().first()?),
None => Some(self.documents.first()?.id),
}
}

pub(crate) fn current_document(&self) -> Option<(DocumentId, Document)> {
let current_id = self.current_document_id()?;
Some((current_id, self.documents.read(&current_id)?))
}

pub(crate) fn documents_before_current(&self) -> Vec<(DocumentId, Document)> {
let Some(current_id) = self.current_document_id() else {
return Vec::new();
};
self.documents
.document_ids()
.into_iter()
.take_while(|id| *id != current_id)
.filter_map(|id| Some((id, self.documents.read(&id)?)))
.collect()
}

pub(crate) fn documents_after_current(&self) -> Vec<(DocumentId, Document)> {
let Some(current_id) = self.current_document_id() else {
return Vec::new();
};
self.documents
.document_ids()
.into_iter()
.skip_while(|id| *id != current_id)
.skip(1)
.filter_map(|id| Some((id, self.documents.read(&id)?)))
.collect()
pub(crate) fn all_documents(&self) -> Vec<(DocumentId, Document)> {
self.documents.iter().filter_map(|doc| Some((doc.id, self.read_document(&doc.id)?))).collect()
}

pub(crate) fn set_current_document(&mut self, id: DocumentId) {
Expand All @@ -68,11 +65,37 @@ impl PersistentData {
}

pub(crate) fn force_document_order(&mut self, order: Vec<DocumentId>) {
self.force_order(order.clone());
self.document_order = Some(order);
self.documents.force_order(self.document_order.as_ref().unwrap());
self.flush();
}

// Reads serialized document content from disk and combines it with the stored metadata
fn read_document(&self, id: &DocumentId) -> Option<Document> {
let info = self.documents.iter().find(|doc| doc.id == *id)?;
let content = std::fs::read_to_string(Self::document_content_path(id)).ok()?;
Some(Document {
content,
name: info.name.clone(),
path: info.path.clone(),
is_saved: info.is_saved,
})
}

// Reorders the documents array to match a desired ordering, keeping unmentioned documents at the end
fn force_order(&mut self, desired_order: Vec<DocumentId>) {
let mut ordered_prefix_length = 0;
for id in &desired_order {
if let Some(offset) = self.documents[ordered_prefix_length..].iter().position(|doc| doc.id == *id) {
let found_index = ordered_prefix_length + offset;
if found_index != ordered_prefix_length {
self.documents[ordered_prefix_length..=found_index].rotate_right(1);
}
ordered_prefix_length += 1;
}
}
}

fn flush(&self) {
let data = match ron::ser::to_string_pretty(self, Default::default()) {
Ok(d) => d,
Expand Down Expand Up @@ -107,86 +130,43 @@ impl PersistentData {
}
};
*self = loaded;
}

fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}
}

#[derive(Default, serde::Serialize, serde::Deserialize)]
struct DocumentStore(Vec<DocumentInfo>);
impl DocumentStore {
fn write(&mut self, id: DocumentId, document: Document) {
let meta = DocumentInfo::new(id, &document);
if let Some(existing) = self.0.iter_mut().find(|meta| meta.id == id) {
*existing = meta;
} else {
self.0.push(meta);
}
if let Err(e) = std::fs::write(Self::document_path(&id), document.content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}
self.garbage_collect_document_files();
}

fn delete(&mut self, id: &DocumentId) {
self.0.retain(|meta| meta.id != *id);
if let Err(e) = std::fs::remove_file(Self::document_path(id)) {
tracing::error!("Failed to delete document {id:?} from disk: {e}");
}
}
// Remove orphaned document content files that have no corresponding entry in the persisted state
fn garbage_collect_document_files(&self) {
let valid_paths: std::collections::HashSet<_> = self.documents.iter().map(|doc| Self::document_content_path(&doc.id)).collect();

fn read(&self, id: &DocumentId) -> Option<Document> {
let meta = self.0.iter().find(|meta| meta.id == *id)?;
let content = std::fs::read_to_string(Self::document_path(id)).ok()?;
Some(Document {
content,
name: meta.name.clone(),
path: meta.path.clone(),
is_saved: meta.is_saved,
})
}
let directory = crate::dirs::app_autosave_documents_dir();
let entries = match std::fs::read_dir(&directory) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
tracing::error!("Failed to read autosave documents directory: {e}");
return;
}
};

fn force_order(&mut self, desired_order: &[DocumentId]) {
let mut ordered_prefix_len = 0;
for id in desired_order {
if let Some(offset) = self.0[ordered_prefix_len..].iter().position(|meta| meta.id == *id) {
let found_index = ordered_prefix_len + offset;
if found_index != ordered_prefix_len {
self.0[ordered_prefix_len..=found_index].rotate_right(1);
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && !valid_paths.contains(&path) {
if let Err(e) = std::fs::remove_file(&path) {
tracing::error!("Failed to remove orphaned document file {path:?}: {e}");
}
ordered_prefix_len += 1;
}
}
}

fn document_ids(&self) -> Vec<DocumentId> {
self.0.iter().map(|meta| meta.id).collect()
fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}

fn document_path(id: &DocumentId) -> std::path::PathBuf {
fn document_content_path(id: &DocumentId) -> std::path::PathBuf {
let mut path = crate::dirs::app_autosave_documents_dir();
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
path
}
}

#[derive(serde::Serialize, serde::Deserialize)]
struct DocumentInfo {
id: DocumentId,
name: String,
path: Option<std::path::PathBuf>,
is_saved: bool,
}
impl DocumentInfo {
fn new(id: DocumentId, Document { name, path, is_saved, .. }: &Document) -> Self {
Self {
id,
name: name.clone(),
path: path.clone(),
is_saved: *is_saved,
}
}
}
9 changes: 3 additions & 6 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
id: document_id,
document: Document {
content: document,
name: details.name,
path: details.path,
content: document,
is_saved: details.is_saved,
},
});
Expand All @@ -95,11 +95,8 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
// Forward this to update the UI
return Some(FrontendMessage::UpdateOpenDocumentsList { open_documents });
}
FrontendMessage::TriggerLoadFirstAutoSaveDocument => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadCurrentDocument);
}
FrontendMessage::TriggerLoadRestAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadRemainingDocuments);
FrontendMessage::TriggerLoadAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadDocuments);
}
FrontendMessage::TriggerOpenLaunchDocuments => {
dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments);
Expand Down
4 changes: 2 additions & 2 deletions desktop/wrapper/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::PathBuf;

pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;

pub use graphite_editor::messages::frontend::utility_types::{PersistedDocumentInfo, PersistedState};
pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
pub use graphite_editor::messages::prelude::DocumentId;
Expand Down Expand Up @@ -49,8 +50,7 @@ pub enum DesktopFrontendMessage {
PersistenceUpdateCurrentDocument {
id: DocumentId,
},
PersistenceLoadCurrentDocument,
PersistenceLoadRemainingDocuments,
PersistenceLoadDocuments,
PersistenceUpdateDocumentsList {
ids: Vec<DocumentId>,
},
Expand Down
3 changes: 1 addition & 2 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ pub enum FrontendMessage {
document: String,
details: DocumentDetails,
},
TriggerLoadFirstAutoSaveDocument,
TriggerLoadRestAutoSaveDocuments,
TriggerLoadAutoSaveDocuments,
TriggerOpenLaunchDocuments,
TriggerLoadPreferences,
TriggerLoadWorkspaceLayout,
Expand Down
21 changes: 19 additions & 2 deletions editor/src/messages/frontend/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,29 @@ pub struct OpenDocument {
pub struct DocumentDetails {
pub name: String,
pub path: Option<PathBuf>,
#[serde(rename = "isSaved")]
#[serde(alias = "isSaved")]
pub is_saved: bool,
#[serde(rename = "isAutoSaved")]
#[serde(alias = "isAutoSaved")]
pub is_auto_saved: bool,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedDocumentInfo {
pub id: DocumentId,
pub name: String,
#[serde(default)]
pub path: Option<PathBuf>,
pub is_saved: bool,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedState {
pub documents: Vec<PersistedDocumentInfo>,
pub current_document: Option<DocumentId>,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum MouseCursorIcon {
Expand Down
Loading
Loading