Transcript Incremental Projection
Nucleic-Session: 0FFD007B-0696-4517-9429-129C7B0FD5AC Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -633,22 +633,28 @@ struct TranscriptList: View {
|
||||
// they scroll away, which lets new content land off-screen below instead of dragging
|
||||
// the viewport, and restore `.bottom` once they're back at the end.
|
||||
.defaultScrollAnchor(isScrolledToBottom ? .bottom : nil)
|
||||
.onScrollPhaseChange { _, newPhase in scrollPhase = newPhase }
|
||||
// Track the live scroll position straight from the scroll view's geometry: how much
|
||||
// content still sits below the viewport bottom. Within the slack threshold means the
|
||||
// user is parked at the end (live output keeps following); scrolling up flips this
|
||||
// false, which drops the anchor (above) and reveals the jump-to-bottom chevron.
|
||||
.onScrollGeometryChange(for: Bool.self) { geo in
|
||||
geo.contentSize.height - geo.containerSize.height - geo.contentOffset.y
|
||||
<= bottomFollowThreshold
|
||||
} action: { _, atBottom in
|
||||
// While the chat is opening the layout grows over a few passes and the scroll
|
||||
// offset lags each growth by a frame — sampling that frame reads "not at bottom"
|
||||
// even though `.defaultScrollAnchor(.bottom)` is about to re-pin. So during the
|
||||
// open window accept only "at bottom" readings; honor real scroll-ups after.
|
||||
if transcriptSettling {
|
||||
if atBottom { isScrolledToBottom = true }
|
||||
} else {
|
||||
isScrolledToBottom = atBottom
|
||||
// content still sits below the viewport bottom. Parked within the follow threshold
|
||||
// means live output keeps following; a *user* scroll past the (wider) unfollow
|
||||
// threshold flips it false, which drops the anchor (above) and reveals the chevron.
|
||||
// Asymmetric on purpose — see `bottomUnfollowThreshold` / `isUserScrolling`: a
|
||||
// programmatic geometry change (anchor re-pin, bar resize, keyboard, autoscroll
|
||||
// animation) may re-engage following but can never disengage it, so no layout
|
||||
// feedback cycle through this gate can sustain itself. Tracking the rounded distance
|
||||
// (not a Bool) also means every scroll emits fresh values, so the gate can't latch
|
||||
// against a stale reading (the old settle-window latch bug).
|
||||
.onScrollGeometryChange(for: CGFloat.self) { geo in
|
||||
(geo.contentSize.height - geo.containerSize.height - geo.contentOffset.y).rounded()
|
||||
} action: { _, distance in
|
||||
if distance <= bottomFollowThreshold {
|
||||
isScrolledToBottom = true
|
||||
} else if !transcriptSettling, distance > bottomUnfollowThreshold, isUserScrolling {
|
||||
// While the chat is opening the layout grows over a few passes and the offset
|
||||
// lags each growth by a frame — those frames read "not at bottom" even though
|
||||
// `.defaultScrollAnchor(.bottom)` is about to re-pin, so settling accepts
|
||||
// only re-engagement (the `!transcriptSettling` above).
|
||||
isScrolledToBottom = false
|
||||
}
|
||||
}
|
||||
// Detect when the opening layout has come to rest: track the content height while
|
||||
|
||||
Reference in New Issue
Block a user