#if !os(tvOS)
import SwiftUI
/// The value delta for one fine/coarse precision step. base = baseStep ?? range/100.
/// fine → ×0.1 (Shift), coarse → ×10 (Option). fine takes precedence if both held.
func precisionStep(range: ClosedRange<Double>, baseStep: Double?, fine: Bool, coarse: Bool) -> Double {
let span = range.upperBound - range.lowerBound
let base = baseStep ?? (span / 100)
let mult: Double = fine ? 0.1 : (coarse ? 10 : 1)
return base * mult
}
// MARK: - macOS precision scroll monitor
#if os(macOS)
import AppKit
/// A zero-size background NSView that installs a local scroll-wheel monitor and
/// nudges when the pointer is over the host view's frame. Placed as a `.background`
/// (behind the Slider) so it NEVER intercepts the slider's clicks/drag.
private struct SliderScrollMonitor: NSViewRepresentable {
let onTick: (_ up: Bool, _ fine: Bool, _ coarse: Bool, _ newGesture: Bool) -> Void
func makeNSView(context: Context) -> NSView { MonitorView(onTick: onTick) }
func updateNSView(_ nsView: NSView, context: Context) { (nsView as? MonitorView)?.onTick = onTick }
final class MonitorView: NSView {
var onTick: (Bool, Bool, Bool, Bool) -> Void
private var monitor: Any?
private var lastScroll: TimeInterval = 0
init(onTick: @escaping (Bool, Bool, Bool, Bool) -> Void) { self.onTick = onTick; super.init(frame: .zero) }
required init?(coder: NSCoder) { nil }
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
if window == nil { removeMonitor(); return }
guard monitor == nil else { return }
monitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { [weak self] e in
guard let self, let win = self.window, e.window === win else { return e }
let frameInWin = self.convert(self.bounds, to: nil)
guard NSPointInRect(e.locationInWindow, frameInWin) else { return e }
let dy = e.scrollingDeltaY != 0 ? e.scrollingDeltaY : e.deltaY
if dy == 0 { return e }
let now = ProcessInfo.processInfo.systemUptime
let newGesture = (now - self.lastScroll) > 0.3
self.lastScroll = now
self.onTick(dy > 0, e.modifierFlags.contains(.shift), e.modifierFlags.contains(.option), newGesture)
return nil // consume so the editor list does not also scroll
}
}
private func removeMonitor() { if let m = monitor { NSEvent.removeMonitor(m); monitor = nil } }
deinit { removeMonitor() }
}
}
#endif
// MARK: - precisionControls modifier
extension View {
/// macOS precision input on a Slider: scroll-wheel + arrow keys nudge `value` by a
/// fine/coarse step (Shift ×0.1, Option ×10); Home/End jump to the range ends.
/// `onEditStart` fires once per gesture so undo captures the pre-value. iOS: no-op.
func precisionControls(value: Binding<Double>, range: ClosedRange<Double>, baseStep: Double?,
onEditStart: @escaping () -> Void) -> some View {
#if os(macOS)
func clamp(_ v: Double) -> Double { min(max(v, range.lowerBound), range.upperBound) }
return AnyView(self
.background(SliderScrollMonitor { up, fine, coarse, newGesture in
if newGesture { onEditStart() }
let step = precisionStep(range: range, baseStep: baseStep, fine: fine, coarse: coarse)
value.wrappedValue = clamp(value.wrappedValue + (up ? step : -step))
})
.focusable()
.onKeyPress(keys: [.leftArrow, .rightArrow, .home, .end]) { press in
let fine = press.modifiers.contains(.shift)
let coarse = press.modifiers.contains(.option)
let step = precisionStep(range: range, baseStep: baseStep, fine: fine, coarse: coarse)
let cur = value.wrappedValue
let nv: Double
switch press.key {
case .leftArrow: nv = cur - step
case .rightArrow: nv = cur + step
case .home: nv = range.lowerBound
case .end: nv = range.upperBound
default: return .ignored
}
onEditStart()
value.wrappedValue = clamp(nv)
return .handled
})
#else
return AnyView(self)
#endif
}
}
#endif