mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 19:06:47 -06:00
#305: auto-save after a pause, off the UI thread
Nothing is collected while auto-save is off. The wait restarts whenever an unsaved file changes, so continuous editing doesn't save every few seconds, and the file is encoded on a thread of its own, then written.
This commit is contained in:
@@ -301,13 +301,26 @@ fn auto_save_writes_a_changed_file_then_rests() {
|
||||
h.run("shape.rectangle", json!({"x": 500, "y": 300, "width": 40, "height": 40}));
|
||||
assert!(h.frame(vec![]) <= Duration::from_secs_f64(crate::AUTO_SAVE_SECS), "wakes up to save it");
|
||||
assert!(h.app.session.ds().unwrap().is_dirty(), "not saved at once");
|
||||
h.time += crate::AUTO_SAVE_SECS;
|
||||
// Another change restarts the wait.
|
||||
h.time += crate::AUTO_SAVE_SECS - 0.5;
|
||||
h.run("shape.rectangle", json!({"x": 600, "y": 300, "width": 40, "height": 40}));
|
||||
h.frame(vec![]);
|
||||
assert!(!h.app.session.ds().unwrap().is_dirty(), "saved");
|
||||
h.time += 1.0;
|
||||
h.frame(vec![]);
|
||||
assert!(h.app.ui.auto_saving.is_empty() && h.app.session.ds().unwrap().is_dirty(), "waits for a pause in the changes");
|
||||
h.time += crate::AUTO_SAVE_SECS;
|
||||
// The file is encoded on a thread of its own, then written.
|
||||
let start = Instant::now();
|
||||
while h.app.session.ds().unwrap().is_dirty() {
|
||||
assert!(start.elapsed() < Duration::from_secs(20), "saved");
|
||||
let delay = h.frame(vec![]);
|
||||
assert!(h.app.ui.auto_saving.is_empty() || delay <= Duration::from_millis(100), "looks again for the file being encoded");
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
let mut t = Session::empty();
|
||||
t.execute("file.open", &json!({"path": path})).unwrap();
|
||||
let ds = t.ds().unwrap();
|
||||
assert!(ds.doc.page_content_bounds(ds.page).unwrap().x1 >= 540.0, "the new rectangle is in the file");
|
||||
assert!(ds.doc.page_content_bounds(ds.page).unwrap().x1 >= 640.0, "the new rectangles are in the file");
|
||||
h.assert_idle("an auto-saved file");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
+75
-28
@@ -271,11 +271,14 @@ pub enum TitleBar {
|
||||
/// How often open documents get an autosave checkpoint (seconds).
|
||||
pub const AUTOSAVE_SECS: f64 = 30.0 * 60.0;
|
||||
|
||||
/// With auto-save on: how long after a file changes it is saved, and how long a failed save waits
|
||||
/// before trying again (seconds).
|
||||
/// With auto-save on: how long after a file last changed it is saved, and how long a failed save
|
||||
/// waits before trying again (seconds).
|
||||
pub const AUTO_SAVE_SECS: f64 = 3.0;
|
||||
const AUTO_SAVE_RETRY_SECS: f64 = 60.0;
|
||||
|
||||
/// A file being encoded for auto-save: its snapshot and, once done, its bytes.
|
||||
pub type AutoSaveEncoding = std::thread::JoinHandle<(figcraft_engine::cmd::file::FileSnapshot, figcraft_engine::Result<Vec<u8>>)>;
|
||||
|
||||
/// How long a toast shows (seconds).
|
||||
const TOAST_SECS: f64 = 3.0;
|
||||
|
||||
@@ -411,8 +414,11 @@ pub struct UiState {
|
||||
pub last_copy: Option<String>,
|
||||
/// When (UI clock, seconds) open documents next get an autosave checkpoint.
|
||||
pub next_autosave: f64,
|
||||
/// With auto-save on: when (UI clock, seconds) files with unsaved changes are next saved.
|
||||
pub auto_save_at: Option<f64>,
|
||||
/// With auto-save on: when (UI clock, seconds) files with unsaved changes are next saved, and
|
||||
/// the revisions of those files it waits on (a change restarts the wait).
|
||||
pub auto_save_at: Option<(f64, u64)>,
|
||||
/// Files being encoded for auto-save on threads of their own (tab `uid`).
|
||||
pub auto_saving: Vec<(u64, AutoSaveEncoding)>,
|
||||
/// The Assets panel shows the libraries view (publish this file, enabled libraries).
|
||||
pub libraries: bool,
|
||||
/// Keyboard navigation: the region with the keyboard, keyboard mode (F6, Tab).
|
||||
@@ -498,6 +504,7 @@ impl Default for UiState {
|
||||
last_copy: None,
|
||||
next_autosave: 0.0,
|
||||
auto_save_at: None,
|
||||
auto_saving: Vec::new(),
|
||||
libraries: false,
|
||||
nav: a11y::Nav::default(),
|
||||
plugins_window: false,
|
||||
@@ -961,39 +968,79 @@ impl FigApp {
|
||||
repaint_in(ctx, AUTOSAVE_SECS);
|
||||
}
|
||||
|
||||
/// With auto-save on, files that have a path are saved [`AUTO_SAVE_SECS`] after they change
|
||||
/// (not during a gesture, a live preview or while a version is shown).
|
||||
/// With auto-save on, files that have a path are saved [`AUTO_SAVE_SECS`] after they last
|
||||
/// changed (not during a gesture, a live preview or while a version is shown). The file is
|
||||
/// encoded on a thread of its own, so a big one doesn't hold up the window, and written here.
|
||||
fn auto_save_tick(&mut self, ctx: &egui::Context) {
|
||||
let s = &self.session;
|
||||
let unsaved = |d: &figcraft_engine::DocState| d.path.is_some() && d.branch_of.is_none() && d.preview.is_none() && d.is_dirty();
|
||||
let due: Vec<usize> = s.docs.iter().enumerate().filter(|(_, d)| unsaved(d)).map(|(i, _)| i).collect();
|
||||
if !s.prefs.auto_save || due.is_empty() {
|
||||
self.ui.auto_save_at = None;
|
||||
return;
|
||||
}
|
||||
let now = ctx.input(|i| i.time);
|
||||
let at = *self.ui.auto_save_at.get_or_insert(now + AUTO_SAVE_SECS);
|
||||
if now < at || s.docs.iter().any(|d| d.interaction.is_some() || d.text_preview.is_some()) {
|
||||
// Due but busy: look again in a second.
|
||||
repaint_in(ctx, if now < at { at - now } else { 1.0 });
|
||||
return;
|
||||
}
|
||||
let active = self.session.active;
|
||||
let mut failed = None;
|
||||
for i in due {
|
||||
self.session.active = i;
|
||||
if let Err(e) = self.session.execute("file.save", &json!({})) {
|
||||
failed.get_or_insert(e.to_string());
|
||||
let mut failed = self.auto_save_done();
|
||||
// The browser has no files to save to.
|
||||
let on = !cfg!(target_arch = "wasm32") && self.session.prefs.auto_save;
|
||||
let saving = &self.ui.auto_saving;
|
||||
let due = |d: &&figcraft_engine::DocState| {
|
||||
d.path.is_some() && d.branch_of.is_none() && d.preview.is_none() && d.is_dirty() && !saving.iter().any(|(uid, _)| *uid == d.uid)
|
||||
};
|
||||
let changes = if on { self.session.docs.iter().filter(due).map(|d| d.revision).reduce(u64::wrapping_add) } else { None };
|
||||
match changes {
|
||||
None => self.ui.auto_save_at = None,
|
||||
Some(changes) => {
|
||||
let at = match self.ui.auto_save_at {
|
||||
Some((at, c)) if c == changes && failed.is_none() => at,
|
||||
_ => now + if failed.is_some() { AUTO_SAVE_RETRY_SECS } else { AUTO_SAVE_SECS },
|
||||
};
|
||||
self.ui.auto_save_at = Some((at, changes));
|
||||
if now < at || self.session.docs.iter().any(|d| d.interaction.is_some() || d.text_preview.is_some()) {
|
||||
// Due but busy: look again in a second.
|
||||
repaint_in(ctx, if now < at { at - now } else { 1.0 });
|
||||
} else {
|
||||
self.ui.auto_save_at = None;
|
||||
let files: Vec<_> =
|
||||
self.session.docs.iter().filter(due).map(|d| (d.uid, figcraft_engine::cmd::file::FileSnapshot::of(d))).collect();
|
||||
for (uid, file) in files {
|
||||
let encode = move || {
|
||||
let bytes = file.encode();
|
||||
(file, bytes)
|
||||
};
|
||||
match std::thread::Builder::new().name("figcraft-save".into()).spawn(encode) {
|
||||
Ok(h) => self.ui.auto_saving.push((uid, h)),
|
||||
Err(e) => {
|
||||
failed.get_or_insert(e.to_string());
|
||||
self.ui.auto_save_at = Some((now + AUTO_SAVE_RETRY_SECS, changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.session.active = active;
|
||||
if !self.ui.auto_saving.is_empty() {
|
||||
// Look again shortly for the files being encoded.
|
||||
repaint_in(ctx, 0.1);
|
||||
}
|
||||
if let Some(e) = failed {
|
||||
self.ui.auto_save_at = Some(now + AUTO_SAVE_RETRY_SECS);
|
||||
repaint_in(ctx, AUTO_SAVE_RETRY_SECS);
|
||||
self.toast(i18n::trf("Couldn't save automatically: {error}", &[("error", &e)]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the files whose auto-save encoding finished; the first error, if one failed.
|
||||
fn auto_save_done(&mut self) -> Option<String> {
|
||||
if self.ui.auto_saving.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut failed = None;
|
||||
let done: Vec<_> = self.ui.auto_saving.extract_if(.., |(_, h)| h.is_finished()).collect();
|
||||
for (_, h) in done {
|
||||
let saved = match h.join() {
|
||||
Ok((snapshot, Ok(bytes))) => snapshot.save(&mut self.session, &bytes).map(|_| ()).map_err(|e| e.to_string()),
|
||||
Ok((_, Err(e))) => Err(e.to_string()),
|
||||
Err(_) => Err("the file couldn't be encoded".to_string()),
|
||||
};
|
||||
if let Err(e) = saved {
|
||||
failed.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
failed
|
||||
}
|
||||
|
||||
/// Draw the whole UI: the main window (the other windows draw themselves).
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
Reference in New Issue
Block a user