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:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+225
View File
@@ -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 {}