Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,225 @@
|
||||
//! File actions, the system file panel, close and quit (FILE-03).
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ely_gpui_component::primitives::Severity;
|
||||
use ely_ui::commands::{CloseTab, Open, Quit, Save, SaveAs};
|
||||
use gpui::{App, AppContext as _, Context, PathPromptOptions, TaskExt as _, Window};
|
||||
|
||||
use crate::{Dialog, Engine, Host, Opening, Saving, Tabs, in_window, text};
|
||||
|
||||
/// What a finished close or save goes on to.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum After {
|
||||
Tab,
|
||||
Window,
|
||||
Quit,
|
||||
}
|
||||
|
||||
pub trait Closing: Host {
|
||||
fn on_open(&mut self, _: &Open, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.pick(false, cx, |host, path, cx| host.open_file(path, cx));
|
||||
}
|
||||
|
||||
fn on_save(&mut self, _: &Save, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.core().active {
|
||||
Some(id) => self.save(id, None, window, cx),
|
||||
None => self.no_document(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_save_as(&mut self, _: &SaveAs, _: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.core().active {
|
||||
Some(id) => self.save_as(id, None, cx),
|
||||
None => self.no_document(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close_tab(&mut self, _: &CloseTab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(id) = self.core().active {
|
||||
self.request_close(id, After::Tab, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_quit(&mut self, _: &Quit, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.proceed(After::Quit, window, cx);
|
||||
}
|
||||
|
||||
fn no_document(&self, cx: &mut Context<Self>) {
|
||||
let text = text(cx, "command.blocked.no_document", &[]);
|
||||
self.toast(text, Severity::Warning, cx);
|
||||
}
|
||||
|
||||
/// A failed system panel is a failure, never a cancel.
|
||||
fn panel_failed(&self, error: &str, cx: &mut App) {
|
||||
let text = text(cx, "office.error.panel", &[("error", error)]);
|
||||
self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
|
||||
/// Asks for one file, or one folder when `folder`.
|
||||
fn pick(
|
||||
&mut self,
|
||||
folder: bool,
|
||||
cx: &mut Context<Self>,
|
||||
then: impl FnOnce(&mut Self, PathBuf, &mut Context<Self>) + 'static,
|
||||
) {
|
||||
let picked = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: !folder,
|
||||
directories: folder,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
let picked = match picked.await {
|
||||
Ok(Ok(paths)) => Ok(paths.and_then(|paths| paths.into_iter().next())),
|
||||
Ok(Err(error)) => Err(error.to_string()),
|
||||
Err(error) => Err(error.to_string()),
|
||||
};
|
||||
in_window(&this, cx, |host, _, cx| match picked {
|
||||
Ok(Some(path)) => then(host, path, cx),
|
||||
Ok(None) => log::info!("office: the file panel was cancelled"),
|
||||
Err(error) => host.panel_failed(&error, cx),
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
/// Closes `id`, asking first when it holds unsaved changes.
|
||||
fn request_close(
|
||||
&mut self,
|
||||
id: u64,
|
||||
after: After,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
log::info!("office: closing tab {id} asked, then {after:?}");
|
||||
if !self.settled(id, cx) {
|
||||
return self.when_settled(id, move |host, window, cx| {
|
||||
host.request_close(id, after, window, cx)
|
||||
});
|
||||
}
|
||||
let Some(tab) = self.tab(id) else {
|
||||
return;
|
||||
};
|
||||
if tab.dirty(cx) {
|
||||
return self.ask_unsaved(id, after, cx);
|
||||
}
|
||||
// The undo history decides; it is read in the background.
|
||||
let edited = Self::Engine::edited(tab.view.read(cx));
|
||||
self.read_in(
|
||||
id,
|
||||
Self::Engine::history,
|
||||
move |host, read, window, cx| {
|
||||
let Some(tab) = host.tab(id) else {
|
||||
return;
|
||||
};
|
||||
if Self::Engine::edited(tab.view.read(cx)) != edited {
|
||||
return host.request_close(id, after, window, cx);
|
||||
}
|
||||
let moved = match read {
|
||||
Ok(history) => history != tab.mark,
|
||||
Err(error) => {
|
||||
log::error!("office: the undo history of tab {id} is unreadable: {error}");
|
||||
true
|
||||
}
|
||||
};
|
||||
if moved || tab.dirty(cx) {
|
||||
return host.ask_unsaved(id, after, cx);
|
||||
}
|
||||
host.close_tab(id, cx);
|
||||
host.proceed(after, window, cx);
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn ask_unsaved(&mut self, id: u64, after: After, cx: &mut Context<Self>) {
|
||||
self.core_mut().dialog = Some(Dialog::Unsaved { tab: id, after });
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Window and quit close each tab, asking for dirty ones.
|
||||
fn proceed(&mut self, after: After, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if after == After::Tab {
|
||||
return;
|
||||
}
|
||||
if let Some(id) = self.core().tabs.first().map(|tab| tab.id) {
|
||||
return self.request_close(id, after, window, cx);
|
||||
}
|
||||
match after {
|
||||
After::Window => window.remove_window(),
|
||||
After::Quit => cx.quit(),
|
||||
After::Tab => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The close button: tabs close in turn, off the window.
|
||||
fn should_close(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
if self.core().tabs.is_empty() {
|
||||
return true;
|
||||
}
|
||||
// Each tab's history is read in the background.
|
||||
self.proceed(After::Window, window, cx);
|
||||
false
|
||||
}
|
||||
|
||||
/// Releases the kernel document, registry entry and scratch folder.
|
||||
fn close_tab(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
let Some(doc) = self.release_tab(id, cx) else {
|
||||
return;
|
||||
};
|
||||
// The kernel lets go in the background.
|
||||
let worker = std::sync::Arc::clone(&self.core().worker);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let closed = cx
|
||||
.background_spawn(async move {
|
||||
Self::Engine::close(&mut worker.lock().expect("the worker lock"), doc)
|
||||
})
|
||||
.await;
|
||||
if let Err(error) = closed
|
||||
&& this
|
||||
.update(cx, |host, cx| host.engine_failed(&error.to_string(), cx))
|
||||
.is_err()
|
||||
{
|
||||
log::error!("office: closing a document failed after its window: {error}");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Drops `id` from the window; its kernel document is returned.
|
||||
fn release_tab(&mut self, id: u64, cx: &mut Context<Self>) -> Option<crate::DocOf<Self>> {
|
||||
let core = self.core_mut();
|
||||
let at = core.tabs.iter().position(|tab| tab.id == id)?;
|
||||
let tab = core.tabs.remove(at);
|
||||
self.tab_closed(&tab, cx);
|
||||
// Its images leave GPUI's atlas next frame (PERF-08).
|
||||
let mut images = Self::Engine::retire(&tab.view, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let freed = crate::in_window(&this, cx, |_, window, _| {
|
||||
window.on_next_frame(move |window, _| images.release_all(window));
|
||||
});
|
||||
if freed.is_err() {
|
||||
log::info!("office: the window went, and its images with it");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
crate::Recovering::forget_tab(self, id, cx);
|
||||
let core = self.core_mut();
|
||||
core.waiting.retain(|(waiting, _)| *waiting != id);
|
||||
if let Some(registry) = tab.registry {
|
||||
core.sessions.close(registry);
|
||||
}
|
||||
if core.active == Some(id) {
|
||||
core.active = core.tabs.last().map(|tab| tab.id);
|
||||
}
|
||||
if let Err(error) = std::fs::remove_dir_all(&tab.work) {
|
||||
log::warn!("office: scratch {} stays: {error}", tab.work.display());
|
||||
}
|
||||
log::info!("office: closed {}", tab.name);
|
||||
cx.notify();
|
||||
Some(tab.doc)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Host> Closing for H {}
|
||||
Reference in new issue
Block a user