Some checks failed
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
1584 lines
80 KiB
Markdown
1584 lines
80 KiB
Markdown
# Document editor module
|
|
|
|
This is a complete behavior-preserving migration of the supplied monolithic `doc/mod.rs`.
|
|
Copy this `doc/` folder to `crates/apps/nigig-build/src/construction_frame/pages/workspace/doc/`.
|
|
|
|
## Ownership
|
|
|
|
- **model**: document data types, formatting, cursor, selection
|
|
- **editing**: command and history boundary
|
|
- **layout**: page metrics, layout records, glyph-hit records
|
|
- **render**: shared Makepad rendering helpers
|
|
- **widgets**: the custom immediate-mode `DocEditor` and toolbar `DocWorkspace`
|
|
- **persistence**: generated-file document storage
|
|
|
|
The custom editor preserves direct Makepad immediate-mode layout/draw/event behavior from the original source. It uses direct live draw fields—not `DrawText::clone()`—which is compatible with current Makepad dev behavior.
|
|
|
|
|
|
## Stage 1: controller boundary
|
|
|
|
`DocEditor` now contains a `DocumentController`, rather than independent
|
|
`blocks`, cursor, selection, and undo/redo fields. The controller separates:
|
|
|
|
- `Document`: shared block content and metadata/revision
|
|
- `DocumentSession`: per-view cursor and selection state
|
|
- `SnapshotHistory`: transitional undo/redo storage
|
|
|
|
Host applications may inject or retrieve a controller via
|
|
`DocEditor::set_controller`, `DocEditor::controller`, and
|
|
`DocEditor::controller_mut`. Snapshot history remains intentionally in Stage 1;
|
|
Stage 2 replaces it with command transactions.
|
|
|
|
## Stage 2: command transactions
|
|
|
|
Undo and redo no longer store serialized document strings. `editing::History`
|
|
stores reversible `Transaction` values made from structured `Command`s. The
|
|
existing editor routines use `Command::RestoreDocument` as a compatibility
|
|
operation while preserving every v1 interaction. New controller APIs can add
|
|
granular commands such as `InsertText`, `DeleteBackward`, `SplitBlock`, and
|
|
style toggles. The next migration step routes each legacy routine directly to
|
|
those granular commands and coalesces typing into a transaction.
|
|
|
|
## Stage 3: persistent layout tree
|
|
|
|
`DocEditor` now owns a per-view `LayoutEngine`, not a standalone glyph-hit
|
|
vector. The engine exposes a renderer-independent `LayoutTree` with page,
|
|
line, run, glyph-hit, content-height, and document-revision fields. Layout
|
|
geometry is explicitly excluded from `Document`, so a shared document can have
|
|
multiple independent views/sessions. The existing immediate-mode traversal now
|
|
populates the persistent tree; Stage 4 moves its draw operations into the
|
|
renderer that consumes this tree.
|
|
|
|
## Stage 4: renderer boundary
|
|
|
|
`render::DocumentRenderer` now owns page, selection, caret, and styled-line
|
|
draw primitives. `DocEditor` provides document/session/layout state and live
|
|
Makepad draw resources; renderer code does not mutate editor state. Table,
|
|
image, and divider drawing remain in the legacy widget traversal for this
|
|
incremental stage and can now be moved one block renderer at a time without
|
|
changing document or layout APIs.
|
|
|
|
## Stage 5: expandable block and inline model
|
|
|
|
`model::advanced` introduces the v2 document schema without breaking the
|
|
currently compiling legacy widget. It supplies `DocumentNode`, `BlockKind`,
|
|
`Inline`, nested table-cell documents, lists, quotes, code, columns,
|
|
containers, images, canvas objects, embedded-widget descriptors, audio, video,
|
|
and diagrams. `Document::nodes` holds these future-ready nodes alongside the
|
|
legacy `blocks` compatibility collection. Stage 6 connects these nodes to the
|
|
layout and renderer through a block registry.
|
|
|
|
## Stage 6: advanced-block layout and embedded-widget hosting
|
|
|
|
The layout tree now includes `advanced_blocks`, generated from v2
|
|
`Document::nodes` by `AdvancedLayout`. `DocumentRenderer` draws document-space
|
|
GPU hosts for rich nodes. `widgets::EmbeddedWidgetRegistry` lets applications
|
|
register serializable embedded-widget type descriptors without placing live
|
|
Makepad widget instances inside the document model.
|
|
|
|
## Stage 7: collaboration and plugins
|
|
|
|
`collaboration::CollaborationSession` queues local `DocumentOperation`s and
|
|
deduplicates remote operations by actor/sequence identity. It also owns
|
|
non-persistent remote presence (cursor/selection) state. No network protocol is
|
|
forced: applications can bridge the queues through WebSocket, WebRTC, files, or
|
|
custom synchronization.
|
|
|
|
`plugins::PluginRegistry` supplies serializable block-plugin descriptors for
|
|
future custom block/layout/render factories. Stage 7 deliberately establishes
|
|
the transport and registry boundaries; conflict transformation/CRDT resolution
|
|
is the next collaboration implementation layer.
|
|
|
|
## Input stabilization: mobile IME and selection
|
|
|
|
This package fixes two current interaction blockers before further milestones:
|
|
|
|
- The editor reasserts `show_text_ime` from `draw_walk` after the focused draw
|
|
area exists, which supports mobile backends that drop a `FingerDown`-time IME
|
|
request.
|
|
- Drag selection is explicitly captured by `pointer_selecting` and selection
|
|
rendering compares document cursor ranges instead of requiring exact glyph-hit
|
|
endpoint matches. End-of-span/end-of-line drag selections now highlight.
|
|
|
|
## Input diagnostics
|
|
|
|
This build emits `[DOC_TRACE]` diagnostics to standard error for document load,
|
|
pointer-down/move/up, glyph hit testing, key events, `TextInput`, insertion, and
|
|
IME activation. On desktop, run the app from its terminal. On Android/iOS,
|
|
capture the native process/device logs and search for `DOC_TRACE`.
|
|
|
|
## Render/input correction
|
|
|
|
The trace demonstrated that typing reaches `Hit::TextInput` and increments the
|
|
cursor. The apparent blank document came from the transparent full-editor input
|
|
area being drawn at depth `0.9`, above text depth `0.2`; it could depth-occlude
|
|
page text despite its tiny alpha. It now draws at depth `-1.0`. The IME path
|
|
also falls back from a zero `clipped_rect` to the valid area rectangle.
|
|
|
|
## Selection-handle correction
|
|
|
|
A non-handle tap in mobile View mode now clears the prior selection immediately,
|
|
so handles disappear. Handle hit tests are now performed in the same window
|
|
coordinate space as `FingerDown` rather than relying on implicit local-space
|
|
conversion. `[DOC_TRACE] handle hit` and `handle moved` logs diagnose handle
|
|
capture and endpoint updates.
|
|
|
|
|
|
## Compact-screen default
|
|
|
|
At the first draw, a known screen width below `700.0` initializes `InteractionMode::View` before any touch event. This prevents the initial mobile tap from focusing the editor or opening the IME.
|
|
|
|
## Mobile viewport gesture routing
|
|
|
|
In compact View mode, `DocEditor` no longer calls `event.hits` on the full page.
|
|
`TouchUpdate` handles only long press and visible selection handles, while normal
|
|
touch movement is left unclaimed so the surrounding `ScrollYView` can pan.
|
|
A short tap moves the passive cursor and dismisses selection without focusing the
|
|
editor or showing the keyboard.
|
|
|
|
## Granular command migration: text insertion
|
|
|
|
`Command::InsertText` and its exact `Command::DeleteText` inverse now mutate
|
|
`Document` through `DocumentController::execute`. Keyboard/IME insertion no
|
|
longer uses a legacy serialized snapshot. Undo/redo applies inverse commands.
|
|
The remaining legacy operations (delete range, split/merge, style and blocks)
|
|
are intentionally left on snapshot compatibility until each has a complete
|
|
inverse implementation.
|
|
|
|
## Granular command migration: replacement and backspace
|
|
|
|
Typing over a same-span selection now runs a single transaction containing
|
|
`DeleteText` followed by `InsertText`. Backspace deletes either the selected
|
|
same-span range or the preceding Unicode character through `Command::DeleteText`.
|
|
Undo restores both operations as one transaction.
|
|
|
|
## Granular command migration: style ranges
|
|
|
|
Bold, italic and underline selections now create `Command::ReplaceSpans` entries
|
|
for every affected paragraph/heading block. The command swaps exact span vectors
|
|
and returns the prior vectors as its inverse, so formatting undo/redo uses the
|
|
same command transaction history as typed text.
|
|
|
|
## Granular command migration: alignment
|
|
|
|
Left, center, and right toolbar controls now execute `Command::SetAlignment`.
|
|
The command swaps the exact previous alignment as its inverse, making alignment
|
|
changes participate in command undo/redo without legacy snapshots.
|
|
|
|
## Granular command migration: block insertion
|
|
|
|
Table, image, and divider toolbar insertion now use `Command::InsertBlock`.
|
|
Its inverse is `Command::RemoveBlock`, so insertions undo and redo without a
|
|
legacy snapshot.
|
|
|
|
## Granular command migration: paragraph split
|
|
|
|
Return in a standard paragraph/heading now creates a `ReplaceBlockRange`
|
|
transaction replacing one block with two paragraph blocks. Its inverse restores
|
|
the exact prior block. Return inside a table remains on the current table-row
|
|
compatibility path.
|
|
|
|
## Granular command migration: paragraph merge
|
|
|
|
Backspace at the start of a standard text block now executes `ReplaceBlockRange`
|
|
to replace the previous/current pair with one merged paragraph. Undo restores
|
|
the exact two original blocks and cursor position is moved to the merge boundary.
|
|
|
|
## Granular command migration: table append row
|
|
|
|
Return while cursor is inside a table now replaces the table block through
|
|
`ReplaceBlockRange` with a copy containing one appended empty row. Undo restores
|
|
the original table block exactly.
|
|
|
|
## Granular command migration: table-cell replacement
|
|
|
|
IME/keyboard text insertion into a table cell now uses `ReplaceTableCell`.
|
|
The command captures the complete previous cell text as its inverse, so typing
|
|
in tables now participates in command undo/redo.
|
|
|
|
## Command history: typing coalescing
|
|
|
|
Contiguous `InsertText` commands in the same block/span now append inverse
|
|
`DeleteText` commands to the prior history transaction. Typing a word becomes
|
|
one Undo action; cursor movement, deletion, selection replacement, formatting,
|
|
or any other command breaks the group.
|
|
|
|
## Granular command migration: non-text block deletion
|
|
|
|
Delete while the cursor is on a table, image, or divider now executes
|
|
`Command::RemoveBlock`. Its inverse is `InsertBlock`, so non-text block removal
|
|
is command-driven and undoable.
|
|
|
|
## Granular command migration: advanced nodes
|
|
|
|
`InsertNode`, `DeleteNode`, and `ReplaceNode` now mutate `Document::nodes` and
|
|
return exact inverse commands. Advanced document nodes are ready for command
|
|
history and future renderer/editor actions.
|
|
|
|
## Collaboration: remote command application
|
|
|
|
`DocumentController::apply_remote_operation` now accepts deduplicated remote
|
|
`DocumentOperation`s and applies their forward commands without adding them to
|
|
local undo history. Conflict transformation/CRDT ordering remains the next
|
|
collaboration layer.
|
|
|
|
## CRDT identity foundation
|
|
|
|
`model::crdt` introduces `AtomId`, Lamport clocks, tombstoned `TextAtom`s,
|
|
RGA-style `RgaText`, `BlockId`, and `CrdtMetadata`. No legacy vector-index
|
|
commands are replaced yet; this package establishes deterministic IDs and text
|
|
sequence semantics required for the next migration.
|
|
|
|
## CRDT local insert bridge
|
|
|
|
Normal unselected local text insertion now lazily seeds the current legacy span
|
|
into `RgaText`, creates stable local `TextAtom`s, and executes `InsertAtoms`.
|
|
Selected replacement and table paths remain on their existing command bridge
|
|
until CRDT range/tombstone selection operations are added.
|
|
|
|
## CRDT selection identity
|
|
|
|
`DocumentController::sync_crdt_selection_from_legacy` maps the legacy visual
|
|
selection anchor/focus into stable `CrdtTextPosition` values. CRDT-backed
|
|
selection ranges can now be transmitted without relying solely on character
|
|
offsets.
|
|
|
|
|
|
## Command and engine roadmap
|
|
|
|
- [x] Text insert/delete and selection replacement
|
|
- [x] Paragraph split and merge
|
|
- [x] Style ranges and alignment
|
|
- [x] Table cell/row commands
|
|
- [x] Block and image commands
|
|
- [x] Typing coalescing
|
|
- [x] Internal copy/cut/paste keyboard commands
|
|
- [x] Native system clipboard copy/cut integration
|
|
- [x] Native system clipboard paste via platform TextInput fallback
|
|
- [x] Table column and cell merge commands (CRDT-native: Shift+Arrow cell range + Ctrl/Cmd+M merge, Ctrl/Cmd+Shift+M split in `CrdtDocEditor`; see "CRDT-native cell range selection and merge/split")
|
|
- [x] Incremental layout invalidation tracking
|
|
- [x] Incremental block fragment cache foundation
|
|
- [x] Block fragment cache population during layout traversal
|
|
- [x] Block fragment cache population during layout traversal
|
|
- [x] Cached text measurement reuse
|
|
- [x] Incremental page/block reflow execution — retired for the legacy
|
|
fallback editor; the active CRDT-native path rebuilds only on
|
|
document change through the version-keyed layout cache
|
|
(`CrdtDocEditor::layout_tree`), see "Legacy perf boxes: retirement
|
|
decision and the CRDT-native layout cache"
|
|
- [x] Renderer extraction: divider block
|
|
- [x] Renderer extraction: image placeholder block
|
|
- [x] Renderer extraction: table cell primitive
|
|
- [x] Renderer extraction: advanced block placeholders
|
|
- [x] Mobile gesture arbitration state machine + tests
|
|
- [x] Mobile gesture router wired into DocEditor touch handling
|
|
- [ ] ScrollYView parent handoff verification on Android/iOS — hardware
|
|
execution pending; the full step matrix with pass/fail criteria
|
|
lives in `DEVICE_VERIFICATION.md` (section 5)
|
|
- [x] AdaptiveView mobile Edit/Done toolbar control
|
|
- [x] CRDT atom/block identities and local text edits
|
|
- [x] CRDT remote buffering, presence, and tombstone frontier foundation
|
|
- [x] Tombstone deletion timestamps and CRDT atom persistence
|
|
- [x] CRDT table stable-ID model foundation
|
|
- [x] CRDT advanced-block stable-ID model foundation
|
|
- [x] Peer synchronization transport boundary + MemoryTransport
|
|
- [x] Visual remote selections/cursors
|
|
|
|
## CRDT tombstone timestamps
|
|
|
|
- [x] Tombstone deletion timestamps
|
|
|
|
Each deleted atom records `deleted_at: Option<LamportTimestamp>`. Garbage
|
|
collection now compacts only atoms whose deletion timestamp is behind the
|
|
collaboration-safe acknowledged frontier.
|
|
|
|
|
|
## Test coverage
|
|
|
|
- [x] Unit tests: reversible text commands
|
|
- [x] Unit tests: controller undo/redo
|
|
- [x] Unit tests: deterministic RGA tombstones
|
|
- [x] Unit tests: table row commands
|
|
- [x] Unit tests: block ID insert/remove invariants
|
|
- [x] Integration tests: causal remote-operation buffering
|
|
- [x] Integration tests: reversible table cell merge
|
|
- [x] Integration tests: memory transport operation loop
|
|
- [x] Unit tests: rectangular table selection
|
|
- [x] Unit tests: advanced block layout
|
|
- [x] Unit tests: typing coalescing
|
|
- [x] Unit tests: tombstone compaction frontier
|
|
- [x] Unit tests: layout invalidation tracking
|
|
- [x] Unit tests: table column command inverse
|
|
- [x] Unit tests: table merge/split inverse
|
|
- [x] Unit tests: CRDT selected-range replacement inverse
|
|
- [x] Unit tests: mobile selection-handle gesture routing
|
|
- [x] Unit tests: advanced JSON canvas round trip
|
|
- [x] Unit tests: advanced JSON recursive table round trip
|
|
- [x] Unit tests: advanced JSON inline link/widget round trip
|
|
- [x] Unit tests: block layout cache invalidation
|
|
- [x] Unit tests: cached text measurement reuse
|
|
- [x] Unit tests: CRDT-native projection table layout, merges and hit testing
|
|
- [x] Unit tests: CRDT-native advanced node layout and unified order
|
|
- [x] Unit tests: CRDT-native word atom ranges and selection-handle geometry
|
|
- [x] Integration tests: widget input, selection, and mobile gestures
|
|
(real-`Cx` runtime harness plus the draw-free `Area::Rect` stub)
|
|
- [x] Integration tests: renderer draw pass — resolved as documented:
|
|
layout/hit/draw-order LOGIC is covered by the draw-free runtime
|
|
harness (real-`Cx` plus `Area::Rect` stubs); painting/clipping
|
|
visual verification stays GPU/Studio-bound and lands with the
|
|
device-verification batch (see "Legacy perf boxes: retirement
|
|
decision and the CRDT-native layout cache"
|
|
|
|
|
|
## Advanced JSON V3 persistence
|
|
|
|
- [x] Versioned JSON DTOs for code, media, canvas, diagrams, and embedded widgets
|
|
- [x] Loss-prevention errors for unsupported advanced node kinds
|
|
- [x] JSON deserialization into runtime advanced nodes
|
|
- [x] Persist paragraph/heading and core inline advanced nodes
|
|
- [x] Persist advanced inline image/link/widget nodes
|
|
- [x] Persist list/quote/columns/container recursive advanced nodes
|
|
- [x] Persist advanced recursive table model
|
|
- [x] Persist advanced image resources and captions
|
|
|
|
|
|
## Layout extraction
|
|
|
|
- [x] Pure `layout_paragraph` block-layout function
|
|
- [x] Paragraph layout unit test
|
|
- [x] Paragraph alignment layout test
|
|
- [x] Renderer support for ParagraphFragment
|
|
- [x] Wire DocEditor paragraph rendering to LayoutEngine fragments
|
|
|
|
- [x] Pure `layout_table` block-layout function
|
|
- [x] Table layout unit test
|
|
- [x] Wire DocEditor base table geometry to LayoutEngine fragments
|
|
|
|
- [x] Pure `layout_image` block-layout function
|
|
- [x] Image layout unit test
|
|
- [x] Wire DocEditor image geometry to LayoutEngine fragments
|
|
|
|
- [x] Pure `layout_divider` block-layout function
|
|
- [x] Divider layout unit test
|
|
- [x] Wire DocEditor divider geometry to LayoutEngine fragments
|
|
|
|
- [x] Pure `layout_pages` page-stack function
|
|
- [x] Page layout unit test
|
|
- [x] Wire DocEditor page stack to LayoutEngine page fragments
|
|
|
|
- [x] Populate LayoutTree line/run records from paragraph fragments
|
|
- [x] Encapsulate paragraph fragment → LayoutTree transfer
|
|
|
|
- [x] Populate LayoutTree table-cell records from table fragments
|
|
|
|
- [x] Populate LayoutTree image records from image layout fragments
|
|
|
|
- [x] Populate LayoutTree divider records from divider layout fragments
|
|
|
|
- [x] LayoutTree caret geometry API
|
|
- [x] Unit tests: LayoutTree caret geometry
|
|
- [x] Unit tests: table-cell caret geometry
|
|
|
|
- [x] LayoutTree selection geometry API
|
|
- [x] Unit tests: LayoutTree selection geometry
|
|
|
|
- [x] LayoutTree nearest-hit API
|
|
- [x] Unit tests: LayoutTree nearest-hit lookup
|
|
|
|
- [x] Wire DocEditor fallback hit testing to LayoutTree nearest-hit API
|
|
|
|
- [x] Table render-cell fragment builder
|
|
- [x] Table render-cell merge geometry test
|
|
- [x] DocumentRenderer table-fragment draw API
|
|
- [x] Wire DocEditor table drawing to TableRenderCell fragments
|
|
|
|
- [x] Renderer extraction: page background/shadow primitive
|
|
|
|
- [x] Renderer extraction: remote presence primitives
|
|
|
|
## Renderer extraction completion
|
|
|
|
- [x] Page background/shadow
|
|
- [x] Paragraph fragments
|
|
- [x] Table fragments and merged cells
|
|
- [x] Image placeholders
|
|
- [x] Dividers
|
|
- [x] Advanced block placeholders
|
|
- [x] Selection/caret/remote presence primitives
|
|
- [x] Remove remaining legacy caret calculations from DocEditor
|
|
|
|
- [x] LayoutTree caret support for empty paragraph spans and table cells
|
|
|
|
- [x] Block cache stores typed layout fragment payloads
|
|
- [x] Reuse cached fragment payloads during draw traversal — retired
|
|
for the legacy fallback editor; the CRDT-native draw walk reuses
|
|
the cached layout tree's glyph/rect payloads directly
|
|
(see "Legacy perf boxes: retirement decision and the CRDT-native
|
|
layout cache"
|
|
|
|
- [x] Per-block document revision foundation
|
|
- [x] Wire commands to touch only affected block revisions — retired
|
|
for the legacy fallback editor; CRDT change detection keys on the
|
|
op version-vector sum instead of per-command revision bumps
|
|
(see "Legacy perf boxes: retirement decision and the CRDT-native
|
|
layout cache"
|
|
|
|
- [x] Replace DocEditor blanket layout invalidation with cursor-block invalidation
|
|
- [x] Command-range-aware invalidation handoff via DocumentController
|
|
|
|
- [x] ParagraphFragment matching-revision cache reuse
|
|
|
|
- [x] TableFragment matching-revision cache reuse
|
|
|
|
- [x] Image/divider matching-revision cache reuse
|
|
|
|
- [x] Unit tests: matching-revision block cache payload reuse
|
|
|
|
- [x] Cache geometry/origin validation
|
|
- [x] Unit tests: cache geometry validation
|
|
|
|
- [x] Cache height-change detection and following-fragment invalidation
|
|
- [x] Unit tests: height-change fragment invalidation
|
|
|
|
- [x] Page layout cache foundation
|
|
- [x] Unit tests: page cache invalidation
|
|
- [x] Populate page cache from page stack traversal
|
|
- [x] Reuse cached page fragments during page stack traversal
|
|
|
|
## Incremental reflow checkpoint
|
|
|
|
- [x] Per-block revisions
|
|
- [x] Command-aware block invalidation
|
|
- [x] Typed fragment cache
|
|
- [x] Paragraph/table/image/divider fragment reuse
|
|
- [x] Height-change following-fragment invalidation
|
|
- [x] Page cache population/reuse
|
|
- [x] Page/block incremental reflow execution foundation
|
|
- [x] Long-document paragraph layout benchmark harness
|
|
|
|
- [x] Collaboration acknowledgement message protocol
|
|
|
|
- [x] Unit tests: collaboration acknowledgement transport
|
|
|
|
- [x] Synchronization-triggered acknowledged tombstone compaction
|
|
|
|
- [x] Unit tests: acknowledgement safe frontier
|
|
|
|
## CRDT engine bridge
|
|
|
|
- [x] Temporary CRDT projection bridge source
|
|
- [x] Add `doc-engine` Cargo dependency to nigig-build
|
|
- [x] Wire CrdtProjectionBridge into DocEditor controller
|
|
|
|
- [x] DocEditor CRDT projection bridge installation API
|
|
- [x] DocEditor CRDT projection refresh API
|
|
- [x] DocEditor CRDT operation dispatch API
|
|
- [x] Route unselected keyboard/IME text insertion through CRDT engine
|
|
- [x] Route unselected Backspace through CRDT engine
|
|
- [x] Route unselected Delete through CRDT engine
|
|
- [x] Migrate selected replacement, formatting, table, and toolbar actions to CRDT operations
|
|
|
|
- [x] Bridge projected CRDT table merge metadata into legacy renderer
|
|
|
|
- [x] Route table merge/split shortcuts to CRDT operations
|
|
|
|
- [x] Bridge CRDT projected advanced nodes into legacy advanced layout
|
|
|
|
- [x] Remove legacy end-of-document advanced-node rendering pass
|
|
|
|
- [x] Render inline AdvancedNodeRef through AdvancedLayout/DocumentRenderer
|
|
|
|
- [x] Preserve CRDT table merge block indexes under unified projection order
|
|
|
|
- [x] Route image/divider insertion toolbar actions through CRDT InsertNode
|
|
|
|
- [x] Route inline advanced node deletion through CRDT DeleteNode
|
|
|
|
- [x] Route table insertion toolbar action through CRDT InsertBlock table
|
|
|
|
- [x] Save/Open CRDT operation log when CRDT engine is active
|
|
- [x] Legacy delimiter save fallback
|
|
|
|
- [x] Automatic legacy-to-CRDT migration on first text edit
|
|
|
|
- [x] Preserve bold/italic/underline StyleSpan formatting during CRDT migration
|
|
- [x] Preserve legacy font size/color during CRDT migration
|
|
|
|
- [x] Route alignment toolbar actions through CRDT SetBlockAlignment
|
|
|
|
- [x] Route Undo/Redo through CRDT controller when active
|
|
|
|
- [x] Unit tests: CRDT projection bridge text/style materialization
|
|
|
|
- [x] Unit tests: CRDT projection bridge table materialization
|
|
|
|
- [x] Unit tests: CRDT projection bridge inline advanced node references
|
|
|
|
- [x] Unit tests: CRDT bridge unified block/node order
|
|
|
|
## CRDT-native widget rewrite
|
|
|
|
- [x] CrdtDocEditor skeleton
|
|
- [x] ProjectionSession skeleton
|
|
- [x] ProjectionLayoutTree skeleton
|
|
- [x] ProjectionRenderer skeleton
|
|
- [x] CRDT-native projected styled text rendering
|
|
- [x] CRDT-native basic text input interaction
|
|
- [x] CRDT-native basic selection highlight
|
|
- [x] CRDT-native basic pointer drag selection
|
|
- [x] CRDT-native mobile long-press/handle selection
|
|
- [x] CRDT-native table rendering
|
|
- [x] CRDT-native advanced node rendering
|
|
- [x] CRDT-native keyboard editing
|
|
- [x] CRDT-native in-cell table editing
|
|
- [x] Switch workspace DSL to CrdtDocEditor
|
|
|
|
- [x] CrdtDocEditor engine installation API
|
|
|
|
- [x] CrdtDocEditor default CRDT paragraph initialization
|
|
|
|
- [x] CrdtDocEditor AtomId cursor anchor updates on input
|
|
- [x] Projection atom glyph layout and pointer hit testing
|
|
- [x] CRDT-native caret rendering
|
|
|
|
## CRDT-native vertical slice
|
|
|
|
- [x] CRDT paragraph creation
|
|
- [x] Atom text insertion
|
|
- [x] Projection layout glyph hit
|
|
- [x] Insert after hit atom
|
|
- [x] CRDT undo
|
|
- [x] CRDT JSON save/load
|
|
- [x] CrdtDocEditor widget runtime integration test
|
|
|
|
- [x] Separate CrdtDocWorkspace runtime-test view
|
|
- [x] Wire application navigation switch to CrdtDocWorkspace
|
|
|
|
## CRDT-native table rendering
|
|
|
|
`CrdtDocEditor` now renders projected tables without passing through the
|
|
legacy bridge. `layout_projection` emits a `ProjectedTableLayout` for every
|
|
projection block of kind `table`:
|
|
|
|
- Fixed 160x28 cell geometry (matching the legacy bridge column width, so
|
|
both paths render the same table) with per-cell rects and text copied
|
|
from the projected `ProjectedTable` cell map.
|
|
- Table merges resolve their stable row/column ids to spans; the anchor
|
|
cell rect expands over the merged range and covered cells are flagged
|
|
`covered` with cleared text, so renderers skip them.
|
|
- Blocks after a table are placed below the table plus an 8px gap via
|
|
`ProjectionLayoutTree::block_origins`, which the text renderer now
|
|
consumes instead of re-deriving line positions.
|
|
- `ProjectionLayoutTree::table_hit_test` maps a layout-space point to the
|
|
visible (merge-anchor) cell under it, ready for cell-targeted editing.
|
|
|
|
`ProjectionRenderer::draw_table_projection` draws the cell grid as 1px
|
|
borders through a new `draw_table_border` live field on `CrdtDocEditor`
|
|
(default `#x9aa0a6`) and centers cell text vertically. Layout-space
|
|
coordinates stay shared between glyphs, block origins and table geometry;
|
|
the renderer maps them into widget space with one offset. Editing table
|
|
content CRDT-natively (cell cursor, in-cell text input) is the next layer
|
|
on top of this geometry.
|
|
|
|
## CRDT-native advanced node rendering
|
|
|
|
`CrdtDocEditor` now renders projected advanced nodes (images, dividers,
|
|
canvas, audio/video/diagram and embedded-widget placeholders) natively
|
|
from the projection:
|
|
|
|
- `layout_projection` walks the unified `DocumentProjection::order`, so
|
|
advanced nodes interleave with paragraphs, headings and tables at their
|
|
exact anchor positions instead of rendering as one trailing strip.
|
|
- `projected_node_metrics` mirrors the legacy `AdvancedLayout` heights,
|
|
labels and interactivity flags per kind (image 220, canvas 240, divider
|
|
18, unknown kinds map to `Widget: <kind>` exactly like the bridge's
|
|
`EmbeddedWidget` fallback), keeping both views visually identical.
|
|
- `ProjectionRenderer::draw_node_projection` reuses the legacy
|
|
`draw_advanced_blocks` colors (interactive/non-interactive fill tint,
|
|
top/bottom borders, kind label); dividers collapse to a centered 1px
|
|
line. Two new live fields, `draw_node_fill` and `draw_node_border`, hold
|
|
the script defaults.
|
|
- `ProjectionLayoutTree::node_hit_test` resolves points to node indexes
|
|
for future node selection and context menus. Container-nested nodes are
|
|
intentionally not top-level render items yet.
|
|
|
|
Engine fix uncovered by this work: the doc-engine after-chain was
|
|
block-only, so any paragraph anchored after an advanced node was
|
|
unreachable during materialization and silently vanished from the
|
|
projection (legacy bridge included). Nodes now participate in the chain
|
|
as connectors, converging `order` and `blocks`; regression tests cover
|
|
node-anchored blocks and mixed block/node sibling ordering in
|
|
`doc-engine/tests/materialize.rs`.
|
|
|
|
## CRDT-native mobile long-press and selection handles
|
|
|
|
`CrdtDocEditor` now shares `MobileGestureRouter` with the legacy
|
|
`DocEditor` and drives the compact-touch selection flow:
|
|
|
|
- `TouchUpdate` Start begins the router with a handle hit test
|
|
(`Option<bool>`: start/end) when a selection is visible, or arms
|
|
long-press detection on the frame clock otherwise.
|
|
- The router's `SelectWord` action maps the press point through the glyph
|
|
hit test into `word_atom_range`, which mirrors the legacy `word_bounds`
|
|
whitespace-pivot semantics and returns the word's first/last atoms as
|
|
the selection.
|
|
- `selection_handles` normalizes anchor/focus into document order (so a
|
|
backwards drag keeps start on the left edge) and yields 12px handle
|
|
rects with a 6px touch-slop hit test, drawn through a new
|
|
`draw_selection_handle` live field (default `#x1f73e6`). Dragging a
|
|
handle moves the corresponding selection endpoint to the hit atom.
|
|
- A short tap ends as `MovePassiveCursor`: passive caret placement, no
|
|
IME, prior selection dismissed. Drags past the slop threshold stay
|
|
unclaimed for a parent ScrollYView. Once a real touch sequence arrives,
|
|
the widget ignores synthesized finger events so tap/drag run through
|
|
the router path only; desktop mouse/IME behavior is unchanged.
|
|
- Physical ScrollYView handoff verification on Android/iOS remains device
|
|
work. (The Edit/Done-mode toggle that opens the IME on mobile arrived
|
|
later — see "Mobile Edit/Done interaction mode".)
|
|
|
|
## CRDT-native keyboard editing
|
|
|
|
`CrdtDocEditor` now handles the full desktop keyboard surface natively
|
|
through doc-engine operations, completing input parity with the legacy
|
|
`DocEditor` ahead of the workspace DSL switch:
|
|
|
|
- `handle_key_down` routes Command/Ctrl + `Z` (undo) and `Shift` + `Z`
|
|
(redo) through `CrdtHistory`, then sanitizes the cursor back onto a
|
|
live projection atom. Command/Ctrl + `B`/`I`/`U` toggle bold, italic
|
|
and underline over the selection's atom range via
|
|
`toggle_selection_style`. Arrow keys move (or with Shift extend) the
|
|
selection edge one glyph at a time across block boundaries using the
|
|
projection-wide `step_glyph` stream.
|
|
- `Backspace`/`Delete` first delete a non-empty selection as one atom
|
|
batch; at block boundaries they merge whole blocks.
|
|
- `Return` opens a table-row append when the caret is in a table block
|
|
(matching the legacy table compatibility path) and otherwise splits
|
|
the paragraph through the new engine op.
|
|
- Engine additions: `Operation::SplitBlock { block, offset }` and
|
|
`Operation::MergeBlocks { block }` with symmetric
|
|
`Compensation::{SplitBlock, MergeBlocks}` so undo/redo replay both
|
|
directions. Materialization keeps a split parent's trailing runs as
|
|
a synthetic trailing child spliced immediately after the parent;
|
|
style runs crossing the split boundary clone their crossing span
|
|
into both halves. `merge_runs` coalesces adjacent runs with equal
|
|
bold/italic/underline/font_size/color so the merged text stays
|
|
minimal.
|
|
- `TextInput` now deletes any active selection before inserting at the
|
|
session caret block (previously it always inserted into the first
|
|
block), mirroring the legacy replace-selection-on-type behavior.
|
|
|
|
Known limits, matching the split-session semantics in the doc-engine
|
|
README: a `DeleteBlock` of a split parent orphans the trailing half
|
|
(consistent with the pre-existing chain-break-on-delete semantic), and
|
|
the caret anchors on a glyph's left edge in layout space while its
|
|
semantic position is "after" that atom.
|
|
|
|
## CRDT workspace DSL switch
|
|
|
|
The active workspace DSL now instantiates the CRDT-native editor: both the
|
|
desktop dock (`docs_workspace`) and the mobile page (`m_doc_content`) in
|
|
`pages/workspace/project/mod.rs` create `mod.widgets.CrdtDocWorkspace`
|
|
instead of the legacy `DocWorkspace`. The classic widget remains fully
|
|
registered and functional as a fallback (`mod.widgets.DocWorkspace`, and
|
|
the legacy `DocEditor` still bridges CRDT projections for its own
|
|
migration path), so the switch is a DSL choice rather than a deletion.
|
|
|
|
To make that switch lossless, `CrdtDocWorkspace` graduated from the
|
|
runtime-test shell to the full workspace surface, mirroring the legacy
|
|
toolbar with CRDT engine routing:
|
|
|
|
- Open/Save/SaveAs serialize the engine document onto the shared
|
|
`#MP_CRDT_V1` wire (`projection_session::crdt_save_wire` /
|
|
`crdt_engine_from_saved`), the same format the legacy editor writes,
|
|
so documents move between both editors losslessly. Legacy
|
|
delimiter-format saves are detected and reported with a status message
|
|
instead of being silently dropped; their migration to CRDT stays with
|
|
the classic editor on first edit.
|
|
- Undo/Redo and bold/italic/underline map to the editor's public
|
|
`undo`/`redo`/`toggle_inline_style` (shared with the Ctrl/Cmd keyboard
|
|
paths), alignment buttons route `Left`/`Center`/`Right` — the same
|
|
`DocAlign` debug strings the legacy toolbar puts on the wire — through
|
|
`set_block_alignment`, and `+Table`/`+Img`/`+Div` call the new
|
|
`insert_table`/`insert_image`/`insert_divider` helpers with the legacy
|
|
anchored-after-caret semantics and default image caption.
|
|
- The stats label counts words/chars from the projection via
|
|
`projected_stats` (text blocks plus merged-once table cells; advanced
|
|
nodes contribute nothing), refreshed on every handled toolbar action
|
|
exactly like the legacy toolbar.
|
|
|
|
Deliberate gaps at switch time, both closed by later milestones: the
|
|
mobile Edit/Done IME toggle (see "Mobile Edit/Done interaction mode")
|
|
and in-cell table editing (see "CRDT-native in-cell table editing").
|
|
|
|
## Application navigation switch to CrdtDocWorkspace
|
|
|
|
With the DSL switch complete, the temporary "Docs CRDT" runtime-test tab
|
|
was an exact duplicate of the real "Docs" tab, so navigation has been
|
|
consolidated onto a single CRDT destination:
|
|
|
|
- The desktop dock's `workspace_tabs` keeps one "Docs" tab
|
|
(`docs_content` containing `CrdtDocWorkspace`); the `crdt_docs_tab`
|
|
definition, its `crdt_docs_content` view, the sidebar's
|
|
"Documents CRDT Test" button and its `select_tab` handler are removed.
|
|
"Documents" in the sidebar and the dock tab bar both land on the CRDT
|
|
editor.
|
|
- Mobile's workspace drawer resolves "Documents" to `doc_page` (whose
|
|
`m_doc_content` is `CrdtDocWorkspace`). That mapping is now the pure
|
|
function `workspace_page_id` in `pages/workspace/project/mod.rs`,
|
|
pinned by unit tests that assert the Documents destination, the
|
|
label-to-page table, page distinctness, and the CAD fallback for
|
|
unknown labels.
|
|
|
|
The legacy fallback posture is unchanged: `mod.widgets.DocWorkspace`
|
|
remains registered, so reverting any navigation node to the classic
|
|
editor is again a one-line DSL change. The standalone runtime-test view
|
|
roadmap item stays checked historically — it served as the pre-switch
|
|
verification surface and was removed only after becoming a duplicate.
|
|
|
|
## CrdtDocEditor runtime integration tests
|
|
|
|
`tests.rs` now runs the real editor widget inside a real `Cx` runtime —
|
|
no mocks of the event surface:
|
|
|
|
- The widget is constructed through the same `ScriptNew::script_new`
|
|
factory the production widget registry calls (a bare `ScriptVm` with
|
|
unit host/std suffices, per makepad's own script test pattern), and
|
|
the engine is installed through the public `set_engine` API.
|
|
- Real `Event::KeyDown` values with real `KeyEvent`/`KeyModifiers`
|
|
payloads enter through `Widget::handle_event` — identical dispatch to
|
|
a running app. Covered end-to-end: caret anchoring from an uncursored
|
|
editor, arrow stepping, Shift+ArrowLeft selection extension, Ctrl+B
|
|
bolding the full selection (projection runs asserted), Enter splitting
|
|
at the caret with the caret following the trailing half, Ctrl+Z
|
|
merging the split back, and Backspace on an empty split tail merging
|
|
into the previous block with the caret re-anchored on a live glyph.
|
|
|
|
Harness decision, documented: the `#[makepad_test]` Studio harness was
|
|
evaluated and rejected for this milestone. It builds and launches the
|
|
full application binary through the in-process StudioHub buildbox — a
|
|
heavy fit for a library-scale package in CI — and the repo's only
|
|
existing examples (`crates/apps/map/tests/ui.rs` and
|
|
`makepad_visual_tests.rs`) were written against aspirational APIs and
|
|
do not compile today.
|
|
|
|
Input routing through `Event::hits` is still covered without a GPU via
|
|
the draw-free `Area::Rect` stub (`CrdtDocEditor::stub_hit_area`):
|
|
tests install a single rect area on the widget's live `Cx` and mirror
|
|
the two platform pre-dispatch steps a real OS pump performs — priming
|
|
`fingers.first_mouse_button` (normally set by the platform mouse
|
|
handler before dispatch) and committing staged key focus by draining
|
|
one queued action through `handle_actions`. Raw `MouseDown` taps then
|
|
resolve to real `Hit` events, `MouseMove` synthesizes real
|
|
`Hit::FingerMove` while a button is down, and IME `TextInput` reaches
|
|
the editor exactly like a compositor delivery. Covered end-to-end:
|
|
caret placement from a tap, selection anchoring and drag extension over
|
|
specific glyphs, `TextInput` inserting at the caret and replacing an
|
|
active selection, and a cell tap + `TextInput` round trip through the
|
|
whole-cell write. This harness also unearthed and fixed two real bugs:
|
|
the projection `hit_test` nearest-glyph fallback resolving taps inside
|
|
tables/nodes to a text glyph (tables/nodes now own their taps), and the
|
|
test fixture actor drifting from production's single `"local"` actor
|
|
(cross-actor mid-run anchoring is deterministic but not chronological —
|
|
see the doc-engine projection invariants). Only the renderer draw pass
|
|
itself (paint and clipping visuals) remains GPU/Studio-bound. The
|
|
keyboard, touch, and frame-clock dispatch is direct (no area hit gate)
|
|
and fully covered by the runtime tests.
|
|
|
|
## CRDT-native in-cell table editing
|
|
|
|
`CrdtDocEditor` now edits table content CRDT-natively on top of the
|
|
`ProjectedTableLayout` geometry:
|
|
|
|
- Tapping (desktop `FingerDown`, mobile short tap) inside a cell parks a
|
|
`TableCellCursor` (table/row/column ids + char offset) on the char
|
|
under the pointer (`cell_char_offset_at` midpoint-splits the 7px char
|
|
grid, clamped to the text end). Text taps restore the text caret and
|
|
clear the cell cursor.
|
|
- Pointer selection works in cells too: a desktop press arms the in-cell
|
|
drag anchor, a drag spans a character selection clamped to the pressed
|
|
cell (crossing an edge clamps at the text ends instead of jumping
|
|
cells), and Shift+tap extends a live selection to the tapped offset.
|
|
Touch keeps its passive caret plus long-press cell-range path.
|
|
- Typing and Backspace/Delete edit inside the cell through whole-cell
|
|
`SetTableCell` replacements — the legacy `ReplaceTableCell` semantics —
|
|
with undo restoring the prior cell text through the symmetric
|
|
compensation. Char offsets are Unicode-scalar safe.
|
|
- Arrows walk the cell text in reading order and hop between cells
|
|
(wrapping across rows); at the table edges the caret exits into the
|
|
nearest text block in unified order (`neighbor_text_block` skips
|
|
advanced nodes), landing on its boundary glyph. Backspace at the start
|
|
of a paragraph following a table no longer dead-ends: it enters the
|
|
table's trailing cell; forward-Delete at a text end before a table
|
|
enters its first cell instead of merging table structure into text.
|
|
- Return inside a cell inserts a row immediately below the cursor's row
|
|
and moves the caret into the same column of the new row.
|
|
- The cell caret draws between rendered characters using the shared
|
|
6px inset / 7px-per-char convention; a stale cursor (its table vanished
|
|
in an undo) clears itself on the next frame.
|
|
- Shift+Arrow inside a cell spans a character selection on the cell's
|
|
text (`ProjectionSession::cell_text_anchor` = anchor offset, the caret
|
|
= focus), drawn as one highlight rect on the shared fixed char grid.
|
|
Typing or Backspace/Delete replaces/removes the span
|
|
(`cell_text_replace_range`), a plain move, tap, or edit collapses it,
|
|
and a Shift step AT the cell edge ends it and promotes to the
|
|
mergeable cell range. Undo staleness self-clears like the cell caret.
|
|
- Ctrl/Cmd+B/I/U (or the toolbar style buttons) with a parked cell
|
|
cursor styles the active in-cell selection, or the WHOLE cell when no
|
|
character selection is spanned, through the engine's cell style ops
|
|
(`SetTableCellStyle`/`Clear`/`Restore`, see the doc-engine invariants).
|
|
The projection emits per-cell styled runs (`cell_runs`) which the
|
|
layout tree mirrors and the renderer draws through the same
|
|
regular/bold/italic/bold-italic pens as block text. Undo/redo of a
|
|
cell style round-trips through the widget like any other op.
|
|
|
|
Engine fix uncovered by this work: `InsertTableRow`/`InsertTableColumn`
|
|
materialization ignored their `after` anchors and ordered rows/columns by
|
|
op id only. Rows and columns now materialize over the anchor chain with
|
|
RGA-style sibling order (counter descending, actor ascending), matching
|
|
text atoms; regression tests live in `doc-engine/tests/materialize.rs`
|
|
and the rule is documented in the doc-engine README invariants.
|
|
|
|
Also fixed while wiring taps: all pointer hit tests and the layout-space
|
|
decorations (selection, handles, text caret) now run through the widget
|
|
origin, so taps and visuals land on the same pixels at any dock position
|
|
or scroll offset instead of assuming the editor sits at (0, 0).
|
|
|
|
Runtime integration tests (real `Cx`, factory-built widget, real key
|
|
events) cover in-cell backspace with undo restore, arrow traversal into
|
|
and out of the table both directions with edge clamping,
|
|
Return-inserts-row-below, in-cell character selection spanning,
|
|
promotion to the cell range at the edge, type-over and
|
|
backspace-over-selection, tap char parking, drag spanning with edge
|
|
clamping, Shift+tap extension, selection-scoped style toggles from the
|
|
keyboard and whole-cell toggles from the toolbar — all with undo/redo;
|
|
pure layout tests cover the edit helpers, cell cursor
|
|
resolution/clamping, caret geometry, neighbor wrapping, layout-carried
|
|
cell style runs, selection-span replace/geometry helpers, char-offset
|
|
parking math, and `neighbor_text_block` skipping.
|
|
|
|
## CRDT-native cell range selection and merge/split
|
|
|
|
`CrdtDocEditor` now spans, renders, merges and splits rectangular table
|
|
cell ranges CRDT-natively, closing the roadmap's "Table column and cell
|
|
merge commands" item on the CRDT surface (the legacy editor never grew
|
|
these commands; its replacement owns them):
|
|
|
|
- `ProjectionSession::cell_selection` is a `TableCellSelection` of stable
|
|
anchor/focus row/column ids. Shift+Arrow at a cell boundary (or on an
|
|
active range) starts/steps the focus cell through `shift_cell_step`;
|
|
the in-cell caret follows the focus cell, table edges clamp the range
|
|
in place, in-cell Shift moves span character selections that end at
|
|
the boundary the range then owns, and any plain arrow, edit, tap or
|
|
drag collapses the range. Undo staleness self-clears exactly like the
|
|
cell caret.
|
|
- `table_cell_range` normalizes the selection to an inclusive
|
|
`(min_row, min_col, max_row, max_col)` rectangle against the projected
|
|
table, so anchor/focus order and row/column inserts between selection
|
|
and command keep the ids live. `cell_selection_rects` yields the
|
|
visible (non-covered) cell rects for the highlight, drawn under the
|
|
cell text with the text-selection color.
|
|
- Merge (`Ctrl/Cmd+M`, `merge_selected_cells`, workspace Merge button)
|
|
routes through the engine's `MergeTableCells` op; the symmetric
|
|
`SplitTableCells` compensation restores the cells on undo. A range is
|
|
mergeable only when it spans more than one cell and touches no
|
|
existing merge (`cell_range_mergeable`) — the engine accepts merge
|
|
ops freely, so the overlap guard lives UI-side where ambiguous nested
|
|
spans are rejected with the range kept for adjustment. The caret parks
|
|
on the merge's anchor cell, which keeps its text; covered cells' text
|
|
is preserved hidden and reappears on split.
|
|
- Split (`Ctrl/Cmd+Shift+M`, `split_cell_at_cursor`, workspace Split
|
|
button) resolves the merge containing the caret cell — covered cells
|
|
resolve to the same merge as their anchor via `merge_at_cell` — and
|
|
splits it through the engine; undo re-merges through
|
|
`RestoreTableMerge`.
|
|
- The workspace toolbar gains Merge/Split buttons (purple, after the
|
|
block-insert buttons). On touch devices they pair with the long-press
|
|
cell-range gesture (see "Touch cell-range selection") since Shift+Arrow
|
|
has no touch equivalent, and report guidance on the status line.
|
|
|
|
Runtime integration tests (real `Cx`, factory-built widget, real key
|
|
events) cover Shift+Arrow range spanning with merge + undo restore,
|
|
edge clamping, split from the covered cell with undo re-merge, plain
|
|
arrow collapse, and overlap rejection; pure layout tests cover range
|
|
normalization/staleness, the mergeable rules, `merge_at_cell` anchor/
|
|
covered resolution, and covered-skip highlight rects.
|
|
|
|
## Mobile Edit/Done interaction mode
|
|
|
|
`CrdtDocEditor` now mirrors the legacy `DocEditor`'s mobile interaction
|
|
policy, closing the last documented toolbar parity gap from the DSL
|
|
switch:
|
|
|
|
- The widget carries the shared `InteractionMode` (`Edit` default,
|
|
`View`) and a `mobile_mode_initialized` latch: the first real touch
|
|
sequence drops the session to View, while desktop mouse/keyboard
|
|
sessions stay in Edit by default (a desktop with a touchscreen keeps
|
|
full editing until it is actually touched).
|
|
- In mobile View mode the gesture router is the entire interaction
|
|
surface: short taps keep moving the passive caret (text and table
|
|
cells), long-press word selection and handle drags keep working, and
|
|
KeyDown handling is gated out entirely — arrows, edits, undo and style
|
|
toggles included — because the IME is closed by design. The area-hit
|
|
match is skipped like the legacy editor's, so ordinary drags fall
|
|
through to a parent ScrollYView.
|
|
- The workspace toolbar gains the mobile-only Edit/Done `AdaptiveView`
|
|
control (empty variant on desktop). Tapping it flips
|
|
`toggle_interaction_mode`: entering Edit takes key focus and mirrors
|
|
the state on the button label ("Edit"/"Done"); entering View calls
|
|
`hide_text_ime` and resets any in-flight gesture so a mode change
|
|
never straddles a touch sequence.
|
|
- Edit-mode touch taps focus the editor and request the IME at the tap
|
|
point; draw_walk then reasserts `show_text_ime` every frame while Edit
|
|
holds key focus, positioned at the live caret (text glyph or table
|
|
cell, bottom-left, relative to the clipped area) — the frame-driven
|
|
reassert several Makepad mobile backends require, ported from the
|
|
legacy editor's IME stabilization.
|
|
- `set_interaction_mode`/`toggle_interaction_mode` and the
|
|
`interaction_mode`/`mobile_mode_initialized` fields are public, so
|
|
hosts can force or inspect the policy (the button label needs it).
|
|
|
|
Runtime integration tests (real `Cx`, factory-built widget, real
|
|
TouchUpdate/KeyDown events) cover the first-touch drop into View and
|
|
key gating (arrows and Ctrl+B asserted inert), the Edit toggle
|
|
restoring keyboard editing, in-cell Backspace gating in View, and
|
|
Edit-mode tap placement with immediate continued editing. Physical IME
|
|
behavior (keyboard actually opening, candidate bar geometry) remains
|
|
device verification on Android/iOS.
|
|
|
|
## Touch cell-range selection (long-press + drag)
|
|
|
|
Merge is now reachable on touch devices, closing the last gap of the
|
|
CRDT-native merge/split milestone:
|
|
|
|
- Long-press inside a table cell falls through the text glyph hit test
|
|
into `table_hit_test` and starts a `TableCellSelection` with anchor =
|
|
focus = the pressed cell (`start_cell_range`), parking the in-cell
|
|
caret on it and clearing any text selection. Long-press on text still
|
|
selects the word's atoms exactly as before — the cell path only ever
|
|
fires when no glyph was hit.
|
|
- The gesture router idles in `Selecting` after a long-press (its `Move`
|
|
yields `None` there), so the widget owns drag tracking: while a cell
|
|
range is active, a continued drag moves the focus cell
|
|
(`extend_cell_range_to`) through the same tap hit geometry. Drags
|
|
landing outside the anchor's table keep the last focus; the caret
|
|
tracks the focus cell, and the existing highlight decorates the span
|
|
with no new draw code. Text word selections share the `Selecting`
|
|
router state but carry no cell range, so their behavior is untouched.
|
|
- Lifting the finger keeps the range; the workspace Merge button or
|
|
Ctrl/Cmd+M consumes it (`merge_selected_cells`, already public), and
|
|
Split works from a cell tap as before. The mode policy is unchanged:
|
|
the gesture runs in View and Edit alike, and merging stays a toolbar
|
|
action (as undo/redo already were).
|
|
|
|
Runtime integration tests (real `Cx`, factory-built widget, real
|
|
TouchUpdate + 24-frame NextFrame long-press clock) cover long-press
|
|
entry, drag extension with caret tracking and full-grid normalization,
|
|
merge consumption (start/end row+column asserted on the wire), drags
|
|
outside the table keeping focus with later extension intact, and the
|
|
text word-selection regression guard.
|
|
|
|
## Clipboard: copy, cut and paste across blocks and cells
|
|
|
|
`CrdtDocEditor` answers the platform clipboard queries (`Hit::TextCopy`
|
|
/`Hit::TextCut`, synthesized by the OS backends from menu and keyboard
|
|
shortcuts, exactly like makepad's own TextInput) and keeps paste on the
|
|
existing TextInput insert path:
|
|
|
|
- Copy yields the selection payload without editing: the text-block
|
|
selection joined with newlines (blocks keep their own line), or the
|
|
in-cell character span when the caret lives in a table. With nothing
|
|
selected the response stays empty, so the platform leaves the
|
|
clipboard alone.
|
|
- Cut fills the same payload and removes it through the shared
|
|
selection-deletion path: same-block spans via `replace_text_range`
|
|
with an empty replacement, in-cell spans via the whole-cell write.
|
|
Undo restores the deleted atoms through the engine's `RestoreText`
|
|
compensation (same-block spans) or tombstones the `ReplaceBlockRange`
|
|
op itself via `CancelBlockRange` (multi-block spans), so a cross-block
|
|
cut or selection delete re-materializes every drained block in a
|
|
single undo step, and redo re-cuts.
|
|
- Paste arrives as regular TextInput: cursor-block typing replaces an
|
|
active selection exactly like typed input; plain in-cell payloads
|
|
splice into the cell text. Payloads containing newlines split
|
|
block-per-line — see the next section, and since the
|
|
tabular-paste milestone an in-cell payload carrying tabs/newlines
|
|
distributes across the table instead of staying a single
|
|
whole-cell write.
|
|
|
|
Two production bugs this uncovered and fixed at the source: same-block
|
|
selection deletion never flipped its applied flag (the empty
|
|
replacement has no trailing atom), so Backspace over a selection
|
|
over-deleted by one char and left the anchors live; and the engine's
|
|
union-minus-union tombstone resolution made delete→undo→redo leave
|
|
targets permanently alive — every tombstone pair (text, blocks, rows,
|
|
columns, merges, cell styles) now resolves chronologically, with
|
|
`delete_text` finally pushing its symmetric `RestoreText` compensation
|
|
to make deletions undoable.
|
|
|
|
Runtime tests cover copy/cut payloads in blocks and cells with undo,
|
|
the no-selection no-payload case, newline-joining across blocks,
|
|
multi-block cut undoing back to every drained block in one step with
|
|
redo re-cutting, and the over-delete + undo/redo regressions; engine
|
|
tests cover the chronological tombstone cycles for text, blocks, rows,
|
|
merges, cell styles and block ranges (cancel/restore, text typed
|
|
beneath a cancelled range surviving, and unresolved-span refusal).
|
|
|
|
## Multi-line paste: block-per-line splitting
|
|
|
|
A TextInput payload containing `\n` (a clipboard paste of several
|
|
lines, or a programmatic multi-line insert) no longer lands as a single
|
|
run of text with literal line feed characters:
|
|
|
|
- The payload splits into one block per line like a desktop editor:
|
|
line 0 splices into the caret block at the caret, middle lines become
|
|
sibling blocks inheriting the caret block's kind, and the trailing
|
|
`SplitBlock` carries the caret block's suffix onto the last pasted
|
|
line, so pasting `l0\nl1\nl2` mid-word into `ab|XY` yields `abl0`,
|
|
`l1`, `l2XY`. The pasted caret parks after the last pasted atom (or
|
|
at the head of the empty tail a trailing newline leaves). CRLF
|
|
payloads have their `\r` stripped per line.
|
|
- The caret anchor handed to the engine may be TOMBSTONED — the caret
|
|
a selection delete leaves behind — and resolution matches the
|
|
single-line typing path exactly (`CrdtDocument::live_offset_of`
|
|
counts the live atoms preceding the tombstone), so pasting over a
|
|
freshly deleted selection lands where the selection began.
|
|
- The whole paste lands on the undo stack as ONE
|
|
`Compensation::Group`: the engine pops each sub-edit's individual
|
|
compensation into a group, so a paste of N lines retracts in a
|
|
single Ctrl+Z instead of walking N+1 entries. Pasting over an active
|
|
selection stays two steps (selection delete, then paste), matching
|
|
typed input. Plain in-cell pastes keep the existing whole-cell
|
|
write path, and a table cell never spawns blocks either way —
|
|
since the tabular-paste milestone below, a tab/newline payload
|
|
into a cell distributes across the table instead.
|
|
|
|
Fixing this surfaced a deeper engine gap at the source: the synthetic
|
|
block a `SplitBlock` opens (Return) used to be a text dead end — atoms
|
|
and styles addressed to it landed in the op log but evaporated from
|
|
the projection, and the child always spliced directly behind its
|
|
parent regardless of blocks authored under that parent first. Split
|
|
children are now first-class materialization targets (suffix atoms
|
|
re-linked head-to-tail keep their ids and inherited style runs,
|
|
child-addressed atoms splice in RGA-wise and take the block default)
|
|
and the child's splice skips the parent's real-chain descendants, so
|
|
multi-line paste and plain repeated splits both order correctly.
|
|
|
|
Runtime tests cover the split/suffix-carry/caret/undo/redo cycle,
|
|
paste-over-selection as two undo steps, and the in-cell newline
|
|
guard; engine tests cover grouped undo chronology, tombstone anchors,
|
|
empty middle lines, CRLF stripping, table refusal, peer convergence,
|
|
and the synthetic-child text/style/order regressions underneath.
|
|
|
|
## Mobile clipboard menu (long-press)
|
|
|
|
The touch clipboard surface is complete: a long-press selection now
|
|
floats the platform clipboard menu (`cx.show_clipboard_actions`, iOS
|
|
and Android backends), and its actions re-enter through the
|
|
synthesized hits the editor already answers — Copy/Cut arrive as
|
|
`Hit::TextCopy`/`Hit::TextCut`, Paste as a TextInput that also flows
|
|
through the multi-line block splitting from the previous milestone.
|
|
|
|
- The request fires from the frame-clock long-press arm right after
|
|
the selection lands — word atoms on text, or the armed cell range on
|
|
a cell — but only in Edit mode: View keeps the gesture for
|
|
merge/highlight exactly as before, and desktop sessions never reach
|
|
the touch-driven clock (their OS backends sink the op anyway).
|
|
- `has_selection` follows the copy-payload availability exactly like
|
|
the native menu: a selected word gets Copy/Cut/Paste, and — since
|
|
the cell-range clipboard milestone below — a cell range gets the
|
|
full action set too (it copies its tabular text), anchored on the
|
|
pressed cell's rect; a word selection anchors on the union of its
|
|
glyph rects.
|
|
- The request is mirrored on the widget as
|
|
`clipboard_menu: Option<ClipboardMenuRequest>`: makepad's platform
|
|
op queue is crate-private, so hosts rendering their own menu (and
|
|
tests asserting the request) read it there instead.
|
|
|
|
Runtime tests (real TouchUpdate + the 24-frame NextFrame long-press
|
|
clock) cover the Edit-mode word menu request with its rect and the
|
|
Copy payload flowing back out, the cell menu rect matching the
|
|
pressed cell with a menu Copy yielding its text and a menu Paste
|
|
splicing into the parked cell (the menu gesture parks the caret at
|
|
the cell end), and View mode making no request at all. Physical
|
|
device menu behavior (menu placement, keyboard-shift adjustment)
|
|
remains device verification on Android/iOS.
|
|
|
|
## Select all (Ctrl/Cmd+A)
|
|
|
|
Ctrl/Cmd+A selects the whole current editing context, mirroring
|
|
makepad's `text_input` select-all. With the caret parked in a table
|
|
cell the context is that cell's text: the in-cell character selection
|
|
spans its full length (any armed merge range collapses), the same
|
|
span Shift+Arrow reaches at the cell edges, so Copy, Cut, style
|
|
toggles and Backspace-over-span all apply to it unchanged. Otherwise
|
|
the anchor lands on the first layout glyph and the focus — with the
|
|
caret — on the last, so the multi-block Copy/Cut/delete and style
|
|
paths treat the result exactly like a maximal Shift+Arrow selection.
|
|
|
|
- Table blocks between the endpoints carry no glyphs; they ride the
|
|
range like any middle block — since the document-payload milestone
|
|
below, their grids join the clipboard payload as tab/newline lines,
|
|
and a cut drains them through `replace_block_range`, so one undo
|
|
step restores the whole document, grid included.
|
|
- A document without glyphs (empty, or tables only) has nothing to
|
|
select — the caret stays put, matching the atom-pair selection
|
|
model where a collapsed anchor reads as no selection.
|
|
- Touch sessions in Edit mode float the platform clipboard menu on
|
|
the fresh selection (the keyboard-select-all pattern from
|
|
`text_input`), mirrored through `clipboard_menu` exactly like the
|
|
long-press request; a desktop Ctrl+A never makes a menu request.
|
|
|
|
Runtime tests cover the document-wide span with the caret parking at
|
|
the last glyph, a select-all cut draining the document to one empty
|
|
block with a single Ctrl+Z restoring both blocks, the in-cell
|
|
whole-text span feeding Copy and Backspace-over-span (with undo), and
|
|
the touch Edit-mode menu request anchored on every glyph's rect.
|
|
|
|
## Cell-range clipboard payload
|
|
|
|
An armed table cell range now participates in the clipboard like any
|
|
other selection. Copy joins the normalized rectangle as tab/newline
|
|
text — rows top to bottom, cells left to right — the spreadsheet
|
|
convention, so a range round-trips through plain text editors and
|
|
other tables. Cut and Backspace/Delete clear every non-empty spanned
|
|
cell: the engine's new `set_table_cells` lands the writes as ONE
|
|
`Compensation::Group` (mirroring multi-line paste), so the span
|
|
un-clears in a single undo step; Backspace previously dropped the
|
|
range and edited only the caret cell.
|
|
|
|
- Cell values holding tabs, newlines, CRs, or quotes are quoted
|
|
RFC-4180-style on the way out and restore verbatim on a later
|
|
paste (the quoting milestone below); already-empty cells are
|
|
skipped so a clear lands neither redundant LWW ops nor dead group
|
|
members. A single-cell "range" write keeps the plain leaf
|
|
compensation, behaving exactly like `set_table_cell`.
|
|
- The long-press cell menu follows automatically: `has_selection`
|
|
reads the same payload, so a range now floats the full action set
|
|
anchored on the pressed cell instead of a paste-only menu.
|
|
- Shipping grouped cell writes surfaced a real engine asymmetry: an
|
|
undo AFTER a redo re-applied the redone cell text, because redo
|
|
pushed the write's own text as its undo compensation (the
|
|
compensation `inverse()` swaps the verb but keeps the text) while
|
|
undo's special-case captured live cell state only in one
|
|
direction. Both transition boundaries now capture the projected
|
|
text — undo's redo-side per group member too — so cell edits
|
|
round-trip through arbitrary undo/redo cycles, in groups and as
|
|
leaves. (The previous "cells stay out of groups" restriction is
|
|
gone.)
|
|
|
|
Runtime tests cover the tabular Copy payload through the TextCopy
|
|
hit, a range Cut clearing both fixture cells with one undo restoring
|
|
them (and the undo→redo→undo cycle re-restoring), Backspace clearing
|
|
the span without touching the parked caret, and the amended
|
|
long-press menu test asserting the full menu with its Copy payload.
|
|
Engine tests cover the leaf redo asymmetry regression, the grouped
|
|
multi-cell one-step undo with round-trip, and single/empty write
|
|
lists keeping leaf semantics.
|
|
|
|
## Tabular paste
|
|
|
|
Completing the cell-range clipboard: a TextInput carrying tabs or
|
|
newlines while the caret lives in a table now pastes the spreadsheet
|
|
way — one cell per tab stop, one row per line — starting at the
|
|
caret cell, or at the armed range's normalized top-left (consuming
|
|
the range like any paste-over-selection). The writes go through the
|
|
grouped `set_table_cells` from the previous milestone, so the whole
|
|
rectangle un-pastes in ONE undo step, and the caret parks at the
|
|
last cell the payload touched ready to keep typing.
|
|
|
|
- Payload rows or columns past the table edge clip (tables do not
|
|
auto-grow), CRLF payloads have their `\r` stripped per line just
|
|
like text-block pastes, empty fields clear their target cells, and
|
|
cells whose text would not change are skipped so a paste lands
|
|
neither redundant LWW ops nor dead group members.
|
|
- A payload without tabs or newlines keeps the whole-cell char-splice
|
|
path unchanged, and cells never spawn blocks with or without the
|
|
distribution — the pre-existing in-cell paste test was rewritten
|
|
from the old "newline stays embedded in the cell" semantics to
|
|
assert the distribution instead (that old behavior is where cell
|
|
strings holding `\n` came from; new pastes no longer create them).
|
|
- The mobile menu integrates for free: its Paste action arrives as a
|
|
TextInput, so a long-press-driven paste distributes from the
|
|
pressed cell across the rectangle.
|
|
|
|
Runtime tests cover the 2x2 rectangle distribution with caret
|
|
parking and the one-undo round trip (and redo re-pasting), edge
|
|
clipping without wrap-around, the backward-spanned range pasting
|
|
from its normalized top-left, CRLF stripping with empty-field
|
|
clears, and the menu-driven paste distributing from the pressed
|
|
cell. The engine integration test replays a grouped multi-cell
|
|
write's op log into a peer and asserts the projections converge —
|
|
the same guarantee every other cell edit already had. The round-trip
|
|
caveat from the copy milestone — cells holding raw tabs or newlines
|
|
re-distributing across the grid — is closed by the RFC-4180 quoting
|
|
milestone below: special values are quoted on copy and restored
|
|
verbatim on paste.
|
|
|
|
## Document-level payloads: table grids in block selections
|
|
|
|
The last hole in the clipboard surface is closed: a block-span
|
|
selection — Shift+Arrow across a table, select-all, any multi-block
|
|
drag — now carries table content in its payload. Table blocks carry
|
|
no glyphs, so anchor and focus still land on text, but every table
|
|
between the endpoints contributes its whole grid at its block
|
|
position, as tab/newline lines through the same `table_grid_tsv`
|
|
builder the cell-range payload uses: one builder, one convention,
|
|
no drift between "copy a range" and "copy across a table".
|
|
|
|
- The export carries stored cell text verbatim: a merge's covered
|
|
cells keep their (hidden) values, and special values are quoted
|
|
RFC-4180-style like any other tabular payload (the quoting
|
|
milestone below). Empty tables (no rows or columns) contribute
|
|
nothing, exactly the blank line they left before.
|
|
- Cutting such a span was already correct at the structural level —
|
|
`replace_block_range` drains the table block and
|
|
`CancelBlockRange` re-materializes it on undo — so the milestone is
|
|
payload-only: the payload now matches what actually disappears,
|
|
and the round trip (copy → paste back through tabular paste)
|
|
rebuilds the grid in any table.
|
|
- The select-all bullet in the earlier section claimed tables were
|
|
skipped; that claim is retired with this milestone.
|
|
|
|
Runtime tests cover select-all over [paragraph, 2x2 table,
|
|
paragraph] yielding "lead\na\tbc\nd\te\ntail" through both
|
|
`copyable_selection_text` and the TextCopy hit, a full-span cut
|
|
draining the document with the grid in the payload and one undo
|
|
restoring blocks AND every cell value, and a partial mid-paragraph
|
|
span splicing the grid between its text fragments in order.
|
|
|
|
## Tabular clipboard quoting: RFC-4180-style round-trip
|
|
|
|
The last documented caveat of the tabular clipboard milestones is
|
|
closed: cells holding tabs, newlines, CRs, or quotes no longer
|
|
re-distribute across the grid on a copy/paste cycle. The writer
|
|
side lives in the single `table_grid_tsv` builder — so the cell-range
|
|
payload, the block-span document payload, and any future consumer
|
|
inherit it at once: a field carrying one of the special characters
|
|
is wrapped in double quotes with every inner quote doubled
|
|
(`quote_tabular_field`); plain and empty fields stay raw, preserving
|
|
byte-for-byte compatibility with payloads from spreadsheets and
|
|
plain text editors.
|
|
|
|
The reader side replaces the naive `split('\n')` / `split('\t')`
|
|
walk in `paste_table_payload` with `split_tabular_payload`, a small
|
|
RFC-4180-style tokenizer:
|
|
|
|
- A quote opens quoted mode only at the very start of a field; a
|
|
quote mid-field is literal text (lenient, like Excel).
|
|
- Inside quotes, a doubled `"` reads as one literal quote, and
|
|
tabs/newlines/CRs are literal field text — so a cell value like
|
|
`"line one\nline two"` lands back in ONE cell.
|
|
- Outside quotes, rows end on `\n` with a trailing CR stripped
|
|
(CRLF tolerance kept from the raw milestone); an unterminated
|
|
quote reads to the end of the payload as best-effort text, and a
|
|
single trailing newline adds no phantom row while a deliberate
|
|
trailing empty row survives.
|
|
- Empty quoted fields round-trip as empty cells, and the
|
|
already-empty/no-op skip and caret-parking semantics of the raw
|
|
paste milestone are unchanged. Caret offset in a multi-line value
|
|
counts its full text, newline included.
|
|
|
|
Unit tests pin the writer (quoting rules plus a quote/split
|
|
round-trip over every special case) and the tokenizer (quoted
|
|
tabs/newlines/CRs, doubled quotes, CRLF rows, mid-field quotes,
|
|
unterminated quotes, empty quoted fields, phantom-row rules).
|
|
Runtime tests pin the integration both ways: an armed range with
|
|
tab/newline/quote values copies as a quoted payload (plain cells
|
|
stay raw), a pasted quoted payload keeps embedded tabs and newlines
|
|
inside their cells with caret parking and a one-undo round trip,
|
|
and an end-to-end copy → cut → paste cycle restores every special
|
|
value verbatim. A doc-engine materialize test pins the data-layer
|
|
guarantee the feature leans on: special-character cell text
|
|
materializes verbatim on peers and restores verbatim through
|
|
undo/redo.
|
|
|
|
Remaining caveats, unchanged or deferred by design:
|
|
|
|
- Rendering of multi-line cell values no longer collapses the
|
|
newline: the next milestone grew rows to fit and draws each
|
|
display line.
|
|
- Merge structure is still not carried by clipboard payloads —
|
|
stored cell text is, and covered cells keep their (hidden) values.
|
|
- Tables still do not auto-grow on an oversized paste; out-of-bounds
|
|
payload rows and columns clip.
|
|
|
|
## In-cell newline rendering: rows grow to fit multi-line values
|
|
|
|
Multi-line cell values — whether legacy strings holding `\n` or fresh
|
|
ones the RFC-4180 quoting round-trip now produces — finally render
|
|
every display line instead of collapsing inline. The change threads
|
|
one shared line model through layout, renderer, caret, highlight,
|
|
hit test, and the keyboard surface, so all of them always agree
|
|
about where a character is:
|
|
|
|
- `layout_projected_table` grows a row by one
|
|
`TABLE_CELL_TEXT_LINE_HEIGHT` (18px) per extra display line of its
|
|
tallest visible cell over the fixed 28px baseline (`TABLE_CELL_HEIGHT`);
|
|
the table rect and every block below shift with it. Column widths
|
|
stay fixed, and single-line tables lay out byte-identical to
|
|
before (the control assertions pin this).
|
|
- Geometry composes with merges: a covered cell's hidden text never
|
|
inflates its row, and a vertical merge anchor sums the grown
|
|
heights of the rows it spans.
|
|
- The renderer draws styled runs segment by segment — a `\n` inside
|
|
a run resets x to the inset and advances one line — with the whole
|
|
text block vertically centered, so single-line cells draw exactly
|
|
where they always did.
|
|
- `table_cell_caret`, the selection bands (`cell_text_span_rect`,
|
|
now `cell_text_span_rects` with one rect per covered display
|
|
line), and the pointer hit test (`cell_char_offset_at`, now
|
|
point-based: y picks the band, x midpoint-splits within that
|
|
line) all resolve offsets through one `cell_text_line_col` /
|
|
`cell_text_offset_at` pair; the round-trip property between them
|
|
is unit-tested for every boundary, including empty lines and the
|
|
newline's own offset (line-end of the previous display line).
|
|
- ArrowUp/ArrowDown, previously dead in cell mode, step between
|
|
display lines keeping the visual column (clamped per line), with
|
|
Shift extending the in-cell character selection vertically; they
|
|
stay inert at the first/last line and on single-line cells — no
|
|
implicit row exit, and an armed cell range is never half-moved by
|
|
a vertical key (range arithmetic stays on the horizontal walk).
|
|
|
|
Defect found and fixed in this milestone: an in-cell character span
|
|
covering a newline copied as a RAW slice (block selections and cell
|
|
ranges quoted since the previous milestone; the in-cell path predates
|
|
both), so copy → paste re-distributed the slice across the table. The
|
|
in-cell branch of `copyable_selection_text` now runs the same
|
|
`quote_tabular_field`, and the Shift+ArrowDown runtime test pins the
|
|
quoted payload end to end.
|
|
|
|
Tests cover the line math boundary-by-boundary (including empty and
|
|
trailing lines), row growth with block flow and merge composition,
|
|
multi-line caret rects, per-line selection bands, point hit testing
|
|
with clamps, vertical-arrow stepping/inertness/collapse behavior, a
|
|
real tap parking on the tapped display line, and the quoted span
|
|
copy through both `copyable_selection_text` and the TextCopy hit.
|
|
|
|
## Legacy perf boxes: retirement decision and the CRDT-native layout cache
|
|
|
|
Four roadmap boxes stayed open long after everything around them
|
|
landed ("Incremental page/block reflow execution", "Reuse cached
|
|
fragment payloads during draw traversal", "Wire commands to touch
|
|
only affected block revisions", and the renderer draw-pass
|
|
integration test). This milestone closes each with an explicit
|
|
decision instead of leaving the list ambiguous.
|
|
|
|
Context: `DocWorkspace`/`DocEditor` is the fallback path;
|
|
`CrdtDocWorkspace`/`CrdtDocEditor` is the active editor
|
|
(`workspace/mod.rs` documents the split). The three legacy perf
|
|
boxes were written for the legacy layout/draw pipeline, whose
|
|
foundations (block revisions, fragment caches, invalidation
|
|
tracking) are checked above but whose final wiring would buy
|
|
performance only on a path nothing ships through. Completing them
|
|
there would be speculative double-maintenance, so each is retired
|
|
against the legacy fallback and, where the underlying need is real,
|
|
answered on the active CRDT path:
|
|
|
|
- **Wire commands → block revisions:** retired. The CRDT-native
|
|
editor does not need per-command revision bumps: change detection
|
|
keys on the engine's op version-vector sum, which every mutating
|
|
op — edit, undo, redo, peer import — bumps exactly once. The
|
|
legacy revisions foundation stays (the fallback keeps its checked
|
|
cache-population semantics); the final per-command wiring is
|
|
retired rather than implemented.
|
|
- **Incremental page/block reflow execution:** retired for the
|
|
legacy path; the CRDT answer is `CrdtDocEditor::layout_tree`, a
|
|
document-keyed cache of the whole `ProjectionLayoutTree`. All
|
|
~two dozen consumers (event handlers, drag tracking, the draw
|
|
walk) recompute once per document change instead of once per
|
|
consumer per keypress — an unchanged document serves an `Rc`
|
|
clone of the same tree. Granularity is one change key rather than
|
|
per-block re-layout: the tree build is a single O(blocks +
|
|
glyphs) pass, so per-block refinement buys nothing until a
|
|
profile says otherwise.
|
|
- **Reuse cached fragment payloads during draw traversal:** retired
|
|
for the legacy renderer; the CRDT draw walk reuses the same
|
|
cached layout tree as every other consumer — the glyph/rect
|
|
payloads ARE the cache, shared rather than duplicated in a
|
|
second, draw-only structure.
|
|
- **Renderer draw-pass integration tests:** resolved by scoping.
|
|
Painting and clipping against a live GPU surface cannot run in
|
|
the sandbox CI (the roadmap note already said so); the parts that
|
|
CAN regress — layout geometry, table/caret/selection rects, hit
|
|
tests, cell ranges, draw-free event flows — are covered by the
|
|
real-`Cx` runtime harness with `Area::Rect` stubs. Visual
|
|
verification lands with the device-verification batch on
|
|
Android/iOS hardware (the same batch as the ScrollYView parent
|
|
handoff next to it in the list).
|
|
|
|
`set_engine` drops the cache slot outright, so a swapped-in engine
|
|
can never inherit another document's tree under a colliding key.
|
|
Runtime tests pin the cache both ways: pointer-identity reuse on an
|
|
unchanged document, invalidation plus fresh geometry after a cell
|
|
edit AND after undo, and no stale-tree inheritance across an engine
|
|
replacement.
|
|
|
|
Open legacy roadmap item, unchanged: ScrollYView parent handoff
|
|
verification on Android/iOS — belongs to the device-verification
|
|
batch.
|
|
|
|
## Device verification runbook (last sandbox-actionable artifact)
|
|
|
|
With every other roadmap item closed, one box legitimately cannot
|
|
execute in the sandbox: ScrollYView parent handoff verification on
|
|
Android/iOS — and with it the whole class of platform-owned behaviors
|
|
deferred across the touch milestones (IME opening and its
|
|
frame-driven reassert, native clipboard-menu placement against the
|
|
soft keyboard, touch drag-vs-scroll arbitration on real event
|
|
streams, and the GPU-bound painting/clipping sweep). This milestone
|
|
writes the checklist that turns a hardware session into pure
|
|
execution: `DEVICE_VERIFICATION.md` in this folder.
|
|
|
|
It is anchored to code, not vibes: every section names the mechanism
|
|
under test (the router's 10 px / 24-frame arbitration,
|
|
`cx.show_text_ime` and its NextFrame reassert,
|
|
`cx.show_clipboard_actions` with the `keyboard_shift` passthrough,
|
|
the `start/extend_cell_range` touch-only spanning path, the
|
|
RFC-4180 quoted payloads, the grown-row multi-line layout) and each
|
|
row has an expected outcome plus explicit fail criteria — including
|
|
which failures must be filed instead of waved through. The legacy
|
|
box is covered on both editors (`crdt_body` AND the fallback
|
|
`body_scroll`); the sign-off table gates checking the box on both
|
|
columns passing.
|
|
|
|
Everything the harness CAN prove stays proven there: the runtime
|
|
suite covers the logic behind each row, so the runbook deliberately
|
|
re-verifies only the platform-owned residuals. No code changes in
|
|
this milestone beyond documentation; the roadmap box gains a pointer
|
|
to the runbook for the hardware session.
|
|
|
|
## Boot-time document init and app-data persistence (Android empty-doc fix)
|
|
|
|
The first hardware run of the roadmap surface exposed a compound
|
|
defect no sandbox gate could see: the Android APK (`pageflipnav`)
|
|
booted the doc workspace to a BLANK page. Two independent causes:
|
|
|
|
1. **The CRDT editor had no boot init.** The legacy `DocEditor` seeds
|
|
the showcase document behind an `initialized` flag on first draw,
|
|
but `CrdtDocEditor` — the ACTIVE editor since the navigation switch
|
|
— starts from `DocumentController::default()` (an empty projection)
|
|
and only ever gained content through an interactive action a fresh
|
|
install has not performed yet.
|
|
2. **Persistence pointed at the build machine's source tree.**
|
|
`persistence.rs` resolved its save file under
|
|
`env!("CARGO_MANIFEST_DIR")`, an absolute path baked in at compile
|
|
time. On device that path does not exist, so Open silently read
|
|
nothing and Save silently wrote nowhere (`.ok()` swallowed the
|
|
failure); on a developer machine the app polluted its own checkout.
|
|
|
|
The fix mirrors the legacy boot contract exactly once, inside the
|
|
editor: the first event handled by a factory-fresh editor runs
|
|
`init_document`, which loads the on-disk save when it decodes as
|
|
`#MP_CRDT_V1` wire (`initial_document_source` is the gate — classic-
|
|
format saves belong to the legacy workspace's first-edit migration and
|
|
must not be shadowed) and otherwise calls `seed_demo_doc`, a CRDT
|
|
mirror of the legacy `demo_doc_blocks()` showcase: styled headings,
|
|
accent runs, a divider, an image node, the 4x3 table with a bold
|
|
header, and the closing hint. `set_engine` flips the same flag, so a
|
|
host that installs its own document before the first event is never
|
|
overwritten by the seed (this also keeps every runtime test harness
|
|
deterministic).
|
|
|
|
Persistence migrates to the crate-wide convention
|
|
(`crate::dir::app_data_dir()`, the root the CAD store already uses):
|
|
writes go ONLY to `nigig_build_store/generated/current.doc.json`
|
|
there, while reads keep a one-way fallback to the legacy source-tree
|
|
file so an unreplicated developer save is honored once. Both the boot
|
|
and the migration emit `[DOC_TRACE]` lines (mirroring the legacy
|
|
boot's instrumentation) so a device `logcat` session confirms which
|
|
branch fired.
|
|
|
|
Tests pin the whole contract: the boot-source gate (valid CRDT wire
|
|
boots verbatim; classic JSON and `None` both route to the demo seed),
|
|
a runtime boot test (first event on a factory-fresh editor flips the
|
|
flag and leaves a non-empty projection, source-agnostic by design), a
|
|
no-overwrite guard for host-installed engines, a full structural
|
|
assertion of the seeded showcase (heading runs, node kinds in order,
|
|
the 4x3/12-cell table with bold header), and four persistence tests
|
|
over temp dirs covering the round trip, store-beats-manifest
|
|
precedence, the manifest fallback, and empty-file rejection.
|
|
`DEVICE_VERIFICATION.md` section 9 gained the matching hardware rows.
|
|
|
|
## Engine source coverage (gated)
|
|
|
|
The doc engine now has what the CAD engine got first: a measured,
|
|
gated coverage number instead of an assertion.
|
|
`tools/test-doc-engine-coverage.sh` runs the crate's unit tests plus
|
|
`tests/materialize.rs` under `-C instrument-coverage` in an isolated,
|
|
self-deleting environment and enforces a total floor (96% lines)
|
|
against a measured baseline of 99.00% (97.54% regions), with per-file
|
|
floors so losing one module's tests cannot hide inside the total. The
|
|
harness needed no shim layer: doc-engine is UI-free (serde +
|
|
serde_json), which is also why the whole run takes seconds. The run
|
|
report named real gaps, closed in the same tranche: offset-addressed
|
|
text insert/delete, block alignment materialization, batched cell
|
|
group undo/redo, the `#MP_CRDT_V1` wire round trip, and
|
|
toggle/batch-reject guards. Two assertions came back inverted and were
|
|
pinned as DOCUMENTED behavior instead: writes and style ops
|
|
addressed to blocks or cells whose anchors have not arrived are
|
|
accepted into the op log (CRDT store tolerance — they must merge when
|
|
the anchor lands) while conjuring no blocks, rows or columns into the
|
|
rendered document. The baseline, the
|
|
exclusions, and what the number does not mean live in
|
|
`crates/apps/doc/doc-engine/COVERAGE.md`; the gate runs in the
|
|
doc-engine workflow.
|
|
|
|
## Clipboard menu re-float on selection-handle drag
|
|
|
|
A mobile selection flow had one stale anchor: the native menu floated
|
|
at long-press word-select (or select-all), but dragging either handle
|
|
afterwards re-anchored nothing — the platform toolbar stayed where the
|
|
untouched word was, or had already been dismissed by the adjustment.
|
|
The router's `end` only distinguishes PendingLongPress from everything
|
|
else, so the Stop arm now samples the gesture state first: when the
|
|
ended gesture was a handle adjustment and the session is in Edit mode,
|
|
the menu re-floats on lift-off via the same
|
|
`cx.show_clipboard_actions` request shape as the long-press arm, with
|
|
`rect` = the ADJUSTED selection's handle union (through the existing
|
|
`clipboard_menu_rect`). Mid-drag stays quiet — matching TextInput
|
|
cadence, which DEVICE_VERIFICATION 3.3 documents — and View mode keeps
|
|
the drag as pure highlight/merge surface (new regression row 3.6).
|
|
Runtime tests drive the full sequence (long-press "hello", grab the
|
|
end handle, drag onto 'w' in "world"): no request mid-drag, a fresh
|
|
request on lift-off whose rect is wider than the stale one, focus
|
|
landed on the dragged-to atom; the View-mode twin asserts the span
|
|
adjusts but `clipboard_menu` stays empty.
|
|
|
|
## Doc-workspace coverage gate (pure layer at 96.76% lines)
|
|
|
|
The doc module had exactly the problem the CAD and doc-engine gates
|
|
were built for: a host-only-testable core that had never been measured
|
|
because `cargo test -p nigig-build` links wayland/X11/GL/alsa/polkit.
|
|
The test suite is now split in two — `tests_pure.rs` holds every test
|
|
that needs no `Cx` (model, layout, editing, collaboration,
|
|
advanced JSON, projection layout/session, CRDT bridge, persistence
|
|
seams, mobile gestures), and `tests.rs` keeps the widget-runtime and
|
|
boot tests. `tools/test-doc-workspace-coverage.sh` then copies the
|
|
pure sources plus `tests_pure.rs` into a temporary host-only crate
|
|
with a makepad-math shim (the same shape as the CAD gate), runs them
|
|
under `-C instrument-coverage`, and enforces a total floor plus a
|
|
per-file floor for every instrumented file. Baseline was **28.55%**
|
|
lines; the gate now holds **96.76%** with floors a few points under
|
|
per file (persistence keeps a documented lower floor — see
|
|
`COVERAGE.md` for the honest exclusion list and exact numbers). A new
|
|
`doc-workspace-coverage` CI job runs the script on every push that
|
|
touches the crate, and the script self-reports any pure file that
|
|
appears without a floor so the classification cannot silently rot.
|
|
|
|
Growing the suite sat on the roadmap long enough that the exercise
|
|
also surfaced real behavior worth pinning, and two outright defects
|
|
that are now fixed: `Command::ReplaceBlockRange` used to validate its
|
|
explicit `block_ids` length after draining blocks out of the document
|
|
(a malformed remote command destroyed content before failing), and
|
|
`RgaText::visit_children` was a dead String-collecting duplicate of
|
|
`visit_atoms` (removed). The controller tests also nail two semantics
|
|
that were previously only folklore: remote typing between two
|
|
`DocumentController`s only converges when each peer owns a distinct
|
|
`document.crdt.local_actor` (the sync test assigns `alice`/`bob`),
|
|
and a mid-range `replace_range_crdt` renders its replacement after
|
|
the tombstoned subtree it replaced, because RGA siblings walk in id
|
|
order (`"hello" -> "hloY"` is pinned with the reasoning inline). The
|
|
baseline, the per-file floors, and what intentionally stays outside
|
|
the measurement (the whole widget layer, the persistence write-path
|
|
wrappers, the defensive traversal guards) live in `COVERAGE.md`; the
|
|
harness writes nothing outside a shell-trap-cleaned mktemp dir.
|