diff --git a/ROADMAP.md b/ROADMAP.md index 92bad5c75..5d7fc8e1a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -278,6 +278,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged | | 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) | |---|---| +| 2026-10-12 | Tables: dragging the border of one selected cell (Select Cell, or a click along its left edge) moves it in that cell's row only, as in Word, like the border of cells selected across (#962) | | 2026-10-13 | Korean interface catalog completed: all 2,463 entries translated; the ko-KR system locale and saved language choice select the interface. The UI prefers Korean embedded/system font faces so Hangul renders instead of boxes, and print settings and printer feedback are translated. Korean proofing remains unavailable; macOS Korean IME fixes are still pending (#163, #164) | | 2026-10-12 | Word default shortcuts expanded (gap #24): 140 commands bound on macOS (169 combinations), 158 on Linux/Windows (188 combinations). Bound Style Inspector (Shift+F1), Macros (Alt+F8), Strikethrough (Mod+Shift+X), Find Next (Shift+F4), Copy/Paste (Mod+Insert, Shift+Insert), Go Back (Mod+Alt+Z), File Open/Save/Save As/Print (Mod+Alt+F2, Alt+Shift+F2, Alt+F2, Mod+Alt+I), Field navigation (Alt+F1, Alt+Shift+F1), Outline show first line (Alt+Shift+L), Next Footnote (Alt+Shift+>), Link to Previous (Alt+Shift+R), AutoText (Alt+F3), Spelling on Mac (Mod+Alt+L) | | 2026-10-13 | Web and Draft Layout now render visible full-resolution raster tiles for exceptionally tall pages rather than downsampling the entire page into one texture; verified with a 440-paragraph headless UI reproduction | @@ -395,6 +396,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Progress log: one selected cell's border drags in its row only (#962) | | 2026-10-12 | minor | Progress log: Word default shortcuts expanded to 140 commands on macOS (169 combinations) and 158 on Linux/Windows (188 combinations), addressing gap #24 | | 2026-10-13 | trivial | Web/Draft long-page blur fixed with visible full-resolution raster tiles | | 2026-10-14 | trivial | Progress log: VML freeforms read and kept on save | diff --git a/crates/engine/src/cmd/table_resize.rs b/crates/engine/src/cmd/table_resize.rs index 0a39febae..70f04f0c4 100644 --- a/crates/engine/src/cmd/table_resize.rs +++ b/crates/engine/src/cmd/table_resize.rs @@ -210,30 +210,20 @@ impl ResizeTarget { } } - /// When the selection runs across cells of this table (Word selects them whole) and the - /// column border grabbed at page y `y` is one of theirs, in one of their rows, the drag - /// moves it only in those rows (`only`), as Word does with selected cells' borders. Not the - /// table's left edge, which moves the whole table, nor a border of a cell merged down into - /// rows outside the selection. Text selected within one cell isn't a cell selection. + /// When cells of this table are selected whole (one cell too) or the selection runs across + /// cells of it (Word selects them whole), and the column border grabbed at page y `y` is one + /// of theirs, in one of their rows, the drag moves it only in those rows (`only`), as Word + /// does with selected cells' borders. Not the table's left edge, which moves the whole table, + /// nor a border of a cell merged down into rows outside the selection. Text selected within + /// one cell isn't a cell selection. pub fn limit_to_selection(&mut self, s: &Session, y: f32) { self.only.clear(); let Edge::Column(line) = self.edge else { return }; - let (a, b) = s.sel.ordered(); - if line == 0 || s.sel.is_collapsed() || a.story != self.story || b.story != self.story { + if line == 0 { return; } - let (Some(t), Some((ra, ca)), Some((rb, cb))) = - (s.doc.table(self.story, &self.table), cell_in(&self.table, &a.path), cell_in(&self.table, &b.path)) - else { - return; - }; - if (ra, ca) == (rb, cb) { - return; - } - let span = |r: usize, c: usize| t.rows.get(r).and_then(|row| row.cells.get(c)).map_or(1, Cell::span); - let (ga, gb) = (t.grid_col(ra, ca), t.grid_col(rb, cb)); - let (lo, hi) = (ga.min(gb), (ga + span(ra, ca)).max(gb + span(rb, cb))); - let (r0, r1) = (ra.min(rb), ra.max(rb)); + let Some(t) = s.doc.table(self.story, &self.table) else { return }; + let Some(((r0, r1), (lo, hi))) = self.selected_block(s, t) else { return }; let here = self.rows.iter().find(|r| y >= r.top && y <= r.bottom).map(|r| r.row); if line < lo || line > hi || !here.is_some_and(|r| (r0..=r1).contains(&r)) { return; @@ -258,6 +248,30 @@ impl ResizeTarget { } self.only = only; } + + /// The selected cells of this table, as their rows (first, last) and the grid lines of + /// their outer borders (left, right): whole cells (`Session::cell_range`), or a selection + /// running from one of its cells into another. `None` for text within one cell. + fn selected_block(&self, s: &Session, t: &Table) -> Option<((usize, usize), (usize, usize))> { + let lines = |cells: &[(usize, usize)]| -> Option<(usize, usize)> { + let span = |r: usize, c: usize| t.rows.get(r).and_then(|row| row.cells.get(c)).map_or(1, Cell::span); + let lo = cells.iter().map(|&(r, c)| t.grid_col(r, c)).min()?; + let hi = cells.iter().map(|&(r, c)| t.grid_col(r, c).saturating_add(span(r, c))).max()?; + Some((lo, hi)) + }; + if let Some(range) = s.cell_range().filter(|c| c.story == self.story && c.table == self.table) { + return Some((range.rows, lines(&range.cells(t))?)); + } + let (a, b) = s.sel.ordered(); + if s.sel.is_collapsed() || a.story != self.story || b.story != self.story { + return None; + } + let ((ra, ca), (rb, cb)) = (cell_in(&self.table, &a.path)?, cell_in(&self.table, &b.path)?); + if (ra, ca) == (rb, cb) { + return None; + } + Some(((ra.min(rb), ra.max(rb)), lines(&[(ra, ca), (rb, cb)])?)) + } } /// Moves the border right of column `i` of `w` (column widths, left to right) by up to `dx` @@ -964,6 +978,27 @@ mod tests { assert_eq!(first_table(&s), start, "one undo step each"); } + /// One selected cell (Select Cell, unlike its text) is a cell selection too: its border + /// moves in its row only (#962). + #[test] + fn a_selected_cells_border_moves_only_in_its_row() { + let mut s = table_session(3, 3); + let (b, c) = (cell_rect(&mut s, 1, 1), cell_rect(&mut s, 1, 2)); + let y = b.y + b.h / 2.0; + s.run("caret.set", &json!({"page": 0, "x": b.x + b.w / 2.0, "y": y})).unwrap(); + s.run("table.selectCell", &json!({})).unwrap(); + assert!(s.cell_range().is_some()); + let start = first_table(&s); + s.run("table.resizeDrag", &json!({"page": 0, "x": b.right(), "y": y, "toX": b.right() + 30.0, "toY": y})).unwrap(); + let (b1, c1) = (cell_rect(&mut s, 1, 1), cell_rect(&mut s, 1, 2)); + assert!((b1.w - (b.w + 30.0)).abs() < 1.0 && (c1.w - (c.w - 30.0)).abs() < 1.0, "{b1:?} {c1:?}"); + for r in [0, 2] { + assert!((cell_rect(&mut s, r, 1).right() - b.right()).abs() < 0.5, "row {r} keeps its border"); + } + s.run("edit.undo", &json!({})).unwrap(); + assert_eq!(first_table(&s), start, "one undo step"); + } + /// A cell merged down past the selection keeps one width: a border of it moves in the whole /// column. #[test] diff --git a/crates/ui-egui/src/table_resize.rs b/crates/ui-egui/src/table_resize.rs index add918d2a..67be6b86a 100644 --- a/crates/ui-egui/src/table_resize.rs +++ b/crates/ui-egui/src/table_resize.rs @@ -565,6 +565,28 @@ pub(crate) mod tests { assert!(widths(&mut h).is_empty(), "gone with the drag"); } + /// A cell selected with a click along its left edge: dragging its right border with the + /// mouse moves it in that cell's row only (#962). + #[test] + fn a_selected_cells_border_drags_in_its_row_only() { + let mut h = table_window(); + let (above, b, c) = (cell(&mut h, 0, 1), cell(&mut h, 1, 1), cell(&mut h, 1, 2)); + let y = b.y + b.h / 2.0; + let at = screen(&mut h, b.x + 4.5, y); + h.event(egui::Event::PointerMoved(at)); + h.step(); + button(&h, at, true); + h.step(); + button(&h, at, false); + steps(&mut h, 2); + assert_eq!(h.state().session.cell_range().map(|r| (r.rows, r.cols)), Some(((1, 1), (1, 1))), "the cell is selected"); + let (start, end) = (screen(&mut h, b.right(), y), screen(&mut h, b.right() + 30.0, y)); + drag(&mut h, start, end); + let (b1, c1) = (cell(&mut h, 1, 1), cell(&mut h, 1, 2)); + assert!((b1.w - (b.w + 30.0)).abs() < 1.0 && (c1.w - (c.w - 30.0)).abs() < 1.0, "{b1:?} {c1:?}"); + assert!((cell(&mut h, 0, 1).right() - above.right()).abs() < 0.5, "the first row keeps its border"); + } + /// Clicks inside a cell still place the caret (the border doesn't steal them), and Esc drops a /// drag without changing anything. #[test] diff --git a/docs/ui-parity.md b/docs/ui-parity.md index bbbb25af8..2b8c4f539 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -52,6 +52,7 @@ Document title opens Rename Document; the account badge opens Options › Genera | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Tables on the page: one selected cell (Select Cell, or a click along its left edge) is a cell selection when its border is dragged: the border moves in its row only (#962) | | 2026-10-12 | minor | Default keyboard shortcuts expanded to 140 commands on macOS (169 combinations) and 158 on Linux/Windows (188 combinations): Style Inspector (Shift+F1), Macros (Alt+F8), Strikethrough (Mod+Shift+X), Find Next (Shift+F4), Copy/Paste Insert keys (Mod+Insert, Shift+Insert), Go Back (Mod+Alt+Z), File Open/Save/Save As/Print (Mod+Alt+F2, Alt+Shift+F2, Alt+F2, Mod+Alt+I), Field navigation (Alt+F1, Alt+Shift+F1), Outline show first line (Alt+Shift+L), Next Footnote (Alt+Shift+>), Link to Previous (Alt+Shift+R), AutoText (Alt+F3), Spelling on Mac (Mod+Alt+L) | | 2026-10-13 | trivial | Web/Draft Layout long-page blur fixed using visible full-resolution raster tiles, tested against a 440-paragraph document | | 2026-10-14 | trivial | A click on the ribbon's chrome that isn't a command button (a Styles gallery tile, a tab name, empty ribbon space, a View check box, a status-bar item) hands the keyboard to the page in the same frame, as a ribbon command button does: typing right after the click reaches the document instead of being swallowed (#1093 follow-up) |