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,273 @@
|
||||
//! Text blocks and drawn objects, picked and dragged (PDF-03, PDF-04).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ely_gpui_component::theme::ActiveTheme as _;
|
||||
use ely_pdf_engine::{Drawn, TextBlock};
|
||||
use gpui::{AnyElement, App, Context, IntoElement, Pixels, Point, Styled as _, div, px};
|
||||
|
||||
use crate::layout::Placed;
|
||||
use crate::pages::rect_on;
|
||||
use crate::view::PdfView;
|
||||
|
||||
/// What a press on a page does.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Tool {
|
||||
#[default]
|
||||
Read,
|
||||
Edit,
|
||||
AddText,
|
||||
Highlight,
|
||||
Underline,
|
||||
StrikeOut,
|
||||
Note,
|
||||
Square,
|
||||
Circle,
|
||||
Line,
|
||||
Ink,
|
||||
Stamp,
|
||||
Field(ely_pdf_engine::FieldKind),
|
||||
Link,
|
||||
/// Marks picked text, or drawn areas, to remove.
|
||||
RedactText,
|
||||
RedactArea,
|
||||
/// Places a signature signed with a certificate.
|
||||
Sign,
|
||||
/// Places a visible signature made in its dialog.
|
||||
PlaceSignature,
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
/// Presses pick text, as in reading.
|
||||
pub fn selects(self) -> bool {
|
||||
matches!(self, Tool::Read | Tool::Highlight | Tool::Underline | Tool::StrikeOut | Tool::RedactText)
|
||||
}
|
||||
|
||||
/// Presses aim at text blocks and objects.
|
||||
pub fn edits(self) -> bool {
|
||||
matches!(self, Tool::Edit | Tool::AddText)
|
||||
}
|
||||
}
|
||||
|
||||
/// A page's text blocks and drawn objects.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Layer {
|
||||
pub blocks: Vec<TextBlock>,
|
||||
pub drawn: Vec<Drawn>,
|
||||
}
|
||||
|
||||
/// What the edit panel works on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Aim {
|
||||
Text { page: usize, index: usize },
|
||||
Object { page: usize, index: usize },
|
||||
/// New text whose box starts here, in page points.
|
||||
New { page: usize, at: (f32, f32) },
|
||||
/// An annotation or form field.
|
||||
Note(crate::annotate::Pick),
|
||||
}
|
||||
|
||||
/// An object being moved, or scaled from its corner.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Drag {
|
||||
pub page: usize,
|
||||
pub index: usize,
|
||||
pub rect: [f32; 4],
|
||||
pub from: (f32, f32),
|
||||
pub now: (f32, f32),
|
||||
pub scale: bool,
|
||||
}
|
||||
|
||||
/// The corner handle's reach, in page points.
|
||||
const HANDLE: f32 = 6.0;
|
||||
|
||||
impl Drag {
|
||||
/// The page-space change the drag makes.
|
||||
pub fn change(&self) -> [f32; 6] {
|
||||
let ([x0, y0, x1, y1], (dx, dy)) = (self.rect, (self.now.0 - self.from.0, self.now.1 - self.from.1));
|
||||
match self.scale {
|
||||
false => [1.0, 0.0, 0.0, 1.0, dx, dy],
|
||||
true => {
|
||||
// Aspect kept, the top left fixed.
|
||||
let s = ((x1 - x0 + dx) / (x1 - x0)).max((y1 - y0 + dy) / (y1 - y0)).max(0.05);
|
||||
[s, 0.0, 0.0, s, x0 - s * x0, y0 - s * y0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The object's box as the drag has it now.
|
||||
pub fn shown(&self) -> [f32; 4] {
|
||||
let [a, _, _, d, e, f] = self.change();
|
||||
let [x0, y0, x1, y1] = self.rect;
|
||||
[a * x0 + e, d * y0 + f, a * x1 + e, d * y1 + f]
|
||||
}
|
||||
}
|
||||
|
||||
fn inside(rect: &[f32; 4], (x, y): (f32, f32)) -> bool {
|
||||
x >= rect[0] && x <= rect[2] && y >= rect[1] && y <= rect[3]
|
||||
}
|
||||
|
||||
impl PdfView {
|
||||
/// A drag or drawing is under way.
|
||||
pub fn gesturing(&self) -> bool {
|
||||
self.drag.is_some() || self.sketch.is_some()
|
||||
}
|
||||
|
||||
/// What the side panel shows; held still mid-gesture.
|
||||
pub fn panel_aim(&self) -> Option<Aim> {
|
||||
if self.gesturing() { self.held } else { self.aim }
|
||||
}
|
||||
|
||||
/// Changes the tool; what was aimed at is let go.
|
||||
pub fn set_tool(&mut self, tool: Tool, cx: &mut Context<Self>) {
|
||||
log::info!("pdf: the {tool:?} tool");
|
||||
(self.tool, self.aim, self.drag, self.sketch, self.selection, self.marking) = (tool, None, None, None, None, None);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Reads the layers of `pages`, one page at a time.
|
||||
pub fn ask_layers(&mut self, pages: &[usize], cx: &mut Context<Self>) {
|
||||
if !self.tool.edits() || self.reading_layer {
|
||||
return;
|
||||
}
|
||||
let Some(page) = pages.iter().copied().find(|page| !self.layers.contains_key(page)) else {
|
||||
return;
|
||||
};
|
||||
self.reading_layer = true;
|
||||
let (doc, version) = (self.doc, self.version);
|
||||
self.off(
|
||||
move |worker| Ok(Layer { blocks: worker.text_blocks(doc, page)?, drawn: worker.objects(doc, page)? }),
|
||||
move |view, layer, cx| {
|
||||
// Cleared either way; reads from before an edit retry.
|
||||
view.reading_layer = false;
|
||||
if view.version != version {
|
||||
return cx.notify();
|
||||
}
|
||||
match layer {
|
||||
Ok(layer) => {
|
||||
view.layers.insert(page, layer);
|
||||
}
|
||||
Err(error) => view.fail("reading what the page holds", &error, cx),
|
||||
}
|
||||
cx.notify();
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// A press aims at an object, text, or new text.
|
||||
pub fn press(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) {
|
||||
let Some((page, point)) = self.page_at(at) else {
|
||||
return;
|
||||
};
|
||||
(self.drag, self.held) = (None, self.aim);
|
||||
self.grip(page);
|
||||
if self.tool == Tool::AddText {
|
||||
self.aim = Some(Aim::New { page, at: point });
|
||||
return cx.notify();
|
||||
}
|
||||
let Some(layer) = self.layers.get(&page) else {
|
||||
return;
|
||||
};
|
||||
let corner = |rect: &[f32; 4]| (point.0 - rect[2]).abs() <= HANDLE && (point.1 - rect[3]).abs() <= HANDLE;
|
||||
let held = match self.aim {
|
||||
Some(Aim::Object { page: on, index }) if on == page => layer.drawn.get(index).filter(|d| corner(&d.rect)).map(|_| (index, true)),
|
||||
_ => None,
|
||||
};
|
||||
let hit = held.or_else(|| layer.drawn.iter().rposition(|d| inside(&d.rect, point)).map(|index| (index, false)));
|
||||
self.aim = match hit {
|
||||
Some((index, scale)) => {
|
||||
let rect = layer.drawn[index].rect;
|
||||
self.drag = Some(Drag { page, index, rect, from: point, now: point, scale });
|
||||
Some(Aim::Object { page, index })
|
||||
}
|
||||
None => (layer.blocks.iter())
|
||||
.position(|b| b.editable && inside(&b.rect, point))
|
||||
.map(|index| Aim::Text { page, index }),
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn drag_to(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) {
|
||||
let Some((page, point)) = self.held_at(at) else {
|
||||
return;
|
||||
};
|
||||
if let Some(drag) = self.drag.as_mut().filter(|drag| drag.page == page) {
|
||||
drag.now = point;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends a drag; a real move is one edit.
|
||||
pub fn release(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(drag) = self.drag.take() else {
|
||||
return;
|
||||
};
|
||||
if (drag.now.0 - drag.from.0).abs() < 1.0 && (drag.now.1 - drag.from.1).abs() < 1.0 {
|
||||
return cx.notify();
|
||||
}
|
||||
// A queued edit may renumber what was dragged.
|
||||
if !self.settled() {
|
||||
let text = crate::viewer::Viewer::text(cx, "pdf.object.changed", &[]);
|
||||
cx.emit(crate::view::Failed(text.to_string()));
|
||||
return cx.notify();
|
||||
}
|
||||
let (page, index, change) = (drag.page, drag.index, drag.change());
|
||||
self.edit(
|
||||
if drag.scale { "scaling an object" } else { "moving an object" },
|
||||
move |worker, doc| worker.place(doc, page, index, change),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Outlines of what edits on `place`; the aimed one bold.
|
||||
pub fn edit_marks(&self, place: &Placed, scale: f32, cx: &App) -> Vec<AnyElement> {
|
||||
let Some(layer) = self.layers.get(&place.page).filter(|_| self.tool == Tool::Edit) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let colors = &cx.theme().colors;
|
||||
let size = self.sizes[place.page];
|
||||
let outline = |rect: [f32; 4], strong: bool| {
|
||||
let (x, y, w, h) = rect_on(rect, size, self.rotate, scale);
|
||||
let color = if strong { colors.focus } else { colors.focus.opacity(0.35) };
|
||||
div().absolute().left(px(x)).top(px(y)).w(px(w)).h(px(h)).border_1().border_color(color).into_any_element()
|
||||
};
|
||||
let aimed = |object: bool, index: usize| match self.aim {
|
||||
Some(Aim::Object { page, index: at }) => object && page == place.page && at == index,
|
||||
Some(Aim::Text { page, index: at }) => !object && page == place.page && at == index,
|
||||
_ => false,
|
||||
};
|
||||
let mut marks: Vec<AnyElement> = (layer.blocks.iter().enumerate())
|
||||
.filter(|(_, b)| b.editable)
|
||||
.map(|(index, b)| outline(b.rect, aimed(false, index)))
|
||||
.collect();
|
||||
for (index, drawn) in layer.drawn.iter().enumerate() {
|
||||
let dragged = self.drag.filter(|d| d.page == place.page && d.index == index);
|
||||
let rect = dragged.map_or(drawn.rect, |d| d.shown());
|
||||
marks.push(outline(rect, aimed(true, index)));
|
||||
if aimed(true, index) {
|
||||
let (x, y, w, h) = rect_on(rect, size, self.rotate, scale);
|
||||
let corner = div().absolute().left(px(x + w - 4.0)).top(px(y + h - 4.0)).size(px(8.0)).bg(colors.focus);
|
||||
marks.push(corner.into_any_element());
|
||||
}
|
||||
}
|
||||
marks
|
||||
}
|
||||
}
|
||||
|
||||
/// Layers by page, dropped with each edit.
|
||||
pub type Layers = HashMap<usize, Layer>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_drag_moves_or_scales_from_the_top_left() {
|
||||
let mut drag = Drag { page: 0, index: 0, rect: [10.0, 20.0, 110.0, 70.0], from: (50.0, 40.0), now: (60.0, 45.0), scale: false };
|
||||
assert_eq!(drag.change(), [1.0, 0.0, 0.0, 1.0, 10.0, 5.0]);
|
||||
assert_eq!(drag.shown(), [20.0, 25.0, 120.0, 75.0]);
|
||||
drag = Drag { from: (110.0, 70.0), now: (210.0, 80.0), scale: true, ..drag };
|
||||
assert_eq!(drag.shown(), [10.0, 20.0, 210.0, 120.0], "twice as wide, twice as tall");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user