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:
@@ -379,18 +379,33 @@ final class DragSession {
|
||||
///
|
||||
/// Only the source board hides anything, and only for a board-side session: a trash card's drag
|
||||
/// carries items that render in the trash column, not in any lane's masonry, so no lane loses a
|
||||
/// card to it.
|
||||
/// card to it. Those rows dim in place instead (`CardFaceView`, `TrashLaneRowView`), which is
|
||||
/// the same treatment a re-admitted board original now wears — one vocabulary for "this is the
|
||||
/// source of the drag", wherever the source stays visible.
|
||||
///
|
||||
/// **The dragged run is lifted out whatever the effective operation is** (DRAG-REORDER.md §
|
||||
/// Resting-layout zones): ⌥ 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 copy's
|
||||
/// originals reappear when the write lands — the hold keeps them lifted for that round trip, so
|
||||
/// the arrangement on screen is the one the release proposed and stays still until the echo.
|
||||
/// **A move lifts the run out; a copy leaves it in** (04-interactions.md ▸ Drag and drop:
|
||||
/// "originals stay", for the within-board ⌥-drag and for the cross-board default alike — ruled
|
||||
/// 2026-08-01, retiring the operation-blind carve-out this used to be). The layout on screen is
|
||||
/// therefore always the arrangement the release would produce: a copy's source board shows what
|
||||
/// it will still hold, a move's shows what it will have given away. Flipping ⌥ or ⌘ mid-drag
|
||||
/// reflows the source board once, and that one-shot reflow *is* the feedback the modifier is
|
||||
/// asking for — the previous rule's fear of "flapping" traded the answer to "what will this
|
||||
/// drop leave behind?" for a stillness nobody asked for.
|
||||
///
|
||||
/// Reading `operation` here is what makes the flip visible at all: the property is observed, so
|
||||
/// every surface that builds a resting layout off this method re-renders when it changes. The
|
||||
/// flip lands on the next `dropUpdated`, since that is where the operation is re-resolved; a
|
||||
/// modifier pressed with the mouse perfectly still waits for the next motion (Backlog ▸
|
||||
/// Stationary modifier flips).
|
||||
///
|
||||
/// **The hold freezes the answer**, because `resolveOperation` refuses to move once a release
|
||||
/// has settled: a settled copy keeps its originals on screen and a settled move keeps them
|
||||
/// lifted, in both cases until the echo snapshot lands and the real faces take over.
|
||||
func hiddenMembers(onBoardRooted root: URL) -> Set<ItemID> {
|
||||
guard isActive, container == .board, let sourceRoot,
|
||||
DragLocality.isSameBoard(root, sourceRoot)
|
||||
else { return [] }
|
||||
return memberSet
|
||||
return operation == .copy ? [] : memberSet
|
||||
}
|
||||
|
||||
/// The proposal's index when it names this board's strip, else `nil` — the lane strip's shadow
|
||||
@@ -575,14 +590,27 @@ final class DragSession {
|
||||
|
||||
/// Re-resolves the effective operation against the board under the cursor and the modifiers
|
||||
/// **right now**, and hands it back for the `DropProposal` the badge tracks.
|
||||
///
|
||||
/// **A settled release freezes it**, `propose`'s guard for a reason that is now the same one:
|
||||
/// the operation decides what the source board's resting layout holds (`hiddenMembers`) as well
|
||||
/// as which write went out, so a stray `dropUpdated` arriving after the commit — with the user's
|
||||
/// finger already off ⌥ — would re-lift originals the write is about to leave in place. The
|
||||
/// write named an operation; the overlay draws that operation until the echo lands.
|
||||
///
|
||||
/// `modifiers` defaults to the live flags, which is the only thing the app ever passes; it is a
|
||||
/// parameter so the resolution is checkable without a keyboard, exactly as `DragLocality`'s own
|
||||
/// pure function is.
|
||||
@discardableResult
|
||||
func resolveOperation(destinationRoot: URL) -> TransferOperation {
|
||||
guard let kind, let sourceRoot else { return operation }
|
||||
func resolveOperation(
|
||||
destinationRoot: URL,
|
||||
modifiers: NSEvent.ModifierFlags = NSEvent.modifierFlags
|
||||
) -> TransferOperation {
|
||||
guard let kind, let sourceRoot, hold == nil else { return operation }
|
||||
let resolved = DragLocality.operation(
|
||||
kind: kind,
|
||||
container: container,
|
||||
isWithinBoard: DragLocality.isSameBoard(sourceRoot, destinationRoot),
|
||||
modifiers: NSEvent.modifierFlags
|
||||
modifiers: modifiers
|
||||
)
|
||||
if resolved != operation { operation = resolved }
|
||||
return resolved
|
||||
@@ -609,10 +637,13 @@ final class DragSession {
|
||||
/// Enters the committed phase: the arrangement the session was showing stays on screen until
|
||||
/// `store` applies its next snapshot (`CommittedHold`).
|
||||
///
|
||||
/// Everything that drives the rendering — the members, the proposal, the source root — is kept
|
||||
/// exactly as it was, so "keeps rendering the arrangement it was showing" needs no second
|
||||
/// mechanism: the shadows stay at the landing slots and the originals stay lifted out until the
|
||||
/// snapshot carrying the write arrives and the real faces take their place.
|
||||
/// Everything that drives the rendering — the members, the proposal, the source root, the
|
||||
/// operation — is kept exactly as it was, so "keeps rendering the arrangement it was showing"
|
||||
/// needs no second mechanism: the shadows stay at the landing slots and the originals stay
|
||||
/// exactly where the operation put them — lifted out for a move, standing dimmed in place for a
|
||||
/// copy — until the snapshot carrying the write arrives and the real faces take their place.
|
||||
/// Freezing the operation is `resolveOperation`'s own guard; the hold this method arms is what
|
||||
/// that guard reads.
|
||||
func commit(into store: BoardStore) {
|
||||
guard isActive else { return }
|
||||
sourceStore?.transient.dragMembers = .empty
|
||||
|
||||
Reference in New Issue
Block a user