From a03deeeb26ce51a87b616d082aa58a6e2b94a9ba Mon Sep 17 00:00:00 2001 From: rabis <340973947+rabis7575@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:59:48 +0200 Subject: [PATCH] #2428: the caret table's handles show where it starts and ends, where the pointer handles are A repeated header row or a row running on to the next page no longer puts them on every page, and they come from the same geometry as the handles under the pointer, so they can be grabbed. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/table_mouse.rs | 37 ++++++++++++----------- crates/engine/src/tests.rs | 44 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/crates/engine/src/cmd/table_mouse.rs b/crates/engine/src/cmd/table_mouse.rs index 6b26ad15c..3cc23dd67 100644 --- a/crates/engine/src/cmd/table_mouse.rs +++ b/crates/engine/src/cmd/table_mouse.rs @@ -200,28 +200,27 @@ pub fn handle_table(l: &DocLayout, doc: &Document, story: StoryRef, page: usize, } /// The move and resize handles of table `table` of `story` on this page, as Word shows them -/// while the caret is in the table: the move handle where its first row is, the resize handle -/// where its last row is. +/// while the caret is in the table: the move handle where it starts, the resize handle where it +/// ends. pub fn caret_handles(l: &DocLayout, doc: &Document, story: StoryRef, table: &Path, page: usize) -> (Option, Option) { let rows = doc.table(story, table).map_or(0, |t| t.rows.len()); - let (mut first, mut last, mut bounds) = (false, false, None::); - for it in l.pages.get(page).map_or(&[][..], |pg| pg.items.as_slice()) { - if let Placed::Cell { rect, table: t, story: st, row, .. } = it - && *st == story - && t == table - { - first |= *row == 0; - last |= *row + 1 == rows; - bounds = Some(bounds.map_or(*rect, |b| b.union(rect))); - } + let has_row = |p: usize, r: usize| { + l.pages.get(p).is_some_and(|pg| { + pg.items.iter().any(|it| matches!(it, Placed::Cell { table: t, story: st, row, .. } if *st == story && t == table && *row == r)) + }) + }; + // A repeated header row, or a row running on to the next page, is drawn on more than one + // page: the handles go where the table starts and where it ends (its pages are consecutive). + let first = has_row(page, 0) && !page.checked_sub(1).is_some_and(|p| has_row(p, 0)); + let last = rows.checked_sub(1).is_some_and(|r| has_row(page, r) && !page.checked_add(1).is_some_and(|p| has_row(p, r))); + if !first && !last { + return (None, None); } - let Some(b) = bounds else { return (None, None) }; - // Where `handle` and `ResizeTarget::handle` put them for the drawn table. - let resize = table_resize::HANDLE; - ( - first.then(|| Rect::new(b.x - HANDLE_GAP - HANDLE, b.y - HANDLE_GAP - HANDLE, HANDLE, HANDLE)), - last.then(|| Rect::new(b.right() + 1.0, b.bottom() + 1.0, resize, resize)), - ) + // The table's geometry on this page that the pointer handles are drawn and grabbed from. + let Some(g) = table_resize::tables_on(l, doc, page).into_iter().find(|g| g.story == story && g.table == *table) else { + return (None, None); + }; + (first.then(|| handle(&g)), last.then(|| g.handle())) } /// The insert button a target is, if it's one. diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 81e12260f..8ce2859fd 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1985,10 +1985,54 @@ fn the_caret_table_has_its_handles() { let (_, x, y) = crate::cmd::table_mouse::corner(&l, s.sel.focus.story, &tp).unwrap(); assert!(m.right() <= x && m.bottom() <= y, "past the top-left corner"); assert!(z.x > m.right() && z.y > m.bottom(), "past the bottom-right one"); + // Exactly where the pointer handles are, so a click on them grabs them. + let mid = |r: wordcraft_geom::Rect| (r.x + r.w / 2.0, r.y + r.h / 2.0); + let (hx, hy) = mid(m); + let g = crate::cmd::table_mouse::handle_table(&l, &s.doc, s.sel.focus.story, 0, hx, hy, 4.0).expect("the move handle grabs"); + assert_eq!(crate::cmd::table_mouse::handle(&g), m); + let (hx, hy) = mid(z); + let g = crate::cmd::table_resize::table_under(&l, &s.doc, 0, hx, hy, 4.0).expect("the resize handle grabs"); + assert_eq!(g.handle(), z); let (m, z) = crate::cmd::table_mouse::caret_handles(&l, &s.doc, s.sel.focus.story, &tp, 1); assert!(m.is_none() && z.is_none(), "not on a page without the table"); } +/// A table over more than one page shows its move handle only where it starts and its resize +/// handle only where it ends, also when its header row repeats or its one row runs on (#2428). +#[test] +fn the_caret_table_shows_its_handles_where_it_starts_and_ends() { + for header in [true, false] { + let mut s = s(); + if header { + // 60 rows under a header row that repeats on the next page. + run(&mut s, "insert.table", json!({"rows": 60, "cols": 2})); + run(&mut s, "table.properties", json!({"headerRow": true})); + } else { + // One row whose cell runs on to the next page. + run(&mut s, "insert.table", json!({"rows": 1, "cols": 1})); + for i in 0..80 { + run(&mut s, "text.insert", json!({"text": format!("Line {i}")})); + run(&mut s, "text.newParagraph", json!({})); + } + } + let (tp, _, _) = s.sel.focus.path.cell().unwrap(); + let story = s.sel.focus.story; + let l = s.layout(); + let holds = |p: usize, r: Option| { + l.pages[p] + .items + .iter() + .any(|it| matches!(it, wordcraft_layout::Placed::Cell { table, row, .. } if *table == tp && r.is_none_or(|r| *row == r))) + }; + assert!(l.pages.len() >= 2 && holds(1, Some(0)), "row 0 is drawn on page 2 too (header {header})"); + let end = (0..l.pages.len()).rev().find(|p| holds(*p, None)).unwrap(); + for p in 0..l.pages.len() { + let (m, z) = crate::cmd::table_mouse::caret_handles(&l, &s.doc, story, &tp, p); + assert_eq!((m.is_some(), z.is_some()), (p == 0, p == end), "page {p} (header {header})"); + } + } +} + /// Right-to-left tables and sections (#362): `table.rtl` and `layout.sectionRtl` set or toggle /// the direction, one Undo step each, and the layout mirrors the table. #[test]