Skip to content

FIX: horizontal value slider with RTL Languages #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -14,5 +14,6 @@ let package = Package(
targets: [
.target(name: "Sliders"),
.testTarget(name: "SlidersTests", dependencies: ["Sliders"])
]
],
swiftLanguageModes: [.version("6")]
)
2 changes: 1 addition & 1 deletion Sources/Sliders/Base/DefaultThumb.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

public struct DefaultThumb: View {
public init() {}
nonisolated public init() {}
public var body: some View {
Capsule()
.foregroundColor(.white)
Expand Down
71 changes: 61 additions & 10 deletions Sources/Sliders/Base/LinearValueMath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,78 @@ import SwiftUI

/// Linear calculations

/// Calculates distance from zero in points
@inlinable func distanceFrom(value: CGFloat, availableDistance: CGFloat, bounds: ClosedRange<CGFloat> = 0.0...1.0, leadingOffset: CGFloat = 0, trailingOffset: CGFloat = 0) -> CGFloat {
/// Calculates the distance in points from the leading edge based on a value within a range.
///
/// This method computes a visual position (e.g., slider thumb) in a given layout space, taking into account offsets and layout direction.
///
/// - Parameters:
/// - value: The input value to convert to a position, typically within `bounds`.
/// - availableDistance: The total space available for the layout, excluding leading/trailing offsets.
/// - bounds: The range representing the minimum and maximum values (default is 0.0...1.0).
/// - leadingOffset: The offset at the start of the available space.
/// - trailingOffset: The offset at the end of the available space.
/// - isRightToLeft: Whether the layout should be mirrored (right-to-left).
/// - Returns: The position in points corresponding to the value.
@inlinable
func distanceFrom(
value: CGFloat,
availableDistance: CGFloat,
bounds: ClosedRange<CGFloat> = 0.0...1.0,
leadingOffset: CGFloat = 0,
trailingOffset: CGFloat = 0,
isRightToLeft: Bool = false
) -> CGFloat {
guard availableDistance > leadingOffset + trailingOffset else { return 0 }
let boundsLenght = bounds.upperBound - bounds.lowerBound
let relativeValue = (value - bounds.lowerBound) / boundsLenght
let offset = (leadingOffset - ((leadingOffset + trailingOffset) * relativeValue))
return offset + (availableDistance * relativeValue)

let boundsLength = bounds.upperBound - bounds.lowerBound
let relativeValue = (value - bounds.lowerBound) / boundsLength

let adjustedRelativeValue = isRightToLeft ? 1.0 - relativeValue : relativeValue
let offset = (leadingOffset - ((leadingOffset + trailingOffset) * adjustedRelativeValue))

return offset + (availableDistance * adjustedRelativeValue)
}

/// Calculates value for relative point in bounds with step.
/// Example: For relative value 0.5 in range 2.0..4.0 produces 3.0
@inlinable func valueFrom(distance: CGFloat, availableDistance: CGFloat, bounds: ClosedRange<CGFloat> = 0.0...1.0, step: CGFloat = 0.001, leadingOffset: CGFloat = 0, trailingOffset: CGFloat = 0) -> CGFloat {
/// Calculates the value corresponding to a point along a layout axis, clamped to the provided bounds and snapped to the nearest step.
///
/// This method is typically the inverse of `distanceFrom`, converting a position (e.g., user touch location) back to a value in a specified range.
///
/// - Parameters:
/// - distance: The visual position in points, such as a touch location.
/// - availableDistance: The total space available for layout.
/// - bounds: The valid value range (default is 0.0...1.0).
/// - step: The smallest allowed step size when calculating the value (default is 0.001).
/// - leadingOffset: Offset before the usable area.
/// - trailingOffset: Offset after the usable area.
/// - isRightToLeft: Whether the layout is in a right-to-left direction.
/// - Returns: The value corresponding to the given distance, clamped and stepped.
@inlinable
func valueFrom(
distance: CGFloat,
availableDistance: CGFloat,
bounds: ClosedRange<CGFloat> = 0.0...1.0,
step: CGFloat = 0.001,
leadingOffset: CGFloat = 0,
trailingOffset: CGFloat = 0,
isRightToLeft: Bool = false
) -> CGFloat {
let relativeValue = (distance - leadingOffset) / (availableDistance - (leadingOffset + trailingOffset))
let newValue = bounds.lowerBound + (relativeValue * (bounds.upperBound - bounds.lowerBound))
let adjustedRelativeValue = isRightToLeft ? 1.0 - relativeValue : relativeValue

let newValue = bounds.lowerBound + (adjustedRelativeValue * (bounds.upperBound - bounds.lowerBound))
let steppedNewValue = (round(newValue / step) * step)
let validatedValue = min(bounds.upperBound, max(bounds.lowerBound, steppedNewValue))

return validatedValue
}

/// Clamps the current value to the specified closed range.
///
/// - Parameter range: The closed range to clamp the value to.
/// - Returns: The value, clamped to the given range.
extension Comparable {
func clamped(to range: ClosedRange<Self>) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

@MainActor
public struct AnyPointSliderStyle: PointSliderStyle {
private let styleMakeBody: (PointSliderStyle.Configuration) -> AnyView

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public extension EnvironmentValues {
}
}

struct PointSliderStyleKey: EnvironmentKey {
@MainActor
struct PointSliderStyleKey: @preconcurrency EnvironmentKey {
static let defaultValue: AnyPointSliderStyle = AnyPointSliderStyle(
RectangularPointSliderStyle()
)
Expand Down
1 change: 1 addition & 0 deletions Sources/Sliders/PointSlider/Style/PointSliderStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SwiftUI
/// To configure the current `PointSlider` for a view hiearchy, use the
/// `.valueSliderStyle()` modifier.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@MainActor
public protocol PointSliderStyle {
/// A `View` representing the body of a `PointSlider`.
associatedtype Body : View
Expand Down
4 changes: 2 additions & 2 deletions Sources/Sliders/PointSlider/Styles/PointSliderOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import SwiftUI
public struct PointSliderOptions: OptionSet {
public let rawValue: Int

public static let interactiveTrack = PointSliderOptions(rawValue: 1 << 0)
public static let defaultOptions: PointSliderOptions = []
@MainActor public static let interactiveTrack = PointSliderOptions(rawValue: 1 << 0)
@MainActor public static let defaultOptions: PointSliderOptions = []

public init(rawValue: Int) {
self.rawValue = rawValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extension EnvironmentValues {
}
}

struct PointTrackConfigurationKey: EnvironmentKey {
@MainActor
struct PointTrackConfigurationKey: @preconcurrency EnvironmentKey {
static let defaultValue: PointTrackConfiguration = .defaultConfiguration
}
1 change: 1 addition & 0 deletions Sources/Sliders/PointTrack/PointTrackConfiguration.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

@MainActor
public struct PointTrackConfiguration {
public static let defaultConfiguration = PointTrackConfiguration()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public extension EnvironmentValues {
}
}

struct RangeSliderStyleKey: EnvironmentKey {
@MainActor
struct RangeSliderStyleKey: @preconcurrency EnvironmentKey {
static let defaultValue: AnyRangeSliderStyle = AnyRangeSliderStyle(
HorizontalRangeSliderStyle()
)
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sliders/RangeSlider/Style/RangeSliderStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import SwiftUI
///
/// To configure the current `RangeSlider` for a view hiearchy, use the
/// `.valueSliderStyle()` modifier.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@MainActor
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) // nonisolated
public protocol RangeSliderStyle {
/// A `View` representing the body of a `RangeSlider`.
associatedtype Body : View
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

@MainActor
public struct HorizontalRangeSliderStyle<Track: View, LowerThumb: View, UpperThumb: View>: RangeSliderStyle {
private let track: Track
private let lowerThumb: LowerThumb
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sliders/RangeSlider/Styles/RangeSliderOptions.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import SwiftUI

@MainActor
public struct RangeSliderOptions: OptionSet {
public let rawValue: Int

public static let forceAdjacentValue = RangeSliderOptions(rawValue: 1 << 0)
public static let defaultOptions: RangeSliderOptions = .forceAdjacentValue

public init(rawValue: Int) {
nonisolated public init(rawValue: Int) {
self.rawValue = rawValue
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

@MainActor
public struct VerticalRangeSliderStyle<Track: View, LowerThumb: View, UpperThumb: View>: RangeSliderStyle {
private let track: Track
private let lowerThumb: LowerThumb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extension EnvironmentValues {
}
}

struct RangeTrackConfigurationKey: EnvironmentKey {
@MainActor
struct RangeTrackConfigurationKey: @preconcurrency EnvironmentKey {
static let defaultValue: RangeTrackConfiguration = .defaultConfiguration
}
7 changes: 4 additions & 3 deletions Sources/Sliders/RangeTrack/RangeTrackConfiguration.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

@MainActor
public struct RangeTrackConfiguration {
public static let defaultConfiguration = RangeTrackConfiguration()

Expand All @@ -10,7 +11,7 @@ public struct RangeTrackConfiguration {
public let upperLeadingOffset: CGFloat
public let upperTrailingOffset: CGFloat

public init(bounds: ClosedRange<CGFloat> = 0.0...1.0, lowerLeadingOffset: CGFloat = 0, lowerTrailingOffset: CGFloat = 0, upperLeadingOffset: CGFloat = 0, upperTrailingOffset: CGFloat = 0) {
nonisolated public init(bounds: ClosedRange<CGFloat> = 0.0...1.0, lowerLeadingOffset: CGFloat = 0, lowerTrailingOffset: CGFloat = 0, upperLeadingOffset: CGFloat = 0, upperTrailingOffset: CGFloat = 0) {
self.bounds = bounds
self.lowerLeadingOffset = lowerLeadingOffset
self.lowerTrailingOffset = lowerTrailingOffset
Expand All @@ -20,7 +21,7 @@ public struct RangeTrackConfiguration {
}

public extension RangeTrackConfiguration {
init(bounds: ClosedRange<CGFloat> = 0.0...1.0, lowerOffset: CGFloat = 0, upperOffset: CGFloat = 0) {
nonisolated init(bounds: ClosedRange<CGFloat> = 0.0...1.0, lowerOffset: CGFloat = 0, upperOffset: CGFloat = 0) {
self.bounds = bounds
self.lowerLeadingOffset = lowerOffset / 2
self.lowerTrailingOffset = lowerOffset / 2 + upperOffset
Expand All @@ -30,7 +31,7 @@ public extension RangeTrackConfiguration {
}

public extension RangeTrackConfiguration {
init(bounds: ClosedRange<CGFloat> = 0.0...1.0, offsets: CGFloat = 0) {
nonisolated init(bounds: ClosedRange<CGFloat> = 0.0...1.0, offsets: CGFloat = 0) {
self.bounds = bounds
self.lowerLeadingOffset = offsets / 2
self.lowerTrailingOffset = offsets / 2 + offsets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public extension EnvironmentValues {
}
}

struct ValueSliderStyleKey: EnvironmentKey {
@MainActor
struct ValueSliderStyleKey: @preconcurrency EnvironmentKey {
static let defaultValue: AnyValueSliderStyle = AnyValueSliderStyle(
HorizontalValueSliderStyle()
)
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sliders/ValueSlider/Style/ValueSliderStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import SwiftUI
///
/// To configure the current `ValueSlider` for a view hiearchy, use the
/// `.valueSliderStyle()` modifier.
///
@MainActor
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ValueSliderStyle {
/// A `View` representing the body of a `ValueSlider`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public struct ValueSliderStyleConfiguration {
public let step: CGFloat
public let onEditingChanged: (Bool) -> Void
public var dragOffset: Binding<CGFloat?>

public init(value: Binding<CGFloat>, bounds: ClosedRange<CGFloat>, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, dragOffset: Binding<CGFloat?>) {
self.value = value
self.bounds = bounds
Expand Down
Loading