diff --git a/ROADMAP.md b/ROADMAP.md index 015905e64..c23951f64 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: sample-text formulas on Enter, #1067; previously trivial: direct run-level native InkML content parts survive DOCX saves; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: a comment balloon's Delete, Reply and Resolve buttons work, #1085; previously trivial: sample-text formulas on Enter, #1067; previously trivial: direct run-level native InkML content parts survive DOCX saves; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -137,6 +137,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm |---|---| | 2026-10-12 | Text boxes from older Word files (VML) keep their internal margins, Resize shape to fit text and vertical alignment, so letterhead address blocks and form headers sit where Word puts them instead of 7 pt in and 4 pt down. Hidden drawings stay hidden (they were drawn as empty boxes) and are kept on save | | 2026-10-13 | Word's privacy options are kept and honoured when saving .docx (#1062): with Remove personal information, comments and tracked changes are saved by "Author" and the file has no creator or last-modified-by; with Remove date and time, they are saved without dates. The open document keeps the real names and dates; `file.privacyOptions` sets both | +| 2026-10-13 | A selected comment balloon's Delete, Reply and Resolve buttons work again (#1085): pressing them no longer counts as a click on the page that closes the balloon first, so a new empty comment, or any other, can be deleted from its balloon | | 2026-10-12 | Sample-text formulas on Enter (#1067): `=rand()` and `=lorem()`, optional paragraph/sentence counts, 5/3 defaults, original English and one-step undo | | 2026-10-12 | File › Export › Word 2003 XML Document (#1081): documents save as WordprocessingML 2003 (`format: "wordml2003"` on the save commands, `wordcraft-cli convert --format wordml2003`); shapes, text boxes, charts and SmartArt don't survive, and it hasn't been opened in Word yet | | 2026-10-12 | Direct run-level native InkML (`w:contentPart`) now survives DOCX edits and repeated saves with its relationship, content type and bytes intact. Native InkML rendering/editing and ink nested in groups or canvases remain missing | @@ -279,6 +280,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| | 2026-10-12 | trivial | Progress log: VML text box margins, fit and alignment; hidden drawings | +| 2026-10-13 | trivial | Progress log: comment balloon buttons work, empty comments can be deleted (#1085) | | 2026-10-12 | trivial | Progress log: sample-text formulas on Enter (#1067) | | 2026-10-12 | trivial | Progress log: direct run-level native InkML content parts survive DOCX edits and repeated saves | | 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) | diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e0b71323c..ed82ad3bf 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1733,6 +1733,27 @@ fn comments() { assert_eq!(s.doc.para_at(&Pos::body(0, 0)).unwrap().objects.len(), 0); } +/// #1085: a new comment with no text, in an empty document or on a word, goes with Review › +/// Delete at the caret (no id) and by id. +#[test] +fn empty_comments_delete() { + for body in ["", "Some text here"] { + for by_id in [false, true] { + let mut s = Session::new(wordcraft_doc::Document::new()); + if !body.is_empty() { + run(&mut s, "text.insert", json!({"text": body})); + run(&mut s, "caret.set", json!({"pos": {"story": "body", "path": [0], "off": 6}})); + } + let id = run(&mut s, "review.newComment", json!({}))["id"].clone(); + assert_eq!(run(&mut s, "review.comments", json!({}))[0]["text"], ""); + run(&mut s, "review.deleteComment", if by_id { json!({"id": id}) } else { json!({}) }); + assert!(s.doc.comments.is_empty(), "{body:?} by id {by_id}"); + assert_eq!(s.doc.para_at(&Pos::body(0, 0)).unwrap().objects.len(), 0, "{body:?}: anchors gone"); + assert_eq!(text(&s), body); + } + } +} + #[test] fn toc_and_fields() { let mut s = Session::new(crate::sample::report()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 7a6369096..0b1ba01a0 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1229,8 +1229,10 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { // the scroll area is built, so it reads last frame's viewport. app.canvas.viewport = Some(out.inner_rect); let (resp, rects) = out.inner; - // A click on the page leaves the selected comment balloon. - if app.canvas.balloon.is_some() && ui.input(|i| i.pointer.any_pressed()) && resp.contains_pointer() { + // A click on the page leaves the selected comment balloon; a click inside a balloon (its + // Delete or Resolve button, say) does not (#1085). + let in_balloon = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| app.canvas.balloon_rects.iter().any(|(_, r)| r.contains(p))); + if app.canvas.balloon.is_some() && ui.input(|i| i.pointer.any_pressed()) && resp.contains_pointer() && !in_balloon { deselect_balloon(app, ui.ctx()); } // Focus: the canvas takes keyboard focus on click and keeps Tab/arrows. @@ -2937,6 +2939,52 @@ mod tests { assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::Default); } + /// #1085: a selected balloon's buttons work (pressing them is not a click on the page that + /// leaves the balloon): a new comment with no text resolves and deletes from its balloon. + #[test] + fn an_empty_comment_balloon_deletes() { + for body in ["", "Some text here"] { + let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default()); + if !body.is_empty() { + a.run("text.insert", json!({"text": body})).unwrap(); + a.run("select.text", json!({"text": "text"})).unwrap(); + } + let id = a.run("review.newComment", json!({})).unwrap()["id"].as_u64().unwrap() as u32; + a.run("view.commentsPane", json!({"value": false})).unwrap(); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + a, + ); + for _ in 0..6 { + h.step(); + } + let click = |h: &mut egui_kittest::Harness<'_, WordApp>, at: egui::Pos2| { + h.event(egui::Event::PointerMoved(at)); + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }); + h.step(); + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }); + for _ in 0..3 { + h.step(); + } + }; + let card = h.state().canvas.balloon_rects.iter().find(|(i, _)| *i == id).map(|(_, r)| *r).expect("the comment has a balloon"); + click(&mut h, card.left_top() + vec2(30.0, 8.0)); + assert_eq!(h.state().canvas.balloon, Some(id), "{body:?}: the click selected the balloon"); + use egui_kittest::kittest::Queryable; + let resolve = h.get_by_label("Resolve").rect().center(); + click(&mut h, resolve); + assert!(h.state().session.doc.comments[&id].resolved, "{body:?}: Resolve worked"); + assert_eq!(h.state().canvas.balloon, Some(id), "{body:?}: the balloon stays selected"); + let del = h.get_by_label("Delete").rect().center(); + click(&mut h, del); + assert!(h.state().session.doc.comments.is_empty(), "{body:?}: the empty comment was deleted"); + } + } + /// A typed character is laid out and painted in the frame that delivers the key, not the next /// one: keys are handled before the canvas takes the layout it paints. #[test]