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,341 @@
|
||||
//! Editing text and objects from the app (C11.3).
|
||||
|
||||
use ely_pdf_engine::sample::{Extras, pdf_with};
|
||||
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::input::over;
|
||||
use super::{frames, settle, shown, windowed};
|
||||
use crate::content::{Aim, Tool};
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Two lines, a stamp, a picture; the edit tool on.
|
||||
fn editing<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let path = dir.join("doc.pdf");
|
||||
let extras = Extras { stamp: Some("STAMP"), picture: true, ..Extras::default() };
|
||||
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world", "Second line"])], &[], &extras)).expect("writes");
|
||||
let (viewer, cx) = windowed(cx, dir, &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
// At actual points the whole page is in view.
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = crate::layout::Zoom::Scale(1.0);
|
||||
view.set_tool(Tool::Edit, cx)
|
||||
}));
|
||||
settle(cx, |app| view.read(app).layers.contains_key(&0));
|
||||
(viewer, cx, view)
|
||||
}
|
||||
|
||||
/// Presses at page point `at`, with no drag.
|
||||
fn click(cx: &mut VisualTestContext, view: &Entity<PdfView>, at: (f32, f32)) {
|
||||
let point = over(cx, view, 0, at);
|
||||
cx.simulate_mouse_down(point, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(point, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
|
||||
/// The page's blocks once the layer is read again.
|
||||
fn blocks(cx: &mut VisualTestContext, view: &Entity<PdfView>) -> Vec<String> {
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0));
|
||||
cx.update(|_, app| view.read(app).layers[&0].blocks.iter().map(|b| b.text.clone()).collect())
|
||||
}
|
||||
|
||||
/// Puts `text` in the panel's field.
|
||||
fn type_in(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, text: &str) {
|
||||
frames(cx);
|
||||
cx.update(|_, app| {
|
||||
let field = viewer.read(app).edit_panel.read(app).field.clone().expect("drawn");
|
||||
field.update(app, |field, cx| field.set_text(text, cx));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_edits_in_its_own_font_without_a_preview() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = editing(&mut cx, dir.path());
|
||||
// Hello world's first line sits 72 points down.
|
||||
click(cx, &view, (80.0, 66.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Text { .. })));
|
||||
type_in(cx, &viewer, "Hi world\nSecond line");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_text(false, cx)));
|
||||
let now = blocks(cx, &view);
|
||||
assert!(now.iter().any(|b| b.starts_with("Hi world")) && !now.iter().any(|b| b.contains("Hello")), "{now:?}");
|
||||
assert!(cx.update(|_, app| viewer.read(app).preview.is_none()), "same font, same space: no preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_font_shows_the_change_first_and_a_missing_glyph_says_so() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = editing(&mut cx, dir.path());
|
||||
click(cx, &view, (80.0, 66.0));
|
||||
type_in(cx, &viewer, "你好");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.edit_panel.update(cx, |panel, _| panel.font = "helvetica".into());
|
||||
viewer.write_text(false, cx)
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).edit_panel.read(app).note.is_some());
|
||||
assert!(cx.update(|_, app| view.read(app).edited) == 0, "nothing changed");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.edit_panel.update(cx, |panel, _| panel.font = "cjk-sc".into());
|
||||
viewer.write_text(false, cx)
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).preview.is_some());
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let preview = viewer.preview.take().expect("shown");
|
||||
viewer.commit_text((preview.tab, preview.version, preview.page), preview.writing, cx)
|
||||
}));
|
||||
assert!(blocks(cx, &view).iter().any(|b| b.contains("你好")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_drags_and_scales_from_its_corner() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
let rect = |cx: &mut VisualTestContext| cx.update(|_, app| view.read(app).layers[&0].drawn[1].rect);
|
||||
let (from, to) = (over(cx, &view, 0, (122.0, 267.0)), over(cx, &view, 0, (172.0, 287.0)));
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0));
|
||||
let moved = rect(cx);
|
||||
assert!((moved[0] - 122.0).abs() < 0.5 && (moved[1] - 262.0).abs() < 0.5, "{moved:?}");
|
||||
// Edits drop the aim; aim, then pull the corner.
|
||||
click(cx, &view, (150.0, 280.0));
|
||||
let (corner, out) = (over(cx, &view, 0, (222.0, 312.0)), over(cx, &view, 0, (322.0, 362.0)));
|
||||
cx.simulate_mouse_down(corner, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(out, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(out, MouseButton::Left, Modifiers::none());
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0));
|
||||
let scaled = rect(cx);
|
||||
assert!((scaled[2] - 322.0).abs() < 0.5 && (scaled[3] - 362.0).abs() < 0.5, "{scaled:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_text_lands_where_pressed_and_objects_delete() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = editing(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.set_tool(Tool::AddText, cx)));
|
||||
click(cx, &view, (100.0, 150.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::New { page: 0, .. })));
|
||||
type_in(cx, &viewer, "Added");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_text(false, cx)));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.set_tool(Tool::Edit, cx)));
|
||||
assert!(blocks(cx, &view).iter().any(|b| b == "Added"));
|
||||
click(cx, &view, (310.0, 370.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Object { index: 0, .. })), "the stamp");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.delete_object(cx)));
|
||||
assert!(!blocks(cx, &view).iter().any(|b| b == "STAMP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_leaves_the_edit_tool() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
cx.update(|window, app| {
|
||||
let focus = view.read(app).focus.clone();
|
||||
window.focus(&focus, app)
|
||||
});
|
||||
cx.simulate_keystrokes("escape");
|
||||
assert_eq!(cx.update(|_, app| view.read(app).tool), Tool::Read);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_replaced_image_stays_with_its_own_tab() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, a) = editing(&mut cx, dir.path());
|
||||
click(cx, &a, (120.0, 267.0));
|
||||
let other = super::file(dir.path(), "b.pdf", &[(612.0, 792.0, &["b"])]);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.replace_image(cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| ely_office::Opening::open_file(viewer, other.clone(), cx)));
|
||||
settle(cx, |app| viewer.read(app).core.tabs.len() == 2);
|
||||
let b = shown(cx, &viewer);
|
||||
assert_ne!(a, b, "B is in front when the picker answers");
|
||||
let square = dir.path().join("square.pnm");
|
||||
std::fs::write(&square, b"P6\n2 2\n255\n\xff\xff\xff\x00\x00\x00\x00\x00\x00\xff\xff\xff").expect("writes");
|
||||
cx.cx.simulate_path_prompt_response(move |_| Some(vec![square]));
|
||||
settle(cx, |app| a.read(app).edited == 1);
|
||||
assert_eq!(cx.update(|_, app| b.read(app).edited), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layer_read_from_before_an_edit_is_read_again() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
// The read starts before the edit lands, returns after.
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.edit("turning", |worker, doc| worker.rotate_pages(doc, &[0], 90), cx);
|
||||
view.layers.clear();
|
||||
view.ask_layers(&[0], cx);
|
||||
assert!(view.reading_layer);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0) && !view.read(app).reading_layer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_from_the_text_field_leaves_the_tool() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = editing(&mut cx, dir.path());
|
||||
click(cx, &view, (80.0, 66.0));
|
||||
frames(cx);
|
||||
cx.update(|window, app| {
|
||||
let field = viewer.read(app).edit_panel.read(app).field.clone().expect("drawn");
|
||||
let focus = gpui::Focusable::focus_handle(field.read(app), app);
|
||||
window.focus(&focus, app)
|
||||
});
|
||||
cx.simulate_keystrokes("escape");
|
||||
assert_eq!(cx.update(|_, app| view.read(app).tool), Tool::Read);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_preview_goes_once_anything_else_lands() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
// Twin pages: a stale aim would hit the wrong one.
|
||||
let same: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["Hello world", "Second line"]), (612.0, 792.0, &["Hello world", "Second line"])];
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[super::file(dir.path(), "twins.pdf", same)], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = crate::layout::Zoom::Scale(1.0);
|
||||
view.set_tool(Tool::Edit, cx)
|
||||
}));
|
||||
settle(cx, |app| view.read(app).layers.contains_key(&0));
|
||||
click(cx, &view, (80.0, 66.0));
|
||||
type_in(cx, &viewer, "Hi world\nSecond line");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.edit_panel.update(cx, |panel, _| panel.font = "times".into());
|
||||
viewer.write_text(false, cx)
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).preview.is_some());
|
||||
let stale = cx.update(|_, app| {
|
||||
let preview = viewer.read(app).preview.as_ref().expect("shown");
|
||||
((preview.tab, preview.version, preview.page), preview.writing.clone())
|
||||
});
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("deleting", |worker, doc| worker.delete_pages(doc, &[0]), cx)));
|
||||
settle(cx, |app| view.read(app).edited == 1 && viewer.read(app).preview.is_none());
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.commit_text(stale.0, stale.1, cx)));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 1, "the old preview writes nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_aimed_before_a_queued_edit_is_done() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = editing(&mut cx, dir.path());
|
||||
let version = cx.update(|_, app| view.read(app).version);
|
||||
// An edit is queued; old numbers may not hold.
|
||||
cx.update(|_, app| view.update(app, |view, _| view.editing = true));
|
||||
let ran = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let ran = ran.clone();
|
||||
viewer.on_same(&view, version, cx, move |_, _| ran.set(true))
|
||||
}));
|
||||
assert!(!ran.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_delete_before_the_first_lands_does_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = editing(&mut cx, dir.path());
|
||||
click(cx, &view, (310.0, 370.0));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.delete_object(cx);
|
||||
viewer.delete_object(cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0));
|
||||
assert_eq!(cx.update(|_, app| view.read(app).layers[&0].drawn.len()), 1, "the picture stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_while_an_edit_is_queued_does_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
let (from, to) = (over(cx, &view, 0, (122.0, 267.0)), over(cx, &view, 0, (172.0, 287.0)));
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
|
||||
cx.update(|_, app| view.update(app, |view, _| view.editing = true));
|
||||
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
|
||||
assert!(cx.update(|_, app| view.read(app).queue.is_empty()), "nothing queued with old numbers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_waits_for_the_drag_so_the_page_stays_put() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
let wide = cx.update(|_, app| view.read(app).room.size.width);
|
||||
let (from, to) = (over(cx, &view, 0, (122.0, 267.0)), over(cx, &view, 0, (140.0, 267.0)));
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).room.size.width), wide, "no panel mid-drag");
|
||||
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
|
||||
settle(cx, |app| view.read(app).settled());
|
||||
frames(cx);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.press(from, cx)));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.release(cx)));
|
||||
frames(cx);
|
||||
assert!(cx.update(|_, app| view.read(app).room.size.width) < wide, "the panel opens after");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_open_panel_stays_open_through_a_second_drag() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
let wide = cx.update(|_, app| view.read(app).room.size.width);
|
||||
click(cx, &view, (122.0, 267.0));
|
||||
// The panel slides open; wait until the view stops narrowing.
|
||||
let mut narrow = wide;
|
||||
for _ in 0..40 {
|
||||
cx.executor().advance_clock(std::time::Duration::from_millis(50));
|
||||
frames(cx);
|
||||
let now = cx.update(|_, app| view.read(app).room.size.width);
|
||||
if now < wide && now == narrow {
|
||||
break;
|
||||
}
|
||||
narrow = now;
|
||||
}
|
||||
assert!(narrow < wide, "the panel opened");
|
||||
// Pull the picture's corner 50 points right.
|
||||
let (from, to) = (over(cx, &view, 0, (172.0, 292.0)), over(cx, &view, 0, (222.0, 292.0)));
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
|
||||
cx.executor().advance_clock(std::time::Duration::from_millis(500));
|
||||
frames(cx);
|
||||
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
|
||||
cx.executor().advance_clock(std::time::Duration::from_millis(500));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).room.size.width), narrow, "the panel stays mid-drag");
|
||||
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0));
|
||||
let rect = cx.update(|_, app| view.read(app).layers[&0].drawn[1].rect);
|
||||
assert!((rect[2] - 222.0).abs() < 1.0 && (rect[3] - 317.0).abs() < 1.0, "{rect:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_follows_the_hand_when_the_page_shifts_under_it() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = editing(&mut cx, dir.path());
|
||||
let from = over(cx, &view, 0, (122.0, 267.0));
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
|
||||
// The view shifts right, as a panel pushes it.
|
||||
cx.update(|_, app| view.update(app, |view, _| view.room.origin.x += gpui::px(100.0)));
|
||||
let to = gpui::point(from.x + gpui::px(50.0), from.y);
|
||||
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).layers.contains_key(&0));
|
||||
let rect = cx.update(|_, app| view.read(app).layers[&0].drawn[1].rect);
|
||||
assert!((rect[0] - 122.0).abs() < 0.5, "moved as far as the hand: {rect:?}");
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Mouse, jumps, search across tabs, failures, tiles (C11.1).
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ely_office::Tabs as _;
|
||||
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext, point, px};
|
||||
|
||||
use super::{file, settle, shown, windowed};
|
||||
use crate::layout::Zoom;
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// The window point over page points `at` of `page`.
|
||||
pub(super) fn over(cx: &mut VisualTestContext, view: &Entity<PdfView>, page: usize, at: (f32, f32)) -> gpui::Point<gpui::Pixels> {
|
||||
cx.update(|_, app| {
|
||||
let view = view.read(app);
|
||||
let laid = view.laid_out();
|
||||
let place = laid.placed.iter().find(|p| p.page == page).expect("placed");
|
||||
let (offset, origin) = (view.scroll.offset(), view.room.origin);
|
||||
point(
|
||||
origin.x + offset.x + px(place.x + at.0 * laid.scale),
|
||||
origin.y + offset.y + px(place.y + at.1 * laid.scale),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_first_drag_picks_text_before_the_page_is_read() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = file(dir.path(), "one.pdf", &[(612.0, 792.0, &["alpha beta gamma"])]);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > px(100.0));
|
||||
// Glyphs sit near 72 points down; none read yet.
|
||||
let (from, to) = (over(cx, &view, 0, (74.0, 62.0)), over(cx, &view, 0, (150.0, 62.0)));
|
||||
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.copy(cx)));
|
||||
settle(cx, |app| app.read_from_clipboard().is_some());
|
||||
let text = cx.update(|_, app| app.read_from_clipboard().and_then(|item| item.text())).expect("copied");
|
||||
assert!(text.starts_with("alpha"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_jump_makes_its_page_the_one_being_read() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"]), (612.0, 792.0, &["three"])];
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "three.pdf", pages)], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.show(1, 0.0, cx)));
|
||||
super::frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).current), 1);
|
||||
}
|
||||
|
||||
/// A: the word on page three only. B: three pages.
|
||||
fn two_tabs<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, u64, u64) {
|
||||
let a = file(dir, "a.pdf", &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"]), (612.0, 792.0, &["three alpha"])]);
|
||||
let b = file(dir, "b.pdf", &[(612.0, 792.0, &["zzz"]), (612.0, 792.0, &["b2"]), (612.0, 792.0, &["b3"])]);
|
||||
let (viewer, cx) = windowed(cx, dir, &[a, b], true);
|
||||
let (first, second) = cx.update(|_, app| (viewer.read(app).core.tabs[0].id, viewer.read(app).core.tabs[1].id));
|
||||
(viewer, cx, first, second)
|
||||
}
|
||||
|
||||
/// Opens find on `tab` and searches `query` at once.
|
||||
fn find_in(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, tab: u64, query: &str) {
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.core.active = Some(tab);
|
||||
viewer.open_find(window, cx);
|
||||
let field = viewer.find.as_ref().expect("open").query.clone();
|
||||
field.update(cx, |field, cx| field.set_text(query, cx));
|
||||
}));
|
||||
}
|
||||
|
||||
fn view_of(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, tab: u64) -> Entity<PdfView> {
|
||||
cx.update(|_, app| viewer.read(app).tab(tab).expect("open").view.clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hit_found_late_never_moves_another_tab() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, first, second) = two_tabs(&mut cx, dir.path());
|
||||
find_in(cx, &viewer, first, "alpha");
|
||||
// Before the batch comes back, the reader turns to B.
|
||||
cx.update(|_, app| viewer.update(app, |viewer, _| viewer.core.active = Some(second)));
|
||||
settle(cx, |app| viewer.read(app).find.as_ref().is_some_and(|find| find.done == 3));
|
||||
let (a, b) = (view_of(cx, &viewer, first), view_of(cx, &viewer, second));
|
||||
let state = cx.update(|_, app| (a.read(app).chosen, b.read(app).current));
|
||||
assert_eq!(state, (Some(0), 0), "the hit is chosen; B stays on its page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_find_box_takes_no_answers_meant_for_the_old() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, first, second) = two_tabs(&mut cx, dir.path());
|
||||
find_in(cx, &viewer, first, "alpha");
|
||||
find_in(cx, &viewer, second, "zzz");
|
||||
settle(cx, |app| viewer.read(app).find.as_ref().is_some_and(|find| find.done == 3));
|
||||
super::frames(cx);
|
||||
let (a, b) = (view_of(cx, &viewer, first), view_of(cx, &viewer, second));
|
||||
let pages = cx.update(|_, app| (a.read(app).found.len(), b.read(app).found.iter().map(|h| h.0).collect::<Vec<_>>()));
|
||||
assert_eq!(pages, (0, vec![0]), "only B's own hit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_that_fails_is_not_asked_again() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "one.pdf", &[(612.0, 792.0, &["x"])])], true);
|
||||
let view = shown(cx, &viewer);
|
||||
// The document goes away behind the viewer's back.
|
||||
let (worker, doc) = cx.update(|_, app| (viewer.read(app).core.worker.clone(), view.read(app).doc));
|
||||
worker.lock().expect("the lock").get().expect("started").close(doc).expect("closes");
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.tiles.clear();
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| !view.read(app).failed.is_empty());
|
||||
// Bounded steps: a retry loop would never go quiet.
|
||||
for _ in 0..10 {
|
||||
super::frames(cx);
|
||||
}
|
||||
assert!(!cx.update(|_, app| view.read(app).painting), "no new tries, nothing waiting");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiles_scrolled_off_a_long_page_go() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "long.pdf", &[(612.0, 14400.0, &["top"])])], true);
|
||||
let view = shown(cx, &viewer);
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = Zoom::Scale(1.0);
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| view.read(app).tiles.keys().any(|key| key.ty == 0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.go_to = Some((0.0, 4000.0));
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| !view.read(app).tiles.is_empty() && view.read(app).tiles.keys().all(|key| key.ty > 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_worker_runs_before_a_file_opens() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[], false);
|
||||
let started = |cx: &mut VisualTestContext| cx.update(|_, app| viewer.read(app).core.worker.lock().expect("the lock").started());
|
||||
assert!(!started(cx));
|
||||
let path = file(dir.path(), "plain.pdf", &[(612.0, 792.0, &["no marks"])]);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| ely_office::Opening::open_file(viewer, path.clone(), cx)));
|
||||
settle(cx, |app| viewer.read(app).active_tab().is_some());
|
||||
assert!(started(cx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiles_beside_the_view_are_neither_asked_nor_kept() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "wide.pdf", &[(612.0, 792.0, &["x"])])], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = Zoom::Scale(4.0);
|
||||
cx.notify();
|
||||
}));
|
||||
let columns = |app: &gpui::App| view.read(app).tiles.keys().map(|key| key.tx).collect::<HashSet<_>>();
|
||||
let last = cx.update(|window, app| {
|
||||
let laid = view.read(app).laid_out();
|
||||
((laid.placed[0].w * window.scale_factor()).ceil() as i32 - 1) / crate::pages::TILE
|
||||
});
|
||||
settle(cx, |app| columns(app).contains(&0) && !view.read(app).painting);
|
||||
assert!(!cx.update(|_, app| columns(app)).contains(&last), "the right edge is off screen");
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
let wide = view.laid_out().size.0 - f32::from(view.room.size.width);
|
||||
let y = view.scroll.offset().y;
|
||||
view.scroll.set_offset(point(px(-wide), y));
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| columns(app).contains(&last) && !columns(app).contains(&0));
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
//! Annotations and forms from the app (C11.4).
|
||||
|
||||
use ely_pdf_engine::{FieldKind, Marks, sample::{Extras, pdf_with}};
|
||||
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::input::over;
|
||||
use super::{frames, settle, shown, windowed};
|
||||
use crate::annotate::Pick;
|
||||
use crate::content::{Aim, Tool};
|
||||
use crate::marking::Change;
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Two lines and a text field, actual size.
|
||||
fn marking<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let path = dir.join("doc.pdf");
|
||||
let extras = Extras { fields: &[0], ..Extras::default() };
|
||||
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world", "Second line"])], &[], &extras)).expect("writes");
|
||||
let (viewer, cx) = windowed(cx, dir, &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = crate::layout::Zoom::Scale(1.0);
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| view.read(app).notes.contains_key(&0));
|
||||
(viewer, cx, view)
|
||||
}
|
||||
|
||||
fn tool(cx: &mut VisualTestContext, view: &Entity<PdfView>, tool: Tool) {
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.set_tool(tool, cx)));
|
||||
}
|
||||
|
||||
/// Presses at `from`, moves through `through`, lets go.
|
||||
fn drag(cx: &mut VisualTestContext, view: &Entity<PdfView>, from: (f32, f32), through: &[(f32, f32)]) {
|
||||
// A hand presses what the last frame shows.
|
||||
frames(cx);
|
||||
let start = over(cx, view, 0, from);
|
||||
cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::none());
|
||||
let mut end = start;
|
||||
for at in through {
|
||||
end = over(cx, view, 0, *at);
|
||||
cx.simulate_mouse_move(end, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
|
||||
fn click(cx: &mut VisualTestContext, view: &Entity<PdfView>, at: (f32, f32)) {
|
||||
drag(cx, view, at, &[]);
|
||||
}
|
||||
|
||||
/// Page one's notes once every edit has landed.
|
||||
fn notes(cx: &mut VisualTestContext, view: &Entity<PdfView>) -> Marks {
|
||||
settle(cx, |app| view.read(app).settled() && view.read(app).notes.contains_key(&0));
|
||||
cx.update(|_, app| view.read(app).notes[&0].clone())
|
||||
}
|
||||
|
||||
/// What the panel shows now, sent `change`.
|
||||
fn send(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, change: Change) {
|
||||
frames(cx);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let shown = viewer.note_panel.read(cx).shown.expect("the panel follows the pick");
|
||||
viewer.change_note(shown, change, cx)
|
||||
}));
|
||||
}
|
||||
|
||||
fn kinds(marks: &Marks) -> Vec<String> {
|
||||
marks.annots.iter().map(|a| a.kind.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picked_text_highlights_in_one_step() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = marking(&mut cx, dir.path());
|
||||
tool(cx, &view, Tool::Highlight);
|
||||
drag(cx, &view, (74.0, 66.0), &[(140.0, 66.0)]);
|
||||
let marks = notes(cx, &view);
|
||||
assert_eq!(kinds(&marks), ["Highlight"]);
|
||||
assert!(!marks.annots[0].author.is_empty(), "signed by the account");
|
||||
assert!(cx.update(|_, app| view.read(app).selection.is_none()), "the pick is spent");
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.undo(false, cx)));
|
||||
assert!(notes(cx, &view).annots.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shapes_and_ink_land_where_they_were_drawn() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = marking(&mut cx, dir.path());
|
||||
tool(cx, &view, Tool::Square);
|
||||
drag(cx, &view, (100.0, 400.0), &[(150.0, 420.0), (200.0, 450.0)]);
|
||||
let square = notes(cx, &view).annots[0].rect;
|
||||
assert!((square[0] - 100.0).abs() < 3.0 && (square[3] - 450.0).abs() < 3.0, "{square:?}");
|
||||
tool(cx, &view, Tool::Ink);
|
||||
drag(cx, &view, (300.0, 500.0), &[(320.0, 520.0), (340.0, 500.0), (360.0, 530.0)]);
|
||||
tool(cx, &view, Tool::Circle);
|
||||
click(cx, &view, (400.0, 400.0));
|
||||
assert_eq!(kinds(¬es(cx, &view)), ["Square", "Ink"], "a click draws no ellipse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notes_and_stamps_ask_first_then_change_and_go() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = marking(&mut cx, dir.path());
|
||||
tool(cx, &view, Tool::Note);
|
||||
click(cx, &view, (300.0, 300.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::NewNote { .. }))));
|
||||
send(cx, &viewer, Change::Note("Check this".into()));
|
||||
notes(cx, &view);
|
||||
tool(cx, &view, Tool::Stamp);
|
||||
click(cx, &view, (300.0, 600.0));
|
||||
send(cx, &viewer, Change::Stamp("Draft".into()));
|
||||
let marks = notes(cx, &view);
|
||||
assert_eq!((kinds(&marks), marks.annots[0].contents.as_str(), marks.annots[1].icon.as_str()), (vec!["Text".into(), "Stamp".into()], "Check this", "Draft"));
|
||||
tool(cx, &view, Tool::Read);
|
||||
click(cx, &view, (305.0, 305.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::Annot { .. }))), "a click picks the note");
|
||||
send(cx, &viewer, Change::Color(0x0000ff));
|
||||
let marks = notes(cx, &view);
|
||||
assert_eq!((marks.annots[0].rgb, marks.annots[0].contents.as_str()), (Some(0x0000ff), "Check this"));
|
||||
click(cx, &view, (305.0, 305.0));
|
||||
send(cx, &viewer, Change::Delete);
|
||||
assert_eq!(kinds(¬es(cx, &view)), ["Stamp"]);
|
||||
}
|
||||
|
||||
/// Draws a field of `kind` over `at`, named `name`.
|
||||
fn add_field(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, view: &Entity<PdfView>, kind: FieldKind, at: [(f32, f32); 2], name: &str, options: &[&str]) {
|
||||
tool(cx, view, Tool::Field(kind));
|
||||
drag(cx, view, at[0], &[at[1]]);
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::NewField { .. }))));
|
||||
send(cx, viewer, Change::Design(name.into(), options.iter().map(|o| o.to_string()).collect()));
|
||||
notes(cx, view);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawn_fields_fill_from_the_page() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = marking(&mut cx, dir.path());
|
||||
add_field(cx, &viewer, &view, FieldKind::Checkbox, [(72.0, 300.0), (72.0, 300.0)], "Agree", &[]);
|
||||
add_field(cx, &viewer, &view, FieldKind::Choice, [(72.0, 340.0), (200.0, 360.0)], "Color", &["Red", "Blue"]);
|
||||
tool(cx, &view, Tool::Read);
|
||||
click(cx, &view, (78.0, 306.0));
|
||||
assert!(notes(cx, &view).fields[1].checked, "a click checks");
|
||||
click(cx, &view, (78.0, 306.0));
|
||||
assert!(!notes(cx, &view).fields[1].checked, "and clears");
|
||||
click(cx, &view, (100.0, 350.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::Field { .. }))));
|
||||
send(cx, &viewer, Change::Fill("Blue".into()));
|
||||
notes(cx, &view);
|
||||
// The old field sits 652 points down.
|
||||
click(cx, &view, (100.0, 660.0));
|
||||
send(cx, &viewer, Change::Fill("Zoe".into()));
|
||||
let values: Vec<String> = notes(cx, &view).fields.iter().map(|f| f.value.clone()).collect();
|
||||
assert_eq!(values, ["Zoe", "Off", "Blue"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_radio_joins_the_group_and_one_stays_on() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = marking(&mut cx, dir.path());
|
||||
add_field(cx, &viewer, &view, FieldKind::Radio, [(72.0, 300.0), (72.0, 300.0)], "Size", &[]);
|
||||
tool(cx, &view, Tool::Field(FieldKind::Radio));
|
||||
click(cx, &view, (100.0, 300.0));
|
||||
frames(cx);
|
||||
let typed = cx.update(|_, app| viewer.read(app).note_panel.read(app).name.clone().expect("drawn").read(app).text().to_string());
|
||||
assert_eq!(typed, "Size", "suggests the page's group");
|
||||
send(cx, &viewer, Change::Design(typed, Vec::new()));
|
||||
notes(cx, &view);
|
||||
tool(cx, &view, Tool::Read);
|
||||
click(cx, &view, (78.0, 306.0));
|
||||
notes(cx, &view);
|
||||
click(cx, &view, (106.0, 306.0));
|
||||
let fields = notes(cx, &view).fields;
|
||||
assert_eq!((fields[1].checked, fields[2].checked), (false, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_on_a_box_while_an_edit_is_queued_does_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = marking(&mut cx, dir.path());
|
||||
add_field(cx, &viewer, &view, FieldKind::Checkbox, [(72.0, 300.0), (72.0, 300.0)], "Agree", &[]);
|
||||
tool(cx, &view, Tool::Read);
|
||||
cx.update(|_, app| view.update(app, |view, _| view.editing = true));
|
||||
click(cx, &view, (78.0, 306.0));
|
||||
assert!(cx.update(|_, app| view.read(app).queue.is_empty()), "nothing queued with old numbers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_list_reads_every_page_and_jumps_to_a_pick() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = dir.path().join("three.pdf");
|
||||
let page: (f32, f32, &[&str]) = (612.0, 792.0, &["Hello"]);
|
||||
std::fs::write(&path, pdf_with(&[page, page, page], &[], &Extras::default())).expect("writes");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
let note = ely_pdf_engine::NewAnnot { page: 2, shape: ely_pdf_engine::Shape::Note((100.0, 500.0)), rgb: None, contents: "Last".into(), author: String::new() };
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.annotate("adding a note", vec![note], cx)));
|
||||
settle(cx, |app| view.read(app).settled());
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.side.update(cx, |side, cx| {
|
||||
side.pane = crate::side::Pane::Notes;
|
||||
cx.notify();
|
||||
})));
|
||||
settle(cx, |app| (0..3).all(|page| view.read(app).notes.contains_key(&page)));
|
||||
let id = cx.update(|_, app| view.read(app).notes[&2].annots[0].id);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.pick_note(2, id, cx)));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| (view.read(app).current, view.read(app).aim)), (2, Some(Aim::Note(Pick::Annot { page: 2, id }))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_field_takes_a_new_name_and_options_and_no_change_is_no_step() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = marking(&mut cx, dir.path());
|
||||
add_field(cx, &viewer, &view, FieldKind::Choice, [(72.0, 340.0), (200.0, 360.0)], "Color", &["Red", "Blue"]);
|
||||
let edited = cx.update(|_, app| view.read(app).edited);
|
||||
click(cx, &view, (100.0, 350.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::Field { .. }))), "the form tool picks a field");
|
||||
send(cx, &viewer, Change::Design("Color".into(), vec!["Red".into(), "Blue".into()]));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), edited, "as it was");
|
||||
send(cx, &viewer, Change::Design("Shade".into(), vec!["Green".into()]));
|
||||
let field = notes(cx, &view).fields[1].clone();
|
||||
assert_eq!((field.name.as_str(), field.options), ("Shade", vec!["Green".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_highlight_before_the_text_is_read_lands_when_it_arrives() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = marking(&mut cx, dir.path());
|
||||
tool(cx, &view, Tool::Highlight);
|
||||
// A read seems in flight, so none starts yet.
|
||||
cx.update(|_, app| view.update(app, |view, _| {
|
||||
view.text.clear();
|
||||
view.asking_text.insert(0);
|
||||
}));
|
||||
drag(cx, &view, (74.0, 66.0), &[(140.0, 66.0)]);
|
||||
assert!(cx.update(|_, app| view.read(app).marking.is_some()), "waits for the text");
|
||||
assert!(notes(cx, &view).annots.is_empty());
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.asking_text.clear();
|
||||
view.page_text(0, cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
assert_eq!(kinds(¬es(cx, &view)), ["Highlight"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_field_on_another_page_gets_a_free_name() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = dir.path().join("two.pdf");
|
||||
let page: (f32, f32, &[&str]) = (612.0, 792.0, &["Hello"]);
|
||||
std::fs::write(&path, pdf_with(&[page, page], &[], &Extras::default())).expect("writes");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
let first = ely_pdf_engine::NewField { kind: FieldKind::Text, name: "Text1".into(), rect: [72.0, 300.0, 272.0, 320.0], options: Vec::new() };
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("adding a field", move |w, doc| w.add_field(doc, 0, &first), cx)));
|
||||
settle(cx, |app| view.read(app).settled());
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.tool = Tool::Field(FieldKind::Text);
|
||||
view.aim = Some(Aim::Note(Pick::NewField { page: 1, rect: [72.0, 300.0, 272.0, 320.0], kind: FieldKind::Text }));
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).note_panel.read(app).shown.is_some());
|
||||
let typed = cx.update(|_, app| viewer.read(app).note_panel.read(app).name.clone().expect("drawn").read(app).text().to_string());
|
||||
assert_eq!(typed, "Text2", "page one holds Text1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_default_name_avoids_a_named_parent() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = dir.path().join("nested.pdf");
|
||||
let extras = Extras { fields: &[0], parent: Some("Text1"), ..Extras::default() };
|
||||
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello"])], &[], &extras)).expect("writes");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.tool = Tool::Field(FieldKind::Text);
|
||||
view.aim = Some(Aim::Note(Pick::NewField { page: 0, rect: [72.0, 300.0, 272.0, 320.0], kind: FieldKind::Text }));
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).note_panel.read(app).shown.is_some());
|
||||
let typed = cx.update(|_, app| viewer.read(app).note_panel.read(app).name.clone().expect("drawn").read(app).text().to_string());
|
||||
assert_eq!(typed, "Text2", "Text1 holds Text1.field0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_default_name_avoids_a_field_with_no_widget() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = dir.path().join("bare.pdf");
|
||||
let extras = Extras { bare: Some(("Text1", "Child")), ..Extras::default() };
|
||||
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello"])], &[], &extras)).expect("writes");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.tool = Tool::Field(FieldKind::Text);
|
||||
view.aim = Some(Aim::Note(Pick::NewField { page: 0, rect: [72.0, 300.0, 272.0, 320.0], kind: FieldKind::Text }));
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).note_panel.read(app).shown.is_some());
|
||||
let typed = cx.update(|_, app| viewer.read(app).note_panel.read(app).name.clone().expect("drawn").read(app).text().to_string());
|
||||
assert_eq!(typed, "Text2", "Text1 has no widget but holds the name");
|
||||
send(cx, &viewer, Change::Design(typed, Vec::new()));
|
||||
assert_eq!(notes(cx, &view).fields.len(), 1, "the field lands");
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//! Organizing, undo, saving and output from the app (C11.2).
|
||||
|
||||
use ely_office::{Closing as _, Dialog, Saving as _, Tabs as _};
|
||||
use ely_pdf_engine::Worker;
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::{file, frames, settle, shown, windowed};
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Three Letter pages: one, two, three.
|
||||
fn three<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"]), (612.0, 792.0, &["three"])];
|
||||
let (viewer, cx) = windowed(cx, dir, &[file(dir, "three.pdf", pages)], true);
|
||||
let view = shown(cx, &viewer);
|
||||
(viewer, cx, view)
|
||||
}
|
||||
|
||||
/// Each page's text, read by a worker of its own.
|
||||
fn texts(path: &std::path::Path) -> Vec<String> {
|
||||
let mut worker = Worker::spawn().expect("a worker");
|
||||
let doc = worker.open(path).expect("opens").doc;
|
||||
let count = worker.pages(doc).expect("counts");
|
||||
(0..count).map(|page| worker.text(doc, page).expect("reads").iter().map(|c| c.c).collect()).collect()
|
||||
}
|
||||
|
||||
fn dirty(cx: &mut VisualTestContext, viewer: &Entity<Viewer>) -> bool {
|
||||
cx.update(|_, app| viewer.read(app).active_tab().expect("a tab").dirty(app))
|
||||
}
|
||||
|
||||
/// Runs `step` on the view, then waits for its edits.
|
||||
fn edit(cx: &mut VisualTestContext, view: &Entity<PdfView>, step: impl FnOnce(&mut PdfView, &mut gpui::Context<PdfView>)) {
|
||||
cx.update(|_, app| view.update(app, step));
|
||||
settle(cx, |app| view.read(app).settled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_delete_marks_the_tab_and_undo_clears_it() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
assert!(!dirty(cx, &viewer));
|
||||
edit(cx, &view, |view, cx| {
|
||||
view.picked = [1].into();
|
||||
view.edit("deleting", |worker, doc| worker.delete_pages(doc, &[1]), cx);
|
||||
});
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).sizes.len()), 2);
|
||||
assert!(dirty(cx, &viewer), "an edit is unsaved");
|
||||
edit(cx, &view, |view, cx| view.undo(false, cx));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).sizes.len()), 3);
|
||||
assert!(!dirty(cx, &viewer), "undone to the file as loaded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_save_writes_the_edits_and_undo_after_it_is_unsaved_again() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
edit(cx, &view, |view, cx| {
|
||||
view.picked = [2].into();
|
||||
let to = 0;
|
||||
view.move_to(to, cx);
|
||||
});
|
||||
assert_eq!(cx.update(|_, app| view.read(app).picked.iter().copied().collect::<Vec<_>>()), [0], "the moved page stays picked");
|
||||
let id = cx.update(|_, app| viewer.read(app).core.active.expect("a tab"));
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx)));
|
||||
settle(cx, |app| !viewer.read(app).active_tab().expect("a tab").dirty(app));
|
||||
assert_eq!(texts(&dir.path().join("three.pdf")), ["three", "one", "two"]);
|
||||
edit(cx, &view, |view, cx| view.undo(false, cx));
|
||||
frames(cx);
|
||||
assert!(dirty(cx, &viewer), "the file still holds the move");
|
||||
edit(cx, &view, |view, cx| view.undo(true, cx));
|
||||
frames(cx);
|
||||
assert!(!dirty(cx, &viewer), "redone to what was saved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edit_queued_behind_another_is_refused() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = three(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.edit("turning", |worker, doc| worker.rotate_pages(doc, &[0], 90), cx);
|
||||
view.edit("deleting", |worker, doc| worker.delete_pages(doc, &[0]), cx);
|
||||
}));
|
||||
// The worker sees one edit at a time.
|
||||
assert_eq!(cx.update(|_, app| (view.read(app).editing, view.read(app).queue.len())), (true, 1));
|
||||
settle(cx, |app| view.read(app).settled());
|
||||
// The delete aimed at the state before the turn.
|
||||
let sizes = cx.update(|_, app| view.read(app).sizes.clone());
|
||||
assert_eq!(sizes, [(792.0, 612.0), (612.0, 792.0), (612.0, 792.0)], "only the turn landed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_read_from_before_an_edit_is_dropped() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = three(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.page_text(0, cx);
|
||||
// As an edit landing first does.
|
||||
view.version += 1;
|
||||
}));
|
||||
frames(cx);
|
||||
assert!(cx.update(|_, app| view.read(app).text.is_empty()), "page one's old text is not kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_an_edited_tab_asks_first() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
edit(cx, &view, |view, cx| view.edit("turning", |worker, doc| worker.rotate_pages(doc, &[0], 90), cx));
|
||||
let id = cx.update(|_, app| viewer.read(app).core.active.expect("a tab"));
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.request_close(id, ely_office::After::Tab, window, cx)));
|
||||
settle(cx, |app| matches!(viewer.read(app).core.dialog, Some(Dialog::Unsaved { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_and_split_write_new_files_and_refuse_to_overwrite() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
let id = cx.update(|_, app| viewer.read(app).core.active.expect("a tab"));
|
||||
let part = dir.path().join("part.pdf");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_pages(id, vec![0, 2], part.clone(), cx)));
|
||||
settle(cx, |_| part.exists());
|
||||
frames(cx);
|
||||
assert_eq!(texts(&part), ["one", "three"]);
|
||||
// The open file itself is refused.
|
||||
let open = dir.path().join("three.pdf");
|
||||
let before = std::fs::read(&open).expect("reads");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_pages(id, vec![0], open.clone(), cx)));
|
||||
frames(cx);
|
||||
assert_eq!(std::fs::read(&open).expect("reads"), before);
|
||||
let out = dir.path().join("out");
|
||||
std::fs::create_dir(&out).expect("a folder");
|
||||
std::fs::write(out.join("three-2.pdf"), b"keep").expect("writes");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_parts(id, 1, out.clone(), cx)));
|
||||
frames(cx);
|
||||
let mut names: Vec<String> = std::fs::read_dir(&out).expect("lists").map(|entry| entry.expect("an entry").file_name().to_string_lossy().into_owned()).collect();
|
||||
names.sort();
|
||||
assert_eq!(names, ["three-2.pdf"], "a taken name writes nothing");
|
||||
std::fs::remove_file(out.join("three-2.pdf")).expect("removes");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_parts(id, 2, out.clone(), cx)));
|
||||
settle(cx, |_| out.join("three-2.pdf").exists());
|
||||
frames(cx);
|
||||
assert_eq!((texts(&out.join("three-1.pdf")), texts(&out.join("three-2.pdf"))), (vec!["one".into(), "two".into()], vec!["three".into()]));
|
||||
assert_eq!(cx.update(|_, app| view.read(app).sizes.len()), 3, "the document stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_margins_are_millimetres() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_crop(window, cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
if let Some(crate::ask::Ask::Crop { margins, .. }) = viewer.ask.as_mut() {
|
||||
*margins = [10.0, 0.0, 0.0, 25.4];
|
||||
}
|
||||
viewer.crop(cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
let size = cx.update(|_, app| view.read(app).sizes[0]);
|
||||
let mm = 72.0 / 25.4;
|
||||
assert!((size.0 - (612.0 - 72.0)).abs() < 0.1 && (size.1 - (792.0 - 10.0 * mm)).abs() < 0.1, "{size:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_follow_plain_range_and_toggle_clicks() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = three(&mut cx, dir.path());
|
||||
let picked = |cx: &mut VisualTestContext| cx.update(|_, app| view.read(app).picked.iter().copied().collect::<Vec<_>>());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.pick(0, (false, false), cx)));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.pick(2, (true, false), cx)));
|
||||
assert_eq!(picked(cx), [0, 1, 2]);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.pick(1, (false, true), cx)));
|
||||
assert_eq!(picked(cx), [0, 2]);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).chosen_pages()), [0, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_range_pick_starts_over_after_pages_move() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.pick(2, (false, false), cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.delete(cx)));
|
||||
settle(cx, |app| view.read(app).sizes.len() == 2);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.pick(0, (true, false), cx)));
|
||||
assert_eq!(cx.update(|_, app| view.read(app).chosen_pages()), [0], "no range from a page that went");
|
||||
edit(cx, &view, |view, cx| view.edit("blank", |worker, doc| worker.insert_blank(doc, 1, (100.0, 100.0)), cx));
|
||||
assert_eq!(cx.update(|_, app| view.read(app).sizes.len()), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_edit_lets_a_waiting_save_go() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
let id = cx.update(|_, app| viewer.read(app).core.active.expect("a tab"));
|
||||
let ran = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("cropping", |worker, doc| worker.crop_pages(doc, &[0], [900.0; 4]), cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, _| {
|
||||
let ran = ran.clone();
|
||||
viewer.when_settled(id, move |_, _, _| ran.set(true));
|
||||
}));
|
||||
settle(cx, |_| ran.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_turned_view_crops_what_is_seen() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, _| view.rotate = 90));
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_crop(window, cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
if let Some(crate::ask::Ask::Crop { margins, .. }) = viewer.ask.as_mut() {
|
||||
*margins = [25.4, 0.0, 0.0, 0.0];
|
||||
}
|
||||
viewer.crop(cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
let (w, h) = cx.update(|_, app| view.read(app).sizes[0]);
|
||||
assert!((w - 540.0).abs() < 0.1 && (h - 792.0).abs() < 0.1, "the page's left went: {w} x {h}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_window_closes_its_tabs_off_the_window() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = three(&mut cx, dir.path());
|
||||
assert!(!cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.should_close(window, cx))), "not at once");
|
||||
drop(view);
|
||||
// Once its tab closes, the window itself goes.
|
||||
for _ in 0..1000 {
|
||||
cx.executor().tick();
|
||||
if cx.cx.update(|app| app.windows().is_empty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("the window stayed");
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! Exports and the repaired copy (C11.8).
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ely_office::{Saving as _, Tabs as _};
|
||||
use ely_pdf_engine::Optimizing;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use super::{file, settle, shown, windowed};
|
||||
use crate::output::{Kind, image_names};
|
||||
|
||||
#[test]
|
||||
fn image_names_pad_alike() {
|
||||
assert_eq!(image_names("doc", &[0, 9], 12), ["doc-01.png", "doc-10.png"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_export_as_images_and_text() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = file(dir.path(), "doc.pdf", &[(612.0, 792.0, &["First"]), (612.0, 792.0, &["Second"])]);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
shown(cx, &viewer);
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
let targets: Vec<PathBuf> = ["doc-1.png", "doc-2.png"].iter().map(|name| dir.path().join(name)).collect();
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_images(id, (vec![0, 1], 0), 72.0, targets.clone(), cx)));
|
||||
settle(cx, |_| targets.iter().all(|target| std::fs::metadata(target).is_ok_and(|meta| meta.len() > 100)));
|
||||
let txt = dir.path().join("doc.txt");
|
||||
let how = Optimizing { compress: true, from: 300, to: 0 };
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_export(id, Kind::Text, how, txt.clone(), cx)));
|
||||
settle(cx, |_| txt.is_file());
|
||||
let text = std::fs::read_to_string(&txt).expect("UTF-8");
|
||||
assert!(text.contains("First") && text.contains("Second"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_file_opens_repaired_and_saves_as_a_new_file() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = file(dir.path(), "broken.pdf", &[(612.0, 792.0, &["Hello"])]);
|
||||
let mut bytes = std::fs::read(&path).expect("reads");
|
||||
let at = bytes.windows(9).rposition(|w| w == b"startxref").expect("the trailer");
|
||||
bytes.truncate(at);
|
||||
bytes.extend_from_slice(b"startxref\n9\n%%EOF\n");
|
||||
std::fs::write(&path, &bytes).expect("writes");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true);
|
||||
let view = shown(cx, &viewer);
|
||||
assert_eq!(cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.risk)), Some(ely_office::Risk::Repaired));
|
||||
assert!(cx.update(|_, app| view.read(app).repair.as_ref().is_some_and(|report| !report.is_empty())));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.show_repair(true, cx)));
|
||||
super::frames(cx);
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, path.clone(), None, window, cx)));
|
||||
super::frames(cx);
|
||||
assert_eq!(std::fs::read(&path).expect("reads"), bytes, "Save As refuses the damaged original");
|
||||
let fixed = dir.path().join("fixed.pdf");
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, fixed.clone(), None, window, cx)));
|
||||
settle(cx, |app| fixed.is_file() && viewer.read(app).active_tab().is_some_and(|tab| tab.risk.is_none() && tab.view != view));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edit_made_while_saving_stays_and_the_tab_reopens_after_the_next_save() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = file(dir.path(), "broken.pdf", &[(612.0, 792.0, &["Hello"])]);
|
||||
let mut bytes = std::fs::read(&path).expect("reads");
|
||||
let at = bytes.windows(9).rposition(|w| w == b"startxref").expect("the trailer");
|
||||
bytes.truncate(at);
|
||||
bytes.extend_from_slice(b"startxref\n9\n%%EOF\n");
|
||||
std::fs::write(&path, &bytes).expect("writes");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let (id, fixed) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("fixed.pdf"));
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.write_as(id, fixed.clone(), None, window, cx);
|
||||
// Lands after the save took its snapshot.
|
||||
view.update(cx, |view, cx| view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx));
|
||||
}));
|
||||
settle(cx, |app| fixed.is_file() && view.read(app).edited == 1 && viewer.read(app).active_tab().is_some_and(|tab| !tab.exporting));
|
||||
for _ in 0..10 {
|
||||
super::frames(cx);
|
||||
}
|
||||
let tab = cx.update(|_, app| viewer.read(app).active_tab().map(|tab| (tab.view == view, tab.dirty(app))));
|
||||
assert_eq!(tab, Some((true, true)), "the edit stays, unsaved");
|
||||
assert_eq!(cx.update(|_, app| view.read(app).sizes[0]), (792.0, 612.0));
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx)));
|
||||
settle(cx, |app| viewer.read(app).active_tab().is_some_and(|tab| tab.view != view && !tab.reopen));
|
||||
let reopened = shown(cx, &viewer);
|
||||
assert_eq!(cx.update(|_, app| reopened.read(app).sizes[0]), (792.0, 612.0), "the reopened file holds the edit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picked_pages_from_before_an_edit_export_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = file(dir.path(), "doc.pdf", &[(100.0, 100.0, &["One"]), (200.0, 100.0, &["Two"]), (300.0, 100.0, &["Three"])]);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
let target = dir.path().join("doc-2.png");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let version = view.read(cx).version;
|
||||
view.update(cx, |view, cx| view.edit("deleting", |w, doc| w.delete_pages(doc, &[0]), cx));
|
||||
viewer.write_images(id, (vec![1], version), 72.0, vec![target.clone()], cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).edited == 1 && view.read(app).settled());
|
||||
for _ in 0..10 {
|
||||
super::frames(cx);
|
||||
}
|
||||
assert!(!target.exists(), "page 2 is another page now");
|
||||
}
|
||||
|
||||
/// A page drawing `/Im`, a picture that is no zlib.
|
||||
fn broken_picture(dir: &std::path::Path, height: f32, y: f32) -> (PathBuf, Vec<u8>) {
|
||||
let picture = "<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length 12 >>\nstream\nnot a zlib!!\nendstream".to_string();
|
||||
let page = format!(" /MediaBox [0 0 612 {height}] /Resources << /XObject << /Im 5 0 R >> >>");
|
||||
let bytes = ely_pdf_engine::sample::one_page("", &page, &format!("q 100 0 0 100 72 {y} cm /Im Do Q"), &[picture]);
|
||||
let path = dir.join("broken.pdf");
|
||||
std::fs::write(&path, &bytes).expect("writes");
|
||||
(path, bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_picture_that_breaks_as_it_draws_stops_an_overwrite() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (path, bytes) = broken_picture(dir.path(), 792.0, 600.0);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true);
|
||||
shown(cx, &viewer);
|
||||
settle(cx, |app| viewer.read(app).active_tab().and_then(|tab| tab.risk) == Some(ely_office::Risk::Repaired));
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx)));
|
||||
for _ in 0..10 {
|
||||
super::frames(cx);
|
||||
}
|
||||
assert_eq!(std::fs::read(&path).expect("reads"), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_picture_never_drawn_is_checked_before_an_overwrite() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (one, _) = broken_picture(dir.path(), 792.0, 600.0);
|
||||
// Thirty blank pages first: the picture's page is never drawn.
|
||||
let mut worker = ely_pdf_engine::Worker::spawn().expect("the worker");
|
||||
let mut doc = worker.open(&one).expect("opens").doc;
|
||||
for _ in 0..30 {
|
||||
let mark = worker.insert_blank(doc, 0, (612.0, 792.0)).expect("inserts");
|
||||
doc = doc.at(&mark).expect("the new state");
|
||||
}
|
||||
let path = dir.path().join("long.pdf");
|
||||
worker.save(doc, &path).expect("saves");
|
||||
let bytes = std::fs::read(&path).expect("reads");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true);
|
||||
shown(cx, &viewer);
|
||||
for _ in 0..10 {
|
||||
super::frames(cx);
|
||||
}
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
assert_eq!(cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.risk)), None, "nothing drawn yet");
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx)));
|
||||
settle(cx, |app| viewer.read(app).active_tab().and_then(|tab| tab.risk) == Some(ely_office::Risk::Repaired));
|
||||
assert_eq!(std::fs::read(&path).expect("reads"), bytes, "Save turned into Save As");
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Limits, the owner password and protected copies (C11.7).
|
||||
|
||||
use ely_pdf_engine::{Lock, Worker, proto::may, sample::{Extras, pdf_with}};
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::{frames, settle, shown, windowed};
|
||||
use crate::protect::Secure;
|
||||
use crate::select::Selection;
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Opens a copy with no open password allowing only `allow`.
|
||||
fn limited<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path, allow: u32) -> (Entity<Viewer>, &'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let plain = dir.join("plain.pdf");
|
||||
std::fs::write(&plain, pdf_with(&[(612.0, 792.0, &["Hello world"])], &[], &Extras::default())).expect("writes");
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let doc = worker.open(&plain).expect("opens").doc;
|
||||
let path = dir.join("limited.pdf");
|
||||
let lock = Lock { user: String::new(), owner: "owner".into(), allow };
|
||||
worker.protect(doc, &path, Some(&lock)).expect("protects");
|
||||
let (viewer, cx) = windowed(cx, dir, &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
(viewer, cx, view)
|
||||
}
|
||||
|
||||
fn type_in(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, owner: bool, text: &str) {
|
||||
cx.update(|_, app| {
|
||||
let field = match viewer.read(app).secure.as_ref() {
|
||||
Some(Secure::Owner { field, .. }) => field.clone(),
|
||||
Some(Secure::Protect { user, owner: field, .. }) => if owner { field.clone() } else { user.clone() },
|
||||
None => panic!("no dialog"),
|
||||
};
|
||||
field.update(app, |field, cx| field.set_text(text.to_string(), cx));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_hold_until_the_owner_password_lifts_them() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = limited(&mut cx, dir.path(), may::PRINT);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).allowed) & (may::ASSEMBLE | may::OWNER), 0);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx)));
|
||||
settle(cx, |app| view.read(app).settled());
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 0, "refused");
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_owner(window, cx)));
|
||||
type_in(cx, &viewer, true, "wrong");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.try_owner(cx)));
|
||||
frames(cx);
|
||||
assert!(cx.update(|_, app| viewer.read(app).secure.is_some()), "asks again");
|
||||
type_in(cx, &viewer, true, "owner");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.try_owner(cx)));
|
||||
settle(cx, |app| view.read(app).allowed & may::OWNER != 0);
|
||||
assert!(cx.update(|_, app| viewer.read(app).secure.is_none()));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx)));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_is_refused_where_the_file_forbids_it() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = limited(&mut cx, dir.path(), may::PRINT);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.page_text(0, cx)));
|
||||
settle(cx, |app| view.read(app).text.contains_key(&0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.selection = Some(Selection { anchor: (0, (70.0, 60.0)), head: (0, (140.0, 75.0)) });
|
||||
view.copy(cx);
|
||||
}));
|
||||
frames(cx);
|
||||
assert!(cx.update(|_, app| app.read_from_clipboard()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protecting_needs_a_distinct_owner_password_and_writes_a_locked_copy() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = super::file(dir.path(), "doc.pdf", &[(612.0, 792.0, &["Hello"])]);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
shown(cx, &viewer);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_protect(window, cx)));
|
||||
type_in(cx, &viewer, false, "same");
|
||||
type_in(cx, &viewer, true, "same");
|
||||
assert!(cx.update(|_, app| viewer.read(app).lock(app)).is_none(), "owner equals open");
|
||||
type_in(cx, &viewer, true, "owner");
|
||||
let lock = cx.update(|_, app| viewer.read(app).lock(app)).flatten().expect("a lock");
|
||||
let tab = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
let target = dir.path().join("locked.pdf");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_protected(tab, Some(lock), target.clone(), cx)));
|
||||
settle(cx, |_| target.is_file());
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let opened = worker.open(&target).expect("opens");
|
||||
assert!(opened.locked);
|
||||
assert!(worker.unlock(opened.doc, "same").expect("tries"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_owner_reply_closes_only_its_own_dialog() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = limited(&mut cx, dir.path(), may::PRINT);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_owner(window, cx)));
|
||||
type_in(cx, &viewer, true, "owner");
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.try_owner(cx);
|
||||
// A newer dialog opens before the answer.
|
||||
viewer.secure = None;
|
||||
viewer.ask_owner(window, cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).allowed & may::OWNER != 0);
|
||||
assert!(cx.update(|_, app| viewer.read(app).secure.is_some()), "the newer dialog stays");
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Marking, removing and cleaning from the app (C11.6).
|
||||
|
||||
use ely_pdf_engine::{Cleaning, Worker, sample::{Extras, pdf_with}};
|
||||
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::input::over;
|
||||
use super::{frames, settle, shown, windowed};
|
||||
use crate::content::Tool;
|
||||
use crate::redact::Removal;
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Two lines and a title, actual size.
|
||||
fn opened<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let path = dir.join("doc.pdf");
|
||||
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world", "Second line"])], &[], &Extras::default())).expect("writes");
|
||||
let (viewer, cx) = windowed(cx, dir, &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = crate::layout::Zoom::Scale(1.0);
|
||||
cx.notify();
|
||||
}));
|
||||
frames(cx);
|
||||
(viewer, cx, view)
|
||||
}
|
||||
|
||||
fn drag(cx: &mut VisualTestContext, view: &Entity<PdfView>, from: (f32, f32), to: (f32, f32)) {
|
||||
frames(cx);
|
||||
let (a, b) = (over(cx, view, 0, from), over(cx, view, 0, to));
|
||||
cx.simulate_mouse_down(a, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(b, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(b, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
|
||||
fn text_of(path: &std::path::Path) -> String {
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let doc = worker.open(path).expect("opens").doc;
|
||||
worker.text(doc, 0).expect("text").iter().map(|c| c.c).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn areas_mark_and_a_click_unmarks() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (_, cx, view) = opened(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.set_tool(Tool::RedactArea, cx)));
|
||||
drag(cx, &view, (100.0, 300.0), (200.0, 350.0));
|
||||
assert_eq!(cx.update(|_, app| view.read(app).redactions.len()), 1);
|
||||
drag(cx, &view, (150.0, 320.0), (150.0, 320.0));
|
||||
assert!(cx.update(|_, app| view.read(app).redactions.is_empty()), "a click inside takes it back");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marked_text_is_removed_into_a_new_file() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.set_tool(Tool::RedactText, cx)));
|
||||
drag(cx, &view, (73.0, 66.0), (110.0, 66.0));
|
||||
settle(cx, |app| !view.read(app).redactions.is_empty());
|
||||
let out = dir.path().join("out.pdf");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.ask_redact(cx)));
|
||||
let asked = cx.update(|_, app| viewer.update(app, |viewer, _| viewer.removal.take().expect("asked")));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_removal(asked, out.clone(), cx)));
|
||||
settle(cx, |_| out.is_file());
|
||||
let left = text_of(&out);
|
||||
assert!(!left.contains("Hello") && left.contains("Second line"), "{left}");
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 0, "the open document is untouched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edit_drops_the_marks_and_none_asks_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.mark_regions(0, vec![[72.0, 60.0, 120.0, 80.0]], None, cx)));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx)));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
assert!(cx.update(|_, app| view.read(app).redactions.is_empty()), "pages may have moved");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.ask_redact(cx)));
|
||||
assert!(cx.update(|_, app| viewer.read(app).removal.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleaning_counts_first_then_writes_a_clean_file() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("titling", |w, doc| w.set_properties(doc, &[("Title", "Plan")]), cx)));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.ask_clean(cx)));
|
||||
settle(cx, |app| viewer.read(app).removal.is_some());
|
||||
let (tab, version, reach) = cx.update(|_, app| match viewer.read(app).removal.as_ref() {
|
||||
Some(Removal::Clean { tab, version, reach, .. }) => (*tab, *version, reach.clone()),
|
||||
_ => panic!("a cleaning"),
|
||||
});
|
||||
assert!(reach.meta >= 1, "{reach:?}");
|
||||
let out = dir.path().join("clean.pdf");
|
||||
let what = Cleaning { meta: true, ..Cleaning::default() };
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_removal(Removal::Clean { tab, version, reach, what }, out.clone(), cx)));
|
||||
settle(cx, |_| out.is_file());
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let doc = worker.open(&out).expect("opens").doc;
|
||||
assert_eq!(worker.properties(doc).expect("reads").title, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_changed_after_asking_write_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.mark_regions(0, vec![[72.0, 52.0, 120.0, 78.0]], None, cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.ask_redact(cx)));
|
||||
let asked = cx.update(|_, app| viewer.update(app, |viewer, _| viewer.removal.take().expect("asked")));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.mark_regions(0, vec![[72.0, 76.0, 200.0, 102.0]], None, cx)));
|
||||
let out = dir.path().join("out.pdf");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_removal(asked, out.clone(), cx)));
|
||||
for _ in 0..10 {
|
||||
frames(cx);
|
||||
}
|
||||
assert!(!out.exists(), "the marks are not what was confirmed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scan_for_a_tab_left_behind_opens_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (a, b) = (dir.path().join("a.pdf"), dir.path().join("b.pdf"));
|
||||
for path in [&a, &b] {
|
||||
std::fs::write(path, pdf_with(&[(612.0, 792.0, &["Hello"])], &[], &Extras::default())).expect("writes");
|
||||
}
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[a, b], true);
|
||||
let first = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.core.active = Some(first);
|
||||
viewer.ask_clean(cx);
|
||||
viewer.core.active = Some(viewer.core.tabs[1].id);
|
||||
}));
|
||||
for _ in 0..20 {
|
||||
frames(cx);
|
||||
}
|
||||
assert!(cx.update(|_, app| viewer.read(app).removal.is_none()), "another tab is in front");
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Signatures beside the pages, signed copies, signed files (C11.7).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ely_office::{Saving as _, Tabs as _};
|
||||
use ely_pdf_engine::{Digest, Signing, Worker, sample::{Extras, pdf_with}};
|
||||
use ely_platform::trust::Trust;
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext};
|
||||
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
use super::{settle, shown, windowed};
|
||||
use crate::sign::SignAt;
|
||||
|
||||
fn signing(name: &str) -> Signing {
|
||||
let pfx = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/pdf/signer.p12");
|
||||
Signing {
|
||||
page: 0,
|
||||
object: None,
|
||||
rect: [72.0, 600.0, 272.0, 660.0],
|
||||
name: name.into(),
|
||||
pfx: pfx.to_str().expect("UTF-8").into(),
|
||||
pfx_password: "ely".into(),
|
||||
reason: String::new(),
|
||||
location: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn plain(dir: &Path) -> PathBuf {
|
||||
let path = dir.join("plain.pdf");
|
||||
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world"])], &[], &Extras::default())).expect("writes");
|
||||
path
|
||||
}
|
||||
|
||||
fn signed(dir: &Path) -> PathBuf {
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let doc = worker.open(&plain(dir)).expect("opens").doc;
|
||||
let path = dir.join("signed.pdf");
|
||||
worker.sign(doc, &path, &signing("Signature1")).expect("signs");
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_signed_file_lists_its_checks_and_saves_as_a_copy() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = signed(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
assert_eq!(cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.risk)), Some(ely_office::Risk::Signed));
|
||||
let path = tab_path(cx, &viewer);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.ask_signatures(path, cx)));
|
||||
settle(cx, |app| view.read(app).signatures.is_some());
|
||||
cx.update(|_, app| {
|
||||
let read = view.read(app);
|
||||
let checked = &read.signatures.as_ref().expect("read").1[0];
|
||||
assert_eq!(checked.field.signed.as_ref().map(|s| s.digest), Some(Digest::Intact));
|
||||
assert!(matches!(checked.trust, Some(Ok(Trust::Untrusted(_)))), "self-signed");
|
||||
});
|
||||
// The dialog draws every fact.
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.show_signatures(true, cx)));
|
||||
super::frames(cx);
|
||||
assert!(cx.update(|_, app| viewer.read(app).signs_open));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signing_writes_a_signed_copy_and_leaves_the_document() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[plain(dir.path())], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let at = SignAt { page: 0, object: None, rect: [72.0, 600.0, 272.0, 660.0], version: 0 };
|
||||
cx.update(|_, app| view.update(app, |view, _| view.sign_at = Some(at)));
|
||||
let (tab, target) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("out.pdf"));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_signed(tab, at, signing(""), target.clone(), cx)));
|
||||
settle(cx, |app| target.is_file() && view.read(app).sign_at.is_none());
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let doc = worker.open(&target).expect("opens").doc;
|
||||
let found = worker.signatures(doc, &target).expect("reads");
|
||||
assert_eq!((found.len(), found[0].name.as_str()), (1, "Signature1"));
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_place_from_before_an_edit_signs_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[plain(dir.path())], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let at = SignAt { page: 0, object: None, rect: [72.0, 600.0, 272.0, 660.0], version: 0 };
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.sign_at = Some(at);
|
||||
view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx)
|
||||
}));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
let (tab, target) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("out.pdf"));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_signed(tab, at, signing(""), target.clone(), cx)));
|
||||
super::frames(cx);
|
||||
assert!(!target.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_finished_signing_clears_only_its_own_place() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (a, b) = (plain(dir.path()), dir.path().join("b.pdf"));
|
||||
std::fs::copy(&a, &b).expect("copies");
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[a, b], true);
|
||||
let (first, second) = cx.update(|_, app| (viewer.read(app).core.tabs[0].view.clone(), viewer.read(app).core.tabs[1].view.clone()));
|
||||
let at = SignAt { page: 0, object: None, rect: [72.0, 600.0, 272.0, 660.0], version: 0 };
|
||||
cx.update(|_, app| {
|
||||
first.update(app, |view, _| view.sign_at = Some(at));
|
||||
second.update(app, |view, _| view.sign_at = Some(at));
|
||||
});
|
||||
let (tab, target) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("out.pdf"));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_signed(tab, at, signing(""), target.clone(), cx)));
|
||||
settle(cx, |app| target.is_file() && first.read(app).sign_at.is_none());
|
||||
assert_eq!(cx.update(|_, app| second.read(app).sign_at), Some(at), "the other tab keeps its place");
|
||||
}
|
||||
|
||||
fn tab_path(cx: &mut VisualTestContext, viewer: &Entity<Viewer>) -> PathBuf {
|
||||
cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.session.path().map(PathBuf::from)).expect("a file"))
|
||||
}
|
||||
|
||||
/// Whether the tab's file lists a signature.
|
||||
fn shows_signed(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, view: &Entity<PdfView>) -> bool {
|
||||
let path = tab_path(cx, viewer);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.ask_signatures(path.clone(), cx)));
|
||||
settle(cx, |app| view.read(app).signatures.as_ref().is_some_and(|(read, _)| *read == path));
|
||||
cx.update(|_, app| view.read(app).signatures.as_ref().expect("read").1.iter().any(|c| c.field.signed.is_some()))
|
||||
}
|
||||
|
||||
/// Saves the signed tab as `copy`; waits for the reopen.
|
||||
fn save_signed_as(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, old: &Entity<PdfView>, copy: &Path) -> Entity<PdfView> {
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
let target = copy.to_path_buf();
|
||||
std::fs::write(copy, b"").expect("a place");
|
||||
let copy = std::fs::canonicalize(copy).expect("the real path");
|
||||
std::fs::remove_file(©).expect("clears the place");
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, target, None, window, cx)));
|
||||
settle(cx, |app| {
|
||||
let read = viewer.read(app);
|
||||
read.core.tabs.len() == 1 && read.active_tab().is_some_and(|tab| tab.session.path() == Some(copy.as_path()) && tab.view != *old)
|
||||
});
|
||||
shown(cx, viewer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_signed_file_saved_as_a_copy_reopens_as_the_copy() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = signed(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
assert!(shows_signed(cx, &viewer, &view));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx)));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
let copy = dir.path().join("copy.pdf");
|
||||
let reopened = save_signed_as(cx, &viewer, &view, ©);
|
||||
let tab = cx.update(|_, app| (viewer.read(app).active_tab().map(|tab| (tab.risk, tab.dirty(app)))));
|
||||
assert_eq!(tab, Some((None, false)), "an unsigned, saved copy");
|
||||
assert!(!shows_signed(cx, &viewer, &reopened), "the copy holds no signature");
|
||||
assert_eq!(cx.update(|_, app| reopened.read(app).sizes[0]), (792.0, 612.0), "the edit is in the copy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signing_after_save_as_signs_the_copy() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = signed(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let copy = dir.path().join("copy.pdf");
|
||||
let reopened = save_signed_as(cx, &viewer, &view, ©);
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let read = worker.open(©).expect("opens").doc;
|
||||
let field = worker.signatures(read, ©).expect("reads")[0].clone();
|
||||
assert!(field.signed.is_none(), "the copy's field is empty");
|
||||
let at = SignAt { page: 0, object: Some(field.object), rect: field.rect, version: cx.update(|_, app| reopened.read(app).version) };
|
||||
cx.update(|_, app| reopened.update(app, |view, _| view.sign_at = Some(at)));
|
||||
let (tab, target) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("resigned.pdf"));
|
||||
let mut how = signing("");
|
||||
how.object = Some(field.object);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_signed(tab, at, how, target.clone(), cx)));
|
||||
settle(cx, |_| target.is_file());
|
||||
let doc = worker.open(&target).expect("opens").doc;
|
||||
let found = worker.signatures(doc, &target).expect("reads");
|
||||
assert_eq!(found.len(), 1, "only the copy's one field");
|
||||
assert_eq!(found[0].signed.as_ref().map(|s| (s.digest, s.later)), Some((Digest::Intact, false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_save_as_keeps_the_signed_file_and_its_signatures() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = signed(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let (id, blocked) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("directory.pdf"));
|
||||
std::fs::create_dir(&blocked).expect("a directory in the way");
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, blocked.clone(), None, window, cx)));
|
||||
for _ in 0..20 {
|
||||
super::frames(cx);
|
||||
}
|
||||
assert_eq!(cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.risk)), Some(ely_office::Risk::Signed));
|
||||
assert!(shows_signed(cx, &viewer, &view), "the signed file and its list are as they were");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reopening_the_dialog_checks_the_file_as_it_is_now() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = signed(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let digest = |cx: &mut VisualTestContext| {
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.show_signatures(true, cx)));
|
||||
settle(cx, |app| view.read(app).signatures.is_some());
|
||||
let digest = cx.update(|_, app| view.read(app).signatures.as_ref().expect("read").1[0].field.signed.as_ref().map(|s| s.digest));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.show_signatures(false, cx)));
|
||||
digest
|
||||
};
|
||||
assert_eq!(digest(cx), Some(Digest::Intact));
|
||||
let mut bytes = std::fs::read(&path).expect("reads");
|
||||
let at = bytes.windows(5).position(|w| w == b"Hello").expect("the text");
|
||||
bytes[at] = b'J';
|
||||
std::fs::write(&path, bytes).expect("writes");
|
||||
assert_eq!(digest(cx), Some(Digest::Changed));
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
//! Links, bookmarks, properties, attachments, OCR from the app (C11.5).
|
||||
|
||||
use ely_pdf_engine::{
|
||||
Worker,
|
||||
sample::{Extras, pdf, pdf_with, scan},
|
||||
};
|
||||
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::input::over;
|
||||
use super::{frames, settle, shown, windowed};
|
||||
use crate::annotate::Pick;
|
||||
use crate::content::{Aim, Tool};
|
||||
use crate::marking::Change;
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Two pages, a bookmark, a link to page two.
|
||||
fn opened<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let path = dir.join("doc.pdf");
|
||||
let pages: [(f32, f32, &[&str]); 2] = [(612.0, 792.0, &["Page one"]), (612.0, 792.0, &["Page two"])];
|
||||
std::fs::write(&path, pdf_with(&pages, &[("One", 0, &[])], &Extras { links: &[(0, 1)], ..Extras::default() })).expect("writes");
|
||||
let (viewer, cx) = windowed(cx, dir, &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.zoom = crate::layout::Zoom::Scale(1.0);
|
||||
view.mode = crate::layout::Mode::Single;
|
||||
cx.notify();
|
||||
}));
|
||||
settle(cx, |app| view.read(app).links.contains_key(&0));
|
||||
(viewer, cx, view)
|
||||
}
|
||||
|
||||
fn press(cx: &mut VisualTestContext, view: &Entity<PdfView>, from: (f32, f32), to: (f32, f32)) {
|
||||
frames(cx);
|
||||
let (a, b) = (over(cx, view, 0, from), over(cx, view, 0, to));
|
||||
cx.simulate_mouse_down(a, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_move(b, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(b, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
|
||||
fn landed(cx: &mut VisualTestContext, view: &Entity<PdfView>, edits: u64) {
|
||||
settle(cx, |app| view.read(app).edited == edits && view.read(app).settled() && view.read(app).links.contains_key(&0));
|
||||
}
|
||||
|
||||
fn send(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, change: Change) {
|
||||
frames(cx);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let shown = viewer.note_panel.read(cx).shown.expect("the panel follows the pick");
|
||||
viewer.change_note(shown, change, cx)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_follows_a_link_and_the_link_tool_adds_retargets_and_deletes() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
// The sample link covers 72..200 across, 692..720 down.
|
||||
press(cx, &view, (100.0, 700.0), (100.0, 700.0));
|
||||
settle(cx, |app| view.read(app).current == 1);
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.show(0, 0.0, cx)));
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.set_tool(Tool::Link, cx)));
|
||||
press(cx, &view, (72.0, 300.0), (200.0, 320.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::NewLink { .. }))));
|
||||
send(cx, &viewer, Change::Target("javascript:x".into()));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 0, "a bad target is refused");
|
||||
send(cx, &viewer, Change::Target("https://example.com/".into()));
|
||||
landed(cx, &view, 1);
|
||||
let links = cx.update(|_, app| view.read(app).links[&0].clone());
|
||||
assert_eq!((links.len(), links[1].uri.as_str()), (2, "https://example.com/"));
|
||||
press(cx, &view, (100.0, 310.0), (100.0, 310.0));
|
||||
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Note(Pick::Link { index: 1, .. }))));
|
||||
send(cx, &viewer, Change::Target("2".into()));
|
||||
landed(cx, &view, 2);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).links[&0][1].page), Some(1));
|
||||
press(cx, &view, (100.0, 310.0), (100.0, 310.0));
|
||||
send(cx, &viewer, Change::Delete);
|
||||
landed(cx, &view, 3);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).links[&0].len()), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookmarks_add_after_the_pick_rename_and_delete() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
let titles = |cx: &mut VisualTestContext| cx.update(|_, app| view.read(app).marks.iter().map(|m| m.title.clone()).collect::<Vec<_>>());
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_bookmark(true, window, cx)));
|
||||
let typed = cx.update(|_, app| viewer.read(app).naming.as_ref().expect("asked").field.read(app).text().to_string());
|
||||
let label = cx.update(|_, app| Viewer::text(app, "pdf.page.label", &[("n", "1")]).to_string());
|
||||
assert_eq!(typed, label, "named for the page");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.name_bookmark("Start".into(), cx)));
|
||||
landed(cx, &view, 1);
|
||||
assert_eq!(titles(cx), ["One", "Start"]);
|
||||
cx.update(|_, app| view.update(app, |view, _| view.mark_pick = Some(0)));
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_bookmark(false, window, cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.name_bookmark("First".into(), cx)));
|
||||
landed(cx, &view, 2);
|
||||
assert_eq!(titles(cx), ["First", "Start"]);
|
||||
cx.update(|_, app| view.update(app, |view, _| view.mark_pick = Some(1)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.delete_bookmark(cx)));
|
||||
landed(cx, &view, 3);
|
||||
assert_eq!(titles(cx), ["First"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_show_and_set_what_changed() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_properties(window, cx)));
|
||||
settle(cx, |app| viewer.read(app).props.is_some());
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let field = viewer.props.as_ref().expect("open").fields[0].clone();
|
||||
field.update(cx, |field, cx| field.set_text("Report", cx));
|
||||
viewer.set_properties(cx)
|
||||
}));
|
||||
landed(cx, &view, 1);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_properties(window, cx)));
|
||||
settle(cx, |app| viewer.read(app).props.is_some());
|
||||
assert_eq!(cx.update(|_, app| viewer.read(app).props.as_ref().expect("open").read.title.clone()), Some("Report".into()));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.set_properties(cx)));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 1, "nothing changed, no step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachments_list_save_and_remove() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, view) = opened(&mut cx, dir.path());
|
||||
let file = dir.path().join("notes.txt");
|
||||
std::fs::write(&file, "notes").expect("writes");
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.edit("attaching", move |w, doc| w.attach(doc, "notes.txt", &file), cx)));
|
||||
landed(cx, &view, 1);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.side.update(cx, |side, cx| {
|
||||
side.pane = crate::side::Pane::Files;
|
||||
cx.notify();
|
||||
})));
|
||||
settle(cx, |app| view.read(app).files.as_ref().is_some_and(|files| files.len() == 1));
|
||||
let out = dir.path().join("saved.txt");
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_attachment(id, "n:notes.txt".into(), out.clone(), cx)));
|
||||
settle(cx, |_| out.is_file());
|
||||
assert_eq!(std::fs::read(&out).expect("reads"), b"notes");
|
||||
cx.update(|_, app| view.update(app, |view, _| view.file_pick = Some("n:notes.txt".into())));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.remove_attachment(cx)));
|
||||
settle(cx, |app| view.read(app).files.as_ref().is_some_and(Vec::is_empty));
|
||||
}
|
||||
|
||||
/// A scan of two English lines.
|
||||
fn scanned(dir: &std::path::Path) -> std::path::PathBuf {
|
||||
let source = dir.join("text.pdf");
|
||||
std::fs::write(&source, pdf(&[(612.0, 792.0, &["Hello world", "Second line"])], &[])).expect("writes");
|
||||
let mut worker = Worker::spawn().expect("the worker");
|
||||
let doc = worker.open(&source).expect("opens").doc;
|
||||
let s: f32 = 200.0 / 72.0;
|
||||
let (w, h) = ((612.0 * s).ceil() as i32, (792.0 * s).ceil() as i32);
|
||||
let bgra = worker.render(doc, 0, (s, 0), [0, 0, w, h], false).expect("draws").bgra;
|
||||
let gray: Vec<u8> = bgra.chunks_exact(4).map(|p| ((u16::from(p[0]) + u16::from(p[1]) + u16::from(p[2])) / 3) as u8).collect();
|
||||
let path = dir.join("scan.pdf");
|
||||
std::fs::write(&path, scan((612.0, 792.0), (w as u32, h as u32), &gray)).expect("writes");
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocr_reads_the_pages_then_lays_their_text_in_one_step() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = scanned(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.ask_ocr(window, cx);
|
||||
viewer.start_ocr(cx);
|
||||
}));
|
||||
assert!(cx.update(|_, app| viewer.read(app).ocr.as_ref().is_some_and(|ocr| ocr.run.is_some())), "reading");
|
||||
settle(cx, |app| viewer.read(app).ocr.is_none() && view.read(app).edited == 1);
|
||||
let text = cx.update(|_, app| {
|
||||
let doc = view.read(app).doc;
|
||||
viewer.read(app).core.worker.lock().expect("the worker lock").get().expect("started").text(doc, 0).expect("text")
|
||||
});
|
||||
let read: String = text.iter().map(|c| c.c).collect();
|
||||
assert!(read.contains("Hello world"), "{read}");
|
||||
// A second run skips the page that has text now.
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.ask_ocr(window, cx);
|
||||
viewer.start_ocr(cx);
|
||||
}));
|
||||
settle(cx, |app| viewer.read(app).ocr.is_none());
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 1, "nothing to lay");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cancelled_ocr_lays_nothing() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = scanned(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.ask_ocr(window, cx);
|
||||
viewer.start_ocr(cx);
|
||||
viewer.ocr = None;
|
||||
}));
|
||||
for _ in 0..20 {
|
||||
frames(cx);
|
||||
}
|
||||
assert_eq!(cx.update(|_, app| view.read(app).edited), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cancelled_run_cannot_touch_the_next() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = scanned(dir.path());
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
let tab = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
let first = cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.ask_ocr(window, cx);
|
||||
viewer.start_ocr(cx);
|
||||
viewer.ocr = None;
|
||||
viewer.ask_ocr(window, cx);
|
||||
viewer.start_ocr(cx);
|
||||
viewer.ocr_runs - 1
|
||||
}));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.ocr_read((tab, first, 0), Err(std::io::Error::other("from the first run")), cx)
|
||||
}));
|
||||
assert!(cx.update(|_, app| viewer.read(app).ocr.is_some()), "the second run goes on");
|
||||
settle(cx, |app| viewer.read(app).ocr.is_none() && view.read(app).edited == 1);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//! Password prompts, zoomed pairs, jumps across (C11.1).
|
||||
|
||||
use ely_office::{Dialog, Dialogs as _, Tabs as _};
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext, point, px};
|
||||
|
||||
use super::{file, frames, settle, shown, windowed};
|
||||
use crate::layout::{Mode, Zoom};
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// The file the open prompt asks for.
|
||||
fn asking(app: &gpui::App, viewer: &Entity<Viewer>) -> Option<std::path::PathBuf> {
|
||||
match &viewer.read(app).core.dialog {
|
||||
Some(Dialog::Password { path, .. }) => Some(path.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two files asked for at once, as locked loads do.
|
||||
fn asked<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<Viewer>, &'a mut VisualTestContext, [std::path::PathBuf; 2]) {
|
||||
let (viewer, cx) = windowed(cx, dir, &[], true);
|
||||
let paths = [file(dir, "a.pdf", &[(612.0, 792.0, &["a"])]), file(dir, "b.pdf", &[(612.0, 792.0, &["b"])])];
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| {
|
||||
for path in &paths {
|
||||
viewer.ask_password(path.clone(), false, false, window, cx);
|
||||
}
|
||||
}));
|
||||
frames(cx);
|
||||
(viewer, cx, paths)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_typed_password_opens_the_file() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, paths) = asked(&mut cx, dir.path());
|
||||
// An open file takes any password.
|
||||
cx.simulate_input("secret");
|
||||
cx.simulate_keystrokes("enter");
|
||||
settle(cx, |app| viewer.read(app).core.tabs.len() == 1);
|
||||
assert_eq!(cx.update(|_, app| asking(app, &viewer)), Some(paths[1].clone()), "the next asks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_gives_up_each_queued_file_in_turn() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (viewer, cx, paths) = asked(&mut cx, dir.path());
|
||||
assert_eq!(cx.update(|_, app| asking(app, &viewer)), Some(paths[0].clone()));
|
||||
cx.simulate_keystrokes("escape");
|
||||
settle(cx, |app| asking(app, &viewer) == Some(paths[1].clone()));
|
||||
frames(cx);
|
||||
cx.simulate_keystrokes("escape");
|
||||
settle(cx, |app| viewer.read(app).core.dialog.is_none());
|
||||
assert!(cx.update(|_, app| viewer.read(app).active_tab().is_none()));
|
||||
}
|
||||
|
||||
/// Two zoomed Letter pages side by side.
|
||||
fn zoomed_pair<'a>(cx: &'a mut TestAppContext, dir: &std::path::Path) -> (Entity<PdfView>, &'a mut VisualTestContext) {
|
||||
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"])];
|
||||
let (viewer, cx) = windowed(cx, dir, &[file(dir, "pair.pdf", pages)], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
(view.mode, view.zoom) = (Mode::Two, Zoom::Scale(4.0));
|
||||
cx.notify();
|
||||
}));
|
||||
frames(cx);
|
||||
(view, cx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_pages_keep_the_right_hand_page() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"]), (612.0, 792.0, &["three"])];
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "three.pdf", pages)], true);
|
||||
let view = shown(cx, &viewer);
|
||||
settle(cx, |app| view.read(app).room.size.width > px(100.0));
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.mode = Mode::Two;
|
||||
view.show(1, 0.0, cx);
|
||||
}));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).current), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_across_to_the_right_page_makes_it_current() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (view, cx) = zoomed_pair(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
let right = view.laid_out().placed[1].x;
|
||||
let y = view.scroll.offset().y;
|
||||
view.scroll.set_offset(point(px(-right), y));
|
||||
cx.notify();
|
||||
}));
|
||||
frames(cx);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).current), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_jump_to_the_right_page_brings_it_across() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (view, cx) = zoomed_pair(&mut cx, dir.path());
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.show(1, 0.0, cx)));
|
||||
frames(cx);
|
||||
let (x, left, wide) = cx.update(|_, app| {
|
||||
let view = view.read(app);
|
||||
(view.laid_out().placed[1].x, -f32::from(view.scroll.offset().x), f32::from(view.room.size.width))
|
||||
});
|
||||
assert!(x >= left && x < left + wide, "page at {x}, view {left}..{}", left + wide);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Visible signatures placed as ink, text or a picture (C11.7).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use gpui::{Entity, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::{settle, shown, windowed};
|
||||
use crate::content::Tool;
|
||||
use crate::view::PdfView;
|
||||
use crate::visible::{Stamp, fitted, placed};
|
||||
|
||||
fn opened<'a>(cx: &'a mut TestAppContext, dir: &Path) -> (&'a mut VisualTestContext, Entity<PdfView>) {
|
||||
let path = super::file(dir, "doc.pdf", &[(612.0, 792.0, &["Hello"])]);
|
||||
let (viewer, cx) = windowed(cx, dir, &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
(cx, view)
|
||||
}
|
||||
|
||||
fn place(cx: &mut VisualTestContext, view: &Entity<PdfView>, stamp: Stamp) {
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.set_tool(Tool::PlaceSignature, cx);
|
||||
view.stamp = Some(stamp);
|
||||
view.place_stamp(0, [100.0, 500.0, 100.0, 500.0], true, cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).edited == 1);
|
||||
assert_eq!(cx.update(|_, app| view.read(app).tool), Tool::Read, "one place, then reading");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strokes_fit_by_their_longer_side_and_a_click_places_a_set_width() {
|
||||
let (strokes, aspect) = fitted(&[vec![(10.0, 10.0), (110.0, 60.0)]]).expect("a stroke");
|
||||
assert_eq!((strokes[0][1], aspect), ((1.0, 0.5), 0.5));
|
||||
assert!(fitted(&[vec![(1.0, 1.0)]]).is_none(), "a dot is not a signature");
|
||||
assert_eq!(placed(0.5, [10.0, 20.0, 10.0, 20.0], true), [10.0, 20.0, 170.0, 100.0]);
|
||||
assert_eq!(placed(0.5, [0.0, 0.0, 300.0, 50.0], false), [0.0, 0.0, 100.0, 50.0], "fits the drag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drawn_signature_lands_as_ink() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (cx, view) = opened(&mut cx, dir.path());
|
||||
place(cx, &view, Stamp::Ink { strokes: vec![vec![(0.0, 0.0), (1.0, 0.3)]], aspect: 0.3 });
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.ask_notes([0], cx)));
|
||||
settle(cx, |app| view.read(app).notes.contains_key(&0));
|
||||
let kinds: Vec<String> = cx.update(|_, app| view.read(app).notes[&0].annots.iter().map(|a| a.kind.clone()).collect());
|
||||
assert_eq!(kinds, vec!["Ink".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_typed_signature_lands_as_text() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (cx, view) = opened(&mut cx, dir.path());
|
||||
place(cx, &view, Stamp::Typed { text: "Ada Lovelace".into(), font: "script".into() });
|
||||
cx.update(|_, app| view.update(app, |view, cx| view.page_text(0, cx)));
|
||||
settle(cx, |app| view.read(app).text.contains_key(&0));
|
||||
let text: String = cx.update(|_, app| view.read(app).text[&0].iter().map(|c| c.c).collect());
|
||||
assert!(text.contains("Ada Lovelace"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_picture_lands_in_its_own_proportions() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let (cx, view) = opened(&mut cx, dir.path());
|
||||
let picture = dir.path().join("sig.png");
|
||||
image::RgbImage::from_pixel(40, 10, image::Rgb([0, 0, 0])).save(&picture).expect("writes");
|
||||
place(cx, &view, Stamp::Picture { path: picture, aspect: 0.25 });
|
||||
cx.update(|_, app| view.update(app, |view, cx| {
|
||||
view.set_tool(Tool::Edit, cx);
|
||||
view.ask_layers(&[0], cx);
|
||||
}));
|
||||
settle(cx, |app| view.read(app).layers.contains_key(&0));
|
||||
let drawn = cx.update(|_, app| view.read(app).layers[&0].drawn.clone());
|
||||
assert_eq!(drawn.len(), 1);
|
||||
let r = drawn[0].rect;
|
||||
assert!(((r[2] - r[0]) - 160.0).abs() < 0.5 && ((r[3] - r[1]) - 40.0).abs() < 0.5, "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dialog_hands_a_drawing_to_the_place_tool() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = super::file(dir.path(), "doc.pdf", &[(612.0, 792.0, &["Hello"])]);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
let view = shown(cx, &viewer);
|
||||
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.ask_visible(window, cx)));
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
let dot = vec![gpui::point(gpui::px(5.0), gpui::px(5.0))];
|
||||
viewer.making.as_mut().expect("open").strokes = vec![dot];
|
||||
viewer.ready_stamp(cx);
|
||||
}));
|
||||
assert!(cx.update(|_, app| viewer.read(app).making.is_some()), "a dot is not ready");
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| {
|
||||
viewer.making.as_mut().expect("open").strokes = vec![vec![gpui::point(gpui::px(0.0), gpui::px(0.0)), gpui::point(gpui::px(80.0), gpui::px(20.0))]];
|
||||
viewer.ready_stamp(cx);
|
||||
}));
|
||||
assert!(cx.update(|_, app| viewer.read(app).making.is_none()));
|
||||
let (tool, stamp) = cx.update(|_, app| (view.read(app).tool, view.read(app).stamp.clone()));
|
||||
assert_eq!(tool, Tool::PlaceSignature);
|
||||
assert!(matches!(stamp, Some(Stamp::Ink { aspect, .. }) if (aspect - 0.25).abs() < 1e-6));
|
||||
}
|
||||
Reference in new issue
Block a user