From bf58d1f6bf0b21c3a94a40a57e17b0eb980d553a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 2 Oct 2026 04:05:25 -0400 Subject: [PATCH] M5: Area highlight (drag the highlighter off text) and Replace Text (strikeout plus a grouped caret holding the replacement; Highlight flyout, comment_add type replace) Co-Authored-By: Claude Opus 5.5 --- crates/annot/src/lib.rs | 49 +++++++++++++++++++++++++++ crates/annot/src/tests.rs | 23 +++++++++++++ crates/automation/src/comments.rs | 14 +++++++- crates/automation/src/tools.rs | 4 +-- crates/automation/tests/automation.rs | 6 ++++ crates/engine/src/commands.rs | 1 + crates/engine/src/lib.rs | 15 ++++++++ crates/ui-egui/src/canvas.rs | 22 ++++++++++-- crates/ui-egui/src/comments.rs | 41 ++++++++++++++++++++-- crates/ui-egui/tests/comments.rs | 33 ++++++++++++++++++ parity/acrobat-features.toml | 20 +++++------ 11 files changed, 209 insertions(+), 19 deletions(-) diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 8e86e49b90..93e5d22a10 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -1448,3 +1448,52 @@ pub fn props(doc: &Document, page: usize, index: usize) -> Option { subtype, }) } + +/// Replace Text (Acrobat's proposal): strike out `quads` and add a caret at the end of the +/// struck text holding `replacement`, grouped with the strikeout (`/IRT` + `/RT /Group`) so they +/// move, list and delete as one. Returns the strikeout's index. +#[allow(clippy::too_many_arguments)] +pub fn add_text_replacement( + doc: &mut Document, + page: usize, + quads: &[[f64; 8]], + replacement: &str, + author: &str, + strike: &Style, + caret: &Style, + meta: &Meta, +) -> Result { + let last = *quads.last().ok_or_else(|| AnnotError::Invalid("select the text to replace first".into()))?; + let s = add_annotation( + doc, + &NewAnnotation { + page, + shape: Shape::TextMarkup { kind: Markup::StrikeOut, quads: quads.to_vec() }, + style: strike.clone(), + contents: String::new(), + author: author.into(), + }, + meta, + )?; + let (_, sr) = annot_ref(doc, page, s)?; + doc.update_dict(sr, |d| d.set(b"IT".to_vec(), Object::name("StrikeOutTextEdit")))?; + // The caret sits on the baseline at the end of the last line, its point at the text. + let xs = [last[0], last[2], last[4], last[6]]; + let ys = [last[1], last[3], last[5], last[7]]; + let x = xs.iter().copied().fold(f64::MIN, f64::max); + let (y0, y1) = (ys.iter().copied().fold(f64::MAX, f64::min), ys.iter().copied().fold(f64::MIN, f64::max)); + let h = ((y1 - y0) * 0.6).clamp(4.0, 24.0); + let rect = [x - h / 2.0, y0 - h * 0.6, x + h / 2.0, y0 + h * 0.4]; + let c = add_annotation( + doc, + &NewAnnotation { page, shape: Shape::Caret { rect }, style: caret.clone(), contents: replacement.into(), author: author.into() }, + meta, + )?; + let (_, cr) = annot_ref(doc, page, c)?; + doc.update_dict(cr, |d| { + d.set(b"IRT".to_vec(), Object::Ref(sr)); + d.set(b"RT".to_vec(), Object::name("Group")); + d.set(b"Subj".to_vec(), PdfString::text("Replace Text")); + })?; + Ok(s) +} diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 93372045d8..38dddb697a 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -504,3 +504,26 @@ fn cloud_ys(path: &str) -> Vec { }) .collect() } + +#[test] +fn replace_text_groups_a_strikeout_and_a_caret() { + let mut doc = fixture(); + let quads = [[100.0, 720.0, 200.0, 720.0, 100.0, 706.0, 200.0, 706.0]]; + let red = Style { color: [0.89, 0.13, 0.13], opacity: 1.0, width: 1.0, fill: None }; + let blue = Style { color: [0.0, 0.47, 0.84], opacity: 1.0, width: 1.0, fill: None }; + let i = add_text_replacement(&mut doc, 0, &quads, "new words", "Ada", &red, &blue, &meta("r")).unwrap(); + let doc = reopen(&doc); + let a = list(&doc, 0); + let strike = &a[i]; + assert_eq!((strike.name(b"Subtype"), strike.name(b"IT")), (Some(&b"StrikeOut"[..]), Some(&b"StrikeOutTextEdit"[..]))); + let caret = a.iter().find(|d| d.name(b"Subtype") == Some(b"Caret")).unwrap(); + assert_eq!((text(caret, b"Contents").as_str(), caret.name(b"RT")), ("new words", Some(&b"Group"[..]))); + assert!(caret.reference(b"IRT").is_some()); + let r = rect(caret); + assert!(r[0] < 200.0 && r[2] > 200.0 && r[1] < 706.0, "at the end of the line, below its baseline: {r:?}"); + // Deleting the strikeout takes the caret with it (the group). + let mut doc = doc; + delete_annotation(&mut doc, 0, i).unwrap(); + assert!(!list(&doc, 0).iter().any(|d| d.name(b"Subtype") == Some(b"Caret"))); + assert!(add_text_replacement(&mut doc, 0, &[], "x", "Ada", &red, &blue, &meta("r")).is_err()); +} diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 9e1ac84020..136b3f4c4c 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -143,7 +143,7 @@ impl Automation { let markup = match kind { "highlight" => Some(Markup::Highlight), "underline" => Some(Markup::Underline), - "strikeout" => Some(Markup::StrikeOut), + "strikeout" | "replace" => Some(Markup::StrikeOut), "squiggly" => Some(Markup::Squiggly), _ => None, }; @@ -291,6 +291,18 @@ impl Automation { } let contents = a.opt_str("contents")?.unwrap_or_default().to_string(); let author = a.opt_str("author")?.unwrap_or(DEFAULT_AUTHOR).to_string(); + // Replace Text: the struck-out text and a grouped caret holding `contents`. + if kind == "replace" { + let Shape::TextMarkup { quads, .. } = shape else { unreachable!("replace marks text") }; + if contents.trim().is_empty() { + return Err(ToolError::InvalidArgs("replace needs `contents`: the replacement text".into())); + } + let strike = style; + let caret = Style::default_for(&Shape::Caret { rect: [0.0; 4] }); + let mut out = self.apply(a, Edit::ReplaceText { page, quads, text: contents, author, strike, caret })?; + out["comment"] = json!({ "page": page + 1, "type": "replace" }); + return Ok(out); + } let marked = match &shape { Shape::TextMarkup { quads, .. } => Some(quads.len()), _ => None, diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 25c302a198..1930fec7ee 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -458,13 +458,13 @@ pub fn tools() -> Vec { "Add a comment as Acrobat's commenting tools do. Geometry is in points with the origin at the top-left of the displayed page, y down (as in page_render images at 72 dpi and text_find rects). \ note: `at` [x, y] (icon top-left). stamp: `at` [x, y] (its centre) and `stamp`: approved, completed, confidential, draft, final, for comment, for public release, information only, not approved, not for public release, preliminary results, void, accepted, initial here, rejected, sign here, witness; dynamic: true for the dynamic approved/confidential/received/reviewed/revised stamps with a By … at … line. highlight/underline/strikeout/squiggly: `find` (text on the page to mark; every match with all: true) or `quads`. \ rectangle/oval/textbox: `rect` [x0, y0, x1, y1]. line/arrow: `from`, `to`. ink: `strokes` [[[x, y], …], …]. \ - polygon/cloud/polyline (connected lines): `points` [[x, y], …]. callout: `rect` (its text box), `to` (the point the arrow touches), optional `knee`. caret (inserted text): `at`, the insertion point on the baseline. Undoable.", + polygon/cloud/polyline (connected lines): `points` [[x, y], …]. callout: `rect` (its text box), `to` (the point the arrow touches), optional `knee`. caret (inserted text): `at`, the insertion point on the baseline. replace (Replace Text): `find` or `quads` like highlight, `contents` the replacement. Undoable.", ) .with(schema( json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, - "type": { "type": "string", "enum": ["note", "highlight", "underline", "strikeout", "squiggly", "rectangle", "oval", "line", "arrow", "ink", "textbox", "stamp", "polygon", "cloud", "polyline", "callout", "caret"] }, + "type": { "type": "string", "enum": ["note", "highlight", "underline", "strikeout", "squiggly", "replace", "rectangle", "oval", "line", "arrow", "ink", "textbox", "stamp", "polygon", "cloud", "polyline", "callout", "caret"] }, "points": { "type": "array", "items": point(), "minItems": 2 }, "knee": point(), "contents": { "type": "string", "description": "The comment text (what a text box shows)." }, diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 4599bc4b80..2709603ede 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -951,6 +951,12 @@ fn drawing_comments_through_tools() { a.call("comment_add", &json!({ "doc": doc, "page": 1, "type": "polygon", "points": [[1, 1]] })), Err(ToolError::Failed(_) | ToolError::InvalidArgs(_)) )); + // Replace Text: a strikeout over the found text and a grouped caret. + ok(&mut a, "comment_add", json!({ "doc": doc, "page": 2, "type": "replace", "find": "page 2", "contents": "second page" })); + let list = ok(&mut a, "comment_list", json!({ "doc": doc, "page": 2 })); + let strike = list["comments"].as_array().unwrap().iter().find(|c| c["type"] == "StrikeOut").unwrap().clone(); + assert_eq!(strike["replies"][0]["contents"], "second page", "the caret threads under the strikeout"); + assert!(matches!(a.call("comment_add", &json!({ "doc": doc, "page": 2, "type": "replace", "find": "page 2" })), Err(ToolError::InvalidArgs(_)))); let png = a.call("page_render", &json!({ "doc": doc, "page": 1, "dpi": 72 })).unwrap(); assert!(matches!(png[0], Content::Png { .. })); } diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 1656827244..ece38ab18f 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -186,6 +186,7 @@ pub const COMMANDS: &[CommandSpec] = &[ c("comment.cloud", "Draw a cloud", None, None, Annotate, "cloud"), c("comment.callout", "Add a callout", None, None, Annotate, "message-square-quote"), c("comment.caret", "Insert text", None, None, Annotate, "text-cursor-input"), + c("comment.replace", "Replace text", None, None, Annotate, "replace"), c("form.fields", "Form fields panel", VIEW, None, Document, "list"), c("form.clear", "Clear form", EDIT, None, FillForms, "eraser"), c("comment.flatten", "Flatten comments", None, None, HasComments, "layers"), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 2ac9458659..b41418c35d 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -88,6 +88,7 @@ fn scope_of(edit: &Edit) -> Scope { | Edit::SetAnnotationStatus { .. } | Edit::MarkAnnotation { .. } | Edit::LockAnnotation { .. } + | Edit::ReplaceText { .. } | Edit::MoveAnnotation { .. } | Edit::ResizeAnnotation { .. } | Edit::StyleAnnotation { .. } @@ -526,6 +527,15 @@ pub enum Edit { state: ReviewState, author: String, }, + /// Replace Text: strike out `quads` and add a grouped caret holding `text`. + ReplaceText { + page: usize, + quads: Vec<[f64; 8]>, + text: String, + author: String, + strike: Style, + caret: Style, + }, /// Acrobat's "Mark with checkmark" (a hidden `/StateModel /Marked` reply by `author`). MarkAnnotation { page: usize, @@ -744,6 +754,7 @@ impl Edit { Edit::SetAnnotationContents { .. } => "Edit comment".into(), Edit::ReplyToAnnotation { .. } => "Reply".into(), Edit::SetAnnotationStatus { state, .. } => format!("Set status {}", state.name()), + Edit::ReplaceText { .. } => "Replace text".into(), Edit::MarkAnnotation { marked: true, .. } => "Mark with checkmark".into(), Edit::MarkAnnotation { .. } => "Remove checkmark".into(), Edit::LockAnnotation { locked: true, .. } => "Lock comment".into(), @@ -859,6 +870,7 @@ fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(), | Edit::ReplyToAnnotation { .. } | Edit::SetAnnotationStatus { .. } | Edit::MarkAnnotation { .. } + | Edit::ReplaceText { .. } | Edit::LockAnnotation { .. } | Edit::MoveAnnotation { .. } | Edit::ResizeAnnotation { .. } @@ -1026,6 +1038,9 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) - Edit::SetAnnotationStatus { page, index, state, author } => { printcraft_annot::set_review_state(doc, *page, *index, *state, author, &cx.meta())?; } + Edit::ReplaceText { page, quads, text, author, strike, caret } => { + printcraft_annot::add_text_replacement(doc, *page, quads, text, author, strike, caret, &cx.meta())?; + } Edit::MarkAnnotation { page, index, marked, author } => { printcraft_annot::set_marked(doc, *page, *index, *marked, author, &cx.meta())?; } diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8b3ec18f43..9653277245 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -919,7 +919,7 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { let tool = app.quick_tool; // Text selection runs for the Select tool and for the markup tools (highlight…). let selects_text = match tool { - QuickTool::Comment(t) => t.markup().is_some(), + QuickTool::Comment(t) => t.markup().is_some() || t == comments::CommentTool::ReplaceText, QuickTool::Select => !preparing, QuickTool::Redact => true, QuickTool::Hand @@ -1140,7 +1140,9 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { } else { tool == QuickTool::Select && crate::forms_ui::page_input(ui, &resp, &xf, i, info, &form, can_fill, view) }; - let boxing = tool == QuickTool::Redact && can_modify && { + // Redact draws boxes off text; so does Highlight (an area highlight, as in Acrobat). + let area_tool = tool == QuickTool::Redact && can_modify || tool == QuickTool::Comment(comments::CommentTool::Highlight) && allowed; + let boxing = area_tool && { let text = view.page_text(i); let over_text = |p: Pos2| { let (vx, vy) = xf.screen_to_view(p); @@ -1226,6 +1228,8 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { comments::page_after_text(&resp, &pcx, view); if tool == QuickTool::Redact && can_modify { crate::redact_ui::after_text(&resp, i, info, view); + } + if area_tool { crate::redact_ui::paint(ui, painter, i, view); } comments::paint_page(ui, painter, &pcx, view); @@ -1463,7 +1467,19 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { } if let Some((page, quads)) = app.views[index].pending_redaction.take() { let author = app.comment_prefs.author.clone(); - app.views[index].pending_edit = Some(app.redact_prefs.mark(page, quads, &author)); + app.views[index].pending_edit = Some(if app.quick_tool == QuickTool::Comment(comments::CommentTool::Highlight) { + // An area highlight: a highlight over the box. + let style = app.comment_prefs.style(comments::CommentTool::Highlight); + printcraft_engine::Edit::AddAnnotation(printcraft_engine::NewAnnotation { + page, + shape: printcraft_engine::Shape::TextMarkup { kind: printcraft_engine::Markup::Highlight, quads }, + style, + contents: String::new(), + author, + }) + } else { + app.redact_prefs.mark(page, quads, &author) + }); } if open_signature { app.signature_draft.clear(); diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index a7008f6b3c..20ed1d9ba8 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -40,12 +40,13 @@ pub enum CommentTool { Cloud, Callout, Caret, + ReplaceText, } /// The quick-bar flyout groups, in Acrobat's order: Comment ▸, Highlight ▸, Draw ▸. pub const GROUPS: [&[CommentTool]; 3] = [ &[CommentTool::Note, CommentTool::TextBox, CommentTool::Callout], - &[CommentTool::Highlight, CommentTool::Underline, CommentTool::StrikeOut, CommentTool::Caret], + &[CommentTool::Highlight, CommentTool::Underline, CommentTool::StrikeOut, CommentTool::Caret, CommentTool::ReplaceText], &[ CommentTool::Ink, CommentTool::Line, @@ -58,7 +59,8 @@ pub const GROUPS: [&[CommentTool]; 3] = [ ], ]; -pub const ALL: [CommentTool; 15] = [ +pub const ALL: [CommentTool; 16] = [ + CommentTool::ReplaceText, CommentTool::Polygon, CommentTool::PolyLine, CommentTool::Cloud, @@ -94,6 +96,7 @@ impl CommentTool { Self::Cloud => "comment.cloud", Self::Callout => "comment.callout", Self::Caret => "comment.caret", + Self::ReplaceText => "comment.replace", } } @@ -118,6 +121,7 @@ impl CommentTool { Self::Cloud => "Cloud", Self::Callout => "Add a callout", Self::Caret => "Insert text", + Self::ReplaceText => "Replace text", } } @@ -138,6 +142,7 @@ impl CommentTool { Self::Cloud => "cloud", Self::Callout => "message-square-quote", Self::Caret => "text-cursor-input", + Self::ReplaceText => "replace", } } @@ -187,6 +192,7 @@ impl CommentTool { Self::PolyLine => Shape::PolyLine { vertices: Vec::new() }, Self::Callout => Shape::Callout { rect: [0.0; 4], knee: [0.0; 2], point: [0.0; 2], font_size: 10.0 }, Self::Caret => Shape::Caret { rect: [0.0; 4] }, + Self::ReplaceText => Shape::TextMarkup { kind: Markup::StrikeOut, quads: Vec::new() }, } } } @@ -314,6 +320,8 @@ pub enum ComposerKind { }, /// Insert text at the anchor (the caret's tip). Caret, + /// Replace the selected text (`CommentView::replace` holds the quads). + Replace, /// Edit the text of the comment at this index on the composer's page. Edit(usize), } @@ -359,6 +367,8 @@ pub struct CommentView { pub props_request: Option<(usize, usize)>, /// Make Current Properties Default was asked for: (page, index). pub default_request: Option<(usize, usize)>, + /// Replace Text: the struck-out text's quads while the replacement is typed. + pub replace: Option>, pub search_focus: bool, /// Where the canvas context menu was opened: (page, user-space point). pub context_at: Option<(usize, [f64; 2])>, @@ -771,9 +781,20 @@ fn select_input( consumed } -/// After text selection ran: a markup tool turns a finished selection into a comment. +/// After text selection ran: a markup tool turns a finished selection into a comment; Replace +/// Text asks for the replacement. pub(crate) fn page_after_text(resp: &egui::Response, cx: &PageCx<'_>, view: &mut DocView) { let QuickTool::Comment(tool) = cx.tool else { return }; + if tool == CommentTool::ReplaceText && cx.allowed && (resp.drag_stopped() || resp.double_clicked()) { + if let Some((page, quads)) = view.selection_quads(cx.info).filter(|(p, _)| *p == cx.page) { + view.clear_selection(); + let last = quads.last().copied().unwrap_or_default(); + let at = [last[2].max(last[6]), last[1].max(last[3])]; + view.comments.replace = Some(quads); + view.comments.composer = Some(Composer { page, at, kind: ComposerKind::Replace, text: String::new(), focus: true }); + } + return; + } if tool.markup().is_none() || !cx.allowed || !(resp.drag_stopped() || resp.double_clicked()) { return; } @@ -919,6 +940,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ComposerKind::TextBox => "Text box", ComposerKind::Callout { .. } => "Callout", ComposerKind::Caret => "Inserted text", + ComposerKind::Replace => "Replacement text", ComposerKind::Edit(_) => "Edit comment", }; let pos = pos2(anchor.x + 12.0, anchor.y); @@ -933,6 +955,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, let hint = match c.kind { ComposerKind::TextBox | ComposerKind::Callout { .. } => "Type text", ComposerKind::Caret => "Text to insert", + ComposerKind::Replace => "Replacement text", _ => "Add a comment", }; let edit = @@ -995,6 +1018,18 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, let knee = [(point[0] + side) / 2.0, (rect[1] + rect[3]) / 2.0]; Some(new_comment(&cx, CommentTool::Callout, Shape::Callout { rect, knee, point, font_size: 10.0 }, text)) } + ComposerKind::Replace => { + view.comments.tool_done = true; + let quads = view.comments.replace.take()?; + Some(Edit::ReplaceText { + page, + quads, + text, + author: prefs.author.clone(), + strike: prefs.style(CommentTool::ReplaceText), + caret: prefs.style(CommentTool::Caret), + }) + } ComposerKind::Caret => { view.comments.tool_done = true; let [x, y] = c.at; diff --git a/crates/ui-egui/tests/comments.rs b/crates/ui-egui/tests/comments.rs index 146ee44126..f7422b4cea 100644 --- a/crates/ui-egui/tests/comments.rs +++ b/crates/ui-egui/tests/comments.rs @@ -454,3 +454,36 @@ fn make_current_properties_default() { 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. + drag_pt(&mut h, (20.0, 155.0), (120.0, 155.0)); + 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); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 5e5d98b069..177db65bf8 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -3515,11 +3515,11 @@ title = "Replace-text proposal (strikeout + caret)" area = "E" tier = "P1" milestone = "M5" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "shipped" +commands = ["comment.replace"] +tools = ["comment_add"] +evidence = ["crates/annot/src/tests.rs::replace_text_groups_a_strikeout_and_a_caret", "crates/ui-egui/tests/comments.rs::replacing_text_strikes_it_and_adds_a_caret", "crates/automation/tests/automation.rs::drawing_comments_through_tools"] +notes = "strikeout (/IT StrikeOutTextEdit) and a grouped caret (/RT /Group) with the replacement" [[feature]] id = "comment.insert-text" @@ -3683,11 +3683,11 @@ title = "Area highlight" area = "E" tier = "P1" milestone = "M5" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "shipped" +commands = ["comment.highlight"] +tools = ["comment_add"] +evidence = ["crates/ui-egui/tests/comments.rs::highlighting_an_area_off_the_text"] +notes = "drag the highlighter off text to highlight a box" [[feature]] id = "comment.attach-file-comment"