ios: more ambitious gestures / navigation
This commit is contained in:
@@ -5,7 +5,7 @@ import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
enum SybilWorkspaceNavigationLeadingControl {
|
||||
enum SybilWorkspaceNavigationLeadingControl: Equatable {
|
||||
case back
|
||||
case hidden
|
||||
case showSidebar
|
||||
@@ -17,6 +17,7 @@ struct SybilWorkspaceView: View {
|
||||
var usesCustomWorkspaceNavigation: Bool = true
|
||||
var navigationLeadingControl: SybilWorkspaceNavigationLeadingControl = .back
|
||||
var onShowSidebar: (() -> Void)? = nil
|
||||
var onRequestBack: ((_ animateNavigation: Bool) -> Void)? = nil
|
||||
var onRequestNewChat: (() -> Void)? = nil
|
||||
@FocusState private var composerFocused: Bool
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@@ -49,7 +50,7 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
|
||||
private var showsCustomWorkspaceNavigation: Bool {
|
||||
usesCustomWorkspaceNavigation && !isSettingsSelected
|
||||
usesCustomWorkspaceNavigation && (!isSettingsSelected || navigationLeadingControl == .back)
|
||||
}
|
||||
|
||||
private var transcriptScrollContextID: String {
|
||||
@@ -91,8 +92,20 @@ struct SybilWorkspaceView: View {
|
||||
canSwipeToCreateChat || newChatSwipeIsCompleting
|
||||
}
|
||||
|
||||
private var workspaceSwipeOffset: CGFloat {
|
||||
newChatSwipeOffset
|
||||
}
|
||||
|
||||
private var workspaceCompletionOffset: CGFloat {
|
||||
newChatSwipeCompletionOffset
|
||||
}
|
||||
|
||||
private var workspaceSwipeBlurRadius: CGFloat {
|
||||
NewChatSwipeMetrics.blurRadius(for: newChatSwipeOffset, width: newChatSwipeContainerWidth)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .trailing) {
|
||||
ZStack {
|
||||
if showsNewChatSwipeBackdrop {
|
||||
NewChatSwipeBackdrop(
|
||||
progress: NewChatSwipeMetrics.progress(for: newChatSwipeOffset, width: newChatSwipeContainerWidth),
|
||||
@@ -105,10 +118,10 @@ struct SybilWorkspaceView: View {
|
||||
|
||||
workspaceContent
|
||||
.compositingGroup()
|
||||
.offset(x: newChatSwipeOffset)
|
||||
.blur(radius: NewChatSwipeMetrics.blurRadius(for: newChatSwipeOffset, width: newChatSwipeContainerWidth))
|
||||
.offset(x: workspaceSwipeOffset)
|
||||
.blur(radius: workspaceSwipeBlurRadius)
|
||||
}
|
||||
.offset(x: newChatSwipeCompletionOffset)
|
||||
.offset(x: workspaceCompletionOffset)
|
||||
.background(SybilTheme.background)
|
||||
.navigationTitle(showsCustomWorkspaceNavigation ? "" : viewModel.selectedTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -161,6 +174,7 @@ struct SybilWorkspaceView: View {
|
||||
ZStack(alignment: .bottom) {
|
||||
if isSettingsSelected {
|
||||
SybilSettingsView(viewModel: viewModel)
|
||||
.padding(.top, showsCustomWorkspaceNavigation ? customWorkspaceNavigationContentInset : 0)
|
||||
} else if viewModel.isSearchMode {
|
||||
SybilSearchResultsView(
|
||||
search: viewModel.displayedSearch,
|
||||
@@ -191,7 +205,8 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background {
|
||||
NewChatSwipePanInstaller(
|
||||
WorkspaceSwipePanInstaller(
|
||||
direction: .left,
|
||||
isEnabled: canRecognizeNewChatSwipe,
|
||||
onBegan: { width in
|
||||
beginNewChatSwipe(containerWidth: width)
|
||||
@@ -199,14 +214,16 @@ struct SybilWorkspaceView: View {
|
||||
onChanged: { translationX, width in
|
||||
updateNewChatSwipe(with: translationX, containerWidth: width)
|
||||
},
|
||||
onEnded: { translationX, width, didFinish in
|
||||
onEnded: { translationX, width, velocityX, didFinish in
|
||||
finishNewChatSwipe(
|
||||
translationX: translationX,
|
||||
containerWidth: width,
|
||||
velocityX: velocityX,
|
||||
didFinish: didFinish
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +256,7 @@ struct SybilWorkspaceView: View {
|
||||
switch navigationLeadingControl {
|
||||
case .back:
|
||||
Button {
|
||||
dismiss()
|
||||
requestBack()
|
||||
} label: {
|
||||
SybilNavigationIcon(systemImage: "chevron.left")
|
||||
}
|
||||
@@ -260,6 +277,14 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func requestBack(animateNavigation: Bool = true) {
|
||||
if let onRequestBack {
|
||||
onRequestBack(animateNavigation)
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private func beginNewChatSwipe(containerWidth: CGFloat) {
|
||||
let update = {
|
||||
newChatSwipeContainerWidth = max(containerWidth, 1)
|
||||
@@ -301,30 +326,56 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func finishNewChatSwipe(translationX: CGFloat, containerWidth: CGFloat, didFinish: Bool) {
|
||||
private func finishNewChatSwipe(
|
||||
translationX: CGFloat,
|
||||
containerWidth: CGFloat,
|
||||
velocityX: CGFloat,
|
||||
didFinish: Bool
|
||||
) {
|
||||
guard newChatSwipeIsActive else {
|
||||
resetNewChatSwipe(animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
let finalOffset = NewChatSwipeMetrics.clampedOffset(for: translationX, width: containerWidth)
|
||||
let finalLatched = NewChatSwipeMetrics.isLatched(
|
||||
offset: finalOffset,
|
||||
width: containerWidth,
|
||||
isCurrentlyLatched: newChatSwipeHasLatched
|
||||
)
|
||||
updateNewChatSwipe(with: translationX, containerWidth: containerWidth)
|
||||
|
||||
if didFinish && newChatSwipeHasLatched {
|
||||
if didFinish && NewChatSwipeMetrics.shouldComplete(
|
||||
offset: finalOffset,
|
||||
velocityX: velocityX,
|
||||
width: containerWidth,
|
||||
isLatched: finalLatched
|
||||
) {
|
||||
Task {
|
||||
await completeNewChatSwipe(containerWidth: containerWidth)
|
||||
await completeNewChatSwipe(
|
||||
containerWidth: containerWidth,
|
||||
releaseVelocityX: velocityX
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resetNewChatSwipe(animated: true)
|
||||
resetNewChatSwipe(animated: true, velocityX: velocityX)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func completeNewChatSwipe(containerWidth: CGFloat) async {
|
||||
private func completeNewChatSwipe(containerWidth: CGFloat, releaseVelocityX: CGFloat) async {
|
||||
newChatSwipeIsCompleting = true
|
||||
let targetOffset = NewChatSwipeMetrics.completionTargetOffset(for: containerWidth)
|
||||
|
||||
withAnimation(.easeIn(duration: NewChatSwipeMetrics.completionAnimationDuration)) {
|
||||
newChatSwipeCompletionOffset = -(containerWidth + NewChatSwipeMetrics.completionOvershoot)
|
||||
withAnimation(
|
||||
NewChatSwipeMetrics.springAnimation(
|
||||
currentOffset: newChatSwipeOffset,
|
||||
targetOffset: targetOffset,
|
||||
velocityX: releaseVelocityX
|
||||
)
|
||||
) {
|
||||
newChatSwipeCompletionOffset = targetOffset - newChatSwipeOffset
|
||||
}
|
||||
|
||||
try? await Task.sleep(for: .milliseconds(NewChatSwipeMetrics.completionAnimationDelayMs))
|
||||
@@ -332,7 +383,8 @@ struct SybilWorkspaceView: View {
|
||||
resetNewChatSwipe(animated: false)
|
||||
}
|
||||
|
||||
private func resetNewChatSwipe(animated: Bool) {
|
||||
private func resetNewChatSwipe(animated: Bool, velocityX: CGFloat = 0) {
|
||||
let currentOffset = newChatSwipeOffset + newChatSwipeCompletionOffset
|
||||
let reset = {
|
||||
newChatSwipeOffset = 0
|
||||
newChatSwipeCompletionOffset = 0
|
||||
@@ -343,7 +395,13 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
|
||||
if animated {
|
||||
withAnimation(.spring(response: 0.28, dampingFraction: 0.82)) {
|
||||
withAnimation(
|
||||
NewChatSwipeMetrics.springAnimation(
|
||||
currentOffset: currentOffset,
|
||||
targetOffset: 0,
|
||||
velocityX: velocityX
|
||||
)
|
||||
) {
|
||||
reset()
|
||||
}
|
||||
} else {
|
||||
@@ -405,7 +463,9 @@ struct SybilWorkspaceView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var workspaceNavigationTrailingControl: some View {
|
||||
if viewModel.isSearchMode {
|
||||
if isSettingsSelected {
|
||||
EmptyView()
|
||||
} else if viewModel.isSearchMode {
|
||||
searchModeNavigationLabel
|
||||
} else {
|
||||
providerModelNavigationMenu
|
||||
@@ -550,7 +610,7 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.top, 34)
|
||||
.padding(.top, 64)
|
||||
.padding(.bottom, 12)
|
||||
.background(alignment: .bottom) {
|
||||
SybilComposerFadeBackground()
|
||||
@@ -717,9 +777,8 @@ enum NewChatSwipeMetrics {
|
||||
static let directionDominanceRatio: CGFloat = 1.22
|
||||
static let minimumLeftwardVelocity: CGFloat = 55
|
||||
static let latchHysteresis: CGFloat = 32
|
||||
static let completionOvershoot: CGFloat = 180
|
||||
static let completionAnimationDuration = 0.24
|
||||
static let completionAnimationDelayMs: UInt64 = 240
|
||||
static let completionOvershoot = WorkspaceSwipePhysics.completionOvershoot
|
||||
static let completionAnimationDelayMs = WorkspaceSwipePhysics.completionAnimationDelayMs
|
||||
|
||||
static func maxTravel(for width: CGFloat) -> CGFloat {
|
||||
min(max(width * 0.46, 156), 240)
|
||||
@@ -775,13 +834,185 @@ enum NewChatSwipeMetrics {
|
||||
}
|
||||
return distance >= latchDistance(for: width)
|
||||
}
|
||||
|
||||
static func shouldComplete(offset: CGFloat, velocityX: CGFloat, width: CGFloat, isLatched: Bool) -> Bool {
|
||||
WorkspaceSwipePhysics.shouldComplete(
|
||||
offset: offset,
|
||||
velocityX: velocityX,
|
||||
width: width,
|
||||
directionSign: -1,
|
||||
isLatched: isLatched,
|
||||
latchDistance: latchDistance(for: width)
|
||||
)
|
||||
}
|
||||
|
||||
static func completionTargetOffset(for width: CGFloat) -> CGFloat {
|
||||
WorkspaceSwipePhysics.completionTargetOffset(for: width, directionSign: -1)
|
||||
}
|
||||
|
||||
static func springAnimation(currentOffset: CGFloat, targetOffset: CGFloat, velocityX: CGFloat) -> Animation {
|
||||
WorkspaceSwipePhysics.springAnimation(
|
||||
currentOffset: currentOffset,
|
||||
targetOffset: targetOffset,
|
||||
velocityX: velocityX
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
enum BackSwipeMetrics {
|
||||
static let referenceWidth: CGFloat = NewChatSwipeMetrics.referenceWidth
|
||||
static let horizontalActivationDistance: CGFloat = NewChatSwipeMetrics.horizontalActivationDistance
|
||||
static let directionDominanceRatio: CGFloat = NewChatSwipeMetrics.directionDominanceRatio
|
||||
static let minimumRightwardVelocity: CGFloat = NewChatSwipeMetrics.minimumLeftwardVelocity
|
||||
static let latchHysteresis: CGFloat = NewChatSwipeMetrics.latchHysteresis
|
||||
static let completionOvershoot: CGFloat = NewChatSwipeMetrics.completionOvershoot
|
||||
static let completionAnimationDelayMs = NewChatSwipeMetrics.completionAnimationDelayMs
|
||||
|
||||
static func maxTravel(for width: CGFloat) -> CGFloat {
|
||||
NewChatSwipeMetrics.maxTravel(for: width)
|
||||
}
|
||||
|
||||
static func latchDistance(for width: CGFloat) -> CGFloat {
|
||||
NewChatSwipeMetrics.latchDistance(for: width)
|
||||
}
|
||||
|
||||
static func clampedOffset(for rawTranslation: CGFloat, width: CGFloat) -> CGFloat {
|
||||
min(max(rawTranslation, 0), maxTravel(for: width))
|
||||
}
|
||||
|
||||
static func progress(for offset: CGFloat, width: CGFloat) -> CGFloat {
|
||||
NewChatSwipeMetrics.progress(for: offset, width: width)
|
||||
}
|
||||
|
||||
static func blurRadius(for offset: CGFloat, width: CGFloat) -> CGFloat {
|
||||
NewChatSwipeMetrics.blurRadius(for: offset, width: width)
|
||||
}
|
||||
|
||||
static func shouldBeginPan(
|
||||
rightwardTravel: CGFloat,
|
||||
verticalTravel: CGFloat,
|
||||
rightwardVelocity: CGFloat,
|
||||
verticalVelocity: CGFloat
|
||||
) -> Bool {
|
||||
guard rightwardTravel > 0 || rightwardVelocity > 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
if rightwardTravel >= horizontalActivationDistance,
|
||||
rightwardTravel >= verticalTravel * directionDominanceRatio {
|
||||
return true
|
||||
}
|
||||
|
||||
return rightwardVelocity >= minimumRightwardVelocity &&
|
||||
rightwardVelocity >= verticalVelocity * directionDominanceRatio
|
||||
}
|
||||
|
||||
static func latchReleaseDistance(for width: CGFloat) -> CGFloat {
|
||||
NewChatSwipeMetrics.latchReleaseDistance(for: width)
|
||||
}
|
||||
|
||||
static func isLatched(offset: CGFloat, width: CGFloat, isCurrentlyLatched: Bool = false) -> Bool {
|
||||
NewChatSwipeMetrics.isLatched(offset: offset, width: width, isCurrentlyLatched: isCurrentlyLatched)
|
||||
}
|
||||
|
||||
static func shouldComplete(offset: CGFloat, velocityX: CGFloat, width: CGFloat, isLatched: Bool) -> Bool {
|
||||
WorkspaceSwipePhysics.shouldComplete(
|
||||
offset: offset,
|
||||
velocityX: velocityX,
|
||||
width: width,
|
||||
directionSign: 1,
|
||||
isLatched: isLatched,
|
||||
latchDistance: latchDistance(for: width)
|
||||
)
|
||||
}
|
||||
|
||||
static func completionTargetOffset(for width: CGFloat) -> CGFloat {
|
||||
WorkspaceSwipePhysics.completionTargetOffset(for: width, directionSign: 1)
|
||||
}
|
||||
|
||||
static func springAnimation(currentOffset: CGFloat, targetOffset: CGFloat, velocityX: CGFloat) -> Animation {
|
||||
WorkspaceSwipePhysics.springAnimation(
|
||||
currentOffset: currentOffset,
|
||||
targetOffset: targetOffset,
|
||||
velocityX: velocityX
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkspaceSwipePhysics {
|
||||
static let velocityProjectionDuration: CGFloat = 0.18
|
||||
static let completionVelocityThreshold: CGFloat = 620
|
||||
static let completionOvershoot: CGFloat = 180
|
||||
static let completionAnimationDelayMs: UInt64 = 320
|
||||
|
||||
private static let springMass: Double = 1
|
||||
private static let springStiffness: Double = 300
|
||||
private static let springDamping: Double = 34
|
||||
private static let maximumInitialVelocity: CGFloat = 10
|
||||
|
||||
static func shouldComplete(
|
||||
offset: CGFloat,
|
||||
velocityX: CGFloat,
|
||||
width: CGFloat,
|
||||
directionSign: CGFloat,
|
||||
isLatched: Bool,
|
||||
latchDistance: CGFloat
|
||||
) -> Bool {
|
||||
let directionalOffset = offset * directionSign
|
||||
let directionalVelocity = velocityX * directionSign
|
||||
|
||||
if directionalVelocity <= -completionVelocityThreshold {
|
||||
return false
|
||||
}
|
||||
|
||||
if directionalVelocity >= completionVelocityThreshold {
|
||||
return true
|
||||
}
|
||||
|
||||
let projectedOffset = directionalOffset + directionalVelocity * velocityProjectionDuration
|
||||
return isLatched || projectedOffset >= latchDistance
|
||||
}
|
||||
|
||||
static func completionTargetOffset(for width: CGFloat, directionSign: CGFloat) -> CGFloat {
|
||||
directionSign * (max(width, 1) + completionOvershoot)
|
||||
}
|
||||
|
||||
static func springAnimation(currentOffset: CGFloat, targetOffset: CGFloat, velocityX: CGFloat) -> Animation {
|
||||
.interpolatingSpring(
|
||||
mass: springMass,
|
||||
stiffness: springStiffness,
|
||||
damping: springDamping,
|
||||
initialVelocity: springInitialVelocity(
|
||||
currentOffset: currentOffset,
|
||||
targetOffset: targetOffset,
|
||||
velocityX: velocityX
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static func springInitialVelocity(currentOffset: CGFloat, targetOffset: CGFloat, velocityX: CGFloat) -> Double {
|
||||
let distance = targetOffset - currentOffset
|
||||
guard abs(distance) > 1 else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let normalizedVelocity = velocityX / distance
|
||||
let clampedVelocity = min(max(normalizedVelocity, -maximumInitialVelocity), maximumInitialVelocity)
|
||||
return Double(clampedVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkspaceSwipeDirection {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
struct WorkspaceSwipePanInstaller: UIViewRepresentable {
|
||||
var direction: WorkspaceSwipeDirection
|
||||
var isEnabled: Bool
|
||||
var onBegan: (CGFloat) -> Void
|
||||
var onChanged: (CGFloat, CGFloat) -> Void
|
||||
var onEnded: (CGFloat, CGFloat, Bool) -> Void
|
||||
var onEnded: (CGFloat, CGFloat, CGFloat, Bool) -> Void
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
@@ -797,6 +1028,7 @@ private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
|
||||
func updateUIView(_ uiView: InstallerView, context: Context) {
|
||||
context.coordinator.update(
|
||||
direction: direction,
|
||||
isEnabled: isEnabled,
|
||||
onBegan: onBegan,
|
||||
onChanged: onChanged,
|
||||
@@ -831,10 +1063,11 @@ private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
private let panGesture = UIPanGestureRecognizer()
|
||||
private var preparedScrollRecognizers: Set<ObjectIdentifier> = []
|
||||
|
||||
private var direction: WorkspaceSwipeDirection = .left
|
||||
private var isEnabled = false
|
||||
private var onBegan: (CGFloat) -> Void = { _ in }
|
||||
private var onChanged: (CGFloat, CGFloat) -> Void = { _, _ in }
|
||||
private var onEnded: (CGFloat, CGFloat, Bool) -> Void = { _, _, _ in }
|
||||
private var onEnded: (CGFloat, CGFloat, CGFloat, Bool) -> Void = { _, _, _, _ in }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -846,11 +1079,13 @@ private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
}
|
||||
|
||||
func update(
|
||||
direction: WorkspaceSwipeDirection,
|
||||
isEnabled: Bool,
|
||||
onBegan: @escaping (CGFloat) -> Void,
|
||||
onChanged: @escaping (CGFloat, CGFloat) -> Void,
|
||||
onEnded: @escaping (CGFloat, CGFloat, Bool) -> Void
|
||||
onEnded: @escaping (CGFloat, CGFloat, CGFloat, Bool) -> Void
|
||||
) {
|
||||
self.direction = direction
|
||||
self.isEnabled = isEnabled
|
||||
self.onBegan = onBegan
|
||||
self.onChanged = onChanged
|
||||
@@ -909,6 +1144,7 @@ private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
|
||||
let width = max(markerView.bounds.width, 1)
|
||||
let translationX = recognizer.translation(in: markerView).x
|
||||
let velocityX = recognizer.velocity(in: markerView).x
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
@@ -919,16 +1155,16 @@ private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
onChanged(translationX, width)
|
||||
|
||||
case .ended:
|
||||
onEnded(translationX, width, true)
|
||||
onEnded(translationX, width, velocityX, true)
|
||||
|
||||
case .cancelled, .failed:
|
||||
onEnded(translationX, width, false)
|
||||
onEnded(translationX, width, velocityX, false)
|
||||
|
||||
case .possible:
|
||||
break
|
||||
|
||||
@unknown default:
|
||||
onEnded(translationX, width, false)
|
||||
onEnded(translationX, width, velocityX, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,12 +1183,22 @@ private struct NewChatSwipePanInstaller: UIViewRepresentable {
|
||||
|
||||
let translation = panGesture.translation(in: markerView)
|
||||
let velocity = panGesture.velocity(in: markerView)
|
||||
return NewChatSwipeMetrics.shouldBeginPan(
|
||||
leftwardTravel: max(-translation.x, 0),
|
||||
verticalTravel: abs(translation.y),
|
||||
leftwardVelocity: max(-velocity.x, 0),
|
||||
verticalVelocity: abs(velocity.y)
|
||||
)
|
||||
switch direction {
|
||||
case .left:
|
||||
return NewChatSwipeMetrics.shouldBeginPan(
|
||||
leftwardTravel: max(-translation.x, 0),
|
||||
verticalTravel: abs(translation.y),
|
||||
leftwardVelocity: max(-velocity.x, 0),
|
||||
verticalVelocity: abs(velocity.y)
|
||||
)
|
||||
case .right:
|
||||
return BackSwipeMetrics.shouldBeginPan(
|
||||
rightwardTravel: max(translation.x, 0),
|
||||
verticalTravel: abs(translation.y),
|
||||
rightwardVelocity: max(velocity.x, 0),
|
||||
verticalVelocity: abs(velocity.y)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(
|
||||
@@ -1027,10 +1273,10 @@ private struct SybilComposerFadeBackground: View {
|
||||
],
|
||||
center: .bottomLeading,
|
||||
startRadius: 8,
|
||||
endRadius: 180
|
||||
endRadius: 80
|
||||
)
|
||||
.blendMode(.screen)
|
||||
.offset(x: -42, y: 42)
|
||||
.offset(y: 42)
|
||||
}
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
}
|
||||
@@ -1076,7 +1322,7 @@ private struct SybilNavigationFadeBackground: View {
|
||||
endRadius: 210
|
||||
)
|
||||
.blendMode(.screen)
|
||||
.offset(x: -44, y: -46)
|
||||
.offset(y: -46)
|
||||
}
|
||||
.frame(height: 200.0)
|
||||
.ignoresSafeArea(edges: .top)
|
||||
|
||||
Reference in New Issue
Block a user