Skip to content

Commit 4261b7d

Browse files
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 <[email protected]>
1 parent 1808bea commit 4261b7d

File tree

11 files changed

+386
-20
lines changed

11 files changed

+386
-20
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ vello = { workspace = true }
4141
derivative = { workspace = true }
4242
rfd = { workspace = true }
4343
open = { workspace = true }
44+
serde = { workspace = true }
4445

4546
# Hardware acceleration dependencies
4647
ash = { version = "0.38", optional = true }

desktop/src/app.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::CustomEvent;
22
use crate::cef::WindowSize;
33
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
4+
use crate::persist::PersistentData;
45
use crate::render::GraphicsState;
56
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform};
67
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
@@ -37,6 +38,7 @@ pub(crate) struct WinitApp {
3738
start_render_sender: SyncSender<()>,
3839
web_communication_initialized: bool,
3940
web_communication_startup_buffer: Vec<Vec<u8>>,
41+
persistent_data: PersistentData,
4042
}
4143

4244
impl WinitApp {
@@ -51,6 +53,9 @@ impl WinitApp {
5153
}
5254
});
5355

56+
let mut persistent_data = PersistentData::default();
57+
persistent_data.load_from_disk();
58+
5459
Self {
5560
cef_context,
5661
window: None,
@@ -65,6 +70,7 @@ impl WinitApp {
6570
start_render_sender,
6671
web_communication_initialized: false,
6772
web_communication_startup_buffer: Vec::new(),
73+
persistent_data,
6874
}
6975
}
7076

@@ -161,6 +167,53 @@ impl WinitApp {
161167
DesktopFrontendMessage::CloseWindow => {
162168
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
163169
}
170+
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
171+
self.persistent_data.write_document(id, document);
172+
}
173+
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
174+
self.persistent_data.delete_document(&id);
175+
}
176+
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
177+
self.persistent_data.set_current_document(id);
178+
}
179+
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
180+
self.persistent_data.set_document_order(ids);
181+
}
182+
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
183+
if let Some((id, document)) = self.persistent_data.current_document() {
184+
let message = DesktopWrapperMessage::LoadDocument {
185+
id,
186+
document,
187+
to_front: false,
188+
select_after_open: true,
189+
};
190+
self.dispatch_desktop_wrapper_message(message);
191+
}
192+
}
193+
DesktopFrontendMessage::PersistenceLoadRemainingDocuments => {
194+
for (id, document) in self.persistent_data.documents_before_current().into_iter().rev() {
195+
let message = DesktopWrapperMessage::LoadDocument {
196+
id,
197+
document,
198+
to_front: true,
199+
select_after_open: false,
200+
};
201+
self.dispatch_desktop_wrapper_message(message);
202+
}
203+
for (id, document) in self.persistent_data.documents_after_current() {
204+
let message = DesktopWrapperMessage::LoadDocument {
205+
id,
206+
document,
207+
to_front: false,
208+
select_after_open: false,
209+
};
210+
self.dispatch_desktop_wrapper_message(message);
211+
}
212+
if let Some(id) = self.persistent_data.current_document_id() {
213+
let message = DesktopWrapperMessage::SelectDocument { id };
214+
self.dispatch_desktop_wrapper_message(message);
215+
}
216+
}
164217
}
165218
}
166219

