From 9bed85bdf06598dd3790cdaf3e0e5fcb5740bb7d Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 10 Sep 2025 20:33:09 +0530 Subject: [PATCH 1/3] Working but small --- .gitignore | 3 +- DynamicIsland/DynamicIslandApp.swift | 3 + DynamicIsland/Localizable.xcstrings | 3 + .../SystemEventIndicatorModifier.swift | 34 +++- .../components/Settings/SettingsView.swift | 12 +- DynamicIsland/models/Constants.swift | 21 +++ segmentedhud.swift.ref | 173 ++++++++++++++++++ 7 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 segmentedhud.swift.ref diff --git a/.gitignore b/.gitignore index ba9d400..2791606 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,5 @@ rough_note.md Solid SettingsView.swift.md logic.md -TheBoringWorker-HUD \ No newline at end of file +TheBoringWorker-HUD +boringnotch + meteo + airpods + traduzione modificabile \ No newline at end of file diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index fc9bf04..04c6ac4 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -378,6 +378,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { coordinator.setupWorkersNotificationObservers() + // Migrate legacy progress bar settings + Defaults.Keys.migrateProgressBarStyle() + // Setup SystemHUD Manager SystemHUDManager.shared.setup(coordinator: coordinator) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 372ee37..570f136 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -1086,6 +1086,9 @@ }, "Seconds" : { + }, + "Segmented" : { + }, "Select a color to view details" : { diff --git a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift index 70f33c7..f6c58ea 100644 --- a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift +++ b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift @@ -102,16 +102,19 @@ struct DraggableProgressBar: View { Capsule() .fill(.tertiary) Group { - if Defaults[.enableGradient] { + switch Defaults[.progressBarStyle] { + case .gradient: Capsule() .fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading)) .frame(width: max(0, min(geo.size.width * value, geo.size.width))) .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) - } else { + case .hierarchical: Capsule() .fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) .frame(width: max(0, min(geo.size.width * value, geo.size.width))) .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) + case .segmented: + SegmentedProgressContent(value: value, geometry: geo) } } .opacity(value.isZero ? 0 : 1) @@ -142,3 +145,30 @@ struct DraggableProgressBar: View { value = max(0, min(newValue, 1)) } } + +struct SegmentedProgressContent: View { + let value: CGFloat + let geometry: GeometryProxy + + private let segmentCount = 20 + + var body: some View { + HStack(spacing: 1) { + ForEach(0..= segmentValue + + RoundedRectangle(cornerRadius: 1) + .fill(isActive ? + (Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) : + .clear) + .frame(width: max(1, (geometry.size.width - CGFloat(segmentCount - 1) * 1) / CGFloat(segmentCount))) + .shadow(color: isActive && Defaults[.systemEventIndicatorShadow] ? + (Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white) : + .clear, radius: 4, x: 1) + .opacity(value.isZero ? 0 : (isActive ? 1 : 0.3)) + } + } + .frame(width: geometry.size.width) + } +} diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 9f8c4ed..e6d33c1 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -419,7 +419,7 @@ struct Downloads: View { struct HUD: View { @EnvironmentObject var vm: DynamicIslandViewModel @Default(.inlineHUD) var inlineHUD - @Default(.enableGradient) var enableGradient + @Default(.progressBarStyle) var progressBarStyle @Default(.enableSystemHUD) var enableSystemHUD @Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD @@ -491,15 +491,17 @@ struct HUD: View { if Defaults[.inlineHUD] { withAnimation { Defaults[.systemEventIndicatorShadow] = false - Defaults[.enableGradient] = false + Defaults[.progressBarStyle] = .hierarchical } } } - Picker("Progressbar style", selection: $enableGradient) { + Picker("Progressbar style", selection: $progressBarStyle) { Text("Hierarchical") - .tag(false) + .tag(ProgressBarStyle.hierarchical) Text("Gradient") - .tag(true) + .tag(ProgressBarStyle.gradient) + Text("Segmented") + .tag(ProgressBarStyle.segmented) } Defaults.Toggle("Enable glowing effect", key: .systemEventIndicatorShadow) Defaults.Toggle("Use accent color", key: .systemEventIndicatorUseAccent) diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 18fd8f3..b52e256 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -97,6 +97,14 @@ enum SneakPeekStyle: String, CaseIterable, Identifiable, Defaults.Serializable { var id: String { self.rawValue } } +enum ProgressBarStyle: String, CaseIterable, Identifiable, Defaults.Serializable { + case hierarchical = "Hierarchical" + case gradient = "Gradient" + case segmented = "Segmented" + + var id: String { self.rawValue } +} + extension Defaults.Keys { // MARK: General static let menubarIcon = Key("menubarIcon", default: true) @@ -171,6 +179,8 @@ extension Defaults.Keys { // MARK: HUD static let inlineHUD = Key("inlineHUD", default: false) + static let progressBarStyle = Key("progressBarStyle", default: .hierarchical) + // Legacy support - keeping for backward compatibility static let enableGradient = Key("enableGradient", default: false) static let systemEventIndicatorShadow = Key("systemEventIndicatorShadow", default: false) static let systemEventIndicatorUseAccent = Key("systemEventIndicatorUseAccent", default: false) @@ -236,4 +246,15 @@ extension Defaults.Keys { return .nowPlaying } } + + // Migration helper to convert from legacy enableGradient Boolean to new ProgressBarStyle enum + static func migrateProgressBarStyle() { + // Check if migration is needed by seeing if the old Boolean was set to gradient + let wasGradientEnabled = Defaults[.enableGradient] + + // Only migrate if we're still using the default hierarchical value but gradient was enabled + if wasGradientEnabled && Defaults[.progressBarStyle] == .hierarchical { + Defaults[.progressBarStyle] = .gradient + } + } } diff --git a/segmentedhud.swift.ref b/segmentedhud.swift.ref new file mode 100644 index 0000000..7fb545e --- /dev/null +++ b/segmentedhud.swift.ref @@ -0,0 +1,173 @@ +import SwiftUI +import Defaults + +struct SegmentedHUD: View { + @EnvironmentObject var vm: BoringViewModel + @Binding var type: SneakContentType + @Binding var value: CGFloat // 0...1 + @Binding var icon: String + + private let segmentCount: Int = 40 + @State private var glowIndex: Int? = nil + @State private var lastValue: CGFloat = 0 + + var body: some View { + HStack(spacing: 16) { + // Icona a sinistra + hudIcon + .frame(width: 25, height: 20) + .offset(x:0, y: 10) + + // Contenuto centrale: percentuale + segmenti + VStack(spacing: 6) { + Text("\(Int(value * 100))%") + .font(.system(size: 14, weight: .medium, design: .rounded)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity, alignment: .center) + .offset(x:182, y:20) + segments + .frame(height: vm.notchState == .open ? 10 : 14) + } + .frame(maxWidth: .infinity) + + // Spacer per bilanciare il layout (stesso width dell'icona) + Spacer() + .frame(width: 20) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .frame(maxWidth: .infinity) + .frame(height: vm.notchState == .open ? vm.closedNotchSize.height + 8 : 70) + .onChange(of: value) { old, newVal in + handleGlow(oldValue: old, newValue: newVal) + } + } + + @ViewBuilder + private var hudIcon: some View { + Group { + switch type { + case .volume: + if icon.isEmpty { + Image(systemName: speakerSymbol(value)) + .symbolVariant(value > 0 ? .none : .slash) + } else { + Image(systemName: icon) + .opacity(value.isZero ? 0.6 : 1) + .scaleEffect(value.isZero ? 0.85 : 1) + } + case .brightness: + Image(systemName: "sun.max.fill") + case .backlight: + Image(systemName: "keyboard") + case .mic: + Image(systemName: "mic") + default: + EmptyView() + } + } + .foregroundStyle(.white) + } + + // Il resto del codice rimane uguale... + private var segments: some View { + GeometryReader { geo in + let width = geo.size.width + let spacing: CGFloat = vm.notchState == .open ? 2.5 : 3.5 // Aumentato spacing + let computed = (width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount) + let barWidth = max(2.5, min(4.5, computed)) // Aumentata larghezza segmenti + let activeCount = Int(round(value * CGFloat(segmentCount))) + + HStack(spacing: spacing) { + ForEach(0.. AnyShapeStyle { + if isActive { + if Defaults[.systemEventIndicatorUseAccent] { + return AnyShapeStyle(Color.accentColor) + } + return AnyShapeStyle(Color.white) + } else { + return AnyShapeStyle(Color.white.opacity(0.2)) + } + } + + private func glowShadowColor(for isActive: Bool, index: Int) -> Color { + if isActive { + if let glowIndex = glowIndex, index == glowIndex { + return Defaults[.systemEventIndicatorUseAccent] + ? Color.accentColor.ensureMinimumBrightness(factor: 0.8) + : Color.white + } else { + return Defaults[.systemEventIndicatorUseAccent] + ? Color.accentColor.ensureMinimumBrightness(factor: 0.7) + : Color.white + } + } + return .clear + } + + private func handleGlow(oldValue: CGFloat, newValue: CGFloat) { + defer { lastValue = newValue } + let oldIndex = Int(round(oldValue * CGFloat(segmentCount))) + let newIndex = Int(round(newValue * CGFloat(segmentCount))) + guard oldIndex != newIndex else { return } + + if newIndex > oldIndex { + animateWave(from: oldIndex, to: newIndex, step: 1) + } else { + animateWave(from: oldIndex - 1, to: newIndex - 1, step: -1) + } + } + + private func animateWave(from start: Int, to end: Int, step: Int) { + let clampedStart = max(0, min(segmentCount - 1, start)) + let clampedEnd = max(-1, min(segmentCount - 1, end)) + let range = stride(from: clampedStart, through: clampedEnd, by: step) + var delay: Double = 0 + for i in range { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) { + glowIndex = i + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + delay + 0.25) { + if glowIndex == i { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + glowIndex = nil + } + } + } + delay += 0.02 + } + } + + private func speakerSymbol(_ value: CGFloat) -> String { + switch value { + case 0: return "speaker" + case 0...0.3: return "speaker.wave.1" + case 0.3...0.8: return "speaker.wave.2" + default: return "speaker.wave.3" + } + } +} From b7e3cff21d76d4cd19aff5c79cf4c7b4540b8d55 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 10 Sep 2025 20:34:19 +0530 Subject: [PATCH 2/3] Untrack --- .gitignore | 3 +- segmentedhud.swift.ref | 173 ----------------------------------------- 2 files changed, 2 insertions(+), 174 deletions(-) delete mode 100644 segmentedhud.swift.ref diff --git a/.gitignore b/.gitignore index 2791606..e488eb9 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,5 @@ Solid SettingsView.swift.md logic.md TheBoringWorker-HUD -boringnotch + meteo + airpods + traduzione modificabile \ No newline at end of file +boringnotch + meteo + airpods + traduzione modificabile +segmentedhud.swift.ref \ No newline at end of file diff --git a/segmentedhud.swift.ref b/segmentedhud.swift.ref deleted file mode 100644 index 7fb545e..0000000 --- a/segmentedhud.swift.ref +++ /dev/null @@ -1,173 +0,0 @@ -import SwiftUI -import Defaults - -struct SegmentedHUD: View { - @EnvironmentObject var vm: BoringViewModel - @Binding var type: SneakContentType - @Binding var value: CGFloat // 0...1 - @Binding var icon: String - - private let segmentCount: Int = 40 - @State private var glowIndex: Int? = nil - @State private var lastValue: CGFloat = 0 - - var body: some View { - HStack(spacing: 16) { - // Icona a sinistra - hudIcon - .frame(width: 25, height: 20) - .offset(x:0, y: 10) - - // Contenuto centrale: percentuale + segmenti - VStack(spacing: 6) { - Text("\(Int(value * 100))%") - .font(.system(size: 14, weight: .medium, design: .rounded)) - .foregroundStyle(.white) - .frame(maxWidth: .infinity, alignment: .center) - .offset(x:182, y:20) - segments - .frame(height: vm.notchState == .open ? 10 : 14) - } - .frame(maxWidth: .infinity) - - // Spacer per bilanciare il layout (stesso width dell'icona) - Spacer() - .frame(width: 20) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .frame(maxWidth: .infinity) - .frame(height: vm.notchState == .open ? vm.closedNotchSize.height + 8 : 70) - .onChange(of: value) { old, newVal in - handleGlow(oldValue: old, newValue: newVal) - } - } - - @ViewBuilder - private var hudIcon: some View { - Group { - switch type { - case .volume: - if icon.isEmpty { - Image(systemName: speakerSymbol(value)) - .symbolVariant(value > 0 ? .none : .slash) - } else { - Image(systemName: icon) - .opacity(value.isZero ? 0.6 : 1) - .scaleEffect(value.isZero ? 0.85 : 1) - } - case .brightness: - Image(systemName: "sun.max.fill") - case .backlight: - Image(systemName: "keyboard") - case .mic: - Image(systemName: "mic") - default: - EmptyView() - } - } - .foregroundStyle(.white) - } - - // Il resto del codice rimane uguale... - private var segments: some View { - GeometryReader { geo in - let width = geo.size.width - let spacing: CGFloat = vm.notchState == .open ? 2.5 : 3.5 // Aumentato spacing - let computed = (width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount) - let barWidth = max(2.5, min(4.5, computed)) // Aumentata larghezza segmenti - let activeCount = Int(round(value * CGFloat(segmentCount))) - - HStack(spacing: spacing) { - ForEach(0.. AnyShapeStyle { - if isActive { - if Defaults[.systemEventIndicatorUseAccent] { - return AnyShapeStyle(Color.accentColor) - } - return AnyShapeStyle(Color.white) - } else { - return AnyShapeStyle(Color.white.opacity(0.2)) - } - } - - private func glowShadowColor(for isActive: Bool, index: Int) -> Color { - if isActive { - if let glowIndex = glowIndex, index == glowIndex { - return Defaults[.systemEventIndicatorUseAccent] - ? Color.accentColor.ensureMinimumBrightness(factor: 0.8) - : Color.white - } else { - return Defaults[.systemEventIndicatorUseAccent] - ? Color.accentColor.ensureMinimumBrightness(factor: 0.7) - : Color.white - } - } - return .clear - } - - private func handleGlow(oldValue: CGFloat, newValue: CGFloat) { - defer { lastValue = newValue } - let oldIndex = Int(round(oldValue * CGFloat(segmentCount))) - let newIndex = Int(round(newValue * CGFloat(segmentCount))) - guard oldIndex != newIndex else { return } - - if newIndex > oldIndex { - animateWave(from: oldIndex, to: newIndex, step: 1) - } else { - animateWave(from: oldIndex - 1, to: newIndex - 1, step: -1) - } - } - - private func animateWave(from start: Int, to end: Int, step: Int) { - let clampedStart = max(0, min(segmentCount - 1, start)) - let clampedEnd = max(-1, min(segmentCount - 1, end)) - let range = stride(from: clampedStart, through: clampedEnd, by: step) - var delay: Double = 0 - for i in range { - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) { - glowIndex = i - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + delay + 0.25) { - if glowIndex == i { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - glowIndex = nil - } - } - } - delay += 0.02 - } - } - - private func speakerSymbol(_ value: CGFloat) -> String { - switch value { - case 0: return "speaker" - case 0...0.3: return "speaker.wave.1" - case 0.3...0.8: return "speaker.wave.2" - default: return "speaker.wave.3" - } - } -} From f8bdda0033fc459bf313cee2f49bf17606dcac60 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 10 Sep 2025 21:02:28 +0530 Subject: [PATCH 3/3] Working better segmented control --- .../SystemEventIndicatorModifier.swift | 147 ++++++++++++++---- 1 file changed, 115 insertions(+), 32 deletions(-) diff --git a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift index f6c58ea..911293b 100644 --- a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift +++ b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift @@ -98,26 +98,34 @@ struct DraggableProgressBar: View { var body: some View { VStack { GeometryReader { geo in - ZStack(alignment: .leading) { - Capsule() - .fill(.tertiary) - Group { - switch Defaults[.progressBarStyle] { - case .gradient: + Group { + if Defaults[.progressBarStyle] == .segmented { + // Segmented progress bar - completely different layout + SegmentedProgressContent(value: value, geometry: geo) + } else { + // Traditional capsule-based progress bar + ZStack(alignment: .leading) { Capsule() - .fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading)) - .frame(width: max(0, min(geo.size.width * value, geo.size.width))) - .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) - case .hierarchical: - Capsule() - .fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) - .frame(width: max(0, min(geo.size.width * value, geo.size.width))) - .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) - case .segmented: - SegmentedProgressContent(value: value, geometry: geo) + .fill(.tertiary) + Group { + switch Defaults[.progressBarStyle] { + case .gradient: + Capsule() + .fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading)) + .frame(width: max(0, min(geo.size.width * value, geo.size.width))) + .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) + case .hierarchical: + Capsule() + .fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) + .frame(width: max(0, min(geo.size.width * value, geo.size.width))) + .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) + case .segmented: + EmptyView() // This case won't be reached due to the outer if condition + } + } + .opacity(value.isZero ? 0 : 1) } } - .opacity(value.isZero ? 0 : 1) } .gesture( DragGesture(minimumDistance: 0) @@ -150,25 +158,100 @@ struct SegmentedProgressContent: View { let value: CGFloat let geometry: GeometryProxy - private let segmentCount = 20 + private let segmentCount = 16 + @State private var glowIndex: Int? = nil + @State private var lastValue: CGFloat = 0 var body: some View { - HStack(spacing: 1) { + let spacing: CGFloat = 1.5 + let computed = (geometry.size.width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount) + let barWidth = max(3.0, min(6.0, computed)) + let activeCount = Int(round(value * CGFloat(segmentCount))) + + HStack(spacing: spacing) { ForEach(0..= segmentValue - - RoundedRectangle(cornerRadius: 1) - .fill(isActive ? - (Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) : - .clear) - .frame(width: max(1, (geometry.size.width - CGFloat(segmentCount - 1) * 1) / CGFloat(segmentCount))) - .shadow(color: isActive && Defaults[.systemEventIndicatorShadow] ? - (Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white) : - .clear, radius: 4, x: 1) - .opacity(value.isZero ? 0 : (isActive ? 1 : 0.3)) + RoundedRectangle(cornerRadius: 2) + .fill(segmentColor(isActive: index < activeCount)) + .shadow( + color: Defaults[.systemEventIndicatorShadow] + ? glowShadowColor(for: index < activeCount, index: index) + : .clear, + radius: Defaults[.systemEventIndicatorShadow] + ? (glowIndex == index ? 12 : (index < activeCount ? 4 : 0)) + : 0, + x: 0, y: 0 + ) + .frame(width: barWidth) + .animation(.spring(response: 0.3, dampingFraction: 0.8), value: activeCount) + .scaleEffect(glowIndex == index ? 1.15 : 1.0) + .animation(.spring(response: 0.2, dampingFraction: 0.6), value: glowIndex == index) } } - .frame(width: geometry.size.width) + .frame(maxWidth: .infinity, alignment: .center) + .opacity(value.isZero ? 0 : 1) + .onChange(of: value) { old, newVal in + handleGlow(oldValue: old, newValue: newVal) + } + } + + private func segmentColor(isActive: Bool) -> Color { + if isActive { + if Defaults[.systemEventIndicatorUseAccent] { + return Defaults[.accentColor] + } + return .white + } else { + return .white.opacity(0.15) + } + } + + private func glowShadowColor(for isActive: Bool, index: Int) -> Color { + if isActive { + if let glowIndex = glowIndex, index == glowIndex { + return Defaults[.systemEventIndicatorUseAccent] + ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.8) + : .white + } else { + return Defaults[.systemEventIndicatorUseAccent] + ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) + : .white + } + } + return .clear + } + + private func handleGlow(oldValue: CGFloat, newValue: CGFloat) { + defer { lastValue = newValue } + let oldIndex = Int(round(oldValue * CGFloat(segmentCount))) + let newIndex = Int(round(newValue * CGFloat(segmentCount))) + guard oldIndex != newIndex else { return } + + if newIndex > oldIndex { + animateWave(from: oldIndex, to: newIndex, step: 1) + } else { + animateWave(from: oldIndex - 1, to: newIndex - 1, step: -1) + } + } + + private func animateWave(from start: Int, to end: Int, step: Int) { + let clampedStart = max(0, min(segmentCount - 1, start)) + let clampedEnd = max(-1, min(segmentCount - 1, end)) + let range = stride(from: clampedStart, through: clampedEnd, by: step) + var delay: Double = 0 + for i in range { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) { + glowIndex = i + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + delay + 0.2) { + if glowIndex == i { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + glowIndex = nil + } + } + } + delay += 0.015 + } } }