mirror of
https://github.com/aphrody-labs/wordcraft.git
synced 2026-10-14 03:29:04 +00:00
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
8 files changed
+79
-9
No files matched your search
@@ -132,6 +132,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| Date | What landed |
|
||||
| 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 | A vertically merged table cell (eraser on a horizontal border) lays out, shows and clicks as one cell spanning its rows (#640) |
|
||||
| 2026-10-11 | Word XML files (#548): Word XML Document (Flat OPC `.xml`) opens and saves through the DOCX reader and writer; Word 2003 XML opens (styles, lists, tables, sections, notes, comments, pictures, hyperlinks) |
|
||||
| 2026-10-11 | Font fallback indexes which characters each installed font maps, reading each font file once (#605): text in a script no installed font covers no longer re-reads every system font per distinct character (200 such characters: 13 s → 0.2 s) |
|
||||
| 2026-10-11 | Find and Replace special characters (`^p`, `^t`, `^#`, `^&` …) and Use wildcards (ranges, counts, word anchors, groups with `\1` in Replace); `^p` finds and splits paragraphs; Special ▾ menu and search options in the dialog (#543) |
|
||||
|
||||
@@ -60,7 +60,7 @@ fn cell_boxes(l: &DocLayout, page: usize) -> Vec<CellBox> {
|
||||
pg.items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
Placed::Cell { rect, table, row, cell, story } => {
|
||||
Placed::Cell { rect, table, row, cell, story, .. } => {
|
||||
Some(CellBox { rect: *rect, table: table.clone(), story: *story, row: *row, cell: *cell })
|
||||
}
|
||||
_ => None,
|
||||
@@ -527,6 +527,29 @@ mod tests {
|
||||
assert!(s.run("table.draw", &json!({})).is_err());
|
||||
}
|
||||
|
||||
/// Erasing a horizontal border merges the cells into one laid-out cell (#640): the lower
|
||||
/// row's box is `covered`, the merged cell's box spans both rows, and erasing the border
|
||||
/// below the merge extends it.
|
||||
#[test]
|
||||
fn erasing_a_horizontal_border_lays_the_merge_out_as_one_cell() {
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
s.run("insert.table", &json!({"rows": 3, "cols": 2})).unwrap();
|
||||
let (a, b) = (cell_rect(&mut s, 0, 0), cell_rect(&mut s, 1, 0));
|
||||
s.run("table.eraser", &json!({"page": 0, "x": a.x + a.w / 2.0, "y": a.bottom() + 0.5})).unwrap();
|
||||
let t = first_table(&s);
|
||||
assert_eq!((t.rows[0].cells[0].props.vmerge, t.rows[1].cells[0].props.vmerge), (VMerge::Restart, VMerge::Continue));
|
||||
let merged = cell_rect(&mut s, 0, 0);
|
||||
assert!((merged.bottom() - b.bottom()).abs() < 0.5 && merged.h > a.h * 1.9, "{merged:?}");
|
||||
let covered = s.layout().pages[0].items.iter().any(|it| matches!(it, Placed::Cell { row: 1, cell: 0, covered: true, .. }));
|
||||
assert!(covered, "the continuation row's box is covered");
|
||||
let hit = s.layout().cell_at(0, merged.x + 3.0, b.y + b.h / 2.0).map(|c| (c.2, c.3));
|
||||
assert_eq!(hit, Some((0, 0)), "a click in the lower half is in the merged cell");
|
||||
// The border below the merge: the merge grows to three rows.
|
||||
s.run("table.eraser", &json!({"page": 0, "x": merged.x + merged.w / 2.0, "y": merged.bottom() + 0.5})).unwrap();
|
||||
let t = first_table(&s);
|
||||
assert_eq!(t.rows[2].cells[0].props.vmerge, VMerge::Continue);
|
||||
}
|
||||
|
||||
/// A vertical line through a cell splits it into two columns; a horizontal one splits a row;
|
||||
/// the eraser merges the cells back; each is one undo step.
|
||||
#[test]
|
||||
|
||||
@@ -655,7 +655,7 @@ impl DocLayout {
|
||||
pub fn cell_at(&self, page: usize, x: f32, y: f32) -> Option<(StoryRef, Path, usize, usize)> {
|
||||
let p = self.pages.get(page)?;
|
||||
p.items.iter().rev().find_map(|it| match it {
|
||||
Placed::Cell { rect, table, row, cell, story } if rect.contains(wordcraft_geom::Point::new(x, y)) => {
|
||||
Placed::Cell { rect, table, row, cell, story, covered: false } if rect.contains(wordcraft_geom::Point::new(x, y)) => {
|
||||
Some((*story, table.clone(), *row, *cell))
|
||||
}
|
||||
_ => None,
|
||||
|
||||
@@ -152,9 +152,12 @@ pub enum Placed {
|
||||
spin: Spin,
|
||||
items: Vec<Placed>,
|
||||
},
|
||||
/// A table cell's area (for hit testing and cell selection).
|
||||
/// A table cell's area (for hit testing and cell selection). A vertically merged cell is
|
||||
/// one `Cell` as tall as all its rows; its continuation rows get a `covered` `Cell` (their
|
||||
/// own row's height) so the rows keep a box each, but it isn't a cell of its own on screen.
|
||||
Cell {
|
||||
rect: Rect,
|
||||
covered: bool,
|
||||
table: Path,
|
||||
row: usize,
|
||||
cell: usize,
|
||||
|
||||
@@ -662,7 +662,14 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
||||
if let Some(e) = b.right.filter(Border::is_visible) {
|
||||
items.push(Placed::Rule { x0: x1, y0, x1, y1, border: e });
|
||||
}
|
||||
items.push(Placed::Cell { rect: Rect::new(c.x, 0.0, c.w, rh), table: wordcraft_doc::Path(path.to_vec()), row: ri, cell: ci, story });
|
||||
items.push(Placed::Cell {
|
||||
rect: Rect::new(c.x, 0.0, c.w, if c.vmerge == VMerge::Continue { rh } else { span_h }),
|
||||
covered: c.vmerge == VMerge::Continue,
|
||||
table: wordcraft_doc::Path(path.to_vec()),
|
||||
row: ri,
|
||||
cell: ci,
|
||||
story,
|
||||
});
|
||||
}
|
||||
out.push(RowLayout { height: rh, items });
|
||||
}
|
||||
@@ -790,9 +797,9 @@ pub fn split_row(row: &RowLayout, cut: f32) -> Option<(RowLayout, RowLayout)> {
|
||||
a.extend(above.map(|rect| Placed::Fill { rect, color: *color }));
|
||||
b.extend(below.map(|rect| Placed::Fill { rect, color: *color }));
|
||||
}
|
||||
Placed::Cell { rect, table, row: r, cell, story } => {
|
||||
Placed::Cell { rect, covered, table, row: r, cell, story } => {
|
||||
let (above, below) = split_rect(*rect, cut, shift);
|
||||
let cell_at = |rect| Placed::Cell { rect, table: table.clone(), row: *r, cell: *cell, story: *story };
|
||||
let cell_at = |rect| Placed::Cell { rect, covered: *covered, table: table.clone(), row: *r, cell: *cell, story: *story };
|
||||
a.extend(above.map(cell_at));
|
||||
b.extend(below.map(cell_at));
|
||||
}
|
||||
|
||||
@@ -498,6 +498,29 @@ fn tables_lay_out_cells() {
|
||||
assert!(l.cell_at(0, c.x, c.top + 2.0).is_some());
|
||||
}
|
||||
|
||||
/// A vertical merge is one cell: its box spans the rows, a click anywhere in it finds it, and the
|
||||
/// continuation row's box is `covered` (no gridline, not a hit).
|
||||
#[test]
|
||||
fn a_vertical_merge_lays_out_as_one_cell() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
let mut t = Table::new(3, 2, 468.0);
|
||||
t.rows[0].cells[0].props.vmerge = wordcraft_doc::props::VMerge::Restart;
|
||||
t.rows[1].cells[0].props.vmerge = wordcraft_doc::props::VMerge::Continue;
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
let cells: Vec<_> = l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|i| if let Placed::Cell { rect, row, cell: 0, covered, .. } = i { Some((*row, *rect, *covered)) } else { None })
|
||||
.collect();
|
||||
let (r0, r1) = (cells[0].1, cells[1].1);
|
||||
assert!(!cells[0].2 && cells[1].2 && !cells[2].2, "{cells:?}");
|
||||
assert!((r0.bottom() - r1.bottom()).abs() < 0.01 && r0.h > r1.h * 1.9, "{r0:?} {r1:?}");
|
||||
// The lower half of the merge hits the restart cell (row 0), not the continuation row.
|
||||
let hit = l.cell_at(0, r1.x + 5.0, r1.y + r1.h / 2.0).unwrap();
|
||||
assert_eq!((hit.2, hit.3), (0, 0));
|
||||
}
|
||||
|
||||
/// A 1×3 table after "before" whose middle cell holds `text` running `dir`.
|
||||
fn turned_table(dir: TextDirection, text: &str, row: Option<f32>) -> Document {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
|
||||
@@ -571,7 +571,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
// Table Layout › View Gridlines: table cell outlines (on screen only).
|
||||
if app.session.view.table_gridlines {
|
||||
for it in &page.items {
|
||||
if let Placed::Cell { rect, .. } = it {
|
||||
if let Placed::Cell { rect, covered: false, .. } = it {
|
||||
let r = Rect::from_min_size(
|
||||
pos2(visual_sr.min.x + rect.x * page_scale, visual_sr.min.y + rect.y * page_scale),
|
||||
vec2(rect.w * page_scale, rect.h * page_scale),
|
||||
|
||||
@@ -2137,14 +2137,27 @@ fn cell_size_boxes(ui: &mut Ui, app: &mut WordApp) {
|
||||
let row = t.rows.get(r).map(|x| x.props.clone()).unwrap_or_default();
|
||||
let stored_w = t.grid.get(t.grid_col(r, c)).copied();
|
||||
let laid = app.session.layout().pages.iter().flat_map(|pg| pg.items.iter()).find_map(|it| match it {
|
||||
wordcraft_layout::Placed::Cell { rect, table, row, cell, story: st } if *table == tp && *row == r && *cell == c && *st == story => {
|
||||
wordcraft_layout::Placed::Cell { rect, table, row, cell, story: st, .. } if *table == tp && *row == r && *cell == c && *st == story => {
|
||||
Some(*rect)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
// The row's own height: a vertically merged cell's box spans several rows, so take the
|
||||
// shortest box in this row.
|
||||
let row_h = app
|
||||
.session
|
||||
.layout()
|
||||
.pages
|
||||
.iter()
|
||||
.flat_map(|pg| pg.items.iter())
|
||||
.filter_map(|it| match it {
|
||||
wordcraft_layout::Placed::Cell { rect, table, row, story: st, .. } if *table == tp && *row == r && *st == story => Some(rect.h),
|
||||
_ => None,
|
||||
})
|
||||
.reduce(f32::min);
|
||||
let unit = crate::options::unit();
|
||||
let k = unit.pt_per_unit();
|
||||
let h0 = row.height.or(laid.map(|x| x.h)).unwrap_or(0.0);
|
||||
let h0 = row.height.or(row_h).unwrap_or(0.0);
|
||||
let w0 = laid.map(|x| x.w).or(stored_w).unwrap_or(0.0);
|
||||
let exact = row.height_rule == wordcraft_doc::props::HeightRule::Exact;
|
||||
// The edited value in points, and whether it continues a drag (joins the previous Undo step).
|
||||
|
||||
Reference in new issue
Block a user