# 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_shift` is 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 /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= --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 `xy` (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 | 11 chars`) | | 2.2 | Tap in a cell, type | A | `doc_insert_table_and_cell_text` (cell "alpha beta" joins stats → `4 words | 21 chars`) | | 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`): 1. 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}` in `doc-ui/src/lib.rs`). 2. `insert_table` produced 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 by `runtime_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 `DocWorkspace` column 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.