Read Mode keeps the keyboard and shows whole, unobscured columns (#1772) (#2591)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-18 03:22:26 -04:00
1 parent 03e713b72f
commit df9c29f673
4 files changed
+194 -8

No files matched your search

+3 -1
View File
@@ -2,7 +2,7 @@
**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-18 · **Change:** trivial (progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial (table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; 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
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial (table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; 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
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: print-area strings translated in every catalog; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; 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
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
@@ -140,6 +140,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|---|---|
| 2026-10-18 | Split Cells and Convert Text to Table dialogs (#2592), matched to Word: Split Cells starts from the selection (twice its columns, its rows; Merge cells before split offered for several cells or one cell merged across columns); `table.split` gains `rows` and `mergeFirst`, a merged cell splits into 1 to its height rows (its height or 1 × 1 unmerges it, more is refused with Word's message) and column splits make equal parts without moving other rows' grid lines. Convert Text to Table guesses columns live, shows the row count, and offers Initial column width (Auto or up to 22 in, kept as each column's preferred width), AutoFit to contents/window and Paragraphs/Commas/Tabs/Other separators; `table.fromText` gains `paragraph`/`other` + `char`, `columns`, `autofit` and `width`. |
| 2026-10-16 | Font list entries of fonts with a very tall ascent (Amiri, Amiri Quran) sit in their row like the others instead of low or cut in half (#2570) |
| 2026-10-16 | Read Mode draws documents cleanly (#1772): the arrows, Page Up/Down and Escape work as soon as it opens (the canvas kept a stable widget id, so it no longer lost the keyboard), only whole columns show, centred, at any column width, text size or window size, and the columns end above the page-turn bar instead of under it. |
| 2026-10-14 | DOCX row table border exceptions render and survive saving. Explicit cell borders win; a row exception does not change other rows. |
| 2026-10-14 | Tab stops edited on the ruler: the corner selector picks Left/Center/Right/Decimal/Bar, a click sets a stop (Alt skips the snap), dragging moves it with a guide line and position tooltip, dragging off the ruler clears it; markers draw per kind and mirror for RTL; one `para.tabs` call and one undo step per gesture |
| 2026-10-12 | Fields keep `w:dirty` through DOCX saves (#877): a begin marker flagged by a generator that couldn't compute a result (pandoc-style TOC placeholders) keeps the flag on open/save — as a buffered field or a range field — so Word still refreshes it on open; computing a result (Update Field, Update Table) clears it, like Word's F9 |
@@ -412,6 +413,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-17 | trivial | Language dialog keeps inherited proofing language and no-proof choices (#2717); readiness estimates unchanged |
| 2026-10-18 | trivial | Progress log: Split Cells and Convert Text to Table dialogs, matched to the Word check (#2592) |
| 2026-10-16 | trivial | Progress log: tall-ascent font list entries no longer cut off (#2570) |
| 2026-10-16 | trivial | Progress log: Read Mode keeps the keyboard, whole columns, page-turn bar clear (#1772) |
| 2026-10-14 | trivial | Progress log: row table border exceptions |
| 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks |
| 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged |
+139 -4
View File
@@ -494,7 +494,28 @@ fn reading_size(avail: egui::Vec2, scale: f32, requested: Option<f32>) -> (f32,
let count = (width / 295.0).round().clamp(1.0, 5.0);
((width - (count + 1.0) * GAP) / count).max(192.0)
};
(px / scale, (height - 2.0 * GAP).max(192.0) / scale)
// The columns end above the page-turn bar, which used to cover their last lines (#1772).
(px / scale, (height - GAP - READ_NAV_BAR).max(192.0) / scale)
}
/// Height kept free under Read Mode's columns for the page-turn bar at the bottom right.
const READ_NAV_BAR: f32 = 44.0;
/// The part of the canvas Read Mode scrolls in: as many whole columns as fit, centred. A chosen
/// column width (or a resize) otherwise left the last column cut off at the right edge (#1772).
/// `scale` is pixels per point: the column width is only re-laid out when it moves by more than
/// half a point, so each column may overrun by that much before one is dropped.
fn reading_viewport(full: Rect, column_px: f32, scale: f32) -> Rect {
if !column_px.is_finite() || column_px <= 0.0 || !full.width().is_finite() {
return full;
}
let step = column_px + GAP;
let slack = if scale.is_finite() { (0.5 * scale).clamp(0.0, 4.0) } else { 0.0 };
// A hair of tolerance too: columns sized to fill the canvas exactly come back from points
// as 2.9999998 of them, and flooring that dropped a whole column.
let whole = ((full.width() - GAP) / (step - slack) + 1e-3).floor().max(1.0);
let width = (whole * step + GAP).min(full.width());
Rect::from_min_size(pos2(full.center().x - width / 2.0, full.min.y), vec2(width, full.height()))
}
/// A single horizontal row of reflowed reading pages; no wrapping at the viewport edge.
@@ -918,6 +939,12 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
} else {
(None, None, full)
};
let area = if app.session.view.read_mode {
let column = app.session.view.read_width * app.session.view.read_zoom.clamp(0.75, 1.5) * PX_PER_PT;
reading_viewport(area, column, app.session.view.read_zoom.clamp(0.75, 1.5) * PX_PER_PT)
} else {
area
};
app.canvas.canvas_rect = Some(area);
// Ctrl/⌘ + wheel (and trackpad pinch) zooms the document, like Word. egui reports both as a
// zoom factor and doesn't scroll for them. Start from the shown scale so fit modes zoom too.
@@ -1023,7 +1050,9 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
// momentum, wheel notches eased in (or all at once with animation effects off). Applied
// before drawing, so it shows this frame. In reading mode, wheel and touchpad events turn
// whole columns instead.
let hovered = wheel_hovered(ui.rect_contains_pointer(area), ui.ctx().dragged_id().is_some(), app.canvas.drag_selecting());
// Read Mode's centred columns leave margins at the sides; the wheel turns pages over them too.
let wheel_area = if app.session.view.read_mode { full } else { area };
let hovered = wheel_hovered(ui.rect_contains_pointer(wheel_area), ui.ctx().dragged_id().is_some(), app.canvas.drag_selecting());
let notch = crate::scroll::notch_px(app.canvas.scale / PX_PER_PT);
let wheel_delta = if app.session.view.read_mode {
let turns = ui.input(|i| read_wheel_turns(&i.events, hovered, &mut app.canvas.read_wheel_accum));
@@ -1109,11 +1138,18 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
let out = scroll_area.show_viewport(&mut ui_area, |ui, viewport| {
origin = ui.min_rect().min - viewport.min.to_vec2();
let content = Rect::from_min_size(ui.min_rect().min, geo.size);
let resp = ui.allocate_rect(content, Sense::click_and_drag());
// A fixed id: an automatic one follows how many widgets came before, which Read Mode's
// chrome changes, and a new id dropped the keyboard focus, so the arrows, Page Up/Down
// and Escape did nothing in Read Mode until the reader clicked a page (#1772).
ui.advance_cursor_after_rect(content);
let resp = ui.interact(content, egui::Id::new("wordcraft_canvas"), Sense::click_and_drag());
if let Some(tr) = scroll_target {
ui.scroll_to_rect(Rect::from_min_size(ui.min_rect().min + tr.min.to_vec2(), tr.size()), None);
}
let painter = ui.painter_at(ui.clip_rect());
// Read Mode's neighbouring columns end right at the viewport's edges: keep their
// shadows from showing there as hairlines.
let clip = if app.session.view.read_mode { ui.clip_rect().shrink2(vec2(3.0, 0.0)) } else { ui.clip_rect() };
let painter = ui.painter_at(clip);
let ppp = ui.ctx().pixels_per_point();
let max_tex = ui.ctx().input(|i| i.max_texture_side).min(wordcraft_render::MAX_SIDE as usize).max(1) as f32;
let mut visible_tile_keys = HashSet::new();
@@ -3189,6 +3225,105 @@ mod tests {
assert!(!wheel_hovered(false, true, true));
}
/// #1772: the page-turn bar at the bottom right covered the last lines of the column under it.
#[test]
fn reading_columns_end_above_the_page_turn_bar() {
let scale = PX_PER_PT;
for (w, h) in [(1280.0, 740.0), (900.0, 700.0), (2000.0, 1200.0)] {
for requested in [None, Some(180.0), Some(350.0), Some(612.0)] {
let (_, ph) = reading_size(vec2(w, h), scale, requested);
let bottom = GAP + ph * scale;
assert!(bottom <= h - READ_NAV_BAR + 0.5, "{w}×{h} {requested:?}: column ends at {bottom}, bar starts at {}", h - READ_NAV_BAR);
}
}
}
/// #1772: a chosen column width (or a resized window) left the last visible column cut off at
/// the right edge. The reading viewport shows only whole columns, centred.
#[test]
fn reading_viewport_shows_whole_columns() {
let scale = PX_PER_PT;
for (w, h) in [(1280.0, 740.0), (900.0, 700.0), (1440.0, 820.0), (500.0, 600.0)] {
let full = Rect::from_min_size(pos2(10.0, 20.0), vec2(w, h));
for requested in [None, Some(180.0), Some(240.0), Some(350.0), Some(612.0)] {
for zoom in [0.75, 1.0, 1.5] {
let (cw, _) = reading_size(full.size(), scale * zoom, requested);
let column = cw * scale * zoom;
let area = reading_viewport(full, column, scale * zoom);
assert!(full.contains_rect(area), "inside the canvas");
assert_eq!(area.height(), full.height());
assert!((area.center().x - full.center().x).abs() < 0.5, "centred");
// Columns at rest start at GAP + k·step from the viewport's left edge. Past
// the whole ones, only a column short by under half a point each may show.
let step = column + GAP;
let whole = ((area.width() - GAP) / step + 1e-3).floor().max(1.0);
let used = whole * step + GAP;
let short = used + step - area.width();
assert!(
area.width() <= used + 0.5 || whole == 1.0 || short <= (whole + 1.0) * 0.5 * scale * zoom + 0.5,
"{w}×{h} {requested:?} {zoom}: {} wide shows a cut column (columns use {used})",
area.width()
);
}
}
}
// With no width chosen the columns are sized to fill the canvas exactly, so none may be
// dropped: the stored width (pt) and back to px must still fit every one of them.
for zoom in [0.75, 1.0, 1.25, 1.5] {
let scale = PX_PER_PT * zoom;
for w in (300..=4000).step_by(7) {
let full = Rect::from_min_size(pos2(0.0, 0.0), vec2(w as f32, 700.0));
let count = (full.width() / 295.0).round().clamp(1.0, 5.0);
let px = (full.width() - (count + 1.0) * GAP) / count;
if px < 192.0 {
continue; // the minimum width overflows; whole columns then shrink the view
}
let (cw, _) = reading_size(full.size(), scale, None);
let column = cw * scale;
let area = reading_viewport(full, column, scale);
let needed = count * column + (count + 1.0) * GAP;
assert!(area.width() >= needed - 0.5, "{w} at {zoom}: {} wide drops a column of {count} ({needed})", area.width());
}
}
// Junk sizes never panic and fall back to the whole canvas.
let full = Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0));
assert_eq!(reading_viewport(full, f32::NAN, 1.0), full);
assert_eq!(reading_viewport(full, 0.0, f32::NAN), full);
}
#[test]
fn reading_viewport_keeps_columns_when_the_window_shrinks_a_little() {
// The column width is only re-laid out when it changes by more than half a point, so a
// window a few pixels narrower keeps slightly too wide columns: none may drop out.
for (zoom, shrink) in [(1.0, 1.0), (1.0, 3.0), (1.5, 1.0), (1.5, 3.0), (0.75, 1.0)] {
let scale = PX_PER_PT * zoom;
for w in (300..=4000).step_by(7) {
let before = vec2(w as f32, 700.0);
let count = (before.x / 295.0).round().clamp(1.0, 5.0);
if (before.x - (count + 1.0) * GAP) / count < 192.0 {
continue;
}
let after = before - vec2(shrink, 0.0);
if (after.x / 295.0).round() != count {
continue; // a different column count reflows anyway
}
let (kept, _) = reading_size(before, scale, None);
let (fresh, _) = reading_size(after, scale, None);
if (kept - fresh).abs() > 0.5 {
continue; // re-laid out at once
}
let full = Rect::from_min_size(pos2(0.0, 0.0), after);
let area = reading_viewport(full, kept * scale, scale);
assert!(
area.width() >= full.width() - 0.5,
"{w}-{shrink} at {zoom}: kept {kept}pt columns, view narrowed to {} of {}",
area.width(),
full.width()
);
}
}
}
#[test]
fn reading_animation_eases_then_lands_exactly_on_column_start() {
let goal = 330.0;
+49 -1
View File
@@ -3177,6 +3177,53 @@ mod tests {
assert_eq!(lines(&mut a), print_lines, "Print Layout is unchanged");
}
/// Read Mode turns columns with the keyboard (#1772): Right/Page Down forward, Left/Page Up back.
#[test]
fn reading_mode_keys_turn_columns() {
let ctx = egui::Context::default();
let mut a = app();
a.run("text.insert", json!({"text": "One readable paragraph in many narrow columns. ".repeat(300)})).unwrap();
a.run("caret.set", json!({"pos": wordcraft_doc::Pos::body(0, 0)})).unwrap();
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 740.0));
let mut t = 0.0;
let mut frame = |a: &mut WordApp, events: Vec<egui::Event>| {
t += 1.0 / 60.0;
let input = egui::RawInput { events, time: Some(t), screen_rect: Some(screen), ..Default::default() };
ctx.run_ui(input, |ui| {
a.logic(ui.ctx());
a.ui(ui);
})
.drop_without_applying_deltas();
};
for _ in 0..3 {
frame(&mut a, vec![]);
}
a.run("view.readMode", json!({})).unwrap();
for _ in 0..3 {
frame(&mut a, vec![]);
}
let key = |key| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE };
let step = a.session.view.read_width * a.canvas.scale + 18.0;
assert!(a.canvas.focused, "the canvas has the keyboard");
frame(&mut a, vec![key(egui::Key::PageDown)]);
for _ in 0..30 {
frame(&mut a, vec![]);
}
assert!((a.canvas.scroll_offset.x - step).abs() < 1.0, "Page Down turns one column: {}", a.canvas.scroll_offset.x);
frame(&mut a, vec![key(egui::Key::ArrowRight)]);
for _ in 0..30 {
frame(&mut a, vec![]);
}
assert!((a.canvas.scroll_offset.x - 2.0 * step).abs() < 1.0, "Right turns one column: {}", a.canvas.scroll_offset.x);
frame(&mut a, vec![key(egui::Key::ArrowLeft)]);
for _ in 0..30 {
frame(&mut a, vec![]);
}
assert!((a.canvas.scroll_offset.x - step).abs() < 1.0, "Left turns back: {}", a.canvas.scroll_offset.x);
frame(&mut a, vec![key(egui::Key::Escape)]);
assert!(!a.session.view.read_mode, "Escape leaves Read Mode");
}
/// Reading Mode: reflowed viewport-height columns, horizontal wheel/navigation, exit restores
/// Print Layout without editing or changing the document's page setup.
#[test]
@@ -3209,7 +3256,8 @@ mod tests {
assert_eq!(a.canvas.cols, rects.len(), "one unwrapped row");
assert!((rects[0].top() - rects[1].top()).abs() < 0.1);
assert!(rects[1].left() > rects[0].right());
assert!((rects[0].height() - (a.canvas.canvas_rect.unwrap().height() - 36.0)).abs() < 2.0);
// Full height, less the top gap and the page-turn bar's strip at the bottom (#1772).
assert!((rects[0].height() - (a.canvas.canvas_rect.unwrap().height() - 62.0)).abs() < 2.0);
let initial_width = a.session.view.read_width;
// A simple mouse click in a later visible column must neither pull that column
// to the leading edge nor select text under the stationary, held pointer.
+3 -2
View File
@@ -1,6 +1,6 @@
# UI parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; 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
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; 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.
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
@@ -43,7 +43,7 @@ Document title opens Rename Document; the account badge opens Options › Genera
| Touchpad smooth/inertial scroll | ✅ (#252) | ✅ | 90% | — |
| Web Layout width | Text wraps to the whole work area (window width at the current zoom, 144–20,000 pt) and re-wraps once a window resize has settled (not on every frame of a drag) or at once on a zoom, keeping the caret in view; the sheet runs edge to edge with no centred page. Draft and Outline keep their 800 pt column; headless sessions keep 800 pt (#1876) | text across the window | 95% | — |
| Web and Draft Layout rendering | Very long pages render only visible full-resolution raster tiles instead of shrinking one huge texture to the GPU size limit; nearby tiles are cached for scrolling | sharp document text | 95% | — |
| Reading Mode | Existing Read Mode ribbon button opens a read-only, viewport-column layout with animated horizontal paging, transient width/text/spacing controls, heading navigation (Navigation pane) and resize anchoring; print/export layout unchanged | reading-optimized view and navigation | 75% | 1–2 |
| Reading Mode | Existing Read Mode ribbon button opens a read-only, viewport-column layout with animated horizontal paging, transient width/text/spacing controls, heading navigation (Navigation pane) and resize anchoring; print/export layout unchanged. The arrows, Page Up/Down and Escape work straight after entering it; only whole columns show, centred, at any width, text size or window size; the columns end above the page-turn bar (#1772) | reading-optimized view and navigation | 75% | 1–2 |
| Status bar (page x of y, words, language, view buttons, zoom slider) | ✅ (the language is the caret's, by name; a click sets the proofing language, or opens Set Proofing Language… for any other) | ✅ | 85% | — |
| Caret motion: keys and typing glide the caret to its new place (100 ms, `cubic-bezier(0, 0, 0, 1)`), the blink fades (530 ms on and off, 100 ms fades); clicks and scripted moves don't glide. File › Options › Advanced › Animation (`ui.caretAnimation`) turns it off. The caret is black or white by contrast with what lies under it (paper, page colour, dark page, shading, highlight), never by the UI theme (#299) | ✅ as measured from Word for Windows (`caret_glide.rs`); zoom levels other than 100%, modifier keys and off-screen moves not measured | ✅ on Windows; Word for Mac not measured | 90% | 1–2 |
| Interface themes (light, dark, system #249, follows OS changes live #311), dark page separate from the interface theme (#194, #312); on a dark page highlights and character shading are dimmed, keeping their hue, so the text on them stays readable; coloured text below 4.5:1 on them is drawn white, and underlines and strikethroughs follow the same rule (#1252) | ✅ | ✅ | 90% | — |
@@ -68,6 +68,7 @@ The Language dialog reads effective character formatting, including document def
| 2026-10-18 | trivial | Split Cells merged-cell limits and equal parts, Convert Text to Table Initial column width, both from the Word check (#2592) |
| 2026-10-16 | trivial | Split Cells and Convert Text to Table dialogs (#2592): 58 modal dialogs |
| 2026-10-16 | trivial | Font list: each entry's capitals are centred in its row, so a font with a very tall ascent (Amiri, Amiri Quran) no longer sits low or is cut in half, and the list reads as one even list (#2570) |
| 2026-10-16 | trivial | Read Mode keeps the keyboard on entering, shows only whole columns, centred, and ends them above the page-turn bar (#1772) |
| 2026-10-13 | trivial | New/Modify Table Style: No Border on a part that inherits its lines hides them; emptying the font box makes the part's font inherited again (#1288) |
| 2026-10-14 | trivial | Tab stops edited on the ruler: corner kind selector, click to set, drag to move or off to clear, per-kind markers mirrored for RTL; each gesture one `para.tabs` call and one undo step; Rulers 85% → 90% |
| 2026-10-12 | trivial | Tables on the page: one selected cell (Select Cell, or a click along its left edge) is a cell selection when its border is dragged: the border moves in its row only (#962) |