collective

Browse / github.com/storytold/wordcraft / patch

The pointer is an arrow on the workspace beside and between pages (#4114)

Bug Report: Inconsistent Mouse Cursor Behavior Around the Document Page The mouse cursor does not change consistently between the text cursor (I-beam) and the arrow pointer when moving between the document page and the surrounding area. In Microsoft Word, the cursor changes naturally depending on the mouse position: - When the mouse is over the document page, the cursor changes to the I-beam, indicating where text can be edited. - When the mouse is outside the document page, the cursor changes to the normal arrow pointer. In Wordcraft, this behavior appears to be inconsistent. Observed Behavior Left side of the document: When the mouse is outside the page, th

Status
open
Score
0.00
Adopters
0 other operators
Author
x-labb (imported from pull request #4427, open)
Base
2cf1d6c7ae94723a45bd6e7e8c884fe375148750
Size
+48 −11 in 4 files
Review
pending
Shared
2026-10-28
Id
5ed73c1dc524b1cd25e28023

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/canvas.rs crates/ui-egui/src/canvas/tests.rs crates/ui-egui/src/ibeam.rs docs/ui-parity.md

diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs--- a/crates/ui-egui/src/canvas.rs+++ b/crates/ui-egui/src/canvas.rs@@ -1380,7 +1380,10 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {                 || latest.and_then(|p| page_at(&rects, &layout, geo.scale, p)).is_some_and(|(pg, x, y)| layout.selection_bar_at(pg, x, y).is_some()));         // An armed Format Painter shows over the text, as Word's brush does; egui has no brush,         // so the copy pointer stands in (#3453). The selection bar keeps its arrow.-        let fallback = if in_bar {+        // The workspace around and between the pages keeps the arrow, as Word's does (#4114); a+        // drag that started on a page keeps the I-beam wherever it goes.+        let off_page = !resp.dragged() && !latest.is_some_and(|p| on_visible_page(&rects, &geo.bands, p));+        let fallback = if in_bar || off_page {             egui::CursorIcon::Default         } else if app.session.painter.is_some() {             egui::CursorIcon::Copydiff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs--- a/crates/ui-egui/src/canvas/tests.rs+++ b/crates/ui-egui/src/canvas/tests.rs@@ -1013,6 +1013,42 @@ fn selection_bar_of_a_right_to_left_paragraph_is_on_the_right() {     assert!(h.state().session.sel.is_collapsed(), "a click in the left margin beside it is a caret, not a line"); } +/// #4114: the pointer is the arrow on the workspace beside the page, on both sides, and the+/// I-beam over the page itself.+#[test]+fn the_pointer_is_an_arrow_beside_the_page_and_an_i_beam_on_it() {+    let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());+    a.run("text.insert", json!({"text": "Hello WordCraft"})).unwrap();+    a.run("view.commentsPane", json!({"value": false})).unwrap();+    let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(+        |ui, app: &mut WordApp| {+            let ctx = ui.ctx().clone();+            app.logic(&ctx);+            app.ui(ui);+        },+        a,+    );+    for _ in 0..6 {+        h.step();+    }+    let page = h.state().canvas.page_rects[0];+    let y = page.center().y;+    let on_page = pos2(page.center().x, y);+    let (left, right) = (pos2(page.min.x - 24.0, y), pos2(page.max.x + 24.0, y));+    assert!(left.x > 0.0 && right.x < 1440.0, "room beside the page: {left:?} {right:?}");+    // Where the I-beam is drawn (Windows, #1746) the system pointer over the page is hidden.+    let ibeam = if h.state().canvas.drawn_ibeam { egui::CursorIcon::None } else { egui::CursorIcon::Text };+    let mut pointer = |at: Pos2| {+        h.event(egui::Event::PointerMoved(at));+        h.step();+        h.output().platform_output.cursor_icon+    };+    assert_eq!(pointer(on_page), ibeam, "on the page");+    assert_eq!(pointer(left), egui::CursorIcon::Default, "beside the page on the left");+    assert_eq!(pointer(on_page), ibeam, "back on the page");+    assert_eq!(pointer(right), egui::CursorIcon::Default, "beside the page on the right");+}+ /// #3453: while the Format Painter is armed the pointer over the text says so, and it goes /// back once the painter is off. The selection bar keeps its arrow while it is armed. #[test]diff --git a/crates/ui-egui/src/ibeam.rs b/crates/ui-egui/src/ibeam.rs--- a/crates/ui-egui/src/ibeam.rs+++ b/crates/ui-egui/src/ibeam.rs@@ -163,18 +163,15 @@ mod tests {         }     } -    /// #1746 review: off the shown page (the gap between pages, the seam where Hide White Space-    /// cuts the margins) the I-beam contrasts with the canvas, not the paper. With the dark page-    /// the two differ: white over the page, black over the light canvas.+    /// #1746 review, #4114: off the shown page (the gap between pages, the seam where Hide White+    /// Space cuts the margins) the pointer is the arrow, as in Word, so no I-beam is drawn there.+    /// Over the dark page it is white.     #[test]-    fn the_drawn_i_beam_takes_the_canvas_colour_off_the_page() {+    fn no_i_beam_is_drawn_off_the_page() {         let text = vec!["A line of text to fill the pages."; 120].join("\n");         let mut h = window_at(true, true, 1.0, 800.0, &text);         h.state_mut().session.view.zoom = 0.5;         h.run_steps(3);-        let canvas = crate::theme::Tokens::get(&h.ctx).canvas;-        let off = crate::canvas::caret_contrast(wordcraft_doc::props::Rgb(canvas.r(), canvas.g(), canvas.b()));-        assert_eq!(off, Color32::BLACK, "the light canvas");         // The fill is drawn last.         let fill = |h: &mut H, at: egui::Pos2| stem_strokes(h, at).last().map(|s| s.color);         let rects = h.state_mut().canvas.page_rects.clone();@@ -183,7 +180,7 @@ mod tests {         let gap = egui::pos2(x, (rects[0].max.y + rects[1].min.y) / 2.0);         assert!(rects[0].max.y < gap.y && gap.y < rects[1].min.y, "a gap between the pages: {rects:?}");         assert_eq!(fill(&mut h, egui::pos2(x, rects[0].center().y)), Some(Color32::WHITE), "on the dark page");-        assert_eq!(fill(&mut h, gap), Some(off), "between the pages");+        assert!(stem_strokes(&mut h, gap).is_empty(), "between the pages");         // On the seam where Hide White Space cuts page 1's bottom margin and page 2's top.         assert!(h.state_mut().run("view.hideWhiteSpace", serde_json::json!({"value": true})).is_ok());         h.run_steps(3);@@ -191,7 +188,7 @@ mod tests {         let body_bottom = h.state_mut().session.layout().pages[0].body.bottom();         let band_end = crate::canvas::page_to_screen(h.state_mut(), 0, 300.0, body_bottom).unwrap().y + crate::canvas::WHITE_SPACE_PAD * scale;         assert_eq!(fill(&mut h, egui::pos2(x, band_end - 20.0)), Some(Color32::WHITE), "on page 1's text area");-        assert_eq!(fill(&mut h, egui::pos2(x, band_end + crate::canvas::SEAM / 2.0)), Some(off), "on the seam over the cut margins");+        assert!(stem_strokes(&mut h, egui::pos2(x, band_end + crate::canvas::SEAM / 2.0)).is_empty(), "on the seam over the cut margins");     }      /// Elsewhere the system's own I-beam stays.diff --git a/docs/ui-parity.md b/docs/ui-parity.md--- a/docs/ui-parity.md+++ b/docs/ui-parity.md@@ -43,7 +43,7 @@ Document title opens Rename Document; the account badge opens Options › Genera | 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 (a nested table's within the cell around it, #971); 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); both handles also show while the caret is in the table, as in Word (#2428); 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, their cells shaded whole (#971). Missing: a single selected cell's border; The Cell Size Height and Width boxes set every selected row and column (#1242). | ✅ | 95% | 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 (only zooms; a coast or notch still gliding stops, #1743), 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% | — |-| 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), selection bar in the left margin (click a line, double-click a paragraph, triple-click or Ctrl+click everything, drag by lines, Shift+click extends; #856) | ✅ Missing: the selection bar in the right margin for right-to-left paragraphs, beside table rows, and Word's right-pointing arrow cursor (egui has none; the plain arrow shows) | ✅ | 92% | 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), selection bar in the left margin (click a line, double-click a paragraph, triple-click or Ctrl+click everything, drag by lines, Shift+click extends; #856); the pointer is an arrow beside and between pages and an I-beam over a page (#4114) | ✅ Missing: the selection bar in the right margin for right-to-left paragraphs, beside table rows, and Word's right-pointing arrow cursor (egui has none; the plain arrow shows) | ✅ | 92% | 1 | | Headers and footers on the page: double-click the top or bottom margin to edit them, the body to leave | ✅ in Print Layout, also on a page with none yet: the header or footer that page shows is created (first page, even page or the default) and the caret put in it; one left empty is taken away again on leaving and is never saved (#960). Making one is an undo step, refused in a protected document. On a later section's page with no header anywhere it goes in the first section and shows on every section linked to it, as Word links to the previous section by default. With Different First Page (or Odd & Even) on, a double-click goes into the header that page shows even from another header being edited, and Insert › Header/Footer and Edit Header with the caret on page 1 use page 1's own (#2826). Closing it (Esc, Close Header and Footer) puts the caret back where it was in the body, as Word does, instead of on page 1 (#3331); a double-click on the body puts it where clicked. While one is being edited, a single click in the other (the footer while editing the header, or another page's own) edits that one with the caret where clicked, and the view stays put. Web Layout, Draft and Read Mode show no margins and leave it alone. A click in the margin band from the body leaves the caret and the view where they are, so double-clicking a later page's footer opens it there (#2103) | ✅ | 95% | 0.5 | | Zoom: Ctrl+scroll (only zooms; a coast or notch still gliding stops, #1743), pinch (#179), zoom buttons (#232), pages side by side when zoomed out (#251) | ✅ | ✅ | 85% | — | | Touchpad smooth/inertial scroll | ✅ (#252) | ✅ | 90% | — |@@ -82,6 +82,7 @@ Footnote and endnote editing keeps body and note text at normal opacity, on ligh  | Date | Change | Summary | |---|---|---|+| 2026-10-26 | trivial | The pointer is the arrow on the workspace beside and between pages, as in Word, and the I-beam only over a page (a drag that started on a page keeps it); on Windows the drawn I-beam follows (#4114) | | 2026-10-22 | trivial | View › Window › New Window opens another window on the document, focused and cascaded; two windows on one document are titled `Name:1` and `Name:2` (one window: the plain name). Each window keeps its own selection, scroll position, zoom, view mode, ruler and panes; the text, undo history, unsaved state and file are the document's, so an edit or Undo in either window shows in both, and a caret in text the other window deleted moves to the nearest place left. Closing one of a document's windows asks nothing; closing its last asks about unsaved changes, and quitting asks once per document. Switch Windows lists every window. The web app keeps one window | | 2026-10-22 | trivial | One native window per open document on the desktop (the web app keeps one window): New and Open open the document in a window of its own, focused and cascaded from the last; Open replaces an untouched blank document in its window and brings an already-open file's window forward; each window keeps its own ribbon tab, File page, dialogs, panes and composition; View › Window › Switch Windows lists the windows and brings the chosen one forward; File › Close and the window's close button close the window (asking about unsaved changes) and the window focused before comes forward, while the last window stays open with no document; quitting asks Save / Don't Save / Cancel about each document in its own window | | 2026-10-22 | trivial | Several open documents in one window (one window per document comes next): View › Window › Switch Windows lists the open documents, the shown one marked and unsaved ones dotted, and shows the one chosen, with its own scroll position, zoom, comment drafts and AutoSave file; File › New, Open, Envelopes, Labels, Finish & Merge and personal templates open beside the open documents without asking about unsaved changes; File › Close shows the next open document; the window's close button asks Save / Don't Save / Cancel about each document with unsaved changes, showing it, and Cancel keeps the app open. Untitled documents are numbered (Document2) in the interface language |