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,176 @@
|
||||
//! Annotation and form menus, and the panel's changes (PDF-05, PDF-06).
|
||||
|
||||
use ely_gpui_component::primitives::Severity;
|
||||
use ely_office::Tabs as _;
|
||||
use ely_pdf_engine::{FieldKind, LinkChange, NewAnnot, NewField, Shape};
|
||||
use gpui::{App, Context, Div, InteractiveElement as _, Menu, MenuItem, actions};
|
||||
|
||||
use crate::annotate::{Pick, around};
|
||||
use crate::content::Tool;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
actions!(
|
||||
pdf,
|
||||
[
|
||||
Highlight,
|
||||
Underline,
|
||||
StrikeOut,
|
||||
AddNote,
|
||||
DrawRectangle,
|
||||
DrawEllipse,
|
||||
DrawLine,
|
||||
DrawFreehand,
|
||||
AddStamp,
|
||||
AddTextField,
|
||||
AddCheckbox,
|
||||
AddRadioButton,
|
||||
AddChoice
|
||||
]
|
||||
);
|
||||
|
||||
/// A stamp's size, in points.
|
||||
const STAMP: (f32, f32) = (190.0, 50.0);
|
||||
|
||||
/// What the note panel asks for.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Change {
|
||||
Note(String),
|
||||
Stamp(String),
|
||||
Contents(String),
|
||||
Color(u32),
|
||||
Delete,
|
||||
/// A field's name and options.
|
||||
Design(String, Vec<String>),
|
||||
Fill(String),
|
||||
/// A link target as typed.
|
||||
Target(String),
|
||||
}
|
||||
|
||||
/// The Annotate and Form menus.
|
||||
pub fn menus(cx: &App) -> [Menu; 2] {
|
||||
let label = |key: &str| Viewer::text(cx, key, &[]);
|
||||
[
|
||||
Menu::new(label("menu.annotate")).items([
|
||||
MenuItem::action(label("pdf.tool.highlight"), Highlight),
|
||||
MenuItem::action(label("pdf.tool.underline"), Underline),
|
||||
MenuItem::action(label("pdf.tool.strikeout"), StrikeOut),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(label("pdf.tool.note"), AddNote),
|
||||
MenuItem::action(label("pdf.tool.stamp"), AddStamp),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(label("pdf.tool.square"), DrawRectangle),
|
||||
MenuItem::action(label("pdf.tool.circle"), DrawEllipse),
|
||||
MenuItem::action(label("pdf.tool.line"), DrawLine),
|
||||
MenuItem::action(label("pdf.tool.ink"), DrawFreehand),
|
||||
]),
|
||||
Menu::new(label("menu.form")).items([
|
||||
MenuItem::action(label("pdf.tool.field_text"), AddTextField),
|
||||
MenuItem::action(label("pdf.tool.field_check"), AddCheckbox),
|
||||
MenuItem::action(label("pdf.tool.field_radio"), AddRadioButton),
|
||||
MenuItem::action(label("pdf.tool.field_choice"), AddChoice),
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
impl Viewer {
|
||||
/// Makes `change` if tab `tab` is still at `version`.
|
||||
pub fn change_note(&mut self, (tab, version, pick): (u64, u64, Pick), change: Change, cx: &mut Context<Self>) {
|
||||
let Some(view) = self.tab(tab).map(|tab| tab.view.clone()) else {
|
||||
let text = Self::text(cx, "pdf.object.changed", &[]);
|
||||
return self.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
self.on_same(&view, version, cx, move |view, cx| {
|
||||
let note = |page, shape, contents| NewAnnot { page, shape, rgb: None, contents, author: String::new() };
|
||||
match (pick, change) {
|
||||
(Pick::NewNote { page, at }, Change::Note(text)) => view.annotate("adding a note", vec![note(page, Shape::Note(at), text)], cx),
|
||||
(Pick::NewStamp { page, at }, Change::Stamp(icon)) => {
|
||||
let rect = around(at, STAMP, view.sizes[page]);
|
||||
view.annotate("adding a stamp", vec![note(page, Shape::Stamp(rect, icon), String::new())], cx)
|
||||
}
|
||||
(Pick::Annot { page, id }, Change::Contents(text)) => {
|
||||
view.edit("changing an annotation", move |w, doc| w.change_annot(doc, page, id, None, Some(&text)), cx)
|
||||
}
|
||||
(Pick::Annot { page, id }, Change::Color(rgb)) => {
|
||||
view.edit("colouring an annotation", move |w, doc| w.change_annot(doc, page, id, Some(rgb), None), cx)
|
||||
}
|
||||
(Pick::Annot { page, id }, Change::Delete) => view.edit("deleting an annotation", move |w, doc| w.delete_annot(doc, page, id), cx),
|
||||
(Pick::Field { page, id }, Change::Delete) => view.edit("deleting a field", move |w, doc| w.delete_field(doc, page, id), cx),
|
||||
(Pick::Field { page, id }, Change::Fill(value)) => view.edit("filling a field", move |w, doc| w.fill(doc, page, id, &value), cx),
|
||||
(Pick::Field { page, id }, Change::Design(name, options)) => {
|
||||
let Some(field) = view.notes.get(&page).and_then(|m| m.fields.iter().find(|f| f.id == id)) else {
|
||||
return log::error!("pdf: a picked field is not read");
|
||||
};
|
||||
let renamed = (field.leaf() != name).then_some(name);
|
||||
let choice = matches!(field.kind, FieldKind::Choice | FieldKind::List);
|
||||
let listed = (choice && field.options != options).then_some(options);
|
||||
if renamed.is_none() && listed.is_none() {
|
||||
return log::info!("pdf: the field is as it was");
|
||||
}
|
||||
view.edit(
|
||||
"changing a field",
|
||||
move |w, doc| w.change_field(doc, page, id, renamed.as_deref(), listed.as_deref()),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
(Pick::NewField { page, rect, kind }, Change::Design(name, options)) => {
|
||||
let field = NewField { kind, name, rect, options };
|
||||
view.edit("adding a field", move |w, doc| w.add_field(doc, page, &field), cx)
|
||||
}
|
||||
(Pick::NewLink { page, rect }, Change::Target(typed)) => {
|
||||
let Some(to) = crate::links::parse(&typed, view.sizes.len()) else {
|
||||
return bad_target(cx);
|
||||
};
|
||||
view.edit("adding a link", move |w, doc| w.change_link(doc, page, &LinkChange::Add { rect, to }), cx)
|
||||
}
|
||||
(Pick::Link { page, index }, change) => {
|
||||
let Some(expect) = view.links.get(&page).and_then(|links| links.get(index)).map(|link| link.uri.clone()) else {
|
||||
return log::error!("pdf: a picked link is not read");
|
||||
};
|
||||
let change = match change {
|
||||
Change::Delete => LinkChange::Delete { index, expect },
|
||||
Change::Target(typed) => match crate::links::parse(&typed, view.sizes.len()) {
|
||||
Some(to) => LinkChange::Edit { index, expect, to },
|
||||
None => return bad_target(cx),
|
||||
},
|
||||
other => return log::error!("pdf: {other:?} does not fit a link"),
|
||||
};
|
||||
view.edit("changing a link", move |w, doc| w.change_link(doc, page, &change), cx)
|
||||
}
|
||||
(pick, change) => log::error!("pdf: {change:?} does not fit {pick:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Says a typed link target is neither page nor address.
|
||||
fn bad_target(cx: &mut Context<crate::view::PdfView>) {
|
||||
let text = Viewer::text(cx, "pdf.link.bad", &[]);
|
||||
cx.emit(crate::view::Failed(text.to_string()));
|
||||
}
|
||||
|
||||
/// Wires the annotation and form actions onto the root.
|
||||
pub fn on(root: Div, cx: &mut Context<Viewer>) -> Div {
|
||||
let tool = |tool: Tool| move |viewer: &mut Viewer, cx: &mut Context<Viewer>| viewer.toggle_tool(tool, cx);
|
||||
let (highlight, underline, strike, note, stamp) =
|
||||
(tool(Tool::Highlight), tool(Tool::Underline), tool(Tool::StrikeOut), tool(Tool::Note), tool(Tool::Stamp));
|
||||
let (square, circle, line, ink) = (tool(Tool::Square), tool(Tool::Circle), tool(Tool::Line), tool(Tool::Ink));
|
||||
let (text, check, radio, choice) = (
|
||||
tool(Tool::Field(FieldKind::Text)),
|
||||
tool(Tool::Field(FieldKind::Checkbox)),
|
||||
tool(Tool::Field(FieldKind::Radio)),
|
||||
tool(Tool::Field(FieldKind::Choice)),
|
||||
);
|
||||
root.on_action(cx.listener(move |v, _: &Highlight, _, cx| highlight(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &Underline, _, cx| underline(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &StrikeOut, _, cx| strike(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &AddNote, _, cx| note(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &AddStamp, _, cx| stamp(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &DrawRectangle, _, cx| square(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &DrawEllipse, _, cx| circle(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &DrawLine, _, cx| line(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &DrawFreehand, _, cx| ink(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &AddTextField, _, cx| text(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &AddCheckbox, _, cx| check(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &AddRadioButton, _, cx| radio(v, cx)))
|
||||
.on_action(cx.listener(move |v, _: &AddChoice, _, cx| choice(v, cx)))
|
||||
}
|
||||
Reference in new issue
Block a user