Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 73 additions & 28 deletions apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -905,18 +905,21 @@ struct AccountRunSummaryView: View {
@State private var placementStore = AccountRunStripPlacementStore()
@State private var scrollProxy = AccountRunStripScrollProxy()
@State private var scrollMetrics = AccountRunStripMetrics()
@State private var showsEdgeControls = false

var body: some View {
HStack(spacing: AccountRunStripLayout.edgeControlSpacing) {
AccountRunStripEdgeButton(
direction: .backward,
isEnabled: scrollMetrics.canScrollBackward
) {
scrollProxy.scrollToAdjacentRun(.backward)
} startContinuousAction: {
scrollProxy.startContinuousScroll(.backward)
} stopContinuousAction: {
scrollProxy.stopContinuousScroll()
if showsEdgeControls {
AccountRunStripEdgeButton(
direction: .backward,
isEnabled: scrollMetrics.canScrollBackward
) {
scrollProxy.scrollToAdjacentRun(.backward)
} startContinuousAction: {
scrollProxy.startContinuousScroll(.backward)
} stopContinuousAction: {
scrollProxy.stopContinuousScroll()
}
}

AccountRunStripScrollView(
Expand All @@ -942,19 +945,21 @@ struct AccountRunSummaryView: View {
.coordinateSpace(name: AccountRunStripLayout.contentCoordinateSpace)
}
.mask {
AccountRunStripFadeMask(metrics: scrollMetrics)
AccountRunStripFadeMask(metrics: showsEdgeControls ? scrollMetrics : AccountRunStripMetrics())
}
.frame(maxWidth: .infinity, alignment: .leading)

AccountRunStripEdgeButton(
direction: .forward,
isEnabled: scrollMetrics.canScrollForward
) {
scrollProxy.scrollToAdjacentRun(.forward)
} startContinuousAction: {
scrollProxy.startContinuousScroll(.forward)
} stopContinuousAction: {
scrollProxy.stopContinuousScroll()
if showsEdgeControls {
AccountRunStripEdgeButton(
direction: .forward,
isEnabled: scrollMetrics.canScrollForward
) {
scrollProxy.scrollToAdjacentRun(.forward)
} startContinuousAction: {
scrollProxy.startContinuousScroll(.forward)
} stopContinuousAction: {
scrollProxy.stopContinuousScroll()
}
}
}
.frame(height: AccountRunChipLayout.height)
Expand All @@ -970,24 +975,39 @@ struct AccountRunSummaryView: View {
}

private func updateScrollMetrics(_ metrics: AccountRunStripMetrics) {
guard metrics != scrollMetrics else {
let nextShowsEdgeControls = shouldShowEdgeControls(for: metrics)
guard metrics != scrollMetrics || nextShowsEdgeControls != showsEdgeControls else {
return
}

if showsEdgeControls && nextShowsEdgeControls == false {
scrollProxy.stopContinuousScroll()
}

var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
scrollMetrics = metrics
showsEdgeControls = nextShowsEdgeControls
}
}

private func shouldShowEdgeControls(for metrics: AccountRunStripMetrics) -> Bool {
let reservedWidth = showsEdgeControls ? AccountRunStripLayout.edgeControlReservedWidth : 0
let viewportWidthWithoutEdgeControls = metrics.viewportWidth + reservedWidth

return metrics.contentWidth > viewportWidthWithoutEdgeControls + AccountRunStripLayout.overflowTolerance
}
}

private enum AccountRunStripLayout {
static let contentCoordinateSpace = "account-run-strip-content"
static let dragActivationDistance: CGFloat = 1
static let edgeControlSpacing: CGFloat = 4
static let edgeControlWidth: CGFloat = 12
static let edgeControlReservedWidth = edgeControlWidth * 2 + edgeControlSpacing * 2
static let fadeWidth: CGFloat = 24
static let overflowTolerance: CGFloat = 1
static let wheelLineDeltaScale: CGFloat = 11
static let wheelMinimumDelta: CGFloat = 0.1
static let clickScrollDuration: TimeInterval = 0.14
Expand Down Expand Up @@ -1027,18 +1047,31 @@ private enum AccountRunStripScrollDirection {
return "Next running lane"
}
}

var disabledHelp: String {
switch self {
case .backward:
return "Already at the first running lane"
case .forward:
return "Already at the last running lane"
}
}
}

private struct AccountRunStripMetrics: Equatable {
var contentWidth: CGFloat = 0
var viewportWidth: CGFloat = 0
var isOverflowing = false
var canScrollBackward = false
var canScrollForward = false

init() {}

init(contentWidth: CGFloat, viewportWidth: CGFloat, offsetX: CGFloat) {
self.contentWidth = contentWidth
self.viewportWidth = viewportWidth
let maxOffsetX = max(0, contentWidth - viewportWidth)
isOverflowing = contentWidth > viewportWidth + 1
isOverflowing = contentWidth > viewportWidth + AccountRunStripLayout.overflowTolerance
canScrollBackward = isOverflowing && offsetX > 1
canScrollForward = isOverflowing && offsetX < maxOffsetX - 1
}
Expand Down Expand Up @@ -1144,13 +1177,12 @@ private struct AccountRunStripEdgeButton: View {
.font(.system(size: 10.5, weight: .semibold))
.symbolRenderingMode(.monochrome)
.foregroundStyle(tint)
.scaleEffect(isPressed ? 0.92 : 1)
.scaleEffect(isEnabled && isPressed ? 0.92 : 1)
.frame(
width: AccountRunStripLayout.edgeControlWidth,
height: AccountRunChipLayout.height
)
.contentShape(Rectangle())
.opacity(isEnabled ? 1 : 0)
.allowsHitTesting(isEnabled)
.highPriorityGesture(
DragGesture(minimumDistance: 0)
Expand All @@ -1164,18 +1196,31 @@ private struct AccountRunStripEdgeButton: View {
.onDisappear {
cancelPress()
}
.help(direction.accessibilityLabel)
.onChange(of: isEnabled) { _, isEnabled in
if isEnabled == false {
cancelPress()
}
}
.help(isEnabled ? direction.accessibilityLabel : direction.disabledHelp)
.accessibilityLabel(direction.accessibilityLabel)
.accessibilityHidden(isEnabled == false)
.accessibilityValue(isEnabled ? "Available" : "Unavailable")
}

private var tint: Color {
PanelPalette.primaryText(colorScheme)
.opacity(isPressed ? 0.92 : colorScheme == .dark ? 0.62 : 0.5)
let opacity: Double
if isEnabled == false {
opacity = colorScheme == .dark ? 0.28 : 0.22
} else if isPressed {
opacity = 0.92
} else {
opacity = colorScheme == .dark ? 0.62 : 0.5
}

return PanelPalette.primaryText(colorScheme).opacity(opacity)
}

private func startPress() {
guard pressTask == nil else {
guard isEnabled, pressTask == nil else {
return
}

Expand Down