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,269 @@
|
||||
//! PDFs in the shared shell, one worker behind (PDF-01).
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use ely_gpui_component::{
|
||||
feedback::EmptyState,
|
||||
primitives::IconName,
|
||||
shell::{StatusBar, StatusBarItem},
|
||||
};
|
||||
use ely_office::{Core, Host, Kind, prelude::*};
|
||||
use ely_platform::AppId;
|
||||
use ely_ui::{Root, Shell, commands};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Window, div,
|
||||
};
|
||||
|
||||
use crate::engine::{Mupdf, Pdf};
|
||||
use crate::view::PdfView;
|
||||
|
||||
pub struct Viewer {
|
||||
pub core: Core<Viewer>,
|
||||
pub side: Entity<crate::side::Side>,
|
||||
/// Crop or split, while asked.
|
||||
pub ask: Option<crate::ask::Ask>,
|
||||
pub edit_panel: Entity<crate::editing::EditPanel>,
|
||||
pub note_panel: Entity<crate::notes::NotePanel>,
|
||||
/// A layout change shown before it is made.
|
||||
pub preview: Option<crate::editing::Preview>,
|
||||
/// Preview pictures no longer drawn, freed next render.
|
||||
pub retired: ely_gpui_component::primitives::Retired,
|
||||
pub find: Option<crate::search::Find>,
|
||||
/// A bookmark title being asked.
|
||||
pub naming: Option<crate::marks::Naming>,
|
||||
pub props: Option<crate::props::Props>,
|
||||
pub ocr: Option<crate::ocr::Ocr>,
|
||||
/// OCR runs started so far.
|
||||
pub ocr_runs: u64,
|
||||
pub removal: Option<crate::redact::Removal>,
|
||||
pub secure: Option<crate::protect::Secure>,
|
||||
pub signer: Option<crate::sign::Signer>,
|
||||
pub making: Option<crate::visible::Making>,
|
||||
pub signs_open: bool,
|
||||
pub exporting: Option<crate::output::Exporting>,
|
||||
pub repair_open: bool,
|
||||
/// Find boxes opened so far.
|
||||
pub finds: u64,
|
||||
}
|
||||
|
||||
impl Host for Viewer {
|
||||
type Extra = ();
|
||||
type Engine = Pdf;
|
||||
const KIND: Kind = Kind {
|
||||
app: AppId::Pdf,
|
||||
kept: &["pdf"],
|
||||
save_as: &["pdf"],
|
||||
macros: None,
|
||||
};
|
||||
|
||||
fn core(&self) -> &Core<Self> {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core<Self> {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn read_at(&self, tab: u64, cx: &gpui::App) -> Option<ely_pdf_engine::Doc> {
|
||||
self.tab(tab).map(|tab| tab.view.read(cx).doc.clone())
|
||||
}
|
||||
|
||||
fn observed(&mut self, id: u64, view: &Entity<PdfView>, cx: &mut Context<Self>) {
|
||||
// Damage found while drawing: no overwrite from now on.
|
||||
if view.read(cx).damage_seen {
|
||||
view.update(cx, |view, _| view.damage_seen = false);
|
||||
if let Some(tab) = self.tab_mut(id) {
|
||||
log::warn!("pdf: tab {id} turned out damaged");
|
||||
tab.risk = Some(ely_office::Risk::Repaired);
|
||||
}
|
||||
}
|
||||
// Words found before an edit are searched again.
|
||||
let version = view.read(cx).version;
|
||||
if self.find.as_ref().is_some_and(|find| find.tab == id && find.version != version) {
|
||||
self.search(cx);
|
||||
}
|
||||
// A preview from before an edit is void.
|
||||
if self.preview.as_ref().is_some_and(|preview| preview.tab == id && preview.version != version) {
|
||||
self.end_preview();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// The window root with no document.
|
||||
pub fn open(app: AppId, window: &mut Window, cx: &mut App) -> Entity<Root> {
|
||||
crate::keys::bind(cx);
|
||||
let work = ely_platform::data_dir(app)
|
||||
.map_err(std::io::Error::other)
|
||||
.and_then(|data| ely_office::fresh_work(&data));
|
||||
let work = match work {
|
||||
Ok(work) => work,
|
||||
Err(error) => ely_platform::startup::fail(app, &format!("{error}")),
|
||||
};
|
||||
// The worker starts with the first file (PERF-01).
|
||||
let viewer = cx.new(|cx| {
|
||||
let mut host = Viewer::new(Arc::default(), work, cx);
|
||||
// Recovery points, crash return, autosave (§6.2, §6.3).
|
||||
ely_office::Recovering::start_recovery(&mut host, cx);
|
||||
<Viewer as ely_office::Autosaving>::start_autosave(cx);
|
||||
host
|
||||
});
|
||||
let root = ely_ui::document_root(app, viewer.clone().into(), &commands::FILE, crate::keys::menus(cx), window, cx);
|
||||
viewer.update(cx, |viewer, cx| {
|
||||
viewer.core.root = root.downgrade();
|
||||
viewer.core.window = Some(window.window_handle());
|
||||
window.focus(&viewer.core.focus, cx);
|
||||
});
|
||||
for path in ely_ui::launch_files(cx) {
|
||||
viewer.update(cx, |viewer, cx| viewer.open_file(path, cx));
|
||||
}
|
||||
let opening = viewer.clone();
|
||||
ely_ui::on_open(cx, move |path, cx| {
|
||||
opening.update(cx, |viewer, cx| viewer.open_file(path, cx))
|
||||
});
|
||||
let closing = viewer.clone();
|
||||
window.on_window_should_close(cx, move |window, cx| {
|
||||
closing.update(cx, |viewer, cx| viewer.should_close(window, cx))
|
||||
});
|
||||
root
|
||||
}
|
||||
|
||||
impl Viewer {
|
||||
pub fn new(worker: Arc<Mutex<Mupdf>>, work: PathBuf, cx: &mut Context<Self>) -> Self {
|
||||
let (this, weak) = (cx.entity(), cx.weak_entity());
|
||||
Viewer {
|
||||
core: Core::new(worker, work, cx),
|
||||
side: cx.new(|cx| crate::side::Side::new(&this, cx)),
|
||||
ask: None,
|
||||
edit_panel: cx.new(|_| crate::editing::EditPanel::new(weak.clone())),
|
||||
note_panel: cx.new(|_| crate::notes::NotePanel::new(weak.clone())),
|
||||
preview: None,
|
||||
retired: Default::default(),
|
||||
find: None,
|
||||
naming: None,
|
||||
props: None,
|
||||
ocr: None,
|
||||
ocr_runs: 0,
|
||||
removal: None,
|
||||
secure: None,
|
||||
signer: None,
|
||||
making: None,
|
||||
signs_open: false,
|
||||
exporting: None,
|
||||
repair_open: false,
|
||||
finds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(cx: &App, key: &str, args: &[(&str, &str)]) -> SharedString {
|
||||
ely_ui::i18n::text(cx, key, args)
|
||||
}
|
||||
|
||||
/// The active tab's view.
|
||||
pub fn active_view(&self) -> Option<Entity<PdfView>> {
|
||||
self.active_tab().map(|tab| tab.view.clone())
|
||||
}
|
||||
|
||||
/// Runs `change` on the active view.
|
||||
pub fn on_view(&self, change: impl FnOnce(&mut PdfView, &mut Context<PdfView>), cx: &mut App) {
|
||||
if let Some(view) = self.active_view() {
|
||||
view.update(cx, change);
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks for files; each opens in its own tab.
|
||||
pub fn open_picked(&mut self, _: &commands::Open, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let picked = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
let picked = match picked.await {
|
||||
Ok(Ok(paths)) => Ok(paths),
|
||||
Ok(Err(error)) => Err(error.to_string()),
|
||||
Err(error) => Err(error.to_string()),
|
||||
};
|
||||
let done = this.update(cx, |viewer, cx| match picked {
|
||||
Ok(Some(paths)) => paths.into_iter().for_each(|path| viewer.open_file(path, cx)),
|
||||
Ok(None) => log::info!("pdf: the open panel was cancelled"),
|
||||
Err(error) => viewer.panel_failed(&error, cx),
|
||||
});
|
||||
if done.is_err() {
|
||||
log::info!("pdf: the viewer closed before the panel");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn status(&self, cx: &mut Context<Self>) -> StatusBar {
|
||||
let state = StatusBarItem::new("pdf-state").label(self.save_state(cx));
|
||||
let Some(view) = self.active_view() else {
|
||||
return StatusBar::new().left(state);
|
||||
};
|
||||
let view = view.read(cx);
|
||||
let (n, count) = ((view.current + 1).to_string(), view.sizes.len().to_string());
|
||||
let page = Self::text(cx, "pdf.status.page", &[("n", &n), ("count", &count)]);
|
||||
StatusBar::new().left(state).left(StatusBarItem::new("pdf-page").label(page))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Viewer {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.retired.release(window);
|
||||
let title = self.active_tab().map_or_else(|| AppId::Pdf.name().into(), |tab| tab.name.clone());
|
||||
let body = match self.active_view() {
|
||||
Some(view) => view.into_any_element(),
|
||||
None => EmptyState::new("pdf-empty", IconName::File, Self::text(cx, "command.blocked.no_document", &[])).into_any_element(),
|
||||
};
|
||||
let shell = Shell::new(title, &self.core.focus)
|
||||
.tabs(self.tab_strip(cx))
|
||||
.left(self.active_view().map(|_| self.side.clone().into()))
|
||||
.right(self.active_view().and_then(|view| view.read(cx).panel_aim()).map(|aim| match aim {
|
||||
crate::content::Aim::Note(_) => self.note_panel.clone().into(),
|
||||
_ => self.edit_panel.clone().into(),
|
||||
}))
|
||||
.status(self.status(cx))
|
||||
.children(self.file_banner(cx))
|
||||
.children(self.find_bar(cx))
|
||||
.child(body);
|
||||
// Esc leaves a tool from pages and panel alike.
|
||||
let editing = self.active_view().is_some_and(|view| view.read(cx).tool != crate::content::Tool::Read);
|
||||
let context = match editing {
|
||||
true => format!("{} {}", commands::CONTEXT, crate::writes::EDITING),
|
||||
false => commands::CONTEXT.to_string(),
|
||||
};
|
||||
let root = div()
|
||||
.size_full()
|
||||
.key_context(context.as_str())
|
||||
.track_focus(&self.core.focus)
|
||||
.on_action(cx.listener(Self::open_picked))
|
||||
.on_action(cx.listener(Self::on_save))
|
||||
.on_action(cx.listener(Self::on_save_as))
|
||||
.on_action(cx.listener(Self::on_export))
|
||||
.on_action(cx.listener(Self::on_print))
|
||||
.on_action(cx.listener(Self::on_quit))
|
||||
.on_action(cx.listener(Self::on_close_tab))
|
||||
.child(shell)
|
||||
.children(self.core_dialog(cx))
|
||||
.children(self.ask_element(cx))
|
||||
.children(self.preview_element(cx))
|
||||
.children(self.naming_element(cx))
|
||||
.children(self.props_element(cx))
|
||||
.children(self.ocr_element(cx))
|
||||
.children(self.removal_element(cx))
|
||||
.children(self.secure_element(cx))
|
||||
.children(self.sign_element(cx))
|
||||
.children(self.visible_element(cx))
|
||||
.children(self.signs_element(cx))
|
||||
.children(self.export_element(cx))
|
||||
.children(self.repair_element(cx));
|
||||
let root = crate::writes::on(crate::organize::on(crate::keys::on(root, cx), cx), cx);
|
||||
crate::document::on(crate::marking::on(root, cx), cx)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user