mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-03 00:40:33 -06:00
#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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Rect>, Option<Rect>) {
|
||||
let rows = doc.table(story, table).map_or(0, |t| t.rows.len());
|
||||
let (mut first, mut last, mut bounds) = (false, false, None::<Rect>);
|
||||
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.
|
||||
|
||||
@@ -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<usize>| {
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user