diff --git a/src/app.rs b/src/app.rs index ca8d386..2e43653 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,11 +2,14 @@ use std::{ fs, path::PathBuf, sync::{Arc, mpsc}, + thread::JoinHandle, + time::{Duration, Instant}, }; use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender}; use egui::{ Align, Button, Color32, FontData, FontDefinitions, PointerButton, RichText, ScrollArea, Stroke, + Ui, }; #[derive(serde::Deserialize, serde::Serialize)] @@ -18,6 +21,8 @@ pub struct App { actions_tx: mpsc::Sender, #[serde(skip)] actions_rx: mpsc::Receiver, + #[serde(skip)] + jobs: Jobs, tabs: Vec<(TabId, Tab)>, open_tab_index: Option, @@ -25,6 +30,33 @@ pub struct App { next_tab_id: TabId, } +pub struct Jobs { + handles: Vec>, + actions_tx: mpsc::Sender, +} + +impl Jobs { + fn start(&mut self, ui: &mut Ui, job: impl FnOnce() -> Option + Send + 'static) { + let ctx = ui.ctx().clone(); + let actions_tx = self.actions_tx.clone(); + self.handles.push(std::thread::spawn(move || { + // start rendering the spinner thingy + ctx.request_repaint(); + + let start = Instant::now(); + + if let Some(action) = job() { + let _ = actions_tx.send(action); + ctx.request_repaint(); + }; + + // Make sure that task takes at least 250ms to run, so that the spinner won't blink + let sleep_for = Duration::from_millis(250).saturating_sub(start.elapsed()); + std::thread::sleep(sleep_for); + })); + } +} + #[derive(serde::Deserialize, serde::Serialize)] enum Tab { File(FileEditor), @@ -55,8 +87,12 @@ impl Default for App { let (actions_tx, actions_rx) = mpsc::channel(); Self { preferences: Preferences::default(), - actions_tx, + actions_tx: actions_tx.clone(/* this is silly, i know */), actions_rx, + jobs: Jobs { + handles: Default::default(), + actions_tx, + }, tabs: vec![(1, Tab::File(FileEditor::new("note.md")))], open_tab_index: None, next_tab_id: 2, @@ -178,6 +214,8 @@ impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.preferences.apply(ctx); + self.jobs.handles.retain(|job| !job.is_finished()); + while let Ok(action) = self.actions_rx.try_recv() { self.handle_action(action); } @@ -205,22 +243,15 @@ impl eframe::App for App { #[cfg(not(target_arch = "wasm32"))] if ui.button("Open File").clicked() { - let actions_tx = self.actions_tx(ui.ctx()); - std::thread::spawn(move || { - let file = rfd::FileDialog::new().pick_file(); + self.jobs.start(ui, move || { + let file_path = rfd::FileDialog::new().pick_file()?; - let Some(file_path) = file else { return }; - - let text = match fs::read_to_string(&file_path) { - Ok(text) => text, - Err(e) => { - log::error!("Failed to read {file_path:?}: {e}"); - return; - } - }; + let text = fs::read_to_string(&file_path) + .inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}")) + .ok()?; let editor = FileEditor::from_file(file_path, &text); - let _ = actions_tx.send(Action::OpenFile(editor)); + Some(Action::OpenFile(editor)) }); } @@ -255,10 +286,11 @@ impl eframe::App for App { if let Some((file_path, file_editor)) = open_file_with_path { let text = file_editor.to_string(); let file_path = file_path.to_owned(); - std::thread::spawn(move || { + self.jobs.start(ui, move || { if let Err(e) = fs::write(file_path, text.as_bytes()) { log::error!("{e}"); }; + None }); } } @@ -268,21 +300,17 @@ impl eframe::App for App { .add_enabled(open_file.is_some(), Button::new("Save As")) .clicked() { - let actions_tx = self.actions_tx(ui.ctx()); let (tab_id, editor) = open_file.expect("We checked that open_file is_some"); let text = editor.to_string(); - std::thread::spawn(move || { - let Some(file_path) = rfd::FileDialog::new().save_file() else { - return; - }; + self.jobs.start(ui, move || { + let file_path = rfd::FileDialog::new().save_file()?; - if let Err(e) = fs::write(&file_path, text.as_bytes()) { - log::error!("{e}"); - return; - }; + fs::write(&file_path, text.as_bytes()) + .inspect_err(|e| log::error!("{e}")) + .ok()?; - let _ = actions_tx.send(Action::MoveFile(tab_id, file_path)); + Some(Action::MoveFile(tab_id, file_path)) }); } @@ -297,6 +325,10 @@ impl eframe::App for App { } }); + if !self.jobs.handles.is_empty() { + ui.spinner(); + } + ui.add_space(16.0); ui.add_space(16.0);