Desktop: Move autosave persistence to native (#3134)

* Move autosave persistence to native 1

* Move autosave persistence to native 2

* Reimplement quirky behavior of the web frontend

* Code revew

* Use select_after_open

* fix fmt

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon
2025-09-09 11:27:54 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 1808bea2cf
commit 4261b7dad1
11 changed files with 387 additions and 21 deletions
+54 -1
View File
@@ -1,6 +1,7 @@
use crate::CustomEvent;
use crate::cef::WindowSize;
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
use crate::persist::PersistentData;
use crate::render::GraphicsState;
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform};
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
@@ -37,6 +38,7 @@ pub(crate) struct WinitApp {
start_render_sender: SyncSender<()>,
web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>,
persistent_data: PersistentData,
}
impl WinitApp {
@@ -51,6 +53,9 @@ impl WinitApp {
}
});
let mut persistent_data = PersistentData::default();
persistent_data.load_from_disk();
Self {
cef_context,
window: None,
@@ -65,6 +70,7 @@ impl WinitApp {
start_render_sender,
web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(),
persistent_data,
}
}
@@ -161,6 +167,53 @@ impl WinitApp {
DesktopFrontendMessage::CloseWindow => {
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
}
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
self.persistent_data.write_document(id, document);
}
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
self.persistent_data.delete_document(&id);
}
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
self.persistent_data.set_current_document(id);
}
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
self.persistent_data.set_document_order(ids);
}
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,
};
self.dispatch_desktop_wrapper_message(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,
};
self.dispatch_desktop_wrapper_message(message);
}
for (id, document) in self.persistent_data.documents_after_current() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: false,
};
self.dispatch_desktop_wrapper_message(message);
}
if let Some(id) = self.persistent_data.current_document_id() {
let message = DesktopWrapperMessage::SelectDocument { id };
self.dispatch_desktop_wrapper_message(message);
}
}
}
}
@@ -307,7 +360,7 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
}
WindowEvent::RedrawRequested => {
let Some(ref mut graphics_state) = self.graphics_state else { return };
// Only rerender once we have a new ui texture to display
// Only rerender once we have a new UI texture to display
if let Some(window) = &self.window {
match graphics_state.render(window.as_ref()) {
Ok(_) => {}
+1
View File
@@ -1,6 +1,7 @@
pub(crate) static APP_NAME: &str = "Graphite";
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
pub(crate) static APP_AUTOSAVE_DIRECTORY_NAME: &str = "documents";
// CEF configuration constants
pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60;
+7 -1
View File
@@ -1,7 +1,7 @@
use std::fs::create_dir_all;
use std::path::PathBuf;
use crate::consts::APP_DIRECTORY_NAME;
use crate::consts::{APP_AUTOSAVE_DIRECTORY_NAME, APP_DIRECTORY_NAME};
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
if !path.exists() {
@@ -14,3 +14,9 @@ pub(crate) fn graphite_data_dir() -> PathBuf {
ensure_dir_exists(&path);
path
}
pub(crate) fn graphite_autosave_documents_dir() -> PathBuf {
let path = graphite_data_dir().join(APP_AUTOSAVE_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
+1
View File
@@ -15,6 +15,7 @@ mod app;
use app::WinitApp;
mod dirs;
mod persist;
use graphite_desktop_wrapper::messages::DesktopWrapperMessage;
use graphite_desktop_wrapper::{NodeGraphExecutionResult, WgpuContext};
+192
View File
@@ -0,0 +1,192 @@
use graphite_desktop_wrapper::messages::{Document, DocumentId};
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct PersistentData {
documents: DocumentStore,
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);
if let Some(order) = &self.document_order {
self.documents.force_order(order.clone());
}
self.flush();
}
pub(crate) fn delete_document(&mut self, id: &DocumentId) {
if Some(*id) == self.current_document {
self.current_document = None;
}
self.documents.delete(id);
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()?),
}
}
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 set_current_document(&mut self, id: DocumentId) {
self.current_document = Some(id);
self.flush();
}
pub(crate) fn set_document_order(&mut self, order: Vec<DocumentId>) {
self.document_order = Some(order);
self.flush();
}
fn flush(&self) {
let data = match ron::to_string(self) {
Ok(d) => d,
Err(e) => {
tracing::error!("Failed to serialize persistent data: {e}");
return;
}
};
if let Err(e) = std::fs::write(Self::persistence_file_path(), data) {
tracing::error!("Failed to write persistent data to disk: {e}");
}
}
pub(crate) fn load_from_disk(&mut self) {
let path = Self::persistence_file_path();
let data = match std::fs::read_to_string(&path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("No persistent data file found at {path:?}, starting fresh");
return;
}
Err(e) => {
tracing::error!("Failed to read persistent data from disk: {e}");
return;
}
};
let loaded = match ron::from_str(&data) {
Ok(d) => d,
Err(e) => {
tracing::error!("Failed to deserialize persistent data: {e}");
return;
}
};
*self = loaded;
}
fn persistence_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::graphite_data_dir();
path.push(format!("{}.ron", crate::consts::APP_AUTOSAVE_DIRECTORY_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}");
}
}
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}");
}
}
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,
})
}
fn force_order(&mut self, desired_order: Vec<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);
}
ordered_prefix_len += 1;
}
}
self.0.truncate(ordered_prefix_len);
}
fn document_ids(&self) -> Vec<DocumentId> {
self.0.iter().map(|meta| meta.id).collect()
}
fn document_path(id: &DocumentId) -> std::path::PathBuf {
let mut path = crate::dirs::graphite_autosave_documents_dir();
path.push(format!("{:x}.graphite", id.0));
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,
}
}
}