mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 20:18:06 -06:00
532 lines
21 KiB
Rust
532 lines
21 KiB
Rust
//! Commenting in the real shell (egui_kittest): tools, gestures, selection, the composer and
|
||
//! the Comments panel.
|
||
|
||
use egui::{Pos2, pos2};
|
||
use egui_kittest::Harness;
|
||
use egui_kittest::kittest::Queryable;
|
||
use printcraft_render::Annotation;
|
||
use printcraft_ui_egui::{PrintCraftApp, QuickTool};
|
||
|
||
/// Two 300×200 pages of Helvetica text.
|
||
const TEXT_FIXTURE: &[u8] = b"%PDF-1.7
|
||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >> endobj
|
||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 5 0 R /Resources << /Font << /F1 7 0 R >> >> >> endobj
|
||
4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 6 0 R /Resources << /Font << /F1 7 0 R >> >> >> endobj
|
||
5 0 obj << /Length 55 >> stream
|
||
BT /F1 14 Tf 20 150 Td (The quick brown fox) Tj ET
|
||
endstream endobj
|
||
6 0 obj << /Length 58 >> stream
|
||
BT /F1 14 Tf 20 150 Td (A second brown animal) Tj ET
|
||
endstream endobj
|
||
7 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
|
||
trailer << /Root 1 0 R >>
|
||
%%EOF";
|
||
|
||
fn harness(setup: impl FnOnce(&mut PrintCraftApp) + 'static) -> Harness<'static, PrintCraftApp> {
|
||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||
let mut app = PrintCraftApp::new();
|
||
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
||
app.set_option("left", "closed").unwrap();
|
||
app.set_option("author", "Tester").unwrap();
|
||
setup(&mut app);
|
||
app
|
||
});
|
||
h.run_steps(4);
|
||
settle(&mut h);
|
||
h
|
||
}
|
||
|
||
fn settle(h: &mut Harness<'static, PrintCraftApp>) {
|
||
for _ in 0..200 {
|
||
h.run_steps(2);
|
||
if !h.state().render_pending() {
|
||
break;
|
||
}
|
||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||
}
|
||
}
|
||
|
||
/// A point on page 1 in PDF user space → screen (no rotation; MediaBox 300×200).
|
||
fn at(h: &Harness<'static, PrintCraftApp>, x: f32, y: f32) -> Pos2 {
|
||
let r = h.state().views[0].page_screen_rect(0).expect("page 1 on screen");
|
||
pos2(r.left() + x / 300.0 * r.width(), r.top() + (200.0 - y) / 200.0 * r.height())
|
||
}
|
||
|
||
fn drag(h: &mut Harness<'static, PrintCraftApp>, from: Pos2, to: Pos2) {
|
||
h.hover_at(from);
|
||
h.run_steps(1);
|
||
h.drag_at(from);
|
||
h.run_steps(1);
|
||
for k in 1..=4 {
|
||
h.hover_at(from + (to - from) * (k as f32 / 4.0));
|
||
h.run_steps(1);
|
||
}
|
||
h.drop_at(to);
|
||
h.run_steps(3);
|
||
}
|
||
|
||
fn click(h: &mut Harness<'static, PrintCraftApp>, p: Pos2) {
|
||
h.hover_at(p);
|
||
h.run_steps(1);
|
||
h.drag_at(p);
|
||
h.run_steps(1);
|
||
h.drop_at(p);
|
||
h.run_steps(3);
|
||
}
|
||
|
||
/// Drag between two page points (user space).
|
||
fn drag_pt(h: &mut Harness<'static, PrintCraftApp>, a: (f32, f32), b: (f32, f32)) {
|
||
let (a, b) = (at(h, a.0, a.1), at(h, b.0, b.1));
|
||
drag(h, a, b);
|
||
}
|
||
|
||
/// Click a page point (user space).
|
||
fn click_pt(h: &mut Harness<'static, PrintCraftApp>, p: (f32, f32)) {
|
||
let p = at(h, p.0, p.1);
|
||
click(h, p);
|
||
}
|
||
|
||
/// A text field by its placeholder ("Add a comment", "Add a reply").
|
||
fn field<'a>(h: &'a Harness<'static, PrintCraftApp>, placeholder: &'a str) -> egui_kittest::Node<'a> {
|
||
h.query_all_by(|n| n.placeholder() == Some(placeholder)).next().unwrap_or_else(|| panic!("no field {placeholder:?}"))
|
||
}
|
||
|
||
fn comments(h: &Harness<'static, PrintCraftApp>) -> Vec<Annotation> {
|
||
let s = h.state();
|
||
s.session.get(s.views[0].id).unwrap().info.annotations.clone()
|
||
}
|
||
|
||
#[test]
|
||
fn dragging_over_text_with_the_highlighter_highlights_it() {
|
||
let mut h = harness(|app| app.set_option("quick", "highlight").unwrap());
|
||
let (a, b) = {
|
||
let v = &h.state().views[0];
|
||
(v.glyph_screen_pos(0, 4).expect("text layer"), v.glyph_screen_pos(0, 14).expect("glyph"))
|
||
};
|
||
drag(&mut h, a, b);
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 1, "{c:?}");
|
||
assert_eq!((c[0].subtype.as_str(), c[0].author.as_deref(), c[0].quads.len()), ("Highlight", Some("Tester"), 1));
|
||
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "the new comment is selected");
|
||
assert_eq!(h.state().quick_tool, QuickTool::Comment(printcraft_ui_egui::comments::CommentTool::Highlight), "the highlighter stays on");
|
||
// The Comments panel opened with the tool and lists it.
|
||
h.get_by_label_contains("Comments");
|
||
assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Add highlight"));
|
||
}
|
||
|
||
#[test]
|
||
fn a_selection_made_first_is_marked_when_a_tool_is_picked() {
|
||
let mut h = harness(|_| {});
|
||
h.state_mut().views[0].select_text(0, 4, 8);
|
||
assert!(h.state_mut().execute("comment.strikeout"));
|
||
h.run_steps(2);
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 1);
|
||
assert_eq!(c[0].subtype, "StrikeOut");
|
||
assert!(h.state().views[0].selected_text().is_none(), "the selection is consumed");
|
||
}
|
||
|
||
#[test]
|
||
fn shapes_and_ink_are_drawn_by_dragging() {
|
||
let mut h = harness(|app| app.set_option("quick", "square").unwrap());
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().set_option("quick", "ink").unwrap();
|
||
drag_pt(&mut h, (160.0, 100.0), (260.0, 40.0));
|
||
h.state_mut().set_option("quick", "arrow").unwrap();
|
||
drag_pt(&mut h, (20.0, 20.0), (120.0, 30.0));
|
||
// A click with a drawing tool draws nothing.
|
||
h.state_mut().set_option("quick", "circle").unwrap();
|
||
click_pt(&mut h, (200.0, 180.0));
|
||
let c = comments(&h);
|
||
let mut c = c;
|
||
c.sort_by_key(|a| a.index);
|
||
let kinds: Vec<&str> = c.iter().map(|a| a.subtype.as_str()).collect();
|
||
assert_eq!(kinds, ["Square", "Ink", "Line"], "{c:?}");
|
||
let r = c[0].rect;
|
||
assert!((r[0] - 40.0).abs() < 3.0 && (r[1] - 40.0).abs() < 3.0 && (r[2] - 140.0).abs() < 3.0 && (r[3] - 100.0).abs() < 3.0, "{r:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn sticky_note_composer_posts_and_returns_to_select() {
|
||
let mut h = harness(|app| app.set_option("quick", "note").unwrap());
|
||
click_pt(&mut h, (250.0, 180.0));
|
||
assert!(h.state().views[0].comments.composer.is_some(), "the composer opened");
|
||
field(&h, "Add a comment").type_text("Please check");
|
||
h.run_steps(2);
|
||
h.get_by_label("Post").click();
|
||
h.run_steps(3);
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 1);
|
||
assert_eq!((c[0].subtype.as_str(), c[0].contents.as_deref()), ("Text", Some("Please check")));
|
||
assert!(h.state().views[0].comments.composer.is_none());
|
||
assert_eq!(h.state().quick_tool, QuickTool::Select, "one-shot tools return to Select");
|
||
// Cancelling another one creates nothing.
|
||
h.state_mut().set_option("quick", "freetext").unwrap();
|
||
click_pt(&mut h, (50.0, 60.0));
|
||
h.get_by_label("Cancel").click();
|
||
h.run_steps(2);
|
||
assert_eq!(comments(&h).len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn comments_are_selected_moved_and_deleted_with_the_select_tool() {
|
||
let mut h = harness(|app| app.set_option("quick", "square").unwrap());
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().set_option("quick", "select").unwrap();
|
||
// Click away, then on the rectangle's border area.
|
||
click_pt(&mut h, (250.0, 20.0));
|
||
assert_eq!(h.state().views[0].comments.selected, None);
|
||
click_pt(&mut h, (90.0, 70.0));
|
||
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)));
|
||
drag_pt(&mut h, (90.0, 70.0), (140.0, 70.0));
|
||
let r = comments(&h)[0].rect;
|
||
assert!((r[0] - 90.0).abs() < 3.0 && (r[2] - 190.0).abs() < 3.0, "moved right by 50 pt: {r:?}");
|
||
assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Move comment"));
|
||
h.key_press(egui::Key::Delete);
|
||
h.run_steps(3);
|
||
assert!(comments(&h).is_empty());
|
||
h.state_mut().undo();
|
||
h.run_steps(2);
|
||
assert_eq!(comments(&h).len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn the_panel_posts_comments_and_replies() {
|
||
let mut h = harness(|app| app.set_option("panel", "comments").unwrap());
|
||
field(&h, "Add a comment").click();
|
||
h.run_steps(2);
|
||
field(&h, "Add a comment").type_text("General remark");
|
||
h.run_steps(1);
|
||
h.key_press(egui::Key::Enter);
|
||
h.run_steps(3);
|
||
let c = comments(&h);
|
||
assert_eq!((c.len(), c[0].subtype.as_str(), c[0].contents.as_deref()), (1, "Text", Some("General remark")));
|
||
// The new comment is selected: reply to it.
|
||
field(&h, "Add a reply").click();
|
||
h.run_steps(2);
|
||
field(&h, "Add a reply").type_text("Agreed");
|
||
h.run_steps(1);
|
||
h.get_by_label("Post").click();
|
||
h.run_steps(3);
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 2);
|
||
let reply = c.iter().find(|a| a.in_reply_to.is_some()).expect("a reply");
|
||
assert_eq!((reply.contents.as_deref(), reply.author.as_deref()), (Some("Agreed"), Some("Tester")));
|
||
h.get_by_label_contains("Agreed");
|
||
}
|
||
|
||
#[test]
|
||
fn comment_properties_change_appearance_and_author() {
|
||
let mut h = harness(|app| app.set_option("quick", "square").unwrap());
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().open_comment_props(0, 0);
|
||
h.run_steps(2);
|
||
h.get_by_label("Rectangle Properties");
|
||
{
|
||
let d = h.state_mut().comment_props.as_mut().expect("open");
|
||
assert_eq!(d.original.width, Some(2.0));
|
||
d.edited.color = Some([0.0, 0.0, 1.0]);
|
||
d.edited.width = Some(5.0);
|
||
d.edited.author = "Grace".into();
|
||
}
|
||
h.get_by_label("General").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Author");
|
||
h.get_by_label("OK").click();
|
||
h.run_steps(3);
|
||
let s = h.state();
|
||
let doc = s.session.get(s.views[0].id).unwrap();
|
||
let p = doc.comment_props(0, 0).unwrap();
|
||
assert_eq!((p.color, p.width, p.author.as_str()), (Some([0.0, 0.0, 1.0]), Some(5.0), "Grace"));
|
||
assert_eq!(doc.can_undo(), Some("Change comment properties"), "one undo step");
|
||
}
|
||
|
||
#[test]
|
||
fn the_panel_filters_and_sorts() {
|
||
let mut h = harness(|app| {
|
||
app.set_option("panel", "comments").unwrap();
|
||
app.set_option("quick", "square").unwrap();
|
||
});
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().set_option("quick", "circle").unwrap();
|
||
drag_pt(&mut h, (160.0, 100.0), (260.0, 40.0));
|
||
h.state_mut().set_option("quick", "select").unwrap();
|
||
h.run_steps(2);
|
||
let ovals = h.query_all_by_label("Oval").count();
|
||
assert!(ovals >= 1);
|
||
h.state_mut().views[0].comments.hidden_types.push("Oval".into());
|
||
h.run_steps(2);
|
||
assert_eq!(h.query_all_by_label("Oval").count(), ovals - 1, "the oval's card is filtered out");
|
||
assert!(h.query_all_by_label("Rectangle").count() >= 1);
|
||
h.state_mut().views[0].comments.hidden_types.clear();
|
||
h.state_mut().views[0].comments.sort = printcraft_ui_egui::comments::SortBy::Type;
|
||
h.run_steps(2);
|
||
// Grouped by type: a group header names each type.
|
||
assert_eq!(h.query_all_by_label("Oval").count(), ovals + 1, "a group header was added");
|
||
}
|
||
|
||
#[test]
|
||
fn comments_take_checkmarks_lock_hide_and_summarize() {
|
||
let mut h = harness(|app| {
|
||
app.set_option("panel", "comments").unwrap();
|
||
app.set_option("quick", "square").unwrap();
|
||
});
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().set_option("quick", "select").unwrap();
|
||
h.run_steps(2);
|
||
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "the new comment is selected");
|
||
// The card's checkmark.
|
||
h.get_by_label("Mark with checkmark").click();
|
||
h.run_steps(3);
|
||
assert!(comments(&h).iter().any(|a| a.state.as_deref() == Some("Marked")));
|
||
h.get_by_label("Remove checkmark");
|
||
|
||
// "…" ▸ Copy text puts the comment's text on the clipboard.
|
||
h.state_mut().apply_edit(printcraft_engine::Edit::SetAnnotationContents { page: 0, index: 0, text: "Copy me".into() });
|
||
h.run_steps(2);
|
||
h.get_by_label("More").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Copy text").click();
|
||
let mut copied = false;
|
||
for _ in 0..4 {
|
||
h.step();
|
||
copied |= h.output().platform_output.commands.iter().any(|c| matches!(c, egui::OutputCommand::CopyText(t) if t == "Copy me"));
|
||
}
|
||
assert!(copied, "the text is copied");
|
||
h.run_steps(2);
|
||
|
||
// Properties ▸ Locked.
|
||
h.state_mut().open_comment_props(0, 0);
|
||
h.run_steps(2);
|
||
h.get_by_label("Locked").click();
|
||
h.run_steps(1);
|
||
h.get_by_label("OK").click();
|
||
h.run_steps(3);
|
||
assert!(comments(&h)[0].locked);
|
||
h.key_press(egui::Key::Delete);
|
||
h.run_steps(3);
|
||
assert!(comments(&h).iter().any(|a| a.in_reply_to.is_none()), "a locked comment isn't deleted");
|
||
|
||
// "…" ▸ Hide all comments, then Show all comments.
|
||
h.get_by_label("More options").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Hide all comments").click();
|
||
h.run_steps(2);
|
||
let s = h.state();
|
||
assert!(s.session.get(s.views[0].id).unwrap().comments_hidden());
|
||
h.get_by_label("More options").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Show all comments").click();
|
||
h.run_steps(2);
|
||
let s = h.state();
|
||
assert!(!s.session.get(s.views[0].id).unwrap().comments_hidden());
|
||
|
||
// "…" ▸ Create PDF comment summary.
|
||
h.get_by_label("More options").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Create PDF comment summary…").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Summarize Options");
|
||
h.get_by_label("Create PDF Comment Summary").click();
|
||
h.run_steps(3);
|
||
let s = h.state();
|
||
assert_eq!(s.views.len(), 2);
|
||
assert!(s.session.get(s.views[1].id).unwrap().name.starts_with("Summary of comments on text"));
|
||
}
|
||
|
||
#[test]
|
||
fn polygons_clouds_connected_lines_callouts_and_inserted_text() {
|
||
let mut h = harness(|app| app.set_option("quick", "polygon").unwrap());
|
||
// Polygon: click the corners, Enter finishes.
|
||
for p in [(40.0, 40.0), (120.0, 40.0), (80.0, 110.0)] {
|
||
click_pt(&mut h, p);
|
||
}
|
||
assert_eq!(comments(&h).len(), 0, "still drawing");
|
||
h.key_press(egui::Key::Enter);
|
||
h.run_steps(3);
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 1);
|
||
assert_eq!(c[0].subtype, "Polygon");
|
||
|
||
// Cloud: clicking the first point again closes it.
|
||
h.state_mut().set_option("quick", "cloud").unwrap();
|
||
for p in [(160.0, 40.0), (260.0, 40.0), (260.0, 100.0), (160.0, 100.0), (160.0, 40.0)] {
|
||
click_pt(&mut h, p);
|
||
}
|
||
assert_eq!(comments(&h).len(), 2);
|
||
|
||
// Connected lines.
|
||
h.state_mut().set_option("quick", "polyline").unwrap();
|
||
for p in [(20.0, 150.0), (60.0, 130.0), (100.0, 150.0)] {
|
||
click_pt(&mut h, p);
|
||
}
|
||
h.key_press(egui::Key::Enter);
|
||
h.run_steps(3);
|
||
assert!(comments(&h).iter().any(|a| a.subtype == "PolyLine"));
|
||
|
||
// Callout: drag from the target to where the box goes, then type.
|
||
h.state_mut().set_option("quick", "callout").unwrap();
|
||
drag_pt(&mut h, (60.0, 160.0), (180.0, 190.0));
|
||
assert!(h.state().views[0].comments.composer.is_some(), "the composer opened");
|
||
field(&h, "Type text").type_text("Why?");
|
||
h.run_steps(2);
|
||
h.get_by_label("Post").click();
|
||
h.run_steps(3);
|
||
let callout = comments(&h).into_iter().find(|a| a.subtype == "FreeText").expect("a callout");
|
||
assert_eq!(callout.contents.as_deref(), Some("Why?"));
|
||
assert!(callout.rect[0] <= 60.0 && callout.rect[1] <= 160.0, "holds the leader: {:?}", callout.rect);
|
||
|
||
// Insert text at a click.
|
||
h.state_mut().set_option("quick", "caret").unwrap();
|
||
click_pt(&mut h, (95.0, 150.0));
|
||
field(&h, "Text to insert").type_text("very ");
|
||
h.run_steps(2);
|
||
h.get_by_label("Post").click();
|
||
h.run_steps(3);
|
||
let caret = comments(&h).into_iter().find(|a| a.subtype == "Caret").expect("a caret");
|
||
assert_eq!(caret.contents.as_deref(), Some("very"));
|
||
assert_eq!(h.state().quick_tool, QuickTool::Select);
|
||
}
|
||
|
||
#[test]
|
||
fn the_panel_filters_by_colour_and_checkmark() {
|
||
let mut h = harness(|app| {
|
||
app.set_option("panel", "comments").unwrap();
|
||
app.set_option("quick", "square").unwrap();
|
||
});
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().set_option("quick", "circle").unwrap();
|
||
drag_pt(&mut h, (160.0, 100.0), (260.0, 40.0));
|
||
h.state_mut().set_option("quick", "select").unwrap();
|
||
h.state_mut().apply_edit(printcraft_engine::Edit::StyleAnnotation {
|
||
page: 0,
|
||
index: 1,
|
||
color: Some([0.0, 0.47, 0.84]),
|
||
opacity: None,
|
||
width: None,
|
||
});
|
||
h.state_mut().apply_edit(printcraft_engine::Edit::MarkAnnotation { page: 0, index: 0, marked: true, author: "Tester".into() });
|
||
h.run_steps(2);
|
||
let cards = |h: &Harness<'static, PrintCraftApp>| (h.query_all_by_label("Rectangle").count(), h.query_all_by_label("Oval").count());
|
||
// The filter menu lists the colours by name (counted with it open: it names the types too).
|
||
h.get_by_label("Filter comments").click();
|
||
h.run_steps(2);
|
||
let before = cards(&h);
|
||
h.get_by_label("Blue").click();
|
||
h.run_steps(2);
|
||
assert_eq!(h.state().views[0].comments.hidden_colors, vec!["0077D6".to_string()]);
|
||
assert_eq!(cards(&h).1, before.1 - 1, "the blue oval is filtered out");
|
||
h.get_by_label("Checked").click();
|
||
h.run_steps(2);
|
||
assert_eq!(cards(&h).0, before.0 - 1, "the checked rectangle is filtered out");
|
||
h.get_by_label("Show all").click();
|
||
h.run_steps(2);
|
||
assert_eq!(cards(&h), before);
|
||
}
|
||
|
||
#[test]
|
||
fn make_current_properties_default() {
|
||
let mut h = harness(|app| {
|
||
app.set_option("panel", "comments").unwrap();
|
||
app.set_option("quick", "square").unwrap();
|
||
});
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
h.state_mut().set_option("quick", "select").unwrap();
|
||
h.state_mut().apply_edit(printcraft_engine::Edit::StyleAnnotation {
|
||
page: 0,
|
||
index: 0,
|
||
color: Some([0.0, 0.47, 0.84]),
|
||
opacity: Some(0.5),
|
||
width: Some(5.0),
|
||
});
|
||
h.run_steps(2);
|
||
h.get_by_label("More").click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Make Current Properties Default").click();
|
||
h.run_steps(3);
|
||
let st = h.state().comment_prefs.style(printcraft_ui_egui::comments::CommentTool::Rectangle);
|
||
assert_eq!((st.color, st.opacity, st.width), ([0.0, 0.47, 0.84], 0.5, 5.0));
|
||
// The next rectangle takes it.
|
||
h.state_mut().set_option("quick", "square").unwrap();
|
||
drag_pt(&mut h, (160.0, 100.0), (260.0, 40.0));
|
||
let c = comments(&h);
|
||
let last = c.iter().rfind(|a| a.subtype == "Square").unwrap();
|
||
assert_eq!(last.color, Some([0.0, 0.47, 0.84]));
|
||
}
|
||
|
||
#[test]
|
||
fn highlighting_an_area_off_the_text() {
|
||
let mut h = harness(|app| app.set_option("quick", "highlight").unwrap());
|
||
// Blank space below the text line (text is at y 150).
|
||
drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0));
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 1, "{c:?}");
|
||
assert_eq!((c[0].subtype.as_str(), c[0].quads.len()), ("Highlight", 1));
|
||
let q = c[0].quads[0];
|
||
assert!((q[0] - 40.0).abs() < 3.0 && (q[1] - 100.0).abs() < 3.0, "{q:?}");
|
||
// Dragging over text still highlights the text (once the text layer is in).
|
||
let mut glyphs = None;
|
||
for _ in 0..200 {
|
||
h.run_steps(1);
|
||
let v = &h.state().views[0];
|
||
if let (Some(a), Some(b)) = (v.glyph_screen_pos(0, 4), v.glyph_screen_pos(0, 14)) {
|
||
glyphs = Some((a, b));
|
||
break;
|
||
}
|
||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||
}
|
||
let (a, b) = glyphs.expect("text layer");
|
||
drag(&mut h, a, b);
|
||
assert_eq!(comments(&h).len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn replacing_text_strikes_it_and_adds_a_caret() {
|
||
let mut h = harness(|app| app.set_option("quick", "replace").unwrap());
|
||
drag_pt(&mut h, (20.0, 155.0), (120.0, 155.0));
|
||
assert!(h.state().views[0].comments.composer.is_some(), "the composer asks for the replacement");
|
||
field(&h, "Replacement text").type_text("slow red");
|
||
h.run_steps(2);
|
||
h.get_by_label("Post").click();
|
||
h.run_steps(3);
|
||
let c = comments(&h);
|
||
let strike = c.iter().find(|a| a.subtype == "StrikeOut").expect("a strikeout");
|
||
let caret = c.iter().find(|a| a.subtype == "Caret").expect("a caret");
|
||
assert_eq!(caret.contents.as_deref(), Some("slow red"));
|
||
assert_eq!(caret.in_reply_to, strike.name, "grouped with the strikeout");
|
||
assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Replace text"));
|
||
assert_eq!(h.state().quick_tool, QuickTool::Select);
|
||
}
|
||
|
||
#[test]
|
||
fn attaching_a_file_as_a_comment() {
|
||
let mut h = harness(|app| app.set_option("quick", "attach").unwrap());
|
||
h.state_mut().attach_override = Some(("notes.txt".into(), b"remember the milk".to_vec()));
|
||
click_pt(&mut h, (250.0, 180.0));
|
||
h.run_steps(3);
|
||
let c = comments(&h);
|
||
assert_eq!((c.len(), c[0].subtype.as_str(), c[0].contents.as_deref()), (1, "FileAttachment", Some("notes.txt")));
|
||
let s = h.state();
|
||
let doc = s.session.get(s.views[0].id).unwrap();
|
||
assert!(doc.info.attachments.iter().any(|a| a.name == "notes.txt"), "listed in the Attachments panel");
|
||
assert_eq!(s.quick_tool, QuickTool::Select);
|
||
}
|
||
|
||
#[test]
|
||
fn erasing_part_of_a_drawing() {
|
||
let mut h = harness(|app| app.set_option("quick", "ink").unwrap());
|
||
drag_pt(&mut h, (40.0, 60.0), (260.0, 60.0));
|
||
assert_eq!(comments(&h).len(), 1);
|
||
h.state_mut().set_option("quick", "eraser").unwrap();
|
||
drag_pt(&mut h, (150.0, 90.0), (150.0, 30.0));
|
||
let c = comments(&h);
|
||
assert_eq!(c.len(), 1);
|
||
let s = h.state();
|
||
let doc = s.session.get(s.views[0].id).unwrap();
|
||
assert_eq!(doc.can_undo(), Some("Erase"));
|
||
// Rubbing along the rest removes the drawing.
|
||
drag_pt(&mut h, (40.0, 60.0), (260.0, 60.0));
|
||
assert!(comments(&h).is_empty(), "{:?}", comments(&h));
|
||
}
|