diff --git a/ROADMAP.md b/ROADMAP.md index a90a67ab4..a567fee85 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-14 · **Change:** trivial (progress log: a press on the ribbon no longer scrolls the document, #1019; previously trivial: title rename and account settings; previously trivial: progress log: Down and Up no longer skip a table, #1007; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: 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-14 · **Change:** trivial (progress log: clicking in a comment's text moves its caret, #1013; previously trivial: progress log: a press on the ribbon no longer scrolls the document, #1019; previously trivial: title rename and account settings; previously trivial: progress log: Down and Up no longer skip a table, #1007; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: 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. @@ -136,6 +136,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | What landed | |---|---| +| 2026-10-12 | Clicking inside a selected comment's balloon moves the caret in its text (#1013); the click used to leave the balloon, so the caret could only be moved with the arrow keys. A press on any balloon now belongs to the balloon, not the page behind it | | 2026-10-12 | Page background pictures and textures (Design › Page Color › Fill Effects) are drawn (tiled or stretched) and kept on save instead of being dropped from the package (#999); PDF export paints them too | | 2026-10-12 | Holding a click on the ribbon or toolbar no longer scrolls the document up (#1019): a double-click in the header or footer area left a text drag running, so the next press anywhere drove the edge auto-scroll; a drag now ends with the button, wherever it's released | | 2026-10-12 | Tracked insertions and deletions inside a field result (a `=SUM(ABOVE)` subtotal) are kept as revisions: Accept All and Reject All act on them and a save writes the `w:del` back (#1037) | @@ -335,6 +336,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Progress log: clicking in a comment's text moves its caret (#1013) | | 2026-10-12 | trivial | Progress log: a press on the ribbon no longer scrolls the document (#1019) | | 2026-10-12 | trivial | Progress log: Ctrl+drag copies an object (#1014) | | 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index e41b1837c..7512a564e 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1248,10 +1248,12 @@ 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; 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 { + // 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() + && !ui.input(|i| i.pointer.latest_pos()).is_some_and(|p| over_balloon(app, p)) + { deselect_balloon(app, ui.ctx()); } // Space and Enter also set `Response::clicked` on a focused click-sensing widget @@ -1710,6 +1712,11 @@ pub fn select_balloon(app: &mut WordApp, ctx: &egui::Context, id: u32) { app.canvas.balloon_h = 0.0; } +/// A press over a balloon card belongs to the card, though the canvas under it also contains the pointer (#1013). +fn over_balloon(app: &WordApp, p: Pos2) -> bool { + app.canvas.balloon_rects.iter().any(|(_, r)| r.contains(p)) +} + /// Leave the selected balloon, writing back what was typed in it (its fields may not be drawn /// again to notice they lost focus). pub fn deselect_balloon(app: &mut WordApp, ctx: &egui::Context) { @@ -1888,7 +1895,7 @@ fn mouse(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[Rect], layo let _ = app.run("select.paragraph", json!({})); return; } - let pressed = ui.input(|i| i.pointer.primary_pressed()) && resp.contains_pointer(); + let pressed = ui.input(|i| i.pointer.primary_pressed()) && resp.contains_pointer() && !over_balloon(app, p); if pressed { let now = ui.input(|i| i.time); let (delay, dist) = ui.ctx().options(|o| (o.input_options.max_double_click_delay, o.input_options.max_click_dist)); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8682ada1f..3f63d2710 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -3153,6 +3153,63 @@ mod tests { assert!(!a.session.sel.is_collapsed(), "the drag left a selection"); } + /// #1013: a press inside the selected comment's balloon (to move the caret in its text) left + /// the balloon, because the canvas under it also "contained" the pointer. + #[test] + fn a_press_inside_the_selected_comment_keeps_it_selected() { + let ctx = egui::Context::default(); + let mut a = app(); + a.run("text.insert", json!({"text": "Some text to comment on. ".repeat(40)})).unwrap(); + a.run("select.word", json!({})).unwrap(); + a.run("review.newComment", json!({"text": "A comment with a few words in it"})).unwrap(); + a.session.view.comments_pane = false; + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 700.0)); + let mut t = 0.0; + let mut frame = |a: &mut WordApp, events: Vec| { + t += 1.0 / 60.0; + let input = egui::RawInput { events, time: Some(t), screen_rect: Some(screen), ..Default::default() }; + ctx.run_ui(input, |ui| { + a.logic(ui.ctx()); + a.ui(ui); + }) + .drop_without_applying_deltas(); + }; + let btn = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + for _ in 0..30 { + frame(&mut a, Vec::new()); + } + let (id, card) = *a.canvas.balloon_rects.first().expect("the comment has a balloon"); + frame(&mut a, vec![egui::Event::PointerMoved(card.center())]); + frame(&mut a, vec![btn(card.center(), true)]); + frame(&mut a, vec![btn(card.center(), false)]); + for _ in 0..5 { + frame(&mut a, Vec::new()); + } + assert_eq!(a.canvas.balloon, Some(id), "a click on the balloon selects it"); + let (_, card) = *a.canvas.balloon_rects.iter().find(|(i, _)| *i == id).unwrap(); + let (focus, scroll) = (a.session.sel.clone(), a.canvas.scroll_offset); + // Press, drag a little and release inside the comment's text. + let (from, to) = (card.left_top() + egui::vec2(30.0, 30.0), card.left_top() + egui::vec2(80.0, 30.0)); + frame(&mut a, vec![egui::Event::PointerMoved(from)]); + frame(&mut a, vec![btn(from, true)]); + assert_eq!(a.canvas.balloon, Some(id), "the press stays in the balloon"); + for _ in 0..3 { + frame(&mut a, vec![egui::Event::PointerMoved(to)]); + } + assert!(!a.canvas.drag_selecting(), "the press didn't start a drag in the document"); + frame(&mut a, vec![btn(to, false)]); + assert_eq!(a.canvas.balloon, Some(id), "still selected after the release"); + assert_eq!(a.session.sel, focus, "the document's caret didn't move"); + assert_eq!(a.canvas.scroll_offset, scroll); + // A press on the page itself still leaves the balloon. + let page = a.canvas.page_rects.first().copied().unwrap(); + let body = page.left_top() + egui::vec2(120.0, 160.0); + frame(&mut a, vec![egui::Event::PointerMoved(body)]); + frame(&mut a, vec![btn(body, true)]); + frame(&mut a, vec![btn(body, false)]); + assert_eq!(a.canvas.balloon, None, "a click on the page leaves the balloon"); + } + /// #1019: a double-click in the header area returned before the release ended the drag, so /// a later press held on the ribbon edge-scrolled the document up. #[test]