The lane-resize release gets its own hold — the strip stops two-stepping through the stale snapshot
LaneWidthHold: after the release the session's frozen standard and the WRITTEN unit count keep governing the strip until a landed snapshot carries that width back — dividing the already-grown window by the stale unit total in between was the visible two-step. A deliberately separate type from CommittedHold: that hold stands in for an arrangement and any landing retires it; this one stands in for a value and only a landing that carries it will do. Echo reads through LaneLayoutMath.displayUnits so the width-1 key-removal case compares right; the watch rides landedReloads so a value-equal echo still answers; width writes now return whether bytes reached disk so a refused or no-op write dissolves the hold instead of arming it; the 1500ms timeout family covers the rest. Drag-perf card 231e3693. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -1518,7 +1518,14 @@ public final class BoardStore: HealHost {
|
||||
/// rethrows, so the rethrow is swallowed here rather than propagated to a gesture that has no
|
||||
/// second thing to do about it. The lane stays at its old width, which is the truth — nothing was
|
||||
/// written.
|
||||
public func setLaneWidth(_ id: ItemID, units: Int) {
|
||||
///
|
||||
/// **Returns whether bytes reached disk**, which is the same question as "is an echo reload
|
||||
/// coming": all three do-nothing paths above answer `false`, and so does a refusal. The edge
|
||||
/// drag's release hold reads it — an overlay that outlived a write nobody made would draw a
|
||||
/// width the board never got (`LaneResizeSession.end`). Discardable because every other caller
|
||||
/// has nothing to do with the answer.
|
||||
@discardableResult
|
||||
public func setLaneWidth(_ id: ItemID, units: Int) -> Bool {
|
||||
writeLaneWidths([(id, max(1, units))])
|
||||
}
|
||||
|
||||
@@ -1529,11 +1536,12 @@ public final class BoardStore: HealHost {
|
||||
///
|
||||
/// Lanes already at the one-unit floor simply hold there on a decrease — the batch is not
|
||||
/// refused because one member has nowhere to go, matching the style batch's silent-skip shape.
|
||||
public func stepLaneWidths(_ ids: Set<ItemID>, by delta: Int) {
|
||||
@discardableResult
|
||||
public func stepLaneWidths(_ ids: Set<ItemID>, by delta: Int) -> Bool {
|
||||
let changes: [(ItemID, Int)] = snapshot.lanes
|
||||
.filter { ids.contains($0.id) }
|
||||
.map { ($0.id, max(1, LaneLayoutMath.displayUnits(of: $0) + delta)) }
|
||||
writeLaneWidths(changes)
|
||||
return writeLaneWidths(changes)
|
||||
}
|
||||
|
||||
/// The one commit point every width mechanism shares — the edge drag, the context-menu stepper,
|
||||
@@ -1545,14 +1553,16 @@ public final class BoardStore: HealHost {
|
||||
/// `background`): a default lane's frontmatter stays clean whichever mechanism wrote it. A
|
||||
/// hand-written `width: 1` is legal and preserved until the app itself next edits width — the
|
||||
/// unchanged-units guard below skips it, so only a real change reaches the remove.
|
||||
private func writeLaneWidths(_ changes: [(id: ItemID, units: Int)]) {
|
||||
///
|
||||
/// Returns whether the bracket actually wrote — see `setLaneWidth` for who asks and why.
|
||||
private func writeLaneWidths(_ changes: [(id: ItemID, units: Int)]) -> Bool {
|
||||
let writes: [(folder: URL, units: Int, prior: FieldValue<Int>, title: String?)] = changes.compactMap { change in
|
||||
guard let lane = snapshot.lanes.first(where: { $0.id == change.id }),
|
||||
LaneLayoutMath.displayUnits(of: lane) != change.units
|
||||
else { return nil }
|
||||
return (rootURL.appendingPathComponent(change.id.rawValue), change.units, lane.width, lane.title.value)
|
||||
}
|
||||
guard !writes.isEmpty else { return }
|
||||
guard !writes.isEmpty else { return false }
|
||||
|
||||
// The closure's signature is spelled out because of `try?`: with the error discarded at the
|
||||
// call site Swift stops inferring the typed `throws(BoardWriteError)` and widens it to `any
|
||||
@@ -1563,7 +1573,7 @@ public final class BoardStore: HealHost {
|
||||
try Self.setWidth(write.units, at: write.folder)
|
||||
}
|
||||
}
|
||||
guard landed != nil else { return }
|
||||
guard landed != nil else { return false }
|
||||
|
||||
// resize → prior width (13-native-undo.md ▸ Rules). One step whatever the batch's size — the
|
||||
// menu items step every selected lane in one gesture, and one gesture is one step.
|
||||
@@ -1589,6 +1599,7 @@ public final class BoardStore: HealHost {
|
||||
try Self.setWidth(write.units, at: write.folder)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/// The width write itself, spelled once so the gesture and its redo cannot drift apart on the
|
||||
|
||||
@@ -172,6 +172,17 @@ struct BoardView: View {
|
||||
.onChange(of: store.snapshotGeneration) { _, generation in
|
||||
appModel.dragSession.handOff(root: store.rootKey, generation: generation)
|
||||
}
|
||||
// **The lane-resize release hold's hand-off** (`LaneWidthHold`), the same shape one rung
|
||||
// narrower: that overlay stands in for an *arrangement* and any landing retires it, this one
|
||||
// for a *width* and only a landing that carries it will do — an unrelated reload arriving
|
||||
// first must not hand the strip back to a snapshot that still says the old unit count.
|
||||
//
|
||||
// **`landedReloads`, not `snapshotGeneration`**: the question is "is the width current",
|
||||
// asked on every walk that landed, so a reload whose tree came back value-equal — which
|
||||
// skips the assignment and moves no generation — still gets to answer it.
|
||||
.onChange(of: store.landedReloads) { _, _ in
|
||||
resize.handOff(against: store.snapshot.lanes)
|
||||
}
|
||||
.trashPurgeAlert(store: store, confirmations: confirmations)
|
||||
// The board's own anchor for the Style… popover — the surface a board-targeted session hangs
|
||||
// off, since the board has no item to attach to (`styleEditorPresentation`'s `nil` anchor).
|
||||
@@ -435,8 +446,12 @@ struct BoardView: View {
|
||||
/// cross-board case alone, ⌥ being ignored by a within-board lane drag.
|
||||
@ViewBuilder
|
||||
private func laneSlot(_ lane: Lane, standard: CGFloat) -> some View {
|
||||
let resizing = resize.isResizing(lane.id)
|
||||
let units = resizing ? resize.units : LaneLayoutMath.displayUnits(of: lane)
|
||||
// **The session governs through the release**, not just the drag: after the release its
|
||||
// frozen standard and the unit count it *wrote* keep answering here until the snapshot
|
||||
// carries that width back (`LaneWidthHold`). Reading `lane.width` in that window would draw
|
||||
// the pre-drag layout for a round trip.
|
||||
let resizing = resize.governs(lane.id)
|
||||
let units = resize.displayUnits(of: lane)
|
||||
let slotWidth = LaneLayoutMath.slotWidth(units: units, standard: standard, gap: spacing)
|
||||
ZStack(alignment: .topLeading) {
|
||||
if resizing {
|
||||
@@ -474,7 +489,7 @@ struct BoardView: View {
|
||||
store: store,
|
||||
session: resize,
|
||||
laneID: lane.id,
|
||||
committedUnits: LaneLayoutMath.displayUnits(of: lane),
|
||||
committedUnits: units,
|
||||
standard: standard,
|
||||
gap: spacing,
|
||||
window: window
|
||||
@@ -561,6 +576,12 @@ struct BoardView: View {
|
||||
/// value equals what the formula yields once the session ends — the handoff is seamless (see
|
||||
/// `LaneResizeSession`).
|
||||
///
|
||||
/// **"Once the session ends" is the echo, not the release** (`LaneWidthHold`). The equality that
|
||||
/// makes the handoff seamless is between the frozen standard and the formula run over the *new*
|
||||
/// unit total, and the write takes a watcher round trip to put that total in the snapshot;
|
||||
/// dividing the already-grown window by the stale total in between is exactly the two-step the
|
||||
/// hold exists to remove.
|
||||
///
|
||||
/// Otherwise it is the ordinary division, over three contributions:
|
||||
///
|
||||
/// - the **live lanes**' units. A lane in flight is still a lane on the board, so a within-board
|
||||
|
||||
@@ -18,7 +18,9 @@ struct LaneResizeHandle: View {
|
||||
|
||||
let laneID: ItemID
|
||||
|
||||
/// The lane's committed unit count — the k the session starts from.
|
||||
/// The lane's committed unit count — the k the session starts from. What is **on screen**, which
|
||||
/// while a release hold stands is the width that release wrote rather than the stale snapshot's
|
||||
/// (`LaneResizeSession.displayUnits(of:)`).
|
||||
let committedUnits: Int
|
||||
|
||||
/// The strip's standard (1×) width THIS render; captured as the frozen standard the instant the
|
||||
@@ -60,7 +62,10 @@ struct LaneResizeHandle: View {
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 2, coordinateSpace: .global)
|
||||
.onChanged { value in
|
||||
if !session.isResizing(laneID) {
|
||||
// `isDragging`, not `governs`: a release hold on this very lane is still
|
||||
// governing the strip, and a new drag begins over it rather than being
|
||||
// mistaken for the old one still running (`LaneResizeSession.begin`).
|
||||
if !session.isDragging(laneID) {
|
||||
// m5-drag: a resize and a card/lane move must not run at once — they
|
||||
// would both mutate the same strip layout. The board's drag state does
|
||||
// not exist yet; when it does, this is where the `isDragging` guard goes.
|
||||
@@ -71,7 +76,7 @@ struct LaneResizeHandle: View {
|
||||
session.update(translation: value.translation.width)
|
||||
}
|
||||
.onEnded { _ in
|
||||
guard session.isResizing(laneID) else { return }
|
||||
guard session.isDragging(laneID) else { return }
|
||||
session.end { id, units in store.setLaneWidth(id, units: units) }
|
||||
popCursor()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,56 @@ import AppKit
|
||||
import QuartzCore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - The release hold
|
||||
|
||||
/// The hand-off condition for **the lane-resize release hold**, as a value so the state machine is
|
||||
/// testable without a filesystem.
|
||||
///
|
||||
/// The release *writes* the lane's new width, and that write rides the one-way flow — Writer → disk
|
||||
/// → watcher → reload (02-architecture.md § Layering). Between the release and that echo the
|
||||
/// snapshot still says the OLD unit count while the window has already grown, so a session that
|
||||
/// cleared at release would hand the strip to `LaneLayoutMath.standardWidth` dividing the *new*
|
||||
/// window width by the *stale* unit total: every lane takes a proportionally wrong width for a beat
|
||||
/// — the resized one snapping back toward its old share — and the board then jumps a second time
|
||||
/// when the echo lands. This is the drag's committed-overlay hold (DRAG-REORDER.md § The
|
||||
/// committed-overlay hold) applied to the resize, which lives outside the drag machinery and so
|
||||
/// needs its own: the frozen standard and the written unit count keep governing until the snapshot
|
||||
/// carries the write.
|
||||
///
|
||||
/// A deliberately separate type from `CommittedHold` rather than a reuse of it. The two share only
|
||||
/// their shape: that hold's subject is a *rearrangement*, retired by any snapshot landing on the
|
||||
/// board, and this one's is a *value*, which can only be retired by a snapshot that actually carries
|
||||
/// it — an unrelated reload landing first must NOT release this hold, or the two-step is back.
|
||||
struct LaneWidthHold: Equatable, Sendable {
|
||||
|
||||
/// The lane the release wrote.
|
||||
var laneID: ItemID
|
||||
|
||||
/// The unit count it wrote — what the landed snapshot has to agree with.
|
||||
var units: Int
|
||||
|
||||
/// How long the hold may stand with no echo arriving. `CommittedHold.timeout`'s figure and its
|
||||
/// reasoning: comfortably longer than a write plus a watcher round trip, short enough that a
|
||||
/// write nobody echoes does not leave the board drawing a width it never got.
|
||||
static let timeout: Duration = .milliseconds(1500)
|
||||
|
||||
/// Whether a snapshot holding `lanes` retires this hold.
|
||||
///
|
||||
/// The width is read exactly as the board reads it (`LaneLayoutMath.displayUnits`), which is the
|
||||
/// whole of the one-unit case: **a width landing on 1 removes the `width` key**
|
||||
/// (`BoardStore.writeLaneWidths`, the remove-at-default family), so the echo carrying that write
|
||||
/// has no key to compare against — only the display reading sees the 1 that was asked for.
|
||||
///
|
||||
/// A lane no longer in the snapshot retires it too: it was deleted or moved away under the
|
||||
/// gesture, no echo naming it is ever coming, and the snapshot is the authority.
|
||||
func isRetired(by lanes: [Lane]) -> Bool {
|
||||
guard let lane = lanes.first(where: { $0.id == laneID }) else { return true }
|
||||
return LaneLayoutMath.displayUnits(of: lane) == units
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The session
|
||||
|
||||
/// Window-local state for an in-flight lane resize — the right-edge drag of 03-board-ui.md § Lane.
|
||||
/// At most one runs per board window at a time; `BoardView` owns it as `@State` and hands it to the
|
||||
/// lanes and their grab strips.
|
||||
@@ -27,20 +77,36 @@ import SwiftUI
|
||||
/// column count) and the host window's width. They animate on matching curves — `Motion.laneResize`
|
||||
/// and `Motion.laneResizeWindowDuration`, the two faces of 03-board-ui.md § Motion's lane-resize
|
||||
/// entry — so the window edge and the lanes to its right travel as one.
|
||||
///
|
||||
/// ### Three phases, not two
|
||||
///
|
||||
/// *Idle* (`laneID == nil`), *dragging* (`isDragging`), and — after the release — *holding*, which
|
||||
/// is the same governance standing over a write that has not echoed back yet (`LaneWidthHold`). The
|
||||
/// layout reads one question throughout, `governs(_:)`, so the strip cannot tell the last two
|
||||
/// apart; only the handle's gesture does, since a hold is not something a mouse is still driving.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class LaneResizeSession {
|
||||
|
||||
/// The lane being resized; `nil` when idle. Observed — flipping it drives `BoardView`'s
|
||||
/// frozen-standard override and the shadow slot on and off, and `LaneView`'s column count.
|
||||
/// The lane this session governs — being dragged, or holding its written width until the echo;
|
||||
/// `nil` when idle. Observed — flipping it drives `BoardView`'s frozen-standard override and the
|
||||
/// shadow slot on and off, and `LaneView`'s column count.
|
||||
private(set) var laneID: ItemID?
|
||||
|
||||
/// The release hold, or `nil` while the drag is still in flight (or the session idle).
|
||||
/// See `LaneWidthHold`.
|
||||
private(set) var hold: LaneWidthHold?
|
||||
|
||||
/// The dragged lane's live rendered width — tracks the cursor continuously (rubber-banded at
|
||||
/// the ends), so its masonry reflows live between ticks.
|
||||
private(set) var liveWidth: CGFloat = 0
|
||||
|
||||
/// The snapped unit count k. Drives the shadow slot width, the layout slot the siblings
|
||||
/// position off, and the resizing lane's masonry column count. Ticks by ±1 and animates.
|
||||
///
|
||||
/// It stops ticking at the release and becomes **the written count** — the number the hold is
|
||||
/// waiting for the snapshot to agree with, and the one the strip lays the lane out at until it
|
||||
/// does.
|
||||
private(set) var units: Int = 1
|
||||
|
||||
/// The strip's standard (1×) width, frozen at drag start. Used for ALL lane widths in
|
||||
@@ -75,9 +141,40 @@ final class LaneResizeSession {
|
||||
/// (`Motion.prefersReducedMotion`).
|
||||
@ObservationIgnored private var reducedMotion = false
|
||||
|
||||
@ObservationIgnored private var holdTimeoutTask: Task<Void, Never>?
|
||||
|
||||
/// How long this session's hold may stand with no echo arriving — `LaneWidthHold.timeout`, and a
|
||||
/// `var` for one reason only: the dissolve is a `Task` sleeping on the main actor, and a test
|
||||
/// that had to wait the real figure out would be 1.5 s of wall clock in the suite
|
||||
/// (`LaneResizeHoldTests`). Nothing in the app writes it.
|
||||
@ObservationIgnored var holdTimeout: Duration = LaneWidthHold.timeout
|
||||
|
||||
/// Whether the session governs the strip's layout at all — the frozen standard is in force for a
|
||||
/// drag and for the hold that follows it alike.
|
||||
var isActive: Bool { laneID != nil }
|
||||
|
||||
func isResizing(_ id: ItemID) -> Bool { laneID == id }
|
||||
/// Whether a gesture is still driving it. False during the hold, which no mouse is holding.
|
||||
var isDragging: Bool { laneID != nil && hold == nil }
|
||||
|
||||
/// Whether the release has **settled**: the width is written and the layout it produced is being
|
||||
/// held until the echo reload lands (`LaneWidthHold`).
|
||||
var isSettled: Bool { hold != nil }
|
||||
|
||||
/// Whether this session — dragging or holding — is the authority on `id`'s width.
|
||||
func governs(_ id: ItemID) -> Bool { laneID == id }
|
||||
|
||||
/// Whether `id` is the lane a gesture is currently dragging.
|
||||
func isDragging(_ id: ItemID) -> Bool { laneID == id && hold == nil }
|
||||
|
||||
/// The unit count `lane` spans **on screen right now**: this session's while it governs that
|
||||
/// lane — the live snapped count mid-drag, the written count mid-hold — and the snapshot's
|
||||
/// otherwise.
|
||||
///
|
||||
/// The mid-hold answer is the point: for that stretch the snapshot's `width` is the *old* one,
|
||||
/// and every reader that took it at face value would draw the pre-drag layout.
|
||||
func displayUnits(of lane: Lane) -> Int {
|
||||
governs(lane.id) ? units : LaneLayoutMath.displayUnits(of: lane)
|
||||
}
|
||||
|
||||
/// The tick-down re-entry distance: how far the live edge must retreat back into a gap it has
|
||||
/// already crossed before the shadow shrinks (03-board-ui.md § Lane's "10pt release
|
||||
@@ -107,7 +204,13 @@ final class LaneResizeSession {
|
||||
|
||||
/// Starts a resize of `laneID`, freezing the standard width and the gap and measuring how far
|
||||
/// the window can grow on its current screen.
|
||||
///
|
||||
/// **A standing hold is superseded, not waited out** — a second drag is the user's newer answer
|
||||
/// about the same strip, and its own release will arm the hold that matters. `units` is the
|
||||
/// anchor the caller reads off the screen (`displayUnits(of:)`), so a drag begun mid-hold starts
|
||||
/// from the width that is showing rather than from the stale snapshot's.
|
||||
func begin(laneID: ItemID, units: Int, standard: CGFloat, gap: CGFloat, window: NSWindow?) {
|
||||
endHold()
|
||||
self.laneID = laneID
|
||||
self.startUnits = units
|
||||
self.units = units
|
||||
@@ -130,7 +233,7 @@ final class LaneResizeSession {
|
||||
/// delta from `units` to the target, so only the FINAL target gets one `tick` call, not one per
|
||||
/// intermediate step.
|
||||
func update(translation: CGFloat) {
|
||||
guard isActive else { return }
|
||||
guard isDragging else { return }
|
||||
let startSlot = LaneLayoutMath.slotWidth(units: startUnits, standard: standard, gap: gap)
|
||||
liveWidth = LaneLayoutMath.resistedWidth(
|
||||
proposed: startSlot + translation,
|
||||
@@ -146,25 +249,83 @@ final class LaneResizeSession {
|
||||
if target != units { tick(to: target) }
|
||||
}
|
||||
|
||||
/// Commits the snapped unit count and dismisses the session. Order matters for a flash-free
|
||||
/// handoff: write the model FIRST (the session is still active, so the frozen standard still
|
||||
/// governs and the shadow slot does not budge), THEN clear the session inside the snap animation
|
||||
/// — at which point `BoardView` reverts to the viewport-derived standard, which the window
|
||||
/// sizing has kept equal to the frozen one, so the resting layout reproduces the same pixels
|
||||
/// while the live width animates the last sub-tick of overflow/underfill away. The window and
|
||||
/// the siblings are already in place; neither is touched here.
|
||||
/// Commits the snapped unit count and enters the **release hold**.
|
||||
///
|
||||
/// The commit is a *write*, not a snapshot mutation: it goes to disk through the Writer and
|
||||
/// comes back as an ordinary reload (02-architecture.md § Layering's one-way flow), so the lane
|
||||
/// briefly renders at its pre-drag width if the write fails — which is exactly the honesty the
|
||||
/// banner then explains.
|
||||
func end(commit: (ItemID, Int) -> Void) {
|
||||
guard let laneID else { return }
|
||||
commit(laneID, units)
|
||||
withAnimation(Motion.laneResize(reduced: reducedMotion)) {
|
||||
self.laneID = nil
|
||||
self.liveWidth = 0
|
||||
/// comes back as an ordinary reload (02-architecture.md § Layering's one-way flow). Clearing the
|
||||
/// session here — as this used to — hands the strip back to the viewport-derived standard one
|
||||
/// whole round trip too early: the window is already at its new width but the snapshot still
|
||||
/// carries the old unit total, so the division is wrong for every lane and the board visibly
|
||||
/// two-steps, once into that stale arrangement and again when the echo lands. So the session
|
||||
/// keeps governing (`LaneWidthHold`), and all that happens here is the last sub-tick of
|
||||
/// overflow/underfill animating away against the width the release actually wrote.
|
||||
///
|
||||
/// `commit` answers **whether bytes reached disk** (`BoardStore.setLaneWidth`). A `false` — a
|
||||
/// refusal the banner is already explaining, or a drag that ended on the width it started from —
|
||||
/// means no echo is ever coming, so the hold is never armed and the snapshot takes the layout
|
||||
/// back immediately: the lane returns to its pre-drag width, which is the truth.
|
||||
func end(commit: (ItemID, Int) -> Bool) {
|
||||
guard let laneID, hold == nil else { return }
|
||||
let committed = units
|
||||
guard commit(laneID, committed) else { return dissolve() }
|
||||
|
||||
let hold = LaneWidthHold(laneID: laneID, units: committed)
|
||||
self.hold = hold
|
||||
let timeout = holdTimeout
|
||||
holdTimeoutTask?.cancel()
|
||||
holdTimeoutTask = Task { @MainActor [weak self] in
|
||||
try? await Task.sleep(for: timeout)
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
self.expire(hold)
|
||||
}
|
||||
|
||||
// The settle: the live width, which has been tracking the cursor, comes to rest on the slot
|
||||
// the written count names — measured against the FROZEN standard, the one still governing.
|
||||
withAnimation(Motion.laneResize(reduced: reducedMotion)) {
|
||||
liveWidth = LaneLayoutMath.slotWidth(units: committed, standard: standard, gap: gap)
|
||||
}
|
||||
}
|
||||
|
||||
/// **The hand-off**: a walk landed, so a hold whose width the snapshot now agrees with dissolves
|
||||
/// and the snapshot is the authority again.
|
||||
///
|
||||
/// Called from `BoardView`'s watch on `BoardStore.landedReloads` — *every* landed walk, not just
|
||||
/// the ones that assigned. The condition is "the width the write named is what the current
|
||||
/// snapshot says", never "a counter moved": a reload whose tree came back value-equal skips the
|
||||
/// assignment and does not bump `snapshotGeneration` (02-architecture.md § Live-reload
|
||||
/// resilience), and a hold keyed to that bump would sit out its whole deadline over a snapshot
|
||||
/// that already agreed with it. Retiring on the *value* is right whichever counter moved.
|
||||
func handOff(against lanes: [Lane]) {
|
||||
guard let hold, hold.isRetired(by: lanes) else { return }
|
||||
dissolve()
|
||||
}
|
||||
|
||||
/// The deadline's own body, spelled as a method rather than inlined in the `Task` so the
|
||||
/// dissolve can be pinned directly as well as through the clock (`LaneResizeHoldTests`).
|
||||
///
|
||||
/// A hold that has already handed off — or been superseded by a second drag's — is not this
|
||||
/// one's to end.
|
||||
func expire(_ hold: LaneWidthHold) {
|
||||
guard self.hold == hold else { return }
|
||||
dissolve()
|
||||
}
|
||||
|
||||
/// Governance ends: the strip goes back to the viewport-derived standard and the snapshot's unit
|
||||
/// counts. Animated on the resize curve because the two are only *meant* to be the same pixels —
|
||||
/// on the echo path they are, and the animation shows nothing; on the timeout path they are not,
|
||||
/// and what moves is the resize un-happening.
|
||||
private func dissolve() {
|
||||
endHold()
|
||||
withAnimation(Motion.laneResize(reduced: reducedMotion)) {
|
||||
laneID = nil
|
||||
liveWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func endHold() {
|
||||
hold = nil
|
||||
holdTimeoutTask?.cancel()
|
||||
holdTimeoutTask = nil
|
||||
}
|
||||
|
||||
// MARK: - Tick
|
||||
|
||||
Reference in New Issue
Block a user