jumpy scroll 3
This commit is contained in:
+25
-18
@@ -1,6 +1,5 @@
|
||||
import AppKit
|
||||
import ByreAPI
|
||||
import os
|
||||
import SwiftUI
|
||||
|
||||
enum ChatKind: Equatable {
|
||||
@@ -18,6 +17,7 @@ final class ChatModel {
|
||||
private(set) var fetching = false
|
||||
private(set) var olderGeneration = 0
|
||||
private(set) var hasUnmerged = false
|
||||
var scannerDrop: CGFloat = 0
|
||||
@ObservationIgnored var stuck = true
|
||||
@ObservationIgnored private var unmergedTimer: Task<Void, Never>?
|
||||
@ObservationIgnored private var offs: [() -> Void] = []
|
||||
@@ -486,13 +486,10 @@ struct CompactButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
let scrollLog = Logger(subsystem: "run.fc.byre.mac", category: "scroll")
|
||||
|
||||
private struct ScrollMetrics: Equatable {
|
||||
let content: CGFloat
|
||||
let visible: CGFloat
|
||||
let distance: CGFloat
|
||||
let offset: CGFloat
|
||||
}
|
||||
|
||||
private struct TranscriptScroll: View {
|
||||
@@ -532,44 +529,39 @@ private struct TranscriptScroll: View {
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
if running && model.key != nil {
|
||||
Scanner(color: accent)
|
||||
.offset(y: 8)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
if running && model.key != nil {
|
||||
ScannerOverlay(model: model, color: accent)
|
||||
}
|
||||
}
|
||||
.scrollPosition($position)
|
||||
.defaultScrollAnchor(.bottom)
|
||||
.onScrollGeometryChange(for: ScrollMetrics.self) { g in
|
||||
ScrollMetrics(content: g.contentSize.height, visible: g.visibleRect.height, distance: g.contentSize.height - g.visibleRect.maxY, offset: g.contentOffset.y)
|
||||
ScrollMetrics(content: g.contentSize.height, visible: g.visibleRect.height, distance: g.contentSize.height - g.visibleRect.maxY)
|
||||
} action: { old, new in
|
||||
let snap = !scrolling && model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible)
|
||||
scrollLog.notice("geom rows=\(model.builder.root.rows.count) content=\(new.content) dContent=\(new.content - old.content) offset=\(new.offset) dOffset=\(new.offset - old.offset) visible=\(new.visible) distance=\(new.distance) scrolling=\(scrolling) stuck=\(model.stuck) snap=\(snap)")
|
||||
model.scannerDrop = new.distance
|
||||
if scrolling {
|
||||
model.stuck = new.distance < 80
|
||||
return
|
||||
}
|
||||
if snap {
|
||||
if model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible) {
|
||||
position.scrollTo(edge: .bottom)
|
||||
}
|
||||
}
|
||||
.onScrollPhaseChange { old, phase in
|
||||
scrollLog.notice("phase \(String(describing: old), privacy: .public) -> \(String(describing: phase), privacy: .public)")
|
||||
.onScrollPhaseChange { _, phase in
|
||||
scrolling = phase.isScrolling
|
||||
}
|
||||
.onChange(of: model.builder.pending.last?.id) { old, new in
|
||||
guard let new, new > old ?? 0 else {
|
||||
return
|
||||
}
|
||||
scrollLog.notice("pending scrollTo bottom")
|
||||
model.stuck = true
|
||||
position.scrollTo(edge: .bottom)
|
||||
}
|
||||
.onChange(of: model.scrollRequest?.seq, initial: true) {
|
||||
if let r = model.scrollRequest {
|
||||
scrollLog.notice("request scrollTo \(String(describing: r.id), privacy: .public)")
|
||||
position.scrollTo(id: r.id, anchor: r.anchor)
|
||||
}
|
||||
}
|
||||
@@ -580,6 +572,21 @@ private struct TranscriptScroll: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct ScannerOverlay: View {
|
||||
let model: ChatModel
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Scanner(color: color)
|
||||
.padding(.leading, 16)
|
||||
.padding(.bottom, 8)
|
||||
.offset(y: model.scannerDrop)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
||||
.clipped()
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
private struct Scanner: View {
|
||||
let color: Color
|
||||
|
||||
|
||||
Reference in New Issue
Block a user