mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
Layout: the caret after spaces hanging past the margin stays at the margin (#2152)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
cdb13144b3
commit
986d7e2a5a
@@ -447,6 +447,7 @@ impl DocLayout {
|
||||
let first = para.lines.get(*l0)?;
|
||||
let l = para.lines.get(li)?;
|
||||
let cx = para.x_of(li, pos.off)?;
|
||||
let cx = para.caret_limit(li).map_or(cx, |m| cx.min(m));
|
||||
// In the lines' own frame (turned text turns it onto the page).
|
||||
let top = l.top - first.top;
|
||||
// Caret spans the text height (ascent + descent), not the full line spacing.
|
||||
|
||||
@@ -2545,6 +2545,21 @@ impl ParaLayout {
|
||||
}
|
||||
l.xs.last().copied()
|
||||
}
|
||||
/// The farthest right a caret goes on left-to-right line `li`: spaces at the end of a line
|
||||
/// hang past its right edge, but the caret after them stays at the edge, as in Word (#2146);
|
||||
/// text that overflows the line still carries it. `None` on bidirectional lines.
|
||||
pub fn caret_limit(&self, li: usize) -> Option<f32> {
|
||||
let l = self.lines.get(li)?;
|
||||
if !l.vis.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ink = (l.c0..l.c1).rev().find(|&k| self.clusters.get(k).is_some_and(|c| c.kind != ClKind::Space));
|
||||
let end = match ink {
|
||||
Some(k) => l.xs.get(k + 1 - l.c0).copied(),
|
||||
None => l.xs.first().copied(),
|
||||
};
|
||||
Some(end.unwrap_or(l.left).max(l.right))
|
||||
}
|
||||
/// The line holding offset `off` (a position at a wrap point belongs to the next line).
|
||||
pub fn line_of(&self, off: usize) -> usize {
|
||||
for (i, l) in self.lines.iter().enumerate() {
|
||||
|
||||
@@ -1336,6 +1336,39 @@ fn headers_and_page_fields() {
|
||||
assert!(c.top > 700.0);
|
||||
}
|
||||
|
||||
/// Spaces typed at the end of a line hang past the right margin (they don't wrap), but the caret
|
||||
/// after them stays at the margin, as in Word, instead of running off the page (#2146). A letter
|
||||
/// typed after them wraps to the next line. Same in the body and in a header.
|
||||
#[test]
|
||||
fn caret_after_hanging_spaces_stays_at_the_margin() {
|
||||
let spaces = format!("a{}", " ".repeat(200));
|
||||
for header in [false, true] {
|
||||
for text in [spaces.clone(), format!("{spaces}b")] {
|
||||
let mut d = Document::new();
|
||||
let p = wordcraft_doc::Paragraph::with_text(&text, Default::default());
|
||||
let pos = if header {
|
||||
let id = d.add_part(wordcraft_doc::PartKind::Header, vec![wordcraft_doc::para_block(p)]);
|
||||
d.last_section.headers.default = Some(id);
|
||||
Pos { story: StoryRef::Part(id), path: Path::top(0), off: text.len() }
|
||||
} else {
|
||||
d.body = vec![wordcraft_doc::para_block(p)];
|
||||
Pos::body(0, text.len())
|
||||
};
|
||||
let right = d.last_section.page_w - d.last_section.margin_right;
|
||||
let l = lay(&d);
|
||||
let items = if header { &l.pages[0].header } else { &l.pages[0].items };
|
||||
let lines = items.iter().find_map(|it| if let Placed::Lines { para, .. } = it { Some(para.lines.len()) } else { None }).unwrap();
|
||||
let c = l.caret(&pos).unwrap();
|
||||
let typed = text.ends_with('b');
|
||||
assert_eq!(lines, if typed { 2 } else { 1 }, "header {header}, letter typed {typed}");
|
||||
assert!(c.x <= right + 0.5, "header {header}, letter typed {typed}: caret at {} past the margin at {right}", c.x);
|
||||
if !typed {
|
||||
assert!(c.x >= right - 0.5, "header {header}: the caret sits at the margin, after the spaces ({})", c.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_first_page_header_pushes_body_down() {
|
||||
let mut d = Document::from_text(&"para\n".repeat(120));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Layout and pagination parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** trivial (covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** trivial (the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
|
||||
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
|
||||
@@ -21,6 +21,7 @@ font can't match Word's metrics.
|
||||
| 2026-10-16 | trivial | Split-cell content retains its resolved top margin and border band on continuation pages, matching original Word Mac16.113.2 controls. Synthetic regressions cover margins, borders, line spacing and image frames. |
|
||||
| Line breaking (UAX #14 with Word's exceptions: no break after `/`) | ✅ | `layout::tests::no_line_break_right_after_a_slash` | — |
|
||||
| Justification, Word 2013+ space shrinking (compat mode 15) | ✅ | #109 | — |
|
||||
| Spaces at the end of a line hang past the right margin instead of wrapping; the caret after them stays at the margin, in the body and in headers and footers | ✅ | `layout::tests::caret_after_hanging_spaces_stays_at_the_margin` (#2146): a letter typed after them wraps to the next line | — |
|
||||
| Complex scripts (Myanmar, Khmer, Indic): shaped per script run with OpenType fallback fonts, one cluster per syllable, Myanmar wraps between syllables | 🟡 | `layout::tests_complex` (#604). Khmer, Thai and Lao still wrap only at spaces (need a dictionary) | 4–8 |
|
||||
| Kashida justification (Arabic) | ❌ | no kashida in `crates/layout` | 3–5 |
|
||||
| Distributed / Thai distributed alignment | 🟡 | read; layout untested | 1–2 |
|
||||
@@ -184,6 +185,7 @@ Theme-inherited dash styles, line caps, ink and open freeform strokes are not co
|
||||
| 2026-10-16 | trivial | Exact OpenType full names (name ID4) select installed static faces after family names; variable-instance aliases remain unsupported |
|
||||
| 2026-10-16 | trivial | Symbol (PCL6) uses the existing Symbol mapping and Symbol face/fallback despite stale Roman font metadata; stored codes/fonts stay unchanged |
|
||||
| 2026-10-16 | trivial | Prefer a covering Times New Roman face for an uncovered black-circle glyph; retain normal label-ascent and other fallback rules |
|
||||
| 2026-10-15 | trivial | The caret after spaces hanging past the right margin stays at the margin instead of running off the page (#2146) |
|
||||
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |
|
||||
| 2026-10-14 | trivial | Consistent cell widths override a stale table grid |
|
||||
| 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |
|
||||
|
||||
Reference in New Issue
Block a user