Redraw a copy's originals at the source — the resting layout follows the operation
The modifier flip (⌥ copy / ⌘ move) now reflows the source board once: a copy re-admits the dragged originals into the resting layout, standing dimmed in place, and a move lifts them out as before. This retires DRAG-REORDER.md's operation-blind carve-out and makes 04-interactions.md's "originals stay" true in flight, not just at echo. The one seam is DragSession.hiddenMembers reading the observed operation. Consequences carried honestly: copyCards now takes its index in the lane's full rendered space (the zones counted the originals, so the geometry's number is the writer's number — the old neighbour remap is deleted); resolveOperation freezes under the committed hold exactly as propose does, fixing a real bug where a settled within-board ⌥-copy drew the move arrangement until the echo and visibly re-shuffled. Stationary flips still wait for the next dropUpdated (their own Backlog card). Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
+9
-3
@@ -23,7 +23,13 @@ While a drag is in flight, the proposal (an insertion index) is computed geometr
|
||||
|
||||
The proposal is a pure function of the cursor over these fixed zones. This is deliberately **not** derived from per-item hover events, which feed back off the very reflow they cause (items move under the cursor → a different item fires → the proposal moves again) and jitter. Because the zones are reconstructed analytically rather than measured, they do not move when the placeholder does.
|
||||
|
||||
**The resting layout excludes the dragged run whatever the effective operation is.** A within-board ⌥-drag copies, and the originals really do stay — but ⌥ can be pressed and released mid-drag, and a layout that re-admitted the originals on every modifier flip would flap the whole board under the cursor. The dragged items are lifted out at pickup and stay out until release; the copy's originals reappear when the write lands. This is why the drop index means the same thing for `moveCards` and `copyCards` (see **The drop commits** below).
|
||||
**The resting layout follows the effective operation: a move lifts the dragged run out, a copy leaves it in.** 04-interactions.md says the originals stay for both copies — the within-board ⌥-drag and the cross-board default — so the source board draws what it will still hold: the originals stand in place, dimmed (`ClipboardTreatment.dimmedOpacity`, the same treatment a trash row being dragged out wears), and the shadow run opens beside them. A move's originals are lifted out at pickup and stay out until the echo lands. The single expression is `DragSession.hiddenMembers(onBoardRooted:)`, which every surface that builds a resting layout goes through.
|
||||
|
||||
Pressing or releasing ⌥/⌘ mid-drag therefore reflows the source board **once**, and that one-shot reflow is the point rather than a cost: the modifier's whole job is to change what the drop will leave behind, and the board answering the question is the feedback being asked for. (This retires an earlier carve-out that lifted the run out for every operation to keep the board still. The stillness was real; it was bought by never answering the question.) The flip lands on the next `dropUpdated`, because that is where the operation is re-resolved — a modifier pressed with the mouse perfectly still waits for the next motion. Once a release has settled, the operation is frozen for the duration of the committed hold (`DragSession.resolveOperation` guards on `hold == nil`, exactly as `propose` does), so a settled copy keeps its originals on screen and a settled move keeps them lifted until the echo snapshot lands.
|
||||
|
||||
The index-space consequence is real and is carried by the writers rather than papered over: **the drop index means different things for `moveCards` and `copyCards`**, and each one is counted in the layout its own gesture was showing. A move's index counts slots among the cards the run vacates; a copy's counts them among the lane's full rendered set, originals included. Neither writer has to guess — the operation picks the writer and the operation picked the layout — and a within-board ⌥-copy therefore lands exactly at the shadow's slot, including when the shadow sits below the dragged run in the same lane (see **The drop commits** below).
|
||||
|
||||
Lanes get this for free and the invariant is worth stating: a within-board lane drag never resolves to `.copy` (⌥ is ignored there), so the strip's zones never see a re-admitted lane under the cursor; a **cross-board** lane copy re-admits the lane into the *source* strip while the cursor is over the foreign board, where none of the source strip's zones are being consulted. `moveLanes` and `receiveLanes` keep the index space they always had. The strip's lane rendering has no dim of its own, so a re-admitted lane reads as an ordinary lane rather than as the source of the drag — the card face and the trash rows are the only surfaces wearing that treatment today.
|
||||
|
||||
Two stability rules on top:
|
||||
|
||||
@@ -108,7 +114,7 @@ Geometry does not change across the boundary. The destination board's own restin
|
||||
|
||||
## The drop commits
|
||||
|
||||
The commit is the store's, and it is one `performWrite` bracket per gesture whatever the set's size: one app-mediated reload, and (on git boards) one commit rather than N. Every one of them takes an index counted **against the destination's rendered items with the dragged run removed** — the resting layout's own convention, so the number the geometry produced is the number the writer consumes, unrewritten.
|
||||
The commit is the store's, and it is one `performWrite` bracket per gesture whatever the set's size: one app-mediated reload, and (on git boards) one commit rather than N. Every one of them takes an index counted **against the destination's rendered items as the resting layout showed them** — so the number the geometry produced is the number the writer consumes, unrewritten. For every move that means "with the dragged run removed"; for the within-board ⌥-copy it means "with the originals still there", because a copy leaves them there and the zones counted them (see **Resting-layout zones**). Cross-board arrivals never face the question: the destination never held the originals.
|
||||
|
||||
| Gesture | Store method | Writer |
|
||||
| --- | --- | --- |
|
||||
@@ -123,7 +129,7 @@ The commit is the store's, and it is one `performWrite` bracket per gesture what
|
||||
Three properties of that table are load-bearing:
|
||||
|
||||
- **Ranks are inserted, never permuted.** A drop writes only the dragged items' `order` — the siblings' files are not touched, so `modified` (and a git commit) stays honest about what actually moved. `Ranks.insertionRanks(amongVisible:at:count:)` produces the N ranks the contiguous run needs; `nil` from it is the renumber trigger, exactly as an exhausted midpoint is everywhere else, and the fallback compacts the destination and places again (`moveLane`'s and `sortSelection`'s pattern).
|
||||
- **A copy's ranks are computed against the destination's *full* rendered set**, because the originals stay and a rank chosen in the gap the lifted originals vacated would collide with them. A move's are computed against the set the moving members vacate. One expression covers both: the ranks are placed among the rendered cards minus whatever will actually leave.
|
||||
- **A copy's ranks are computed against the destination's *full* rendered set**, because the originals stay and are still on disk holding their ranks. A move's are computed against the set the moving members vacate. One expression covers both: the ranks are placed among the rendered cards minus whatever will actually leave — which is also, exactly, the layout each gesture's zones were built over, so the index needs no translation on the way in.
|
||||
- **Cross-board writes are executed by the destination store**, inside *its* bracket. The source board's tree changes outside its own store's bracket, which is correct and needs no coordination: the source store's watcher sees a foreign change and reloads, which is what a foreign change is.
|
||||
|
||||
Identity follows 01-storage-format.md exactly. A copy mints fresh UUIDs at every level and keeps `created` (a copy is a fork). A move keeps the UUID; only the **import boundary** remints, per folder, at the finest grain — a lane arriving with one colliding card is still a lane move with one reminted card.
|
||||
|
||||
Reference in New Issue
Block a user