diff --git a/crates/engine/src/cmd/table_mouse.rs b/crates/engine/src/cmd/table_mouse.rs index 6b26ad15cd..3cc23dd673 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 81e12260f4..8ce2859fd5 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]