mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-27 05:28:37 +00:00

Fixes #8731 The progress view in macOS 26 is broken in ways we can't work around directly. Instead, we must create our own custom progress bar. Luckily, our usage of the progress view is very simple. Amp threads: https://ampcode.com/threads/T-88b550b7-5e0d-4ab9-97d9-36fb63d18f21 https://ampcode.com/threads/T-721d6085-21d5-497d-b6ac-9f203aae0b94
114 lines
3.6 KiB
Swift
114 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
/// The progress bar to show a surface progress report. We implement this from scratch because the
|
|
/// standard ProgressView is broken on macOS 26 and this is simple anyways and gives us a ton of
|
|
/// control.
|
|
struct SurfaceProgressBar: View {
|
|
let report: Ghostty.Action.ProgressReport
|
|
|
|
private var color: Color {
|
|
switch report.state {
|
|
case .error: return .red
|
|
case .pause: return .orange
|
|
default: return .accentColor
|
|
}
|
|
}
|
|
|
|
private var progress: UInt8? {
|
|
// If we have an explicit progress use that.
|
|
if let v = report.progress { return v }
|
|
|
|
// Otherwise, if we're in the pause state, we act as if we're at 100%.
|
|
if report.state == .pause { return 100 }
|
|
|
|
return nil
|
|
}
|
|
|
|
private var accessibilityLabel: String {
|
|
switch report.state {
|
|
case .error: return "Terminal progress - Error"
|
|
case .pause: return "Terminal progress - Paused"
|
|
case .indeterminate: return "Terminal progress - In progress"
|
|
default: return "Terminal progress"
|
|
}
|
|
}
|
|
|
|
private var accessibilityValue: String {
|
|
if let progress {
|
|
return "\(progress) percent complete"
|
|
} else {
|
|
switch report.state {
|
|
case .error: return "Operation failed"
|
|
case .pause: return "Operation paused at completion"
|
|
case .indeterminate: return "Operation in progress"
|
|
default: return "Indeterminate progress"
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
ZStack(alignment: .leading) {
|
|
if let progress {
|
|
// Determinate progress bar with specific percentage
|
|
Rectangle()
|
|
.fill(color)
|
|
.frame(
|
|
width: geometry.size.width * CGFloat(progress) / 100,
|
|
height: geometry.size.height
|
|
)
|
|
.animation(.easeInOut(duration: 0.2), value: progress)
|
|
} else {
|
|
// Indeterminate states without specific progress - all use bouncing animation
|
|
BouncingProgressBar(color: color)
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 2)
|
|
.clipped()
|
|
.allowsHitTesting(false)
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityAddTraits(.updatesFrequently)
|
|
.accessibilityLabel(accessibilityLabel)
|
|
.accessibilityValue(accessibilityValue)
|
|
}
|
|
}
|
|
|
|
/// Bouncing progress bar for indeterminate states
|
|
private struct BouncingProgressBar: View {
|
|
let color: Color
|
|
@State private var position: CGFloat = 0
|
|
|
|
private let barWidthRatio: CGFloat = 0.25
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
ZStack(alignment: .leading) {
|
|
Rectangle()
|
|
.fill(color.opacity(0.3))
|
|
|
|
Rectangle()
|
|
.fill(color)
|
|
.frame(
|
|
width: geometry.size.width * barWidthRatio,
|
|
height: geometry.size.height
|
|
)
|
|
.offset(x: position * (geometry.size.width * (1 - barWidthRatio)))
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(
|
|
.easeInOut(duration: 1.2)
|
|
.repeatForever(autoreverses: true)
|
|
) {
|
|
position = 1
|
|
}
|
|
}
|
|
.onDisappear {
|
|
position = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
|