@@ -307,7 +360,7 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
307360
}
308361
WindowEvent::RedrawRequested => {
309362
let Some(ref mut graphics_state) = self.graphics_state else { return };
310-
// Only rerender once we have a new ui texture to display
363+
// Only rerender once we have a new UI texture to display
311364
if let Some(window) = &self.window {
312365
match graphics_state.render(window.as_ref()) {
313366
Ok(_) => {}

desktop/src/consts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub(crate) static APP_NAME: &str = "Graphite";
22
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
33
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
4+
pub(crate) static APP_AUTOSAVE_DIRECTORY_NAME: &str = "documents";
45

56
// CEF configuration constants
67
pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60;

desktop/src/dirs.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::fs::create_dir_all;
22
use std::path::PathBuf;
33

4-
use crate::consts::APP_DIRECTORY_NAME;
4+
use crate::consts::{APP_AUTOSAVE_DIRECTORY_NAME, APP_DIRECTORY_NAME};
55

66
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
77
if !path.exists() {
@@ -14,3 +14,9 @@ pub(crate) fn graphite_data_dir() -> PathBuf {
1414
ensure_dir_exists(&path);
1515
path
1616
}
17+
18+
pub(crate) fn graphite_autosave_documents_dir() -> PathBuf {
19+
let path = graphite_data_dir().join(APP_AUTOSAVE_DIRECTORY_NAME);
20+
ensure_dir_exists(&path);
21+
path
22+
}

desktop/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod app;
1515
use app::WinitApp;
1616

1717
mod dirs;
18+
mod persist;
1819

1920
use graphite_desktop_wrapper::messages::DesktopWrapperMessage;
2021
use graphite_desktop_wrapper::{NodeGraphExecutionResult, WgpuContext};

desktop/src/persist.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use graphite_desktop_wrapper::messages::{Document, DocumentId};
2+
3+
#[derive(Default, serde::Serialize, serde::Deserialize)]
4+
pub(crate) struct PersistentData {
5+
documents: DocumentStore,
6+
current_document: Option<DocumentId>,
7+
#[serde(skip)]
8+
document_order: Option<Vec<DocumentId>>,
9+
}
10+
11+
impl PersistentData {
12+
pub(crate) fn write_document(&mut self, id: DocumentId, document: Document) {
13+
self.documents.write(id, document);
14+
if let Some(order) = &self.document_order {
15+
self.documents.force_order(order.clone());
16+
}
17+
self.flush();
18+
}
19+
20+
pub(crate) fn delete_document(&mut self, id: &DocumentId) {
21+
if Some(*id) == self.current_document {
22+
self.current_document = None;
23+
}
24+
self.documents.delete(id);
25+
self.flush();
26+
}
27+
28+
pub(crate) fn current_document_id(&self) -> Option<DocumentId> {
29+
match self.current_document {
30+
Some(id) => Some(id),
31+
None => Some(*self.documents.document_ids().first()?),
32+
}
33+
}
34+
35+
pub(crate) fn current_document(&self) -> Option<(DocumentId, Document)> {
36+
let current_id = self.current_document_id()?;
37+
Some((current_id, self.documents.read(&current_id)?))
38+
}
39+
40+
pub(crate) fn documents_before_current(&self) -> Vec<(DocumentId, Document)> {
41+
let Some(current_id) = self.current_document_id() else {
42+
return Vec::new();
43+
};
44+
self.documents
45+
.document_ids()
46+
.into_iter()
47+
.take_while(|id| *id != current_id)
48+
.filter_map(|id| Some((id, self.documents.read(&id)?)))
49+
.collect()
50+
}
51+
52+
pub(crate) fn documents_after_current(&self) -> Vec<(DocumentId, Document)> {
53+
let Some(current_id) = self.current_document_id() else {
54+
return Vec::new();
55+
};
56+
self.documents
57+
.document_ids()
58+
.into_iter()
59+
.skip_while(|id| *id != current_id)
60+
.skip(1)
61+
.filter_map(|id| Some((id, self.documents.read(&id)?)))
62+
.collect()
63+
}
64+
65+
pub(crate) fn set_current_document(&mut self, id: DocumentId) {
66+
self.current_document = Some(id);
67+
self.flush();
68+
}
69+
70+
pub(crate) fn set_document_order(&mut self, order: Vec<DocumentId>) {
71+
self.document_order = Some(order);
72+
self.flush();
73+
}
74+
75+
fn flush(&self) {
76+
let data = match ron::to_string(self) {
77+
Ok(d) => d,
78+
Err(e) => {
79+
tracing::error!("Failed to serialize persistent data: {e}");
80+
return;
81+
}
82+
};
83+
if let Err(e) = std::fs::write(Self::persistence_file_path(), data) {
84+
tracing::error!("Failed to write persistent data to disk: {e}");
85+
}
86+
}
87+
88+
pub(crate) fn load_from_disk(&mut self) {
89+
let path = Self::persistence_file_path();
90+
let data = match std::fs::read_to_string(&path) {
91+
Ok(d) => d,
92+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
93+
tracing::info!("No persistent data file found at {path:?}, starting fresh");
94+
return;
95+
}
96+
Err(e) => {
97+
tracing::error!("Failed to read persistent data from disk: {e}");
98+
return;
99+
}
100+
};
101+
let loaded = match ron::from_str(&data) {
102+
Ok(d) => d,
103+
Err(e) => {
104+
tracing::error!("Failed to deserialize persistent data: {e}");
105+
return;
106+
}
107+
};
108+
*self = loaded;
109+
}
110+
111+
fn persistence_file_path() -> std::path::PathBuf {
112+
let mut path = crate::dirs::graphite_data_dir();
113+
path.push(format!("{}.ron", crate::consts::APP_AUTOSAVE_DIRECTORY_NAME));
114+
path
115+
}
116+
}
117+
118+
#[derive(Default, serde::Serialize, serde::Deserialize)]
119+
struct DocumentStore(Vec<DocumentInfo>);
120+
impl DocumentStore {
121+
fn write(&mut self, id: DocumentId, document: Document) {
122+
let meta = DocumentInfo::new(id, &document);
123+
if let Some(existing) = self.0.iter_mut().find(|meta| meta.id == id) {
124+
*existing = meta;
125+
} else {
126+
self.0.push(meta);
127+
}
128+
if let Err(e) = std::fs::write(Self::document_path(&id), document.content) {
129+
tracing::error!("Failed to write document {id:?} to disk: {e}");
130+
}
131+
}
132+
133+
fn delete(&mut self, id: &DocumentId) {
134+
self.0.retain(|meta| meta.id != *id);
135+
if let Err(e) = std::fs::remove_file(Self::document_path(id)) {
136+
tracing::error!("Failed to delete document {id:?} from disk: {e}");
137+
}
138+
}
139+
140+
fn read(&self, id: &DocumentId) -> Option<Document> {
141+
let meta = self.0.iter().find(|meta| meta.id == *id)?;
142+
let content = std::fs::read_to_string(Self::document_path(id)).ok()?;
143+
Some(Document {
144+
content,
145+
name: meta.name.clone(),
146+
path: meta.path.clone(),
147+
is_saved: meta.is_saved,
148+
})
149+
}
150+
151+
fn force_order(&mut self, desired_order: Vec<DocumentId>) {
152+
let mut ordered_prefix_len = 0;
153+
for id in desired_order {
154+
if let Some(offset) = self.0[ordered_prefix_len..].iter().position(|meta| meta.id == id) {
155+
let found_index = ordered_prefix_len + offset;
156+
if found_index != ordered_prefix_len {
157+
self.0[ordered_prefix_len..=found_index].rotate_right(1);
158+
}
159+
ordered_prefix_len += 1;
160+
}
161+
}
162+
self.0.truncate(ordered_prefix_len);
163+
}
164+
165+
fn document_ids(&self) -> Vec<DocumentId> {
166+
self.0.iter().map(|meta| meta.id).collect()
167+
}
168+
169+
fn document_path(id: &DocumentId) -> std::path::PathBuf {
170+
let mut path = crate::dirs::graphite_autosave_documents_dir();
171+
path.push(format!("{:x}.graphite", id.0));
172+
path
173+
}
174+
}
175+
176+
#[derive(serde::Serialize, serde::Deserialize)]
177+
struct DocumentInfo {
178+
id: DocumentId,
179+
name: String,
180+
path: Option<std::path::PathBuf>,
181+
is_saved: bool,
182+
}
183+
impl DocumentInfo {
184+
fn new(id: DocumentId, Document { name, path, is_saved, .. }: &Document) -> Self {
185+
Self {
186+
id,
187+
name: name.clone(),
188+
path: path.clone(),
189+
is_saved: *is_saved,
190+
}
191+
}
192+
}

desktop/wrapper/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ dirs = { workspace = true }
3030
ron = { workspace = true}
3131
vello = { workspace = true }
3232
image = { workspace = true }
33+
serde = { workspace = true }

desktop/wrapper/src/handle_desktop_wrapper_message.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,27 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
118118
let message = AppWindowMessage::AppWindowUpdatePlatform { platform };
119119
dispatcher.queue_editor_message(message.into());
120120
}
121+
DesktopWrapperMessage::LoadDocument {
122+
id,
123+
document,
124+
to_front,
125+
select_after_open,
126+
} => {
127+
let message = PortfolioMessage::OpenDocumentFileWithId {
128+
document_id: id,
129+
document_name: Some(document.name),
130+
document_path: document.path,
131+
document_serialized_content: document.content,
132+
document_is_auto_saved: true,
133+
document_is_saved: document.is_saved,
134+
to_front,
135+
select_after_open,
136+
};
137+
dispatcher.queue_editor_message(message.into());
138+
}
139+
DesktopWrapperMessage::SelectDocument { id } => {
140+
let message = PortfolioMessage::SelectDocument { document_id: id };
141+
dispatcher.queue_editor_message(message.into());
142+
}
121143
}
122144
}

0 commit comments

Comments
 (0)