import CoreGraphics /// Where a drag preview attaches to the pointer, as pure arithmetic (`DragPreviewAnchorTests`). /// /// **SwiftUI gives no anchor control.** `.onDrag(_:preview:)` lays the preview over the *grabbed /// view's* frame, centred on it — which is exactly right while the preview is the size of what was /// grabbed (a card face lifting off its own plate), and wrong the moment it is not. A lane is /// grabbed by its title bar and its replica is the whole lane (03-board-ui.md § Motion: "a faithful, /// full-size replica of the dragged item — the whole lane, not the strip of title bar that was /// grabbed"), so centring that replica on the bar hangs half a lane above the pointer: the cursor /// ends up in the middle of the image instead of on the pixel it grabbed. /// /// The one lever the API does leave is the preview's own geometry, and **transparent padding moves a /// view's centre without moving the view**: pad the side opposite the anchor by however much the /// anchor is off-centre and the padded box's centre lands exactly on it. The system then centres /// *that* box on the grabbed view, which puts the replica's title bar over the real one — and, since /// the replica is drawn at the lane's own size, every other pixel of it over the lane it was lifted /// from. That is the Finder-icon promise stated as geometry: the cursor stays where it grabbed. /// /// Axis-agnostic, and said once because both axes ask the same question. The horizontal one answers /// zero for a lane — a lane replica is as wide as its lane, so its header's centre is already the /// box's — and the arithmetic is written for both anyway rather than assuming that stays true. /// /// The compensation is **invariant under symmetric padding**: the drag replicas wear a transparent /// margin so their shadows are not clipped (`BoardMetrics.replicaPadding`), and adding `p` to both /// ends adds `p` to the length and `p` to the anchor, which cancels. So a caller may compute this /// from the replica's own size and apply it outside that margin, in either order. enum DragPreviewAnchor { /// Extra transparent length to add before and after a preview along one axis. struct Padding: Equatable, Sendable { var before: CGFloat = 0 var after: CGFloat = 0 static let none = Padding() } /// The padding that makes `anchor` — a distance from the preview's leading edge — the centre of /// a preview `length` long. /// /// Only ever one side of it: an anchor above the middle needs room before it, one below needs /// room after it, and the exact middle needs none. The anchor is clamped into the preview, so a /// stale measurement can shift the image by at most its own length. /// /// Degenerate input — an unmeasured, zero or non-finite length — pads nothing, which draws the /// preview exactly as it was drawn before this existed rather than flinging it off the pointer. static func padding(length: CGFloat, anchor: CGFloat) -> Padding { guard length.isFinite, anchor.isFinite, length > 0 else { return .none } let clamped = min(max(anchor, 0), length) let delta = length - 2 * clamped return delta > 0 ? Padding(before: delta) : Padding(after: -delta) } }