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:?}");
|
||||
}
|
||||
Reference in new issue
Block a user