20 KiB
Doc workspace device verification (Android/iOS)
The doc workspace logic that can be verified without hardware already
is: the real-Cx runtime harness (tests.rs in this folder) covers
event routing, selection/caret math, cell ranges, clipboard semantics,
gesture arbitration, and multi-line layout against stub areas. What it
cannot prove is anything the platform owns: the soft keyboard, the
native clipboard action menu, touch-event delivery and hit
transformation, scroll handoff to the parent ScrollYView, and actual
pixels. THIS runbook is the last-remaining checklist — every section
maps to a behavior the milestones deliberately deferred to hardware:
- IME opening deferred to device verification (mobile Edit/Done milestone).
- Clipboard menu placement and keyboard-shift deferred (mobile
clipboard menu milestone;
cx.keyboard_shiftis passed straight through to the platform). - ScrollYView parent handoff is the one unchecked roadmap box.
- Painting/clipping visual pass was scoped here by the legacy perf-box retirement (draw logic is harness-covered; pixels are not).
Execute the sections in order against BOTH the CRDT workspace
(CrdtDocWorkspace, active) and the legacy DocWorkspace (fallback)
where a section says BOTH; otherwise the CRDT workspace is the target.
Record each row PASS/FAIL/notes in section 10.
0. Prerequisites — build to device
The Makepad fork pinned in Cargo.lock ships tools/cargo_makepad;
install once from the checkout (or from a fetched copy):
cargo install --path <makepad-checkout>/tools/cargo_makepad
Android (device on USB, adb visible):
cargo makepad android install-toolchain
cargo makepad android run -p nigig-build
iOS (Xcode + device; provisioning per the tool's notes):
cargo makepad apple list # certificates/profiles/devices
cargo makepad apple ios run-device -p nigig-build --org=<ORG> --app=<APP>
Flag reference lives in the tool itself (cargo makepad --help;
--package-name, --app-label, --abi, --sdk-path on Android;
--profile, --cert, --device on iOS). Logs: adb logcat on
Android, Xcode devices console on iOS — filter for the app process.
Before starting, open the document workspace and set some content: at
least two paragraphs, one 2x2 table with cell text a | bc / d | e,
one cell edited through an external plain-text editor to hold
x<newline>y (paste the two-line string into the cell through the
system clipboard — see section 6; it round-trips verbatim by design).
1. Interaction mode: View ↔ Edit
Code anchors: first real TouchUpdate flips interaction_mode to
View (crdt_widget.rs, the mobile_mode_initialized arm); the mobile
toolbar's AdaptiveView button (edit_mode_btn in both workspaces)
calls toggle_interaction_mode and relabels Edit ↔ Done
(widgets/workspace.rs).
| # | Action | Expected |
|---|---|---|
| 1.1 | Cold-launch on device, tap inside the document once | Keyboard does NOT open (first touch dropped the session to View; the tap parks a passive cursor) |
| 1.2 | Drag vertically right after 1.1 | The page scrolls (View skips the editor's area hits entirely) |
| 1.3 | Tap the toolbar Edit button | Button relabels Done; a subsequent tap in text opens the IME (keyboard shows, caret blinks at the tap char boundary) |
| 1.4 | Tap Done | Keyboard closes; taps are passive again |
Fail criteria: IME opens in View mode; Edit/Done desynchronizes from the label; a tap in Edit does not open the keyboard (see section 2's frame-driven reassert before calling it a bug).
2. IME opening and text input
Code anchors: taps in Edit call cx.show_text_ime(self.draw_bg.area(), abs); a frame-driven reassert mirrors the request on backends that
honor it only after the focus area has been drawn (mobile milestone
note in crdt_widget.rs).
| # | Action | Expected |
|---|---|---|
| 2.1 | Edit mode: tap mid-word in a paragraph, type | Characters insert at the caret; the caret tracks |
| 2.2 | Tap inside a table cell, type | Characters insert at the char under the pointer (midpoint split); no whole-cell stomping |
| 2.3 | While typing in a cell, accept an autocorrect/autocomplete suggestion | The committed text lands as the cell content, once (whole-cell write path consumes the commit; composing text should not stack) |
| 2.4 | Tap an empty cell and type | Text appears; undo once restores the empty cell |
| 2.5 | Type a long word that passes the cell's right edge | Text visually overflows the cell (known single-line behavior — multi-character overflow is drawn, not wrapped; NOT a failure, note only) |
Fail criteria: doubled commits, keyboard opening but input dropping, caret/IME disagreement (the IME anchor is the caret rect + its height; a misplaced suggestion popup usually means the anchor rect is off).
3. Long-press selection, handles, clipboard menu
Code anchors: the gesture router arms long-press at 24 frames and
hands control to SelectWord (mobile_gesture.rs,
long_press_frames: 24); handles come from
compute_selection_handles; the menu is requested through
cx.show_clipboard_actions(has_selection, rect, cx.keyboard_shift)
with rect = the selection's handle union (text) or the pressed
cell's rect (cell range), mirrored in widget.clipboard_menu.
| # | Action | Expected |
|---|---|---|
| 3.1 | Edit mode: press and hold on a word WITHOUT moving for ~0.5s | Word selects, handles appear at its edges, the native menu floats near the selection with Copy/Cut (and Paste if the system clipboard is non-empty) |
| 3.2 | Repeat 3.1 with the keyboard OPEN | The menu floats clear of the keyboard area (platform places it from keyboard_shift) |
| 3.3 | Drag the START handle onto another word; drag the END handle | Selection follows the fingers; menu does NOT re-open WHILE dragging (initiating matches TextInput cadence) |
| 3.6 | Lift the finger after a handle drag | The native menu re-floats, anchored on the NEW selection span (not the stale word rect), with Copy/Cut offered for the copyable span. Only in Edit mode; in View the drag still adjusts the highlight but no menu appears |
| 3.4 | Long-press on empty space between paragraphs | Nothing arms; no menu |
| 3.5 | Long-press inside a table cell (not on a handle) | The cell range arms on the pressed cell; dragging expands the range rectangle; the menu offers the full action set (a range copies tabular text, so has_selection = true) |
Fail criteria: long-press fires while the finger has moved (should have routed to scroll, section 5); the menu appears under the keyboard or at stale coordinates; handles select inverted (start/end swapped).
4. Table gestures: ranges, merge/split, in-cell editing
Code anchors: long-press cell → start_cell_range; drag with a live
range → extend_cell_range_to (the touch-only spanning path; the
router idles in Selecting while a range is active); Merge/Split
toolbar buttons → merge_selected_cells / split_cell_at_cursor.
| # | Action | Expected |
|---|---|---|
| 4.1 | Long-press a cell, drag diagonally across four cells, release | 2x2 range highlighted (per-cell bands; covered cells draw under the anchor when merged) |
| 4.2 | With the range armed, tap Merge | Cells merge; my content keeps in the anchor; Undo reverses it in one step |
| 4.3 | Tap into the merged cell, tap Split | The merge splits back to individual cells |
| 4.4 | Long-press a cell, drag past the table edge mid-gesture | Range clamps at the table bounds; no wrap to other rows/columns |
| 4.5 | Insert a second table (toolbar +Table), tap its top-left cell, and Paste the range copied in 3.5/4.1 | The rectangle distributes one cell per tab stop; caret parks at the last written cell; one undo restores the pasted cells |
Fail criteria: ranges extending after lift-off; merge/undo splitting into many undo steps; paste redistributing shifted (quoting covered in section 6).
5. Scroll handoff to the parent ScrollYView — the open roadmap box
Code anchors: router PendingLongPress + move > 10 px before the
24-frame arm → PassToScroll and the drag is never claimed; in View
mode the area-hit match is skipped outright. Both workspaces embed the
editor in a ScrollYView (crdt_body for CRDT, body_scroll for
legacy DocWorkspace). Run BOTH editors through this section — the
legacy box on the roadmap names exactly this handoff.
| # | Action | Expected |
|---|---|---|
| 5.1 | CRDT workspace, View mode: drag up/down inside the document area | The page pans; no selection arms, no caret moves |
| 5.2 | CRDT workspace, Edit mode: quick vertical drag over text | Same: the gesture routes to the scroll view BEFORE long-press arms (10 px / 24 frames), so the page pans and no selection appears |
| 5.3 | Edit mode: long-press a word (selection arms), lift, then drag vertically | With no handle touched, a fresh drag still scrolls; the existing selection stays |
| 5.4 | Long-press a word, keep the finger down and drag WITHOUT lifting | Selection-adjust path: the selection follows the finger instead of scrolling (Selecting state owns the drag) |
| 5.5 | Legacy DocWorkspace: repeat 5.1–5.4 | Identical behavior (shared router + legacy hit-skip path) |
| 5.6 | Scroll to the document's end and keep dragging | Rubber-band/stop at content end per platform convention — no stuck gestures after release |
Fail criteria: a quick flick selects text instead of scrolling; a long-press drag scrolls the page while adjusting the selection; the scroll position jumps when the keyboard opens/closes. ANY failure here closes the roadmap box as FAILED — file it, don't check it.
6. Clipboard round-trips through the system clipboard
Code anchors: range/copy payloads via table_grid_tsv, quoting via
quote_tabular_field/split_tabular_payload (RFC-4180-style),
document-level payloads splice table grids at block position.
Prerequisite for 6.2: use an external app to prepare two forms of the
same content — the RAW two-line string x newline y, and the QUOTED
form "x newline y" (exactly what spreadsheet apps emit when copying
a single cell that contains a newline; a desktop text editor plus a
shared note is the easiest path).
| # | Action | Expected |
|---|---|---|
| 6.1 | Copy a 2x2 range from the doc table, paste into the external notes app | Rows/columns appear as tab/newline text, cells in reading order |
| 6.2 | Paste the RAW x\ny into a cell |
It distributes across TWO ROWS (one line per row — the spreadsheet convention for raw text). Then paste the QUOTED form "x\ny" into a cell: it lands as ONE cell holding both lines verbatim (the tokenizer honors quoted fields). Finally copy a range INCLUDING that multi-line cell → paste it elsewhere: the value round-trips as one two-line cell (our payload quoted it on copy) |
| 6.3 | Select-all in the document, Copy, paste into the notes app | Paragraph text lines with the table's grid spliced in at the table's position |
| 6.4 | Cut the full selection, verify document empties, undo once | Blocks AND every cell value restore (range tombstones + grouped cell writes) |
Fail criteria: tab/newline/quote characters mangled in either
direction (round-trip must be verbatim once the payload is ours);
external apps receiving nothing (the TextCopy hit must answer with
copyable_selection_text()).
7. Multi-line cell rendering (visual)
Code anchors: rows grow 18 px per extra display line over the 28 px
baseline (layout_projected_table); runs draw split at \n
vertically centered.
| # | Action | Expected |
|---|---|---|
| 7.1 | After 6.2, look at the cell holding x\ny |
Two stacked lines inside one taller row; borders outline the grown row; the document below reflows down |
| 7.2 | Tap the second line's text | The caret parks on the tapped line/char (not the first line) |
| 7.3 | ArrowDown/ArrowUp on a hardware or virtual keyboard inside that cell | Caret steps between the two lines keeping its column; inert at first/last line |
| 7.4 | Merge a grown row's cell with a plain row's cell; split it back | Anchor spans the summed heights; split restores both rows' geometries |
Fail criteria: lines clipped by the row bottom; caret drawn on the wrong band; the row below overlapping the grown row.
8. Visual painting/clipping sweep (draw-pass scope)
No automation exists — this is the GPU-bound residual. Sweep and eyeball; photograph failures.
| # | What to look at | Expected |
|---|---|---|
| 8.1 | Selection overlay on text and on cell ranges | Blue tint stays inside glyph/line bands and cell rects; no bleed into neighbors or across page margins |
| 8.2 | Table borders, including merged regions | 1 px grid outlines; merged anchor outlines the whole span; no double borders inside a merge |
| 8.3 | Caret | Blue 2 px bar on the correct band (single- and multi-line cells), never floating outside its cell |
| 8.4 | Selection handles after a long-press | Both handles at selection edges, above content, trigger drag on touch with the 6 px slop |
| 8.5 | Advanced placeholders (image/divider nodes) | Fills/borders/labels drawn once per node; divider is one centered line |
| 8.6 | Rapid typing for 30 seconds in a ~200-line document | Frame pacing stays smooth; no visible full-document flicker between keystrokes (layout cache: unchanged content redraws from the cached tree; this is the perf smoke check) |
9. Boot content and persistence round trip
Since the Android empty-doc fix, saves live in the platform app-data
store (app_data_dir()/nigig_build_store/generated/current.doc.json)
— NOT the source tree (dev checkouts get a one-way read fallback for
old saves; new writes never go there). Boot emits [DOC_TRACE] CRDT init: lines in logcat naming the branch that fired.
| # | Action | Expected |
|---|---|---|
| 9.0 | Fresh install, first launch (no save on device) | Demo document renders: bold title, styled paragraphs, divider, image placeholder, 4x3 table (bold header), closing hint. logcat: CRDT init: no saved document; seeding demo document |
| 9.1 | Edit, force-close, relaunch | Document restores (platform save/load path); table contents AND cell text intact. logcat: CRDT init: loading saved document (N bytes) |
| 9.2 | Open a previously saved file with a table | Grid renders; merges present; no phantom rows/cols |
| 9.3 | Boot the CRDT workspace with a CLASSIC-format save present | Demo document seeds (classic saves are not shadowed); logcat notes the classic-format branch. The legacy workspace still opens the classic file on Open. FAIL criteria: blank page, or the classic file silently dropped |
10. Sign-off
Device / OS / build: Galaxy A60 (SM-A6060, R28M52LJP2Y) · Android ·
pageflipnav release APK, driven by the doc_* tests in
crates/pageflipnav/tests/ui.rs. All 14 tests PASS on device (8
nav/state + 6 content-operation: doc_type_text_inserts_document,
doc_bold_italic_underline_ops, doc_insert_table_and_cell_text,
doc_merge_split_cell_ops, doc_long_press_empty_space_does_not_arm,
doc_diagonal_cell_range_merges, plus doc_diag_* used to pin geometry).
Verified devices: Galaxy A60 (SM-A6060, R28M52LJP2Y, 411 dp) and Galaxy
A16 (SM-A165F, RF8Y103NERA, 384 dp). One A60 full-suite run had 3
adb: device not found USB drops (test-infra flakes, not logic); all 3
reran clean on the A16.
Coverage legend: A = automated (ran green on device),
A⚠ = automated but only proves a subset, M = manual-only (cannot
be driven by makepad_test — reason in the Note column).
| # | Row | Result | Test / Note |
|---|---|---|---|
| 1.1 | Cold-launch tap → no keyboard | A | doc_view_mode_scroll_by_touch (View cold-release path); first tap never opens IME |
| 1.2 | Vertical drag → page scrolls | A | doc_view_mode_scroll_by_touch |
| 1.3 | Edit button relabels Done; tap opens IME | A | doc_interaction_mode_view_and_edit, doc_ime_text_input_in_edit_mode |
| 1.4 | Done closes keyboard | A⚠ | doc_interaction_mode_view_and_edit relabels back to Edit; keyboard visibility itself is not snapshot-observable, so "closes" is inferred from the Edit state + subsequent passive taps |
| 2.1 | Edit: tap mid-word, type | A | doc_type_text_inserts_document (stats `2 words |
| 2.2 | Tap in a cell, type | A | doc_insert_table_and_cell_text (cell "alpha beta" joins stats → `4 words |
| 2.3 | Accept autocorrect suggestion | M | makepad_test cannot press the OS keyboard's suggestion bar; needs a human tap |
| 2.4 | Type in empty cell, undo once | A⚠ | Undo not asserted on device; covered by doc-ui unit runtime_cell_backspace_edits_cell_and_ctrl_z_restores_it |
| 2.5 | Long word overflows cell | M | Visual single-line overflow check — note only, eyeball |
| 3.1 | Long-press word → word selects | A⚠ | doc_long_press_arms_selection proves the arm runs and the doc stays alive; handle/menu pixels are visual |
| 3.2 | Long-press with keyboard open | M | Cannot open/clamp the OS keyboard from the harness |
| 3.3 | Drag start/end handle | M | Handle hit-testing + native menu placement need visual confirmation |
| 3.4 | Long-press empty space → nothing arms | A | doc_long_press_empty_space_does_not_arm |
| 3.5 | Long-press cell → cell range arms | A | arming used by doc_merge_split_cell_ops (drag spans col1) |
| 3.6 | Menu re-floats after handle drag | M | Native menu is OS-owned, not a snapshot-able widget |
| 4.1 | Diagonal 2x2 range + highlight | A | doc_diagonal_cell_range_merges |
| 4.2 | Merge; undo in one step | A | doc_merge_split_cell_ops (merge); undo is unit-covered |
| 4.3 | Split merged cell | A | doc_merge_split_cell_ops (Split merged cell) |
| 4.4 | Drag past table edge clamps | A⚠ | Merge path proves the range stays in-col; out-of-table clamp is unit-covered |
| 4.5 | Paste range into second table | M | Requires system clipboard content + external app (section 6) |
| 5.1 | View drag → pans | A | doc_view_mode_scroll_by_touch |
| 5.2 | Edit quick drag → pans, no selection | A | doc_scroll_handoff_view_and_edit |
| 5.3 | Selection armed, lift, then drag scrolls | M | Requires a held selection + OS interactions not snapshot-able; unit/gesture-covered in doc-ui |
| 5.4 | Hold after long-press → selection tracks | M | Gesture requires frame-true finger sequencing only partially reproducible; covered by doc-ui runtime_* gesture tests |
| 5.5 | Legacy DocWorkspace 5.1–5.4 | M | Legacy workspace untested; blocked by in-progress user work (nigig-build, xls_import) |
| 5.6 | End-of-content rubber-band | M | Platform convention, visual |
| 6.1–6.4 | Clipboard round-trips | M | Needs adb clipboard + an external notes app to paste INTO — no harness API for system clipboard reads/veto |
| 7.1–7.4 | Multi-line cell rendering | M | Visual inspection of row growth / caret bands / overlap |
| 8.1–8.6 | Visual paint/clip sweep | M | GPU painting — eyeball, photograph failures |
| 9.0 | Fresh install demo doc | M | Needs app data wipe (reinstall) between launches; harness can't reset app-data |
| 9.1 | Force-close + relaunch restores | M | Harness cannot kill/relaunch the process to exercise the load path |
| 9.2 | Open saved file with table | M | Same process-restart limitation |
| 9.3 | Classic-format save fallback | M | Needs a pre-written classic save on the device + relaunch; unit-covered elsewhere |
Two real mobile-only bugs were found and fixed by these device runs
(unit-testable parts covered in doc-ui/src/tests.rs):
- The CRDT/legacy toolbars overflowed the ~411 dp phone screen,
clipping Italic and pushing Underline/Table/Merge/Split off-screen —
the toolbars now wrap (
flow: Right {wrap: true}indoc-ui/src/lib.rs). insert_tableproduced a zero-size, un-typeable table — it now seeds a 2x2 grid and parks the caret in the top-left cell (crdt_widget.rs, covered byruntime_insert_table_seeds_default_grid_and_parks_caret_in_first_cell).
Closing notes:
- The roadmap's ScrollYView handoff box (section 5) is NOT closed: most
of section 5 and the entire legacy
DocWorkspacecolumn remain manual (5.3, 5.4, 5.5, 5.6). - Rows still open: 2.3, 2.5, 3.1 (pixels), 3.2, 3.3, 3.5/3.6 (menu),
4.4 (pixels), 4.5, 5.3–5.6 (legacy + gestures), 6 (clipboard), 7
(visual), 8 (visual), 9 (process-restart). Most are genuinely not
automatable through
makepad_test; the reasons are in the table.