The pickup seeds its own slot — the lift and the shadow become one transaction
"At drag start it replaces the item's original space" is now true from the very first frame: begin takes the run's own resting slot as a seed, set in the same transaction that lifts the run out. Without it the vacated gap closed un-animated and sprang back open at the first dropUpdated — a shuffle carrying no information. The seed bypasses propose deliberately (a pickup is not a new landing spot, so no alignment tick), and the first real sample's re-propose of the same slot is the early-out's ordinary silence. A ⌥-pickup seeds nothing — a copy's resting layout keeps the originals in place, so there is no vacated space to hold. Card pickups compute the seed through the same LaneView.rendered rules the retargets count in; lane pickups seed their strip slot the same way. DRAG-REORDER.md's own-slot paragraph records the rule, and SeededPickupTests pins all four corners. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -611,17 +611,19 @@ final class DragSession {
|
||||
/// - members: the dragged cards in flatten order (`SelectionGrammar.boardCards`, or the
|
||||
/// trash's own order for a trash-card drag).
|
||||
/// - heights: their measured heights, captured **before** the pickup transition starts.
|
||||
/// - seed: the run's own resting slot, proposed from the first frame — see `begin`.
|
||||
func beginCards(
|
||||
_ members: [ItemID],
|
||||
folders: [URL],
|
||||
heights: [CGFloat],
|
||||
container: ItemContainer,
|
||||
source: BoardStore,
|
||||
mixesKinds: Bool = false
|
||||
mixesKinds: Bool = false,
|
||||
seed: DropTarget? = nil
|
||||
) {
|
||||
begin(
|
||||
kind: .cards, members: members, folders: folders,
|
||||
container: container, source: source, mixesKinds: mixesKinds
|
||||
container: container, source: source, mixesKinds: mixesKinds, seed: seed
|
||||
)
|
||||
cardHeights = heights
|
||||
laneUnits = []
|
||||
@@ -637,23 +639,37 @@ final class DragSession {
|
||||
units: [Int],
|
||||
container: ItemContainer = .board,
|
||||
source: BoardStore,
|
||||
mixesKinds: Bool = false
|
||||
mixesKinds: Bool = false,
|
||||
seed: DropTarget? = nil
|
||||
) {
|
||||
begin(
|
||||
kind: .lanes, members: members, folders: folders,
|
||||
container: container, source: source, mixesKinds: mixesKinds
|
||||
container: container, source: source, mixesKinds: mixesKinds, seed: seed
|
||||
)
|
||||
laneUnits = units
|
||||
cardHeights = []
|
||||
}
|
||||
|
||||
/// `seed` is the pickup's own-slot proposal — "at drag start it replaces the item's original
|
||||
/// space" (DRAG-REORDER.md § The pieces), made true from the very first frame rather than from
|
||||
/// the first `dropUpdated`. Without it the lift-out and the shadow's arrival land in different
|
||||
/// transactions: the masonry closes the vacated gap (un-animated — `shadowRun` hasn't moved) and
|
||||
/// then springs it back open when the first hover sample proposes the own slot, a shuffle for no
|
||||
/// information. Seeded, the lift and the shadow are one transaction with identical geometry —
|
||||
/// the run's frozen sizes at the run's own resting position — so nothing on the board moves at
|
||||
/// pickup, which is what the own-slot fixed point always promised.
|
||||
///
|
||||
/// Set directly rather than through `propose`, because a pickup is not a *new landing spot*:
|
||||
/// the alignment tick stays reserved for genuine retargets. The first real `dropUpdated`
|
||||
/// re-proposes the same slot and `propose`'s early-out makes it a silent no-op.
|
||||
private func begin(
|
||||
kind: DragKind,
|
||||
members: [ItemID],
|
||||
folders: [URL],
|
||||
container: ItemContainer,
|
||||
source: BoardStore,
|
||||
mixesKinds: Bool
|
||||
mixesKinds: Bool,
|
||||
seed: DropTarget?
|
||||
) {
|
||||
// A new drag's first `sinceLastMs` must not be the gap since the previous drag's last sample.
|
||||
#if DEBUG
|
||||
@@ -669,7 +685,7 @@ final class DragSession {
|
||||
self.mixesKinds = mixesKinds
|
||||
self.sourceStore = source
|
||||
self.sourceRoot = source.rootKey
|
||||
self.proposal = nil
|
||||
self.proposal = seed
|
||||
self.operation = .move
|
||||
// The reload-resolved drag set: vanished members leave it silently, which is what
|
||||
// `survivors` reads and what "an emptied drag cancels itself" is stated in terms of.
|
||||
|
||||
Reference in New Issue
Block a user