mirror of
https://github.com/aphrody-labs/wordcraft.git
synced 2026-10-13 19:26:53 +00:00
Tables: Ctrl, Alt, left-edge and selected-cells border drags (#720)
* Rulers: table column and row markers; indents in a cell measured from it (#700) With the caret in a table, the rulers show the table, as Word's do: - The horizontal ruler marks the column borders of the caret's row (past the table's left edge); the vertical ruler marks the bottom of each of the table's rows on the page. Each marker has the resize pointer, a tooltip and a name for screen readers. - Dragging a marker drags that border exactly like a drag on the page (#600): a dashed guide, Escape cancels, one undo step on release through `table.resizeDrag`. On the horizontal ruler only the column left of the border changes and the columns right of it move along, so the table's width changes (Word's ruler); a row marker sets the row's height. The markers follow any drag of the table's borders, on the ruler or on the page. - In a cell, the indent markers and tabs are measured from the cell's text area (its box less its margins); they were placed as if the paragraph started at the page's margin. - Layout: `cell_margins` (a cell's own margins, else the table's, else Word's), so the ruler uses what the layout does. - UI: crates/ui-egui/src/table_ruler.rs; table_resize.rs starts a drag from a ruler marker (`grab`) and tells the ruler where a dragged border is (`moved`), sharing that computation with the dashed guide. - Tests: layout (cell_margins matches where each cell's text starts); kittest with real pointer events: a column marker drag (the marker follows, one undo step, the columns right of it keep their widths), a row marker drag, and an 18 pt drag of the left-indent marker in a cell indenting the paragraph 18 pt. Progress docs updated: ui-parity (rulers 70% -> 85%), gaps (the ruler's column markers are off the list), target-app-parity, ROADMAP. No new commands or labels (the tooltips reuse "Table Column Width" and "Table Row Height"). `cargo xtask ci` passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rulers: Shift works the other way round on the ruler; row markers beside a vertical merge Checked against Word (2019, Windows): on the ruler a column marker drag changes only the column on its left and the columns right of it move along (as before), and with Shift the neighbour makes up the difference, so the table keeps its width; on the page Shift does the opposite. The ruler now follows that: `ResizeDrag::plan` takes the Shift key and flips it for a drag started on a ruler marker. Since #669 a vertically merged cell's box spans all its rows, so a row marker took the merge's bottom when the merge was the row's leftmost cell. The marker now sits on the row's shortest box. (`Placed::Cell` patterns take `..` for #669's new field.) - Test (kittest, real pointer events): a Shift drag of a column marker on the ruler gives the neighbour's width to the column on the left and keeps the table's width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tables: Ctrl, Alt, left-edge and selected-cells border drags (#711) Word's border drags that #600 left out: - Ctrl while dragging a column border: the columns right of it share the change in proportion to their widths, so the table keeps its width (none narrower than the minimum). Ctrl wins over Shift; on the table's right edge only the last column changes. - The table's left edge can be dragged, from just outside it: the table's indent moves and the first column makes up the difference, so the other borders stay. A centred or right-aligned table becomes left-aligned where the edge was dropped; a floating table moves its horizontal offset. The horizontal ruler has a marker on it too. - Alt while dragging a column border: the horizontal ruler shows each column's width as it is going to be. - With the selection across two or more cells, dragging one of their column borders (in one of their rows) moves it only in their rows; the grid gets a line for the new border and the cells' spans follow. A border of a cell merged down past the selection moves in the whole column. Text selected within one cell isn't a cell selection (it can't be told from one selected cell yet), so it moves the whole column. - Engine: `Share` (who makes up a column border's move), `Edge::Column(0)`, `ResizeTarget::limit_to_selection`/`only`/ `planned_x`, `Plan::indent_by`/`cells`; `table.resizeDrag` takes `"ctrl"`. `plan` still takes a bool for Shift. Each drag is one command and one undo step. - UI: table_resize.rs (keys, a guide down the rows that move, `measuring`), table_ruler.rs (left-edge marker, `widths`). - Tests: engine (Ctrl in proportion and at the minimum; the left edge, also of a centred table; selected cells' rows only, with the grid and spans; a merged cell past the selection); kittest with real pointer events (a Ctrl drag, the left edge and its ruler marker, Alt's widths mid-drag). Progress docs updated: ui-parity (tables on the page 80% -> 90%), target-app-parity, ROADMAP. No new commands or labels. `cargo xtask ci` passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tables: a centred or right-aligned table keeps its alignment when its left edge is dragged Checked against Word (2019, Windows): dragging the left edge of a centred table widens the first column by the whole drag and the table stays centred (each side moves half); a right-aligned table keeps its right edge and grows to the left. Only a left-aligned table's indent moves. #711 made such a table left-aligned where the edge was dropped; now `move_left_edge` leaves a centred or right-aligned table's alignment and indent alone and says whether it moved anything, and the drop-point correction runs only then. The ruler's Shift (from #710) now goes through `share`: on a ruler marker Shift makes the neighbour take up the change, without it only the column on the left changes. - Tests: the left-edge test checks the centred table (first column +20 pt, the others unchanged, still centred) and a right-aligned one (only the left edge moves); a kittest Shift drag on the ruler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
6 files changed
+609
-97
No files matched your search
+3
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
**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-11 · **Change:** 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-11 · **Change:** trivial (table border drags with Ctrl and Alt, from the left edge and of selected cells, #711; 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
|
||||||
|
|
||||||
WordCraft aims for complete parity with Microsoft Word, then goes further on speed, openness and
|
WordCraft aims for complete parity with Microsoft Word, then goes further on speed, openness and
|
||||||
agent control. This page is the summary; the assessment is
|
agent control. This page is the summary; the assessment is
|
||||||
@@ -168,6 +168,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 | 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-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-11 | Table border drags like Word's (#711): Ctrl shares a column border's change among the columns right of it (the table keeps its width), the table's left edge moves a left-aligned table's indent (a centred or right-aligned table keeps its alignment, as in Word), a border of cells selected across moves only in their rows, and Alt shows the column widths on the ruler |
|
||||||
| 2026-10-11 | Tables on the rulers (#700): with the caret in a table the horizontal ruler marks its row's column borders and the vertical ruler its rows' bottoms; dragging a marker resizes the column (the columns right of it move along; with Shift the neighbour makes up the difference) or the row, one undo step, Escape cancels; in a cell the indents and tabs are measured from the cell |
|
| 2026-10-11 | Tables on the rulers (#700): with the caret in a table the horizontal ruler marks its row's column borders and the vertical ruler its rows' bottoms; dragging a marker resizes the column (the columns right of it move along; with Shift the neighbour makes up the difference) or the row, one undo step, Escape cancels; in a cell the indents and tabs are measured from the cell |
|
||||||
| 2026-10-12 | AutoCorrect capitals (#544): Correct TWo INitial CApitals (with exceptions), accidental cAPS LOCK, first letter of table cells; each undoable and switchable |
|
| 2026-10-12 | AutoCorrect capitals (#544): Correct TWo INitial CApitals (with exceptions), accidental cAPS LOCK, first letter of table cells; each undoable and switchable |
|
||||||
| 2026-10-11 | Turkish (`tr`) interface completed (#774): 2,325 entries, every key of the Russian catalog (equation gallery, built-in style names, watermarks, paper sizes, preview tiles); the interface audit checks Turkish too |
|
| 2026-10-11 | Turkish (`tr`) interface completed (#774): 2,325 entries, every key of the Russian catalog (equation gallery, built-in style names, watermarks, paper sizes, preview tiles); the interface audit checks Turkish too |
|
||||||
@@ -258,6 +259,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) |
|
||||||
| 2026-10-11 | trivial | Progress log: table markers on the rulers (#700) |
|
| 2026-10-11 | trivial | Progress log: table markers on the rulers (#700) |
|
||||||
| 2026-10-11 | trivial | Progress log: the table context menu |
|
| 2026-10-11 | trivial | Progress log: the table context menu |
|
||||||
| 2026-10-11 | trivial | Progress log: drags and Shift+arrows over table cells select whole cells |
|
| 2026-10-11 | trivial | Progress log: drags and Shift+arrows over table cells select whole cells |
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
//! Resizing a table with the mouse, like Word: drag a border between columns (or the table's
|
//! Resizing a table with the mouse, like Word: drag a border between columns (or the table's
|
||||||
//! right edge), a row's bottom border, or the small handle just outside the table's bottom-right
|
//! left or right edge), a row's bottom border, or the small handle just outside the table's
|
||||||
//! corner to scale the whole table. The geometry comes from the layout (what is on the page, in
|
//! bottom-right corner to scale the whole table. With cells selected, a border of theirs moves
|
||||||
|
//! only in their rows. The geometry comes from the layout (what is on the page, in
|
||||||
//! page points), so the result is what the drag showed even when autofit or a percentage width
|
//! page points), so the result is what the drag showed even when autofit or a percentage width
|
||||||
//! made the drawn columns differ from the stored grid. The app sends each finished drag here,
|
//! made the drawn columns differ from the stored grid. The app sends each finished drag here,
|
||||||
//! so every drag is one undo step.
|
//! so every drag is one undo step.
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use wordcraft_doc::props::HeightRule;
|
use wordcraft_doc::props::{Align, HeightRule, VMerge};
|
||||||
use wordcraft_doc::{Document, Path, StoryRef, Table};
|
use wordcraft_doc::table::MAX_COLS;
|
||||||
|
use wordcraft_doc::{Cell, Document, Path, StoryRef, Table};
|
||||||
use wordcraft_geom::Rect;
|
use wordcraft_geom::Rect;
|
||||||
use wordcraft_layout::{DocLayout, Placed};
|
use wordcraft_layout::{DocLayout, Placed};
|
||||||
|
|
||||||
@@ -27,15 +29,15 @@ const MAX_LEN: f32 = 1584.0;
|
|||||||
|
|
||||||
pub fn specs() -> Vec<CommandSpec> {
|
pub fn specs() -> Vec<CommandSpec> {
|
||||||
vec![CommandSpec::new("table.resizeDrag", "Resize Table by Dragging", "Table Layout › Cell Size", resize_drag).params(
|
vec![CommandSpec::new("table.resizeDrag", "Resize Table by Dragging", "Table Layout › Cell Size", resize_drag).params(
|
||||||
r#"{"page": n, "x": pt, "y": pt, "toX": pt, "toY": pt, "shift"?: bool, "reach"?: pt} (a mouse drag in page points, 0-based page, starting on a table border or the resize handle: a column border moves (the column to its right gives or takes the difference, so the table keeps its width; with shift, or on the table's right edge, only the column left of it changes); a row's bottom border sets that row's height; the handle just outside the bottom-right corner scales every column and the rows on that page. `reach` (default 3) is how near the start has to be.)"#,
|
r#"{"page": n, "x": pt, "y": pt, "toX": pt, "toY": pt, "shift"?: bool, "ctrl"?: bool, "reach"?: pt} (a mouse drag in page points, 0-based page, starting on a table border or the resize handle: a column border moves (the column to its right gives or takes the difference, so the table keeps its width; with ctrl the columns right of it share it in proportion; with shift, or on the table's right edge, only the column left of it changes); the table's left edge moves a left-aligned table's indent, the first column making up the difference (a centred or right-aligned table keeps its alignment: only the first column changes); with cells of the table selected, a border of theirs moves only in their rows; a row's bottom border sets that row's height; the handle just outside the bottom-right corner scales every column and the rows on that page. `reach` (default 3) is how near the start has to be.)"#,
|
||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a press on the page would grab.
|
/// What a press on the page would grab.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum Edge {
|
pub enum Edge {
|
||||||
/// The vertical border at grid line `line` (1 = between the first two columns; the last is
|
/// The vertical border at grid line `line` (0 = the table's left edge, 1 = between the first
|
||||||
/// the table's right edge).
|
/// two columns; the last is the table's right edge).
|
||||||
Column(usize),
|
Column(usize),
|
||||||
/// The bottom border of row `row`.
|
/// The bottom border of row `row`.
|
||||||
Row(usize),
|
Row(usize),
|
||||||
@@ -43,6 +45,27 @@ pub enum Edge {
|
|||||||
Corner,
|
Corner,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Who makes up the difference when a column border moves: Word's keys while dragging.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum Share {
|
||||||
|
/// No key: the column on the right, so the table keeps its width.
|
||||||
|
#[default]
|
||||||
|
Neighbour,
|
||||||
|
/// Shift (and a ruler marker): nobody; only the column on the left changes and the ones
|
||||||
|
/// right of it move along.
|
||||||
|
LeftOnly,
|
||||||
|
/// Ctrl: the columns on the right, in proportion to their widths, so the table keeps its
|
||||||
|
/// width.
|
||||||
|
Proportional,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether Shift is held.
|
||||||
|
impl From<bool> for Share {
|
||||||
|
fn from(shift: bool) -> Share {
|
||||||
|
if shift { Share::LeftOnly } else { Share::Neighbour }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A row of the table as drawn on one page.
|
/// A row of the table as drawn on one page.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct DrawnRow {
|
pub struct DrawnRow {
|
||||||
@@ -65,6 +88,10 @@ pub struct ResizeTarget {
|
|||||||
pub rows: Vec<DrawnRow>,
|
pub rows: Vec<DrawnRow>,
|
||||||
/// Whether the table's last row is on this page (the handle is shown only then).
|
/// Whether the table's last row is on this page (the handle is shown only then).
|
||||||
pub ends_here: bool,
|
pub ends_here: bool,
|
||||||
|
/// When cells of the table are selected and the column border is one of theirs
|
||||||
|
/// (`limit_to_selection`): their rows, each with the grid lines its cells' borders are on.
|
||||||
|
/// The border moves only there; empty, it moves in the whole column.
|
||||||
|
pub only: Vec<(usize, Vec<usize>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a drag to a point would make of the table.
|
/// What a drag to a point would make of the table.
|
||||||
@@ -74,6 +101,13 @@ pub struct Plan {
|
|||||||
pub widths: Option<Vec<f32>>,
|
pub widths: Option<Vec<f32>>,
|
||||||
/// New heights (points) of these rows.
|
/// New heights (points) of these rows.
|
||||||
pub heights: Vec<(usize, f32)>,
|
pub heights: Vec<(usize, f32)>,
|
||||||
|
/// How far (points) the table's left edge moves: a left-aligned table's indent (or a
|
||||||
|
/// floating table's offset) changes by as much; a centred or right-aligned table keeps its
|
||||||
|
/// alignment.
|
||||||
|
pub indent_by: Option<f32>,
|
||||||
|
/// New cell widths (points, left to right) of the rows whose border moves on its own
|
||||||
|
/// (`ResizeTarget::only`).
|
||||||
|
pub cells: Vec<(usize, Vec<f32>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResizeTarget {
|
impl ResizeTarget {
|
||||||
@@ -98,35 +132,52 @@ impl ResizeTarget {
|
|||||||
Rect::new(self.right() + 1.0, self.bottom() + 1.0, HANDLE, HANDLE)
|
Rect::new(self.right() + 1.0, self.bottom() + 1.0, HANDLE, HANDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The new widths and heights a drag that moved the pointer by (`dx`, `dy`) points gives.
|
/// The new widths and heights a drag that moved the pointer by (`dx`, `dy`) points gives;
|
||||||
pub fn plan(&self, dx: f32, dy: f32, shift: bool) -> Plan {
|
/// `share` says who makes up a column border's move (`true`: Shift is held).
|
||||||
|
pub fn plan(&self, dx: f32, dy: f32, share: impl Into<Share>) -> Plan {
|
||||||
|
let share = share.into();
|
||||||
let (dx, dy) =
|
let (dx, dy) =
|
||||||
(if dx.is_finite() { dx.clamp(-MAX_LEN, MAX_LEN) } else { 0.0 }, if dy.is_finite() { dy.clamp(-MAX_LEN, MAX_LEN) } else { 0.0 });
|
(if dx.is_finite() { dx.clamp(-MAX_LEN, MAX_LEN) } else { 0.0 }, if dy.is_finite() { dy.clamp(-MAX_LEN, MAX_LEN) } else { 0.0 });
|
||||||
let mut w = self.widths();
|
let mut w = self.widths();
|
||||||
match self.edge {
|
match self.edge {
|
||||||
Edge::Column(line) => {
|
// The table's left edge: the table moves in or out and the first column makes up the
|
||||||
let Some(left) = line.checked_sub(1).and_then(|i| w.get(i).copied()) else { return Plan::default() };
|
// difference, so the other borders stay.
|
||||||
let right = w.get(line).copied();
|
Edge::Column(0) => {
|
||||||
let (new_left, new_right) = match right {
|
let Some(first) = w.first_mut() else { return Plan::default() };
|
||||||
// An inner border: the neighbour on the right makes up the difference.
|
let d = dx.clamp(*first - MAX_LEN, (*first - MIN_COL).max(*first - MAX_LEN));
|
||||||
Some(right) if !shift => {
|
*first -= d;
|
||||||
let d = dx.clamp(MIN_COL - left, (right - MIN_COL).max(MIN_COL - left));
|
Plan { widths: Some(w), indent_by: Some(d), ..Plan::default() }
|
||||||
(left + d, Some(right - d))
|
|
||||||
}
|
|
||||||
// The right edge (or Shift): only the column on the left changes.
|
|
||||||
_ => ((left + dx).clamp(MIN_COL, MAX_LEN), None),
|
|
||||||
};
|
|
||||||
if let Some(x) = w.get_mut(line - 1) {
|
|
||||||
*x = new_left;
|
|
||||||
}
|
|
||||||
if let (Some(r), Some(x)) = (new_right, w.get_mut(line)) {
|
|
||||||
*x = r;
|
|
||||||
}
|
|
||||||
Plan { widths: Some(w), heights: Vec::new() }
|
|
||||||
}
|
}
|
||||||
|
// A border of selected cells: it moves in their rows only, as far as all of them let
|
||||||
|
// it, each row sharing the move out among its own cells.
|
||||||
|
Edge::Column(line) if !self.only.is_empty() => {
|
||||||
|
let rows: Vec<(usize, Vec<f32>, usize)> = self
|
||||||
|
.only
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(r, lines)| {
|
||||||
|
let i = lines.iter().position(|g| *g == line)?.checked_sub(1)?;
|
||||||
|
let xs: Vec<f32> = lines.iter().map(|g| self.cols.get(*g).copied()).collect::<Option<_>>()?;
|
||||||
|
Some((*r, xs.windows(2).map(|x| (x[1] - x[0]).max(0.0)).collect(), i))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let d = rows.iter().filter_map(|(_, w, i)| share_out(&mut w.clone(), *i, dx, share)).min_by(|a, b| a.abs().total_cmp(&b.abs()));
|
||||||
|
let Some(d) = d else { return Plan::default() };
|
||||||
|
let cells = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|(r, mut w, i)| {
|
||||||
|
share_out(&mut w, i, d, share);
|
||||||
|
(r, w)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Plan { cells, ..Plan::default() }
|
||||||
|
}
|
||||||
|
Edge::Column(line) => match line.checked_sub(1).and_then(|i| share_out(&mut w, i, dx, share)) {
|
||||||
|
Some(_) => Plan { widths: Some(w), ..Plan::default() },
|
||||||
|
None => Plan::default(),
|
||||||
|
},
|
||||||
Edge::Row(row) => {
|
Edge::Row(row) => {
|
||||||
let Some(r) = self.rows.iter().find(|r| r.row == row) else { return Plan::default() };
|
let Some(r) = self.rows.iter().find(|r| r.row == row) else { return Plan::default() };
|
||||||
Plan { widths: None, heights: vec![(row, (r.bottom - r.top + dy).clamp(MIN_ROW, MAX_LEN))] }
|
Plan { heights: vec![(row, (r.bottom - r.top + dy).clamp(MIN_ROW, MAX_LEN))], ..Plan::default() }
|
||||||
}
|
}
|
||||||
Edge::Corner => {
|
Edge::Corner => {
|
||||||
let (width, height) = (self.right() - self.left(), self.bottom() - self.top());
|
let (width, height) = (self.right() - self.left(), self.bottom() - self.top());
|
||||||
@@ -138,10 +189,119 @@ impl ResizeTarget {
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
Plan { widths, heights }
|
Plan { widths, heights, ..Plan::default() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Page x of grid line `line` once `plan` is applied: in row `row`, when that row's border
|
||||||
|
/// moves on its own (`Plan::cells`), else in every row.
|
||||||
|
pub fn planned_x(&self, plan: &Plan, row: Option<usize>, line: usize) -> f32 {
|
||||||
|
let left = self.left() + plan.indent_by.unwrap_or(0.0);
|
||||||
|
if let Some(r) = row
|
||||||
|
&& let Some((_, w)) = plan.cells.iter().find(|(c, _)| *c == r)
|
||||||
|
&& let Some(k) = self.only.iter().find(|(o, _)| *o == r).and_then(|(_, lines)| lines.iter().position(|g| *g == line))
|
||||||
|
{
|
||||||
|
return left + w.iter().take(k).sum::<f32>();
|
||||||
|
}
|
||||||
|
match &plan.widths {
|
||||||
|
Some(w) => left + w.iter().take(line).sum::<f32>(),
|
||||||
|
None => self.cols.get(line).copied().unwrap_or(left),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
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 {
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
let mut only = Vec::new();
|
||||||
|
for r in r0..=r1 {
|
||||||
|
let Some(row) = t.rows.get(r) else { break };
|
||||||
|
let mut lines = vec![0usize];
|
||||||
|
for cell in &row.cells {
|
||||||
|
let g = lines.last().copied().unwrap_or(0);
|
||||||
|
let end = g.saturating_add(cell.span());
|
||||||
|
// A merged cell that goes on above or below the selection keeps one width.
|
||||||
|
let below =
|
||||||
|
|| t.cell_at_grid(r + 1, g).and_then(|c| t.rows.get(r + 1)?.cells.get(c)).is_some_and(|c| c.props.vmerge == VMerge::Continue);
|
||||||
|
let past = (r == r0 && cell.props.vmerge == VMerge::Continue) || (r == r1 && cell.props.vmerge != VMerge::None && below());
|
||||||
|
if past && (g == line || end == line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lines.push(end);
|
||||||
|
}
|
||||||
|
only.push((r, lines));
|
||||||
|
}
|
||||||
|
self.only = only;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the border right of column `i` of `w` (column widths, left to right) by up to `dx`
|
||||||
|
/// points, `share` saying who makes up the difference, and no column narrower than `MIN_COL`:
|
||||||
|
/// how far it moved, or `None` when there's no column `i`.
|
||||||
|
fn share_out(w: &mut [f32], i: usize, dx: f32, share: Share) -> Option<f32> {
|
||||||
|
let left = *w.get(i)?;
|
||||||
|
let rest: f32 = w.iter().skip(i + 1).sum();
|
||||||
|
let narrowest = w.iter().skip(i + 1).copied().fold(f32::INFINITY, f32::min);
|
||||||
|
// On the table's right edge (or with no room on the right) only the left column changes.
|
||||||
|
let share = match share {
|
||||||
|
Share::Neighbour if i + 1 < w.len() => Share::Neighbour,
|
||||||
|
Share::Proportional if rest > 0.0 && narrowest > 0.0 => Share::Proportional,
|
||||||
|
_ => Share::LeftOnly,
|
||||||
|
};
|
||||||
|
let least = MIN_COL - left;
|
||||||
|
let d = match share {
|
||||||
|
Share::Neighbour => dx.clamp(least, (w.get(i + 1).copied().unwrap_or(0.0) - MIN_COL).max(least)),
|
||||||
|
// As far as the narrowest column on the right, shrunk in proportion, stays `MIN_COL`.
|
||||||
|
Share::Proportional => dx.clamp(least, (rest * (1.0 - (MIN_COL / narrowest).min(1.0))).max(least)),
|
||||||
|
Share::LeftOnly => (left + dx).clamp(MIN_COL, MAX_LEN) - left,
|
||||||
|
};
|
||||||
|
if let Some(x) = w.get_mut(i) {
|
||||||
|
*x = left + d;
|
||||||
|
}
|
||||||
|
match share {
|
||||||
|
Share::Neighbour => {
|
||||||
|
if let Some(x) = w.get_mut(i + 1) {
|
||||||
|
*x -= d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Share::Proportional => w.iter_mut().skip(i + 1).for_each(|x| *x *= (rest - d) / rest),
|
||||||
|
Share::LeftOnly => {}
|
||||||
|
}
|
||||||
|
Some(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The (row, cell) of table `tp` that a position at `p` is in.
|
||||||
|
fn cell_in(tp: &Path, p: &Path) -> Option<(usize, usize)> {
|
||||||
|
let n = tp.0.len();
|
||||||
|
if p.0.len() < n + 3 || !p.0.starts_with(&tp.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((*p.0.get(n)? as usize, *p.0.get(n + 1)? as usize))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A table cell's area on a page.
|
/// A table cell's area on a page.
|
||||||
@@ -244,7 +404,7 @@ pub(crate) fn tables_on(l: &DocLayout, doc: &Document, page: usize) -> Vec<Resiz
|
|||||||
let mine: Vec<&CellBox> = boxes.iter().filter(|b| b.story == story && b.table == *path).collect();
|
let mine: Vec<&CellBox> = boxes.iter().filter(|b| b.story == story && b.table == *path).collect();
|
||||||
let (cols, rows) = geometry(t, &mine)?;
|
let (cols, rows) = geometry(t, &mine)?;
|
||||||
let ends_here = rows.last().is_some_and(|r| r.row + 1 == t.rows.len());
|
let ends_here = rows.last().is_some_and(|r| r.row + 1 == t.rows.len());
|
||||||
Some(ResizeTarget { edge: Edge::Corner, story, table: path.clone(), cols, rows, ends_here })
|
Some(ResizeTarget { edge: Edge::Corner, story, table: path.clone(), cols, rows, ends_here, only: Vec::new() })
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -293,8 +453,11 @@ pub fn resize_target(l: &DocLayout, doc: &Document, page: usize, x: f32, y: f32,
|
|||||||
// Column borders: in the row under the pointer, only lines that are cell borders there.
|
// Column borders: in the row under the pointer, only lines that are cell borders there.
|
||||||
let row_here = target.rows.iter().find(|r| y >= r.top && y <= r.bottom).cloned();
|
let row_here = target.rows.iter().find(|r| y >= r.top && y <= r.bottom).cloned();
|
||||||
if let Some(r) = &row_here {
|
if let Some(r) = &row_here {
|
||||||
for line in 1..target.cols.len() {
|
for line in 0..target.cols.len() {
|
||||||
|
// The table's left edge is grabbed from outside it (a point in, for the border's
|
||||||
|
// width): inside starts the first cell.
|
||||||
if r.edges.contains(&line)
|
if r.edges.contains(&line)
|
||||||
|
&& (line > 0 || x <= left + 1.0)
|
||||||
&& let Some(cx) = target.cols.get(line).copied()
|
&& let Some(cx) = target.cols.get(line).copied()
|
||||||
{
|
{
|
||||||
offer((x - cx).abs(), Edge::Column(line), &mut target);
|
offer((x - cx).abs(), Edge::Column(line), &mut target);
|
||||||
@@ -316,16 +479,18 @@ fn resize_drag(s: &mut Session, v: &Value) -> CmdResult {
|
|||||||
p::u64(v, "page").map(|n| usize::try_from(n).unwrap_or(usize::MAX)).ok_or_else(|| CmdError::Params("`page` (number) is required".into()))?;
|
p::u64(v, "page").map(|n| usize::try_from(n).unwrap_or(usize::MAX)).ok_or_else(|| CmdError::Params("`page` (number) is required".into()))?;
|
||||||
let (x, y) = (p::req_f32(v, "x")?, p::req_f32(v, "y")?);
|
let (x, y) = (p::req_f32(v, "x")?, p::req_f32(v, "y")?);
|
||||||
let (to_x, to_y) = (p::req_f32(v, "toX")?, p::req_f32(v, "toY")?);
|
let (to_x, to_y) = (p::req_f32(v, "toX")?, p::req_f32(v, "toY")?);
|
||||||
let shift = p::bool(v, "shift").unwrap_or(false);
|
// Ctrl wins over Shift, as in Word.
|
||||||
|
let share = if p::bool(v, "ctrl").unwrap_or(false) { Share::Proportional } else { Share::from(p::bool(v, "shift").unwrap_or(false)) };
|
||||||
let reach = p::f32(v, "reach").unwrap_or(REACH);
|
let reach = p::f32(v, "reach").unwrap_or(REACH);
|
||||||
let l = s.layout();
|
let l = s.layout();
|
||||||
if l.pages.get(page).is_none() {
|
if l.pages.get(page).is_none() {
|
||||||
return Err(CmdError::Params(format!("no page {page}")));
|
return Err(CmdError::Params(format!("no page {page}")));
|
||||||
}
|
}
|
||||||
let target =
|
let mut target =
|
||||||
resize_target(&l, &s.doc, page, x, y, reach).ok_or_else(|| CmdError::Failed("there's no table border or resize handle there".into()))?;
|
resize_target(&l, &s.doc, page, x, y, reach).ok_or_else(|| CmdError::Failed("there's no table border or resize handle there".into()))?;
|
||||||
let plan = target.plan(to_x - x, to_y - y, shift);
|
target.limit_to_selection(s, y);
|
||||||
if plan.widths.is_none() && plan.heights.is_empty() {
|
let plan = target.plan(to_x - x, to_y - y, share);
|
||||||
|
if plan.widths.is_none() && plan.heights.is_empty() && plan.cells.is_empty() {
|
||||||
return sel_result(s);
|
return sel_result(s);
|
||||||
}
|
}
|
||||||
let t = s.doc.table_mut(target.story, &target.table)?;
|
let t = s.doc.table_mut(target.story, &target.table)?;
|
||||||
@@ -340,7 +505,21 @@ fn resize_drag(s: &mut Session, v: &Value) -> CmdResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let moved = plan.indent_by.is_some_and(|d| move_left_edge(t, d));
|
||||||
|
if !plan.cells.is_empty() {
|
||||||
|
regrid(t, &target.cols, &plan.cells)?;
|
||||||
|
}
|
||||||
touch(s, &target)?;
|
touch(s, &target)?;
|
||||||
|
// Where a moved left edge lands isn't always its indent or offset alone (a floating table's
|
||||||
|
// anchor): measure where it came to and put it right once.
|
||||||
|
if moved
|
||||||
|
&& let Some(d) = plan.indent_by
|
||||||
|
&& let Some(got) = drawn_left(&s.layout(), page, &target)
|
||||||
|
&& (got - (target.left() + d)).abs() > 0.05
|
||||||
|
{
|
||||||
|
move_left_edge(s.doc.table_mut(target.story, &target.table)?, target.left() + d - got);
|
||||||
|
touch(s, &target)?;
|
||||||
|
}
|
||||||
if plan.heights.is_empty() {
|
if plan.heights.is_empty() {
|
||||||
return sel_result(s);
|
return sel_result(s);
|
||||||
}
|
}
|
||||||
@@ -378,6 +557,84 @@ fn resize_drag(s: &mut Session, v: &Value) -> CmdResult {
|
|||||||
sel_result(s)
|
sel_result(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves `t`'s left edge `by` points: its indent, or a floating table's offset; whether it did.
|
||||||
|
/// A centred or right-aligned table keeps its alignment, as in Word: the first column's new width
|
||||||
|
/// is all that changes, and the table stays centred or on its right edge.
|
||||||
|
fn move_left_edge(t: &mut Table, by: f32) -> bool {
|
||||||
|
match t.props.float.as_mut() {
|
||||||
|
Some(f) => {
|
||||||
|
f.h_align = None;
|
||||||
|
f.x = (f.x + by).clamp(-MAX_LEN, MAX_LEN);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None if matches!(t.props.align, Some(Align::Center | Align::Right)) => false,
|
||||||
|
None => {
|
||||||
|
t.props.indent = Some((t.props.indent.unwrap_or(0.0) + by).clamp(-MAX_LEN, MAX_LEN));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets `t`'s grid when some rows' cells change width on their own: `rows` gives their new
|
||||||
|
/// widths, left to right; the other rows keep their cells' borders where they are drawn (the
|
||||||
|
/// grid lines at `cols`). The grid gets a line wherever a row has a cell border, and each cell
|
||||||
|
/// spans the columns between its own.
|
||||||
|
fn regrid(t: &mut Table, cols: &[f32], rows: &[(usize, Vec<f32>)]) -> Result<(), CmdError> {
|
||||||
|
let left = cols.first().copied().unwrap_or(0.0);
|
||||||
|
let last = cols.len().saturating_sub(1);
|
||||||
|
// Every row's cell borders, measured from the table's left edge.
|
||||||
|
let borders: Vec<Vec<f32>> = t
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(r, row)| match rows.iter().find(|(i, w)| *i == r && w.len() == row.cells.len()) {
|
||||||
|
Some((_, w)) => std::iter::once(0.0)
|
||||||
|
.chain(w.iter().scan(0.0, |x, cw| {
|
||||||
|
*x += cw;
|
||||||
|
Some(*x)
|
||||||
|
}))
|
||||||
|
.collect::<Vec<f32>>(),
|
||||||
|
None => std::iter::once(0.0)
|
||||||
|
.chain(row.cells.iter().scan(0usize, |g, c| {
|
||||||
|
*g = g.saturating_add(c.span());
|
||||||
|
Some(cols.get((*g).min(last)).copied().unwrap_or(left) - left)
|
||||||
|
}))
|
||||||
|
.collect::<Vec<f32>>(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut lines: Vec<f32> = borders.iter().flatten().copied().collect();
|
||||||
|
lines.sort_by(f32::total_cmp);
|
||||||
|
lines.dedup_by(|a, b| (*a - *b).abs() < 0.01);
|
||||||
|
if lines.len() > MAX_COLS + 1 {
|
||||||
|
return Err(CmdError::Failed(format!("a table can't have more than {MAX_COLS} columns")));
|
||||||
|
}
|
||||||
|
let line_at = |x: f32| lines.iter().enumerate().min_by(|(_, a), (_, b)| (*a - x).abs().total_cmp(&(*b - x).abs())).map_or(0, |(i, _)| i);
|
||||||
|
let mut spans: Vec<Vec<u32>> = Vec::with_capacity(borders.len());
|
||||||
|
for b in &borders {
|
||||||
|
let at: Vec<usize> = b.iter().map(|x| line_at(*x)).collect();
|
||||||
|
let row: Vec<u32> = at.windows(2).map(|g| g[1].saturating_sub(g[0]) as u32).collect();
|
||||||
|
if row.contains(&0) {
|
||||||
|
return Err(CmdError::Failed("the cells there are too narrow to move that border".into()));
|
||||||
|
}
|
||||||
|
spans.push(row);
|
||||||
|
}
|
||||||
|
t.grid = lines.windows(2).map(|x| x[1] - x[0]).collect();
|
||||||
|
for (row, row_spans) in t.rows.iter_mut().zip(spans) {
|
||||||
|
for (cell, span) in row.cells.iter_mut().zip(row_spans) {
|
||||||
|
cell.props.span = span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.props.width = None;
|
||||||
|
t.props.width_pct = None;
|
||||||
|
super::table::sync_cell_widths(t);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The page x of the target table's left edge on `page`, as drawn.
|
||||||
|
fn drawn_left(l: &DocLayout, page: usize, target: &ResizeTarget) -> Option<f32> {
|
||||||
|
cell_boxes(l, page).iter().filter(|b| b.story == target.story && b.table == target.table).map(|b| b.rect.x).min_by(f32::total_cmp)
|
||||||
|
}
|
||||||
|
|
||||||
/// Make `widths` (one per grid column) the table's column widths, as drawn: no preferred table
|
/// Make `widths` (one per grid column) the table's column widths, as drawn: no preferred table
|
||||||
/// width scales them again.
|
/// width scales them again.
|
||||||
pub(crate) fn set_widths(t: &mut Table, widths: Vec<f32>) {
|
pub(crate) fn set_widths(t: &mut Table, widths: Vec<f32>) {
|
||||||
@@ -573,4 +830,128 @@ mod tests {
|
|||||||
s.run("edit.undo", &json!({})).unwrap();
|
s.run("edit.undo", &json!({})).unwrap();
|
||||||
assert_eq!(first_table(&s), start);
|
assert_eq!(first_table(&s), start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// With Ctrl the columns right of the border share the change in proportion to their
|
||||||
|
/// widths, so the table keeps its width (Ctrl wins over Shift); none gets narrower than the
|
||||||
|
/// minimum.
|
||||||
|
#[test]
|
||||||
|
fn ctrl_shares_a_border_drag_among_the_columns_right_of_it() {
|
||||||
|
let mut s = table_session(1, 4);
|
||||||
|
let (a, b, d) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 1), cell_rect(&mut s, 0, 3));
|
||||||
|
let y = a.y + a.h / 2.0;
|
||||||
|
s.run("table.resizeDrag", &json!({"page": 0, "x": a.right(), "y": y, "toX": a.right() + 30.0, "toY": y, "ctrl": true, "shift": true}))
|
||||||
|
.unwrap();
|
||||||
|
let a1 = cell_rect(&mut s, 0, 0);
|
||||||
|
assert!((a1.w - (a.w + 30.0)).abs() < 1.0, "{a:?} → {a1:?}");
|
||||||
|
for c in 1..4 {
|
||||||
|
let w = cell_rect(&mut s, 0, c).w;
|
||||||
|
assert!((w - (b.w - 10.0)).abs() < 1.0, "column {c}: {} → {w}", b.w);
|
||||||
|
}
|
||||||
|
assert!((cell_rect(&mut s, 0, 3).right() - d.right()).abs() < 1.0, "the table keeps its width");
|
||||||
|
// In proportion: 40 and 80 give up 10 and 20 of a 30-point move.
|
||||||
|
let mut w = [100.0, 40.0, 80.0];
|
||||||
|
assert_eq!(share_out(&mut w, 0, 30.0, Share::Proportional), Some(30.0));
|
||||||
|
assert!((w[1] - 30.0).abs() < 0.01 && (w[2] - 60.0).abs() < 0.01, "{w:?}");
|
||||||
|
// As far as the narrowest stays at the minimum.
|
||||||
|
let mut w = [100.0, 12.0, 120.0];
|
||||||
|
share_out(&mut w, 0, 500.0, Share::Proportional);
|
||||||
|
assert!((w[1] - MIN_COL).abs() < 0.01 && (w.iter().sum::<f32>() - 232.0).abs() < 0.01, "{w:?}");
|
||||||
|
assert_eq!(share_out(&mut w, 3, 1.0, Share::Proportional), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The table's left edge, grabbed from just outside it, moves the table in or out: its
|
||||||
|
/// indent changes and the first column makes up the difference, so the other borders stay.
|
||||||
|
/// A centred or right-aligned table keeps its alignment, as in Word: only the first column
|
||||||
|
/// changes, and the table stays centred or on its right edge.
|
||||||
|
#[test]
|
||||||
|
fn the_left_edge_moves_the_indent_and_the_first_column() {
|
||||||
|
let mut s = table_session(2, 3);
|
||||||
|
let start = first_table(&s);
|
||||||
|
let (a, b) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 1));
|
||||||
|
let y = a.y + a.h / 2.0;
|
||||||
|
let l = s.layout();
|
||||||
|
assert_eq!(resize_target(&l, &s.doc, 0, a.x - 2.0, y, REACH).map(|t| t.edge), Some(Edge::Column(0)));
|
||||||
|
assert!(resize_target(&l, &s.doc, 0, a.x + 2.5, y, REACH).is_none(), "inside the first cell");
|
||||||
|
s.run("table.resizeDrag", &json!({"page": 0, "x": a.x - 1.0, "y": y, "toX": a.x + 35.0, "toY": y})).unwrap();
|
||||||
|
let (a1, b1) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 1));
|
||||||
|
assert!((a1.x - (a.x + 36.0)).abs() < 0.5 && (a1.right() - a.right()).abs() < 0.5, "{a:?} → {a1:?}");
|
||||||
|
assert!((b1.x - b.x).abs() < 0.5 && (b1.w - b.w).abs() < 0.5, "the other columns stay: {b:?} → {b1:?}");
|
||||||
|
assert!((cell_rect(&mut s, 1, 0).x - a1.x).abs() < 0.01, "the whole table moved in");
|
||||||
|
assert!((first_table(&s).props.indent.unwrap() - 36.0).abs() < 0.5, "{:?}", first_table(&s).props.indent);
|
||||||
|
s.run("edit.undo", &json!({})).unwrap();
|
||||||
|
assert_eq!(first_table(&s), start, "one undo step");
|
||||||
|
s.run("table.properties", &json!({"align": "center", "width": 300})).unwrap();
|
||||||
|
let (c, e) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 2));
|
||||||
|
let y = c.y + c.h / 2.0;
|
||||||
|
s.run("table.resizeDrag", &json!({"page": 0, "x": c.x, "y": y, "toX": c.x - 20.0, "toY": y})).unwrap();
|
||||||
|
let (c1, e1) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 2));
|
||||||
|
assert!((c1.w - (c.w + 20.0)).abs() < 0.5, "the first column takes the drag: {c:?} → {c1:?}");
|
||||||
|
assert!((e1.w - e.w).abs() < 0.5, "the other columns keep their widths");
|
||||||
|
assert!(((c1.x + e1.right()) - (c.x + e.right())).abs() < 1.0, "the table stays centred: {c:?} {e:?} → {c1:?} {e1:?}");
|
||||||
|
assert_eq!(first_table(&s).props.align, Some(Align::Center));
|
||||||
|
s.run("edit.undo", &json!({})).unwrap();
|
||||||
|
s.run("table.properties", &json!({"align": "right"})).unwrap();
|
||||||
|
let (c, e) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 2));
|
||||||
|
s.run("table.resizeDrag", &json!({"page": 0, "x": c.x, "y": y, "toX": c.x - 20.0, "toY": y})).unwrap();
|
||||||
|
let (c1, e1) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 0, 2));
|
||||||
|
assert!((c1.x - (c.x - 20.0)).abs() < 0.5 && (e1.right() - e.right()).abs() < 0.5, "only the left edge moves: {c:?} → {c1:?}");
|
||||||
|
assert_eq!(first_table(&s).props.align, Some(Align::Right));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With the selection across cells, a border of theirs moves only in their rows: the grid
|
||||||
|
/// gets a line for those rows' border and the other rows keep theirs. Text within one cell
|
||||||
|
/// isn't a cell selection; a border grabbed in a row outside the selection moves in the
|
||||||
|
/// whole column.
|
||||||
|
#[test]
|
||||||
|
fn with_cells_selected_their_border_moves_only_in_their_rows() {
|
||||||
|
let mut s = table_session(3, 3);
|
||||||
|
let (b, d) = (cell_rect(&mut s, 1, 1), cell_rect(&mut s, 2, 1));
|
||||||
|
s.run("caret.set", &json!({"page": 0, "x": b.x + b.w / 2.0, "y": b.y + b.h / 2.0})).unwrap();
|
||||||
|
s.run("text.insert", &json!({"text": "Wide"})).unwrap();
|
||||||
|
let start = first_table(&s);
|
||||||
|
let (b, c) = (cell_rect(&mut s, 1, 1), cell_rect(&mut s, 1, 2));
|
||||||
|
let y = b.y + b.h / 2.0;
|
||||||
|
let drag = json!({"page": 0, "x": b.right(), "y": y, "toX": b.right() + 30.0, "toY": y});
|
||||||
|
s.run("select.text", &json!({"text": "Wide"})).unwrap();
|
||||||
|
s.run("table.resizeDrag", &drag).unwrap();
|
||||||
|
assert!((cell_rect(&mut s, 0, 1).w - (b.w + 30.0)).abs() < 1.0, "text in one cell: every row");
|
||||||
|
s.run("edit.undo", &json!({})).unwrap();
|
||||||
|
// From the second row's cell down into the third's.
|
||||||
|
s.run("caret.set", &json!({"page": 0, "x": d.x + d.w / 2.0, "y": d.y + d.h / 2.0, "extend": true})).unwrap();
|
||||||
|
s.run("table.resizeDrag", &drag).unwrap();
|
||||||
|
for r in [1, 2] {
|
||||||
|
let (b1, c1) = (cell_rect(&mut s, r, 1), cell_rect(&mut s, r, 2));
|
||||||
|
assert!((b1.w - (b.w + 30.0)).abs() < 1.0 && (c1.w - (c.w - 30.0)).abs() < 1.0, "row {r}: {b1:?} {c1:?}");
|
||||||
|
}
|
||||||
|
assert!((cell_rect(&mut s, 0, 1).right() - b.right()).abs() < 0.5, "the first row keeps its border");
|
||||||
|
let t = first_table(&s);
|
||||||
|
assert_eq!(t.grid.len(), 4, "{:?}", t.grid);
|
||||||
|
assert_eq!(t.rows[0].cells.iter().map(Cell::span).collect::<Vec<_>>(), [1, 1, 2]);
|
||||||
|
assert_eq!(t.rows[2].cells.iter().map(Cell::span).collect::<Vec<_>>(), [1, 2, 1]);
|
||||||
|
let a = cell_rect(&mut s, 0, 0);
|
||||||
|
let ya = a.y + a.h / 2.0;
|
||||||
|
s.run("table.resizeDrag", &json!({"page": 0, "x": a.right(), "y": ya, "toX": a.right() - 10.0, "toY": ya})).unwrap();
|
||||||
|
assert!((cell_rect(&mut s, 1, 0).w - (a.w - 10.0)).abs() < 1.0, "a row outside the selection: the whole column");
|
||||||
|
s.run("edit.undo", &json!({})).unwrap();
|
||||||
|
s.run("edit.undo", &json!({})).unwrap();
|
||||||
|
assert_eq!(first_table(&s), start, "one undo step each");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cell merged down past the selection keeps one width: a border of it moves in the whole
|
||||||
|
/// column.
|
||||||
|
#[test]
|
||||||
|
fn a_border_of_a_cell_merged_past_the_selection_moves_in_every_row() {
|
||||||
|
let mut s = table_session(3, 3);
|
||||||
|
let (a, b) = (cell_rect(&mut s, 0, 1), cell_rect(&mut s, 1, 1));
|
||||||
|
s.run("caret.set", &json!({"page": 0, "x": a.x + a.w / 2.0, "y": a.y + a.h / 2.0})).unwrap();
|
||||||
|
s.run("caret.set", &json!({"page": 0, "x": b.x + b.w / 2.0, "y": b.y + b.h / 2.0, "extend": true})).unwrap();
|
||||||
|
s.run("table.merge", &json!({})).unwrap();
|
||||||
|
let (c, e) = (cell_rect(&mut s, 1, 2), cell_rect(&mut s, 2, 2));
|
||||||
|
let y = c.y + c.h / 2.0;
|
||||||
|
s.run("caret.set", &json!({"page": 0, "x": c.x + c.w / 2.0, "y": y})).unwrap();
|
||||||
|
s.run("caret.set", &json!({"page": 0, "x": e.x + e.w / 2.0, "y": e.y + e.h / 2.0, "extend": true})).unwrap();
|
||||||
|
s.run("table.resizeDrag", &json!({"page": 0, "x": c.x, "y": y, "toX": c.x + 20.0, "toY": y})).unwrap();
|
||||||
|
assert_eq!(first_table(&s).grid.len(), 3, "no new grid line");
|
||||||
|
assert!((cell_rect(&mut s, 0, 2).x - (c.x + 20.0)).abs() < 1.0, "the first row follows");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Resizing tables with the mouse, like Word: over a column border the pointer turns into a
|
//! Resizing tables with the mouse, like Word: over a column border (or the left edge) the pointer
|
||||||
//! column-resize arrow, over a row's bottom border a row-resize arrow; over a table a small
|
//! turns into a column-resize arrow, over a row's bottom border a row-resize arrow; over a table
|
||||||
//! handle shows past its bottom-right corner. Dragging shows a dashed preview of the new border
|
//! a small handle shows past its bottom-right corner. Dragging shows a dashed preview of the new border
|
||||||
//! (or outline); releasing runs `table.resizeDrag` with page coordinates, one undo step. Shift
|
//! (or outline); releasing runs `table.resizeDrag` with page coordinates, one undo step. Shift
|
||||||
//! while releasing a column border changes only the column on its left; a double-click on a
|
//! while releasing a column border changes only the column on its left; a double-click on a
|
||||||
//! column border fits the column to its contents (`table.fitColumn`). The geometry decisions
|
//! column border fits the column to its contents (`table.fitColumn`). The geometry decisions
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
use egui::{Pos2, Rect, Stroke, Ui, pos2};
|
use egui::{Pos2, Rect, Stroke, Ui, pos2};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use wordcraft_engine::cmd::table_resize::{self, Edge, Plan, ResizeTarget};
|
use wordcraft_engine::cmd::table_resize::{self, Edge, Plan, ResizeTarget, Share};
|
||||||
use wordcraft_layout::DocLayout;
|
use wordcraft_layout::DocLayout;
|
||||||
|
|
||||||
use crate::WordApp;
|
use crate::WordApp;
|
||||||
@@ -31,33 +31,36 @@ pub(crate) struct ResizeDrag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResizeDrag {
|
impl ResizeDrag {
|
||||||
/// What the drag so far makes of the table, with Shift held (`shift`) or not: on the page
|
/// Who makes up a column border's move, by the keys held now: Ctrl the columns on the
|
||||||
/// Shift changes only the column left of a column border, on a ruler marker it's the other way
|
/// right; Shift nobody on the page but the neighbour on a ruler marker, which works the other
|
||||||
/// round.
|
/// way round; else the neighbour on the page and nobody on the ruler.
|
||||||
fn plan(&self, shift: bool) -> Plan {
|
fn share(&self, ctx: &egui::Context) -> Share {
|
||||||
self.target.plan(self.to.0 - self.from.0, self.to.1 - self.from.1, shift != self.ruler)
|
let keys = ctx.input(|i| i.modifiers);
|
||||||
|
if keys.command {
|
||||||
|
Share::Proportional
|
||||||
|
} else if keys.shift != self.ruler {
|
||||||
|
Share::LeftOnly
|
||||||
|
} else {
|
||||||
|
Share::Neighbour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the drag so far makes of the table, with the keys held now.
|
||||||
|
fn plan(&self, ctx: &egui::Context) -> Plan {
|
||||||
|
self.target.plan(self.to.0 - self.from.0, self.to.1 - self.from.1, self.share(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the dragged column border (page x) or row bottom (page y) is now; `None` for the
|
/// Where the dragged column border (page x) or row bottom (page y) is now; `None` for the
|
||||||
/// handle.
|
/// handle.
|
||||||
fn line_now(&self, shift: bool) -> Option<f32> {
|
fn line_now(&self, plan: &Plan) -> Option<f32> {
|
||||||
let plan = self.plan(shift);
|
|
||||||
match self.target.edge {
|
match self.target.edge {
|
||||||
Edge::Column(i) => Some(planned_line(&self.target, &plan, i)),
|
Edge::Column(i) => Some(self.target.planned_x(plan, plan.cells.first().map(|(r, _)| *r), i)),
|
||||||
Edge::Row(r) => planned_bottom(&self.target, &plan, r),
|
Edge::Row(r) => planned_bottom(&self.target, plan, r),
|
||||||
Edge::Corner => None,
|
Edge::Corner => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Page x of `tg`'s grid line `line` with `plan` applied.
|
|
||||||
fn planned_line(tg: &ResizeTarget, plan: &Plan, line: usize) -> f32 {
|
|
||||||
match &plan.widths {
|
|
||||||
Some(w) => tg.left() + w.iter().take(line).sum::<f32>(),
|
|
||||||
None => tg.cols.get(line).copied().unwrap_or(tg.left()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Page y of the bottom of `tg`'s row `row` with `plan` applied (rows above it that change move
|
/// Page y of the bottom of `tg`'s row `row` with `plan` applied (rows above it that change move
|
||||||
/// it too); `None` when the row isn't drawn on the page.
|
/// it too); `None` when the row isn't drawn on the page.
|
||||||
fn planned_bottom(tg: &ResizeTarget, plan: &Plan, row: usize) -> Option<f32> {
|
fn planned_bottom(tg: &ResizeTarget, plan: &Plan, row: usize) -> Option<f32> {
|
||||||
@@ -104,8 +107,10 @@ fn target_at(app: &WordApp, layout: &DocLayout, rects: &[Rect], scale: f32, p: P
|
|||||||
Some((page, x, y, t))
|
Some((page, x, y, t))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mouse input: a press on a border starts a drag, moving updates it, releasing applies it.
|
/// Mouse input: a press on a border starts a drag (in the selected cells' rows only, when it's a
|
||||||
/// `false` (nothing done) when the pointer isn't on a border, so the caret logic runs.
|
/// border of theirs), moving updates it, releasing applies it with the keys held then (Ctrl: the
|
||||||
|
/// columns right of the border share the change). `false` (nothing done) when the pointer isn't
|
||||||
|
/// on a border, so the caret logic runs.
|
||||||
pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[Rect], layout: &DocLayout, scale: f32) -> bool {
|
pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[Rect], layout: &DocLayout, scale: f32) -> bool {
|
||||||
let latest = ui.input(|i| i.pointer.latest_pos());
|
let latest = ui.input(|i| i.pointer.latest_pos());
|
||||||
if let Some(mut d) = app.canvas.table_resize.take() {
|
if let Some(mut d) = app.canvas.table_resize.take() {
|
||||||
@@ -134,12 +139,11 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[Rect]
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// The command's "shift": only the column left of a column border changes.
|
let share = d.share(ui.ctx());
|
||||||
let shift = ui.input(|i| i.modifiers.shift) != d.ruler;
|
|
||||||
let sc = page_scale(rects, layout, scale, d.page).unwrap_or(1.0);
|
let sc = page_scale(rects, layout, scale, d.page).unwrap_or(1.0);
|
||||||
let params = json!({
|
let params = json!({
|
||||||
"page": d.page, "x": d.from.0, "y": d.from.1, "toX": d.to.0, "toY": d.to.1,
|
"page": d.page, "x": d.from.0, "y": d.from.1, "toX": d.to.0, "toY": d.to.1,
|
||||||
"shift": shift, "reach": REACH_PX / sc,
|
"shift": share == Share::LeftOnly, "ctrl": share == Share::Proportional, "reach": REACH_PX / sc,
|
||||||
});
|
});
|
||||||
if let Err(e) = app.run("table.resizeDrag", params) {
|
if let Err(e) = app.run("table.resizeDrag", params) {
|
||||||
app.status(e);
|
app.status(e);
|
||||||
@@ -150,7 +154,8 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, rects: &[Rect]
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let Some(p) = latest else { return false };
|
let Some(p) = latest else { return false };
|
||||||
let Some((page, x, y, target)) = target_at(app, layout, rects, scale, p) else { return false };
|
let Some((page, x, y, mut target)) = target_at(app, layout, rects, scale, p) else { return false };
|
||||||
|
target.limit_to_selection(&app.session, y);
|
||||||
app.canvas.table_resize = Some(ResizeDrag { page, from: (x, y), to: (x, y), target, ruler: false });
|
app.canvas.table_resize = Some(ResizeDrag { page, from: (x, y), to: (x, y), target, ruler: false });
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -162,28 +167,37 @@ pub(crate) fn grab(app: &mut WordApp, layout: &DocLayout, page: usize, x: f32, y
|
|||||||
if !available(app) {
|
if !available(app) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(target) = table_resize::resize_target(layout, &app.session.doc, page, x, y, 1.0) {
|
if let Some(mut target) = table_resize::resize_target(layout, &app.session.doc, page, x, y, 1.0) {
|
||||||
|
target.limit_to_selection(&app.session, y);
|
||||||
app.canvas.table_resize = Some(ResizeDrag { page, from: (x, y), to: (x, y), target, ruler: true });
|
app.canvas.table_resize = Some(ResizeDrag { page, from: (x, y), to: (x, y), target, ruler: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the caret's table's column border drawn at page x `at` (`column`), or row bottom drawn
|
/// The drag of one of the caret's table's borders on `page`, if one is going on.
|
||||||
/// at page y `at`, is while one of its borders is dragged on `page` (`shift` held): the rulers'
|
fn caret_drag(app: &WordApp, page: usize) -> Option<&ResizeDrag> {
|
||||||
/// markers follow the drag.
|
|
||||||
pub(crate) fn moved(app: &WordApp, page: usize, shift: bool, column: bool, at: f32) -> f32 {
|
|
||||||
let caret = app.session.sel.focus.path.cell().map(|(tp, _, _)| tp);
|
let caret = app.session.sel.focus.path.cell().map(|(tp, _, _)| tp);
|
||||||
let Some(d) = app
|
app.canvas
|
||||||
.canvas
|
|
||||||
.table_resize
|
.table_resize
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|d| d.page == page && d.target.story == app.session.sel.focus.story && caret.as_ref() == Some(&d.target.table))
|
.filter(|d| d.page == page && d.target.story == app.session.sel.focus.story && caret.as_ref() == Some(&d.target.table))
|
||||||
else {
|
}
|
||||||
return at;
|
|
||||||
};
|
/// Whether the rulers show the caret's table's column widths: Alt is held while one of its
|
||||||
let (tg, plan) = (&d.target, d.plan(shift));
|
/// column borders (or its left edge) is dragged on `page`, as in Word.
|
||||||
|
pub(crate) fn measuring(app: &WordApp, ctx: &egui::Context, page: usize) -> bool {
|
||||||
|
ctx.input(|i| i.modifiers.alt) && caret_drag(app, page).is_some_and(|d| matches!(d.target.edge, Edge::Column(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the caret's table's column border drawn at page x `at` (`column`; in the caret's row),
|
||||||
|
/// or row bottom drawn at page y `at`, is while one of its borders is dragged on `page`: the
|
||||||
|
/// rulers' markers follow the drag.
|
||||||
|
pub(crate) fn moved(app: &WordApp, ctx: &egui::Context, page: usize, column: bool, at: f32) -> f32 {
|
||||||
|
let Some(d) = caret_drag(app, page) else { return at };
|
||||||
|
let (tg, plan) = (&d.target, d.plan(ctx));
|
||||||
let near = |v: f32| (v - at).abs() < 0.5;
|
let near = |v: f32| (v - at).abs() < 0.5;
|
||||||
if column {
|
if column {
|
||||||
tg.cols.iter().position(|x| near(*x)).map_or(at, |line| planned_line(tg, &plan, line))
|
let row = app.session.sel.focus.path.cell().map(|(_, r, _)| r);
|
||||||
|
tg.cols.iter().position(|x| near(*x)).map_or(at, |line| tg.planned_x(&plan, row, line))
|
||||||
} else {
|
} else {
|
||||||
tg.rows.iter().find(|r| near(r.bottom)).and_then(|r| planned_bottom(tg, &plan, r.row)).unwrap_or(at)
|
tg.rows.iter().find(|r| near(r.bottom)).and_then(|r| planned_bottom(tg, &plan, r.row)).unwrap_or(at)
|
||||||
}
|
}
|
||||||
@@ -213,9 +227,8 @@ pub fn paint(app: &WordApp, painter: &egui::Painter, t: &Tokens, layout: &DocLay
|
|||||||
let s = |page: usize, x: f32, y: f32| to_screen(rects, layout, scale, page, x, y);
|
let s = |page: usize, x: f32, y: f32| to_screen(rects, layout, scale, page, x, y);
|
||||||
let dash = Stroke::new(1.0, t.text);
|
let dash = Stroke::new(1.0, t.text);
|
||||||
if let Some(d) = &app.canvas.table_resize {
|
if let Some(d) = &app.canvas.table_resize {
|
||||||
let shift = painter.ctx().input(|i| i.modifiers.shift);
|
|
||||||
let tg = &d.target;
|
let tg = &d.target;
|
||||||
let plan = d.plan(shift);
|
let plan = d.plan(painter.ctx());
|
||||||
let (left, top) = (tg.left(), tg.top());
|
let (left, top) = (tg.left(), tg.top());
|
||||||
let right = plan.widths.as_ref().map_or(tg.right(), |w| left + w.iter().sum::<f32>());
|
let right = plan.widths.as_ref().map_or(tg.right(), |w| left + w.iter().sum::<f32>());
|
||||||
let bottom = top + tg.rows.iter().map(|r| plan.heights.iter().find(|(i, _)| *i == r.row).map_or(r.bottom - r.top, |(_, h)| *h)).sum::<f32>();
|
let bottom = top + tg.rows.iter().map(|r| plan.heights.iter().find(|(i, _)| *i == r.row).map_or(r.bottom - r.top, |(_, h)| *h)).sum::<f32>();
|
||||||
@@ -225,9 +238,17 @@ pub fn paint(app: &WordApp, painter: &egui::Painter, t: &Tokens, layout: &DocLay
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// The dragged column border's x, or row bottom's y.
|
// The dragged column border's x, or row bottom's y.
|
||||||
let at = d.line_now(shift).unwrap_or_default();
|
let at = d.line_now(&plan).unwrap_or_default();
|
||||||
match tg.edge {
|
match tg.edge {
|
||||||
Edge::Column(_) => line(s(d.page, at, top), s(d.page, at, tg.bottom())),
|
Edge::Column(_) => {
|
||||||
|
// Down the rows it moves in (the selected cells'), else the whole table.
|
||||||
|
let rows: Vec<_> = tg.rows.iter().filter(|r| plan.cells.is_empty() || plan.cells.iter().any(|(c, _)| *c == r.row)).collect();
|
||||||
|
let (y0, y1) =
|
||||||
|
(rows.iter().map(|r| r.top).fold(f32::INFINITY, f32::min), rows.iter().map(|r| r.bottom).fold(f32::NEG_INFINITY, f32::max));
|
||||||
|
if y0 <= y1 {
|
||||||
|
line(s(d.page, at, y0), s(d.page, at, y1));
|
||||||
|
}
|
||||||
|
}
|
||||||
Edge::Row(_) => line(s(d.page, left, at), s(d.page, tg.right(), at)),
|
Edge::Row(_) => line(s(d.page, left, at), s(d.page, tg.right(), at)),
|
||||||
Edge::Corner => {
|
Edge::Corner => {
|
||||||
if let (Some(a), Some(b)) = (s(d.page, left, top), s(d.page, right, bottom)) {
|
if let (Some(a), Some(b)) = (s(d.page, left, top), s(d.page, right, bottom)) {
|
||||||
@@ -474,6 +495,76 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(h.state().session.doc.para_at(&focus).and_then(|p| p.props.indent_left), Some(18.0));
|
assert_eq!(h.state().session.doc.para_at(&focus).and_then(|p| p.props.indent_left), Some(18.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// With Ctrl held, the change of a dragged column border is shared by the columns right of
|
||||||
|
/// it, so the table keeps its width.
|
||||||
|
#[test]
|
||||||
|
fn a_ctrl_drag_shares_the_change_among_the_columns_right_of_it() {
|
||||||
|
let mut h = table_window();
|
||||||
|
let (a, b, c) = (cell(&mut h, 0, 0), cell(&mut h, 0, 1), cell(&mut h, 0, 2));
|
||||||
|
let y = a.y + a.h / 2.0;
|
||||||
|
let (start, end) = (screen(&mut h, a.right(), y), screen(&mut h, a.right() + 30.0, y));
|
||||||
|
h.event(egui::Event::ModifiersChanged(egui::Modifiers::COMMAND));
|
||||||
|
drag(&mut h, start, end);
|
||||||
|
h.event(egui::Event::ModifiersChanged(egui::Modifiers::NONE));
|
||||||
|
let (a1, b1, c1) = (cell(&mut h, 0, 0), cell(&mut h, 0, 1), cell(&mut h, 0, 2));
|
||||||
|
assert!((a1.w - (a.w + 30.0)).abs() < 1.0, "{a:?} → {a1:?}");
|
||||||
|
assert!((b1.w - (b.w - 15.0)).abs() < 1.0 && (c1.w - (c.w - 15.0)).abs() < 1.0, "{b1:?} {c1:?}");
|
||||||
|
assert!((c1.right() - c.right()).abs() < 1.0, "the table keeps its width");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The table's left edge, under the resize arrow just outside it, drags the table in; the
|
||||||
|
/// first column makes up the difference. The horizontal ruler has a marker on it too.
|
||||||
|
#[test]
|
||||||
|
fn the_left_edge_drags_the_table_in_and_has_a_ruler_marker() {
|
||||||
|
let mut h = table_window();
|
||||||
|
let (a, b) = (cell(&mut h, 0, 0), cell(&mut h, 0, 1));
|
||||||
|
let y = a.y + a.h / 2.0;
|
||||||
|
let (start, end) = (screen(&mut h, a.x - 1.5, y), screen(&mut h, a.x + 20.0, y));
|
||||||
|
h.event(egui::Event::PointerMoved(start));
|
||||||
|
steps(&mut h, 2);
|
||||||
|
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeColumn, "the resize arrow on the left edge");
|
||||||
|
drag(&mut h, start, end);
|
||||||
|
let (a1, b1) = (cell(&mut h, 0, 0), cell(&mut h, 0, 1));
|
||||||
|
assert!((a1.x - (a.x + 21.5)).abs() < 1.0 && (a1.right() - a.right()).abs() < 0.5, "{a:?} → {a1:?}");
|
||||||
|
assert!((b1.x - b.x).abs() < 0.5 && (b1.w - b.w).abs() < 0.5, "{b:?} → {b1:?}");
|
||||||
|
let near = screen(&mut h, a1.x, a1.y);
|
||||||
|
let marker = ruler_marker(&h, "Table Column Width", near).center();
|
||||||
|
assert!((marker.x - near.x).abs() < 0.5, "a marker on the left edge");
|
||||||
|
let back = marker - egui::vec2(near.x - screen(&mut h, a.x, a1.y).x, 0.0);
|
||||||
|
drag(&mut h, marker, back);
|
||||||
|
assert!((cell(&mut h, 0, 0).x - a.x).abs() < 1.0, "the marker drags it back out");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alt while a column border is dragged shows each column's width on the ruler, as it is
|
||||||
|
/// going to be.
|
||||||
|
#[test]
|
||||||
|
fn alt_shows_the_column_widths_while_dragging() {
|
||||||
|
let mut h = table_window();
|
||||||
|
let (a, b) = (cell(&mut h, 0, 0), cell(&mut h, 0, 1));
|
||||||
|
let y = a.y + a.h / 2.0;
|
||||||
|
let (start, end) = (screen(&mut h, a.right(), y), screen(&mut h, a.right() + 18.0, y));
|
||||||
|
h.event(egui::Event::PointerMoved(start));
|
||||||
|
h.step();
|
||||||
|
button(&h, start, true);
|
||||||
|
h.step();
|
||||||
|
h.event(egui::Event::PointerMoved(end));
|
||||||
|
h.step();
|
||||||
|
let widths = |h: &mut H| {
|
||||||
|
let layout = h.state_mut().session.layout();
|
||||||
|
crate::table_ruler::widths(h.state(), &h.ctx, &layout, 0)
|
||||||
|
};
|
||||||
|
assert!(widths(&mut h).is_empty(), "not without Alt");
|
||||||
|
h.event(egui::Event::ModifiersChanged(egui::Modifiers::ALT));
|
||||||
|
h.step();
|
||||||
|
let w = widths(&mut h);
|
||||||
|
assert_eq!(w.len(), 3, "{w:?}");
|
||||||
|
assert!((w[0].1 - (a.w + 18.0)).abs() < 0.5 && (w[1].1 - (b.w - 18.0)).abs() < 0.5, "{w:?}");
|
||||||
|
h.event(egui::Event::ModifiersChanged(egui::Modifiers::NONE));
|
||||||
|
button(&h, end, false);
|
||||||
|
steps(&mut h, 3);
|
||||||
|
assert!(widths(&mut h).is_empty(), "gone with the drag");
|
||||||
|
}
|
||||||
|
|
||||||
/// Clicks inside a cell still place the caret (the border doesn't steal them), and Esc drops a
|
/// Clicks inside a cell still place the caret (the border doesn't steal them), and Esc drops a
|
||||||
/// drag without changing anything.
|
/// drag without changing anything.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//! A table on the rulers, like Word's: with the caret in a table, the horizontal ruler marks the
|
//! A table on the rulers, like Word's: with the caret in a table, the horizontal ruler marks the
|
||||||
//! caret row's column borders and the vertical ruler the bottom of each of the table's rows on
|
//! table's left edge and the caret row's column borders, and the vertical ruler the bottom of
|
||||||
//! the page. Dragging a marker drags that border as on the page (`table_resize::grab`), except
|
//! each of the table's rows on the page. Dragging a marker drags that border as on the page
|
||||||
//! that on the horizontal ruler only the column left of the border changes and the columns right
|
//! (`table_resize::grab`), except that on the horizontal ruler only the column left of the border
|
||||||
//! of it move along. A paragraph's indents and tabs in a cell are measured from the cell's text
|
//! changes and the columns right of it move along. Alt while dragging a column border shows the
|
||||||
//! area (`text_area`).
|
//! row's column widths (`widths`). A paragraph's indents and tabs in a cell are measured from the
|
||||||
|
//! cell's text area (`text_area`).
|
||||||
|
|
||||||
use egui::{Rect, Sense, Stroke, Ui, pos2, vec2};
|
use egui::{Rect, Sense, Stroke, Ui, pos2, vec2};
|
||||||
use wordcraft_layout::{DocLayout, Placed};
|
use wordcraft_layout::{DocLayout, Placed};
|
||||||
@@ -63,19 +64,54 @@ fn grip(app: &mut WordApp, ui: &Ui, layout: &DocLayout, zone: Rect, column: bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The caret row's cells on `page`, left to right.
|
||||||
|
fn caret_row(app: &WordApp, layout: &DocLayout, page: usize) -> Vec<wordcraft_geom::Rect> {
|
||||||
|
let Some((_, r, _)) = app.session.sel.focus.path.cell() else { return Vec::new() };
|
||||||
|
let mut row: Vec<wordcraft_geom::Rect> = cells(app, layout, page).into_iter().filter(|(row, ..)| *row == r).map(|(.., b)| b).collect();
|
||||||
|
row.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
|
/// While Alt is held during a column border's drag (`table_resize::measuring`), each of the
|
||||||
|
/// caret row's cells on `page` as it is going to be: (middle, width), page points.
|
||||||
|
pub(crate) fn widths(app: &WordApp, ctx: &egui::Context, layout: &DocLayout, page: usize) -> Vec<(f32, f32)> {
|
||||||
|
if !crate::table_resize::measuring(app, ctx, page) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let at = |x: f32| crate::table_resize::moved(app, ctx, page, true, x);
|
||||||
|
caret_row(app, layout, page)
|
||||||
|
.iter()
|
||||||
|
.map(|b| {
|
||||||
|
let (l, r) = (at(b.x), at(b.right()));
|
||||||
|
((l + r) / 2.0, r - l)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// The horizontal ruler's markers (`bar`; the page's left edge at screen x `x0`, `scale` pixels per
|
/// The horizontal ruler's markers (`bar`; the page's left edge at screen x `x0`, `scale` pixels per
|
||||||
/// point): one on each column border of the caret's row past the table's left edge.
|
/// point): one on the table's left edge and on each column border of the caret's row, and with
|
||||||
|
/// Alt held while one is dragged, the columns' widths between them.
|
||||||
pub(crate) fn columns(app: &mut WordApp, ui: &Ui, layout: &DocLayout, page: usize, bar: Rect, x0: f32, scale: f32) {
|
pub(crate) fn columns(app: &mut WordApp, ui: &Ui, layout: &DocLayout, page: usize, bar: Rect, x0: f32, scale: f32) {
|
||||||
let Some((_, r, _)) = app.session.sel.focus.path.cell() else { return };
|
let row = caret_row(app, layout, page);
|
||||||
|
let Some(first) = row.first().copied() else { return };
|
||||||
let t = Tokens::get(ui.ctx());
|
let t = Tokens::get(ui.ctx());
|
||||||
let painter = ui.painter_at(bar.expand(1.0));
|
let painter = ui.painter_at(bar.expand(1.0));
|
||||||
let shift = ui.input(|i| i.modifiers.shift);
|
// The table's left edge, then each cell's right border: where it is, or while a border of
|
||||||
for (i, (.., b)) in cells(app, layout, page).into_iter().filter(|(row, ..)| *row == r).enumerate() {
|
// the table is dragged, where it's going.
|
||||||
// Where it is, or while a border of the table is dragged, where it's going.
|
let edges: Vec<(f32, f32)> = std::iter::once(first.x)
|
||||||
let sx = x0 + crate::table_resize::moved(app, page, shift, true, b.right()) * scale;
|
.chain(row.iter().map(|b| b.right()))
|
||||||
|
.map(|x| (x, x0 + crate::table_resize::moved(app, ui.ctx(), page, true, x) * scale))
|
||||||
|
.collect();
|
||||||
|
for (i, (x, sx)) in edges.into_iter().enumerate() {
|
||||||
marker(&painter, Rect::from_center_size(pos2(sx, bar.center().y), vec2(7.0, bar.height() - 2.0)), &t);
|
marker(&painter, Rect::from_center_size(pos2(sx, bar.center().y), vec2(7.0, bar.height() - 2.0)), &t);
|
||||||
let zone = Rect::from_center_size(pos2(sx, bar.center().y), vec2(8.0, bar.height()));
|
let zone = Rect::from_center_size(pos2(sx, bar.center().y), vec2(8.0, bar.height()));
|
||||||
grip(app, ui, layout, zone, true, i, (page, b.right(), b.y + b.h / 2.0));
|
grip(app, ui, layout, zone, true, i, (page, x, first.y + first.h / 2.0));
|
||||||
|
}
|
||||||
|
for (mid, w) in widths(app, ui.ctx(), layout, page) {
|
||||||
|
let at = pos2(x0 + mid * scale, bar.center().y);
|
||||||
|
let text = painter.layout_no_wrap(wordcraft_geom::Unit::default().format(w), crate::theme::regular(9.5), t.text);
|
||||||
|
painter.rect_filled(Rect::from_center_size(at, text.size() + vec2(4.0, 0.0)), 1.0, t.ruler);
|
||||||
|
painter.galley(at - text.size() / 2.0, text, t.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +132,7 @@ pub(crate) fn rows(app: &mut WordApp, ui: &Ui, layout: &DocLayout, page: usize,
|
|||||||
let t = Tokens::get(ui.ctx());
|
let t = Tokens::get(ui.ctx());
|
||||||
let painter = ui.painter_at(bar.expand(1.0));
|
let painter = ui.painter_at(bar.expand(1.0));
|
||||||
for (r, b) in rows {
|
for (r, b) in rows {
|
||||||
let sy = y0 + crate::table_resize::moved(app, page, false, false, b.bottom()) * scale;
|
let sy = y0 + crate::table_resize::moved(app, ui.ctx(), page, false, b.bottom()) * scale;
|
||||||
marker(&painter, Rect::from_center_size(pos2(bar.center().x, sy), vec2(bar.width() - 2.0, 7.0)), &t);
|
marker(&painter, Rect::from_center_size(pos2(bar.center().x, sy), vec2(bar.width() - 2.0, 7.0)), &t);
|
||||||
let zone = Rect::from_center_size(pos2(bar.center().x, sy), vec2(bar.width(), 8.0));
|
let zone = Rect::from_center_size(pos2(bar.center().x, sy), vec2(bar.width(), 8.0));
|
||||||
grip(app, ui, layout, zone, false, r, (page, b.x + b.w / 2.0, b.bottom()));
|
grip(app, ui, layout, zone, false, r, (page, b.x + b.w / 2.0, b.bottom()));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WordCraft parity with Microsoft Word
|
# WordCraft parity with Microsoft Word
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** 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: all 12 bibliography styles, #383; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; 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; replaces the parity tables that lived in ROADMAP.md) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (table border drags with Ctrl and Alt, from the left edge and of selected cells, #711; 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: all 12 bibliography styles, #383; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; 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; replaces the parity tables that lived in ROADMAP.md) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||||
|
|
||||||
The authoritative assessment of how close WordCraft is to Microsoft Word. [`ROADMAP.md`](../ROADMAP.md)
|
The authoritative assessment of how close WordCraft is to Microsoft Word. [`ROADMAP.md`](../ROADMAP.md)
|
||||||
summarizes it; [`gaps.md`](gaps.md) itemizes every shortfall; the deep checklists are
|
summarizes it; [`gaps.md`](gaps.md) itemizes every shortfall; the deep checklists are
|
||||||
@@ -60,7 +60,7 @@ full parity for the area.
|
|||||||
| Character and paragraph formatting | 11 | 82% | 85% | 8–12 | Nearly every property, Font/Paragraph dialogs, character border, Phonetic Guide (#288), Enclose Characters (#297). Missing: Asian Typography, kerning/ligature/stylistic-set options in the Font dialog's Advanced tab. |
|
| Character and paragraph formatting | 11 | 82% | 85% | 8–12 | Nearly every property, Font/Paragraph dialogs, character border, Phonetic Guide (#288), Enclose Characters (#297). Missing: Asian Typography, kerning/ligature/stylistic-set options in the Font dialog's Advanced tab. |
|
||||||
| Styles and themes | 8 | 74% | 75% | 8–12 | Gallery, pane, create/modify, style sets, themes. Style Inspector landed (#236). Missing: Manage Styles (#270 open), style separators, linked-style edge cases; Word ships 11 Quick Style sets plus dozens of themes, we ship our own smaller set. |
|
| Styles and themes | 8 | 74% | 75% | 8–12 | Gallery, pane, create/modify, style sets, themes. Style Inspector landed (#236). Missing: Manage Styles (#270 open), style separators, linked-style edge cases; Word ships 11 Quick Style sets plus dozens of themes, we ship our own smaller set. |
|
||||||
| Lists | 6 | 78% | 75% | 5–8 | Bullets, numbering, multilevel, restart, `w:lvlOverride` levels (#134), Word's list AutoFormat and Enter/Backspace behaviour. Missing: Define New Multilevel List dialog depth, list styles, legal numbering edge cases. |
|
| Lists | 6 | 78% | 75% | 5–8 | Bullets, numbering, multilevel, restart, `w:lvlOverride` levels (#134), Word's list AutoFormat and Enter/Backspace behaviour. Missing: Define New Multilevel List dialog depth, list styles, legal numbering edge cases. |
|
||||||
| Tables | 8 | 76% | 72% | 10–16 | Merge/split, styles (cell text takes style formatting, #104), custom table styles saved in the .docx (#256), Word 2013 edge, floating tables, rows split across pages, sort, formula, cell gridlines (#243), Draw Table and Eraser (#303). Missing: splitting or erasing around vertically merged cells, nested-table polish. Dragging column and row borders and the resize handle on the page (#217) and the column and row markers on the rulers (#700) landed, and so did selecting columns, rows and cells, the "+" insert buttons, the move handle and double-click AutoFit on the page, Select Column, and drags and Shift+arrows over cells selecting whole cells; right-clicking a cell offers the table's commands, following the selection as in Word (Delete Cells, Rows, Columns or Table, Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…); cell text direction landed (#245). |
|
| Tables | 8 | 76% | 72% | 10–16 | Merge/split, styles (cell text takes style formatting, #104), custom table styles saved in the .docx (#256), Word 2013 edge, floating tables, rows split across pages, sort, formula, cell gridlines (#243), Draw Table and Eraser (#303). Missing: splitting or erasing around vertically merged cells, nested-table polish. Dragging column and row borders and the resize handle on the page (#217) and the column and row markers on the rulers (#700) landed, with Word's Ctrl, Alt, left-edge and selected-cells drags (#711), and so did selecting columns, rows and cells, the "+" insert buttons, the move handle and double-click AutoFit on the page, Select Column, and drags and Shift+arrows over cells selecting whole cells; right-clicking a cell offers the table's commands, following the selection as in Word (Delete Cells, Rows, Columns or Table, Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…); cell text direction landed (#245). |
|
||||||
| Page layout and sections | 7 | 70% | 72% | 12–20 | Margins, size, columns, breaks, page borders, line numbers, vertical alignment, drop caps, hyphenation. Columns don't balance; RTL sections missing. Vertical sections (tbRl/btLr) landed (#491). Detail: [layout-parity.md](layout-parity.md). |
|
| Page layout and sections | 7 | 70% | 72% | 12–20 | Margins, size, columns, breaks, page borders, line numbers, vertical alignment, drop caps, hyphenation. Columns don't balance; RTL sections missing. Vertical sections (tbRl/btLr) landed (#491). Detail: [layout-parity.md](layout-parity.md). |
|
||||||
| Headers, footers, page numbers, fields | 5 | 70% | 70% | 8–12 | First/even/odd, link to previous, TOC as an updatable field (#52), cross-references to real bookmarks (#227). Field coverage is a subset of Word's ~90 field codes; Field dialog missing. |
|
| Headers, footers, page numbers, fields | 5 | 70% | 70% | 8–12 | First/even/odd, link to previous, TOC as an updatable field (#52), cross-references to real bookmarks (#227). Field coverage is a subset of Word's ~90 field codes; Field dialog missing. |
|
||||||
| Footnotes and endnotes | 3 | 60% | 60% | 3–5 | Placed and editable, numbers from Word files (#103); long notes continue onto the next page under the continuation separator (#352). Footnote and Endnote dialog with options per section: below text, end of section, number format, start at, restart each section or page, custom mark, convert (#385). |
|
| Footnotes and endnotes | 3 | 60% | 60% | 3–5 | Placed and editable, numbers from Word files (#103); long notes continue onto the next page under the continuation separator (#352). Footnote and Endnote dialog with options per section: below text, end of section, number format, start at, restart each section or page, custom mark, convert (#385). |
|
||||||
@@ -251,6 +251,7 @@ can be used without committing Word output.
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-11 | trivial | Tables: Ctrl, Alt, left-edge and selected-cells border drags (#711); Tables row updated, no percentage change |
|
||||||
| 2026-10-11 | trivial | Tables: column and row markers on the rulers (#700); Tables row updated, no percentage change |
|
| 2026-10-11 | trivial | Tables: column and row markers on the rulers (#700); Tables row updated, no percentage change |
|
||||||
| 2026-10-11 | trivial | Tables: the table context menu, its items following the selection as in Word (Delete Cells, Rows, Columns or Table, Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…); Tables row updated, no percentage change |
|
| 2026-10-11 | trivial | Tables: the table context menu, its items following the selection as in Word (Delete Cells, Rows, Columns or Table, Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…); Tables row updated, no percentage change |
|
||||||
| 2026-10-11 | trivial | Tables: ordinary drags, Shift+arrows and Shift+click over cells select whole cells, as in Word; Tables row updated, no percentage change |
|
| 2026-10-11 | trivial | Tables: ordinary drags, Shift+arrows and Shift+click over cells select whole cells, as in Word; Tables row updated, no percentage change |
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
# UI parity with Microsoft Word
|
# UI parity with Microsoft Word
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (table border drags with Ctrl and Alt, from the left edge and of selected cells, #711; 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 major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||||
|
|
||||||
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
|
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
|
||||||
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
|
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
|
||||||
@@ -24,7 +24,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
|
|||||||
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485), Print page with preview, page range and copies (#641); launched with nothing to open, the app starts on Home (the Start screen: Blank document, templates, Recent; Esc leaves a blank document), with File › Options › General › Start-up to turn it off (#766) | | 68% | 4–6 |
|
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485), Print page with preview, page range and copies (#641); launched with nothing to open, the app starts on Home (the Start screen: Blank document, templates, Recent; Esc leaves a blank document), with File › Options › General › Start-up to turn it off (#766) | | 68% | 4–6 |
|
||||||
| Rulers: indents, tabs, margins, table columns | indents draggable (one undo, #62); tabs; table column and row markers drag their borders (one undo) and in a cell the indents are measured from it (#700) | ✅ | 85% | 1–2 |
|
| Rulers: indents, tabs, margins, table columns | indents draggable (one undo, #62); tabs; table column and row markers drag their borders (one undo) and in a cell the indents are measured from it (#700) | ✅ | 85% | 1–2 |
|
||||||
| On-canvas objects: select, drag, resize handles, nudge | ✅ tested (#228) | ✅ | 75% | — |
|
| On-canvas objects: select, drag, resize handles, nudge | ✅ tested (#228) | ✅ | 75% | — |
|
||||||
| Tables on the page: drag column and row borders, resize handle past the bottom-right corner; select columns, rows and cells; "+" insert buttons; move handle | ✅ resize cursors, dashed guide, one undo step per drag, selection kept; Shift moves one column; Esc cancels (#217); double-click a column border to AutoFit it; selection arrows above a column, left of a row and along a cell's left edge (a drag selects more; the cells are shaded and a column is edited cell by cell); "+" left of a row border and above a column border; move handle past the top-left corner (a click selects the table, a drag floats it where it's dropped); an ordinary drag, Shift+arrows or Shift+click from one cell into another select the rectangle of whole cells between them, and into or out of a table whole rows. Missing: Ctrl and Alt drags, the left edge, borders of selected cells only | ✅ | 90% | 1–2 |
|
| Tables on the page: drag column and row borders, resize handle past the bottom-right corner; select columns, rows and cells; "+" insert buttons; move handle | ✅ resize cursors, dashed guide, one undo step per drag, selection kept; Shift moves one column; Esc cancels (#217); Ctrl shares the change among the columns on the right, the left edge moves a left-aligned table's indent (a centred or right-aligned one keeps its alignment), a border of cells selected across moves only in their rows, Alt shows the column widths on the ruler (#711); double-click a column border to AutoFit it; selection arrows above a column, left of a row and along a cell's left edge (a drag selects more; the cells are shaded and a column is edited cell by cell); "+" left of a row border and above a column border; move handle past the top-left corner (a click selects the table, a drag floats it where it's dropped); an ordinary drag, Shift+arrows or Shift+click from one cell into another select the rectangle of whole cells between them, and into or out of a table whole rows. Missing: a single selected cell's border | ✅ | 95% | 1 |
|
||||||
| Rotation handle, crop handles on canvas, alignment guides, smart guides | rotation handle (Shift: 15° steps), turned frames resize along their axes (#332); crop handles and guides missing | ✅ | 25% | 3–5 |
|
| Rotation handle, crop handles on canvas, alignment guides, smart guides | rotation handle (Shift: 15° steps), turned frames resize along their axes (#332); crop handles and guides missing | ✅ | 25% | 3–5 |
|
||||||
| Selection: word/sentence/paragraph clicks, dragging after a double-click/triple-click/Ctrl+click extends by whole words/paragraphs/sentences (#549), Shift extend, F8 extend mode, column (Alt+drag, #237) | ✅ | ✅ | 90% | 1 |
|
| Selection: word/sentence/paragraph clicks, dragging after a double-click/triple-click/Ctrl+click extends by whole words/paragraphs/sentences (#549), Shift extend, F8 extend mode, column (Alt+drag, #237) | ✅ | ✅ | 90% | 1 |
|
||||||
| Zoom: Ctrl+scroll, pinch (#179), zoom buttons (#232), pages side by side when zoomed out (#251), still one per row after One Page or Page Width (#768) | ✅ | ✅ | 85% | — |
|
| Zoom: Ctrl+scroll, pinch (#179), zoom buttons (#232), pages side by side when zoomed out (#251), still one per row after One Page or Page Width (#768) | ✅ | ✅ | 85% | — |
|
||||||
@@ -44,6 +44,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-11 | trivial | Tables on the page: Ctrl shares a column border's change among the columns right of it, the left edge moves a left-aligned table's indent (a centred or right-aligned one keeps its alignment), a border of cells selected across moves only in their rows, Alt shows the column widths on the ruler (#711); 80% → 90% |
|
||||||
| 2026-10-11 | trivial | Rulers: table column and row markers drag their borders (one undo step; from the ruler only the column left of a border changes, with Shift the neighbour makes up the difference), and in a cell the indents and tabs are measured from it (#700) |
|
| 2026-10-11 | trivial | Rulers: table column and row markers drag their borders (one undo step; from the ruler only the column left of a border changes, with Shift the neighbour makes up the difference), and in a cell the indents and tabs are measured from it (#700) |
|
||||||
| 2026-10-11 | trivial | Table context menu as in Word: its items follow the selection (the caret in a cell, several cells, whole columns or rows, the whole table), adding Delete Cells, Split Cells, Distribute Rows and Columns, AutoFit, Text Direction, Insert Caption… and Table Properties… to Insert, Delete and Merge Cells; a right-click beside selected cells drops them |
|
| 2026-10-11 | trivial | Table context menu as in Word: its items follow the selection (the caret in a cell, several cells, whole columns or rows, the whole table), adding Delete Cells, Split Cells, Distribute Rows and Columns, AutoFit, Text Direction, Insert Caption… and Table Properties… to Insert, Delete and Merge Cells; a right-click beside selected cells drops them |
|
||||||
| 2026-10-11 | trivial | Tables on the page: ordinary drags, Shift+arrows and Shift+click over cells select a rectangle of whole cells, as in Word |
|
| 2026-10-11 | trivial | Tables on the page: ordinary drags, Shift+arrows and Shift+click over cells select a rectangle of whole cells, as in Word |
|
||||||
|
|||||||
Reference in new issue
Block a user