Skip to content

Commit

Permalink
Add scroll wheel events for macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
buh committed Dec 18, 2024
1 parent a2031f8 commit e5219ff
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 10 deletions.
38 changes: 29 additions & 9 deletions Sources/CompactSlider/CompactSlider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public struct CompactSlider<Value: BinaryFloatingPoint, ValueLabel: View>: View
@State var lowerProgress: Double = 0
@State var upperProgress: Double = 0
@State private var dragLocationX: CGFloat = 0
@State private var deltaLocationX: CGFloat = 0
@State var adjustedDragLocationX: (lower: CGFloat, upper: CGFloat) = (0, 0)

/// Creates a slider to select a value from a given bounds.
Expand Down Expand Up @@ -227,6 +228,13 @@ public struct CompactSlider<Value: BinaryFloatingPoint, ValueLabel: View>: View
isHovering = isEnabled && $0
updateState()
}
.onScrollWheel(isEnabled: gestureOptions.contains(.scrollWheel)) { delta in
guard isHovering, abs(delta.x) > abs(delta.y) else { return }

Task {
deltaLocationX = delta.x
}
}
#endif
.dragGesture(
options: gestureOptions,
Expand Down Expand Up @@ -274,6 +282,7 @@ public struct CompactSlider<Value: BinaryFloatingPoint, ValueLabel: View>: View
}
.frame(width: proxy.size.width, height: proxy.size.height)
.onChange(of: dragLocationX) { onDragLocationXChange($0, size: proxy.size) }
.onChange(of: deltaLocationX) { onDeltaLocationXChange($0, size: proxy.size) }
}

HStack { valueLabel }
Expand Down Expand Up @@ -302,29 +311,40 @@ public struct CompactSlider<Value: BinaryFloatingPoint, ValueLabel: View>: View
private extension CompactSlider {

func onDragLocationXChange(_ newValue: CGFloat, size: CGSize) {
guard !bounds.isEmpty else { return }
guard !bounds.isEmpty, size.width > 0 else { return }

updateProgress(max(0, min(1, newValue / size.width)))
}

func onDeltaLocationXChange(_ newDelta: CGFloat, size: CGSize) {
guard !bounds.isEmpty, size.width > 0 else { return }

let newProgress = max(0, min(1, newValue / size.width))
let deltaProgressStep = progressStep * (newDelta.sign == .minus ? -0.5 : 0.5)
let newProgress = max(0, min(1, lowerProgress + newDelta / size.width + deltaProgressStep))
updateProgress(newProgress)
}

func updateProgress(_ newValue: Double) {
let isProgress2Nearest: Bool

// Check which progress is closest and should be in focus.
if abs(upperProgress - lowerProgress) < 0.01 {
isProgress2Nearest = newProgress > upperProgress
isProgress2Nearest = newValue > upperProgress
} else {
isProgress2Nearest = isRangeValues && abs(lowerProgress - newProgress) > abs(upperProgress - newProgress)
isProgress2Nearest = isRangeValues && abs(lowerProgress - newValue) > abs(upperProgress - newValue)
}

guard progressStep > 0 else {
if isProgress2Nearest {
if upperProgress != newProgress {
upperProgress = newProgress
if upperProgress != newValue {
upperProgress = newValue

if upperProgress == 1 {
HapticFeedback.vibrate(disabledHapticFeedback)
}
}
} else if lowerProgress != newProgress {
lowerProgress = newProgress
} else if lowerProgress != newValue {
lowerProgress = newValue

if lowerProgress == 0 || lowerProgress == 1 {
HapticFeedback.vibrate(disabledHapticFeedback)
Expand All @@ -334,7 +354,7 @@ private extension CompactSlider {
return
}

let rounded = (newProgress / progressStep).rounded() * progressStep
let rounded = (newValue / progressStep).rounded() * progressStep

if isProgress2Nearest {
if rounded != upperProgress {
Expand Down
4 changes: 3 additions & 1 deletion Sources/CompactSlider/GestureOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public enum GestureOption: Hashable {
case highPriorityGesture
/// Enables delay when sliders inside ``ScrollView`` or ``Form``. Enabled by default for iOS.
case delayedGesture
/// Enables the scroll wheel.
case scrollWheel
}

/// A set of drag gesture options: minimum drag distance, delayed touch, and high priority.
Expand All @@ -22,7 +24,7 @@ extension Set<GestureOption> {
/// For macOS: minimum drag distance 0.
public static var `default`: Self {
#if os(macOS)
[.dragGestureMinimumDistance(0)]
[.dragGestureMinimumDistance(0), .scrollWheel]
#else
[.dragGestureMinimumDistance(1), .delayedGesture]
#endif
Expand Down
56 changes: 56 additions & 0 deletions Sources/CompactSlider/ScrollWheelModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// The MIT License (MIT)
//
// Copyright (c) 2024 Alexey Bukhtin (github.com/buh).
//

import SwiftUI
#if canImport(AppKit)
import AppKit
import Combine

struct ScrollWheelModifier: ViewModifier {
let action: (_ delta: CGPoint) -> Void
private var cancellable = Set<AnyCancellable>()

init(action: @escaping (_ delta: CGPoint) -> Void) {
self.action = action
subscribeForScrollWheelEvents()
}

func body(content: Content) -> some View {
content
}

mutating func subscribeForScrollWheelEvents() {
NSApp.publisher(for: \.currentEvent)
.filter { $0?.type == .scrollWheel }
.compactMap {
if let event = $0 {
return CGPoint(x: event.deltaX, y: event.deltaY)
}

return nil
}
.removeDuplicates()
.sink { [action] in action($0) }
.store(in: &cancellable)
}
}

extension View {
@ViewBuilder
func onScrollWheel(isEnabled: Bool = true, action: @escaping (_ delta: CGPoint) -> Void) -> some View {
if isEnabled {
modifier(ScrollWheelModifier(action: action))
} else {
self
}
}
}
#else
extension View {
func onScrollWheel(isEnabled: Bool = false, action: @escaping (_ delta: CGPoint) -> Void) -> some View {
self
}
}
#endif

0 comments on commit e5219ff

Please # to comment.