Keyboard ←/→ keep their place — a sticky ordinal that outlives the clamp
Lateral card navigation was pure geometry: the nearest drawn frame in the direction. That loses the walk in the card's own title — stepping from a 10-card lane's 8th card into a 3-card lane clamps to its 3rd, and coming back out, "the nearest frame at that height" is the 3rd card's height. The information the user was walking at stopped being on screen, so no rule over rectangles could have recovered it. So it is remembered instead. `TransientBoardState.lateralOrdinal` holds the 1-based position a run of ←/→ started from, counted over the cards the board is showing, and `NavigationMath.lateralHop` lands each hop on `min(ordinal, target lane's count)` of the next lane that is showing cards — collapsed and query-emptied lanes hopped over on `firstCard`'s rule rather than by the accident of registering no frames. 8th → 3rd → 8th. Every reset comes from one funnel and needs no enumeration anywhere: the ordinal is a defaulted `nil` parameter on `select`, so a click, a marquee, a ↑/↓ step, an ⌥-jump, a ⌫ successor, a lane-domain arrow and the reload's focus recovery all end the run by saying nothing. `resolve` adds the one rule a value referencing no item can need — the ordinal never outlives the head it was counted from — while a reload that leaves the cursor standing leaves the run standing too. A wide lane's interior masonry columns keep their spatial step and carry the ordinal through untouched: a column hop is not a lane hop, and stickiness is lane-granular over the logical order. With no lane in the direction the geometry has the last word, which is how → still reaches the shown trash. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -4799,15 +4799,23 @@ public final class BoardStore: HealHost {
|
||||
///
|
||||
/// - Parameter defaultsSoleMember: forwarded verbatim — see `TransientBoardState.select`, whose
|
||||
/// sole-member default the rubber band opts out of.
|
||||
/// - Parameter lateralOrdinal: forwarded verbatim — see `TransientBoardState.lateralOrdinal`,
|
||||
/// whose `nil` default is what makes every caller here a reset of the ←/→ run.
|
||||
public func select(
|
||||
_ ids: Set<ItemID>,
|
||||
in container: ItemContainer,
|
||||
anchor: ItemID? = nil,
|
||||
head: ItemID? = nil,
|
||||
defaultsSoleMember: Bool = true
|
||||
defaultsSoleMember: Bool = true,
|
||||
lateralOrdinal: Int? = nil
|
||||
) {
|
||||
transient.select(
|
||||
ids, in: container, anchor: anchor, head: head, defaultsSoleMember: defaultsSoleMember
|
||||
ids,
|
||||
in: container,
|
||||
anchor: anchor,
|
||||
head: head,
|
||||
defaultsSoleMember: defaultsSoleMember,
|
||||
lateralOrdinal: lateralOrdinal
|
||||
)
|
||||
transient.noteActiveLane(Self.lane(holding: ids, in: snapshot))
|
||||
}
|
||||
|
||||
@@ -282,6 +282,29 @@ public final class TransientBoardState {
|
||||
/// by `resolve(against:)` under the same universe rule.
|
||||
public private(set) var selectionHead: ItemID?
|
||||
|
||||
/// **What position in its lane a run of ←/→ is holding on to** — the sticky ordinal (ruled
|
||||
/// 2026-08-09), 1-based over the cards the board is *showing*, `nil` when no lateral run is in
|
||||
/// flight.
|
||||
///
|
||||
/// The third memory of a gesture, and the one that exists because a *screen* cannot remember it:
|
||||
/// stepping from a 10-card lane's 8th card into a 3-card lane clamps to its 3rd, and every rule
|
||||
/// that reads drawn rectangles — `NavigationMath.nearest` included — then has only "3rd card"
|
||||
/// to step back out with. The number the user is walking at survives here instead, unclamped, so
|
||||
/// the hop back lands on the 8th again (`NavigationMath.lateralHop`).
|
||||
///
|
||||
/// **`nil` is a reset, and every existing caller gets one for free**: `select` takes it as a
|
||||
/// defaulted parameter and stores it verbatim, so a click, a marquee, a ↑/↓ step, an ⌥-jump, a
|
||||
/// ⌫ successor, a lane-domain arrow and the reload's focus recovery all end the run without any
|
||||
/// of them naming it. Only the lateral hop passes a value. Reading `nil` on the next hop is not a
|
||||
/// missing answer either — it means "start a run here", which captures the cursor's *actual*
|
||||
/// position, which is the ruling's "resets to the actual new position" arrived at lazily.
|
||||
///
|
||||
/// **An ordinal rather than a card**, deliberately: the thing being preserved is a position in a
|
||||
/// list, and the lists on either side of a hop are different lanes with different lengths. It
|
||||
/// therefore references no item, which is why `resolve(against:)` has only one thing to say about
|
||||
/// it — see there.
|
||||
public private(set) var lateralOrdinal: Int?
|
||||
|
||||
/// The items a drag is carrying — **empty when no drag is in flight**, which is what "no drag"
|
||||
/// means here rather than a separate flag.
|
||||
///
|
||||
@@ -406,26 +429,33 @@ public final class TransientBoardState {
|
||||
/// `resolve(against:)` on the next reload is what keeps the set honest over time.
|
||||
///
|
||||
/// - Parameter defaultsSoleMember: whether a `nil` anchor or head falls back to a sole member.
|
||||
/// - Parameter lateralOrdinal: the sticky ordinal a run of ←/→ is carrying, or `nil` — which is
|
||||
/// what **every** caller but the lateral hop passes, and is why "a non-lateral selection change
|
||||
/// resets it" needs no enumeration of the gestures anywhere. See `lateralOrdinal`.
|
||||
public func select(
|
||||
_ ids: Set<ItemID>,
|
||||
in container: ItemContainer,
|
||||
anchor: ItemID? = nil,
|
||||
head: ItemID? = nil,
|
||||
defaultsSoleMember: Bool = true
|
||||
defaultsSoleMember: Bool = true,
|
||||
lateralOrdinal: Int? = nil
|
||||
) {
|
||||
selection = ItemReferenceSet(ids: ids, container: container)
|
||||
let sole = defaultsSoleMember && ids.count == 1 ? ids.first : nil
|
||||
selectionAnchor = anchor ?? sole
|
||||
selectionHead = head ?? sole
|
||||
self.lateralOrdinal = lateralOrdinal
|
||||
}
|
||||
|
||||
/// Selects nothing — Escape's last step outward (04-interactions.md ▸ Grammar). The anchor and
|
||||
/// the head go with it: an empty selection has no origin to range from and no cursor to step
|
||||
/// from — which is exactly the state the arrows' seed rule answers.
|
||||
/// from — which is exactly the state the arrows' seed rule answers. The sticky ordinal goes for
|
||||
/// the same reason: there is no run left to be in the middle of.
|
||||
public func clearSelection() {
|
||||
selection = .empty
|
||||
selectionAnchor = nil
|
||||
selectionHead = nil
|
||||
lateralOrdinal = nil
|
||||
}
|
||||
|
||||
/// Records that `laneID` is where the user is working — a lane selected, or created into.
|
||||
@@ -681,6 +711,13 @@ public final class TransientBoardState {
|
||||
if let anchor = selectionAnchor, !universe.contains(anchor) { selectionAnchor = nil }
|
||||
if let head = selectionHead, !universe.contains(head) { selectionHead = nil }
|
||||
}
|
||||
// **The sticky ordinal never outlives the head it was counted from** — the one thing a reload
|
||||
// has to say about a value that references no item. A run of ←/→ whose cursor vanished has
|
||||
// nothing left to be a run *of*, and the next arrow re-derives its head from the selection's
|
||||
// last member, which is a position the run never named. A reload that leaves the head standing
|
||||
// deliberately leaves the run standing too: an agent filing a card mid-walk is not the user
|
||||
// changing their mind.
|
||||
if selectionHead == nil { lateralOrdinal = nil }
|
||||
|
||||
constrainToSearch(in: snapshot, commentMatches: commentMatches)
|
||||
}
|
||||
@@ -726,6 +763,9 @@ public final class TransientBoardState {
|
||||
selection = selection.constrained(to: universe)
|
||||
if let anchor = selectionAnchor, !universe.contains(anchor) { selectionAnchor = nil }
|
||||
if let head = selectionHead, !universe.contains(head) { selectionHead = nil }
|
||||
// A hidden cursor ends the lateral run, for `resolve`'s reason: the ordinal counts what the
|
||||
// user can see, and a run stepping from a card the filter took away counts from nowhere.
|
||||
if selectionHead == nil { lateralOrdinal = nil }
|
||||
}
|
||||
|
||||
/// The placeholder's two discard rules, as a pure function of the placeholder and the snapshot.
|
||||
|
||||
Reference in New Issue
Block a user