@onekeyfe/react-native-segment-slider 3.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/SegmentSlider.podspec +29 -0
  4. package/android/CMakeLists.txt +26 -0
  5. package/android/build.gradle +128 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/segmentslider/HybridSegmentSlider.kt +364 -0
  9. package/android/src/main/java/com/margelo/nitro/segmentslider/SegmentSliderColorParser.kt +56 -0
  10. package/android/src/main/java/com/margelo/nitro/segmentslider/SegmentSliderPackage.kt +29 -0
  11. package/ios/HybridSegmentSlider.swift +412 -0
  12. package/ios/SegmentSliderColorParser.swift +152 -0
  13. package/lib/module/SegmentSlider.nitro.js +4 -0
  14. package/lib/module/SegmentSlider.nitro.js.map +1 -0
  15. package/lib/module/index.js +43 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  19. package/lib/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  20. package/lib/nitrogen/generated/android/c++/JHybridSegmentSliderSpec.cpp +265 -0
  21. package/lib/nitrogen/generated/android/c++/JHybridSegmentSliderSpec.hpp +106 -0
  22. package/lib/nitrogen/generated/android/c++/views/JHybridSegmentSliderStateUpdater.cpp +136 -0
  23. package/lib/nitrogen/generated/android/c++/views/JHybridSegmentSliderStateUpdater.hpp +49 -0
  24. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/Func_void.kt +80 -0
  25. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/Func_void_double.kt +80 -0
  26. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/HybridSegmentSliderSpec.kt +203 -0
  27. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/segmentsliderOnLoad.kt +35 -0
  28. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/views/HybridSegmentSliderManager.kt +50 -0
  29. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/views/HybridSegmentSliderStateUpdater.kt +23 -0
  30. package/lib/nitrogen/generated/android/segmentslider+autolinking.cmake +83 -0
  31. package/lib/nitrogen/generated/android/segmentslider+autolinking.gradle +27 -0
  32. package/lib/nitrogen/generated/android/segmentsliderOnLoad.cpp +50 -0
  33. package/lib/nitrogen/generated/android/segmentsliderOnLoad.hpp +25 -0
  34. package/lib/nitrogen/generated/ios/SegmentSlider+autolinking.rb +60 -0
  35. package/lib/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Bridge.cpp +49 -0
  36. package/lib/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Bridge.hpp +116 -0
  37. package/lib/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Umbrella.hpp +44 -0
  38. package/lib/nitrogen/generated/ios/SegmentSliderAutolinking.mm +33 -0
  39. package/lib/nitrogen/generated/ios/SegmentSliderAutolinking.swift +25 -0
  40. package/lib/nitrogen/generated/ios/c++/HybridSegmentSliderSpecSwift.cpp +11 -0
  41. package/lib/nitrogen/generated/ios/c++/HybridSegmentSliderSpecSwift.hpp +208 -0
  42. package/lib/nitrogen/generated/ios/c++/views/HybridSegmentSliderComponent.mm +196 -0
  43. package/lib/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  44. package/lib/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
  45. package/lib/nitrogen/generated/ios/swift/HybridSegmentSliderSpec.swift +76 -0
  46. package/lib/nitrogen/generated/ios/swift/HybridSegmentSliderSpec_cxx.swift +423 -0
  47. package/lib/nitrogen/generated/shared/c++/HybridSegmentSliderSpec.cpp +62 -0
  48. package/lib/nitrogen/generated/shared/c++/HybridSegmentSliderSpec.hpp +105 -0
  49. package/lib/nitrogen/generated/shared/c++/views/HybridSegmentSliderComponent.cpp +327 -0
  50. package/lib/nitrogen/generated/shared/c++/views/HybridSegmentSliderComponent.hpp +128 -0
  51. package/lib/nitrogen/generated/shared/json/SegmentSliderConfig.json +30 -0
  52. package/lib/nitrogen/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  53. package/lib/nitrogen/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  54. package/lib/nitrogen/nitrogen/generated/android/c++/JHybridSegmentSliderSpec.cpp +265 -0
  55. package/lib/nitrogen/nitrogen/generated/android/c++/JHybridSegmentSliderSpec.hpp +106 -0
  56. package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridSegmentSliderStateUpdater.cpp +136 -0
  57. package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridSegmentSliderStateUpdater.hpp +49 -0
  58. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/Func_void.kt +80 -0
  59. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/Func_void_double.kt +80 -0
  60. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/HybridSegmentSliderSpec.kt +203 -0
  61. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/segmentsliderOnLoad.kt +35 -0
  62. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/views/HybridSegmentSliderManager.kt +50 -0
  63. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/views/HybridSegmentSliderStateUpdater.kt +23 -0
  64. package/lib/nitrogen/nitrogen/generated/android/segmentslider+autolinking.cmake +83 -0
  65. package/lib/nitrogen/nitrogen/generated/android/segmentslider+autolinking.gradle +27 -0
  66. package/lib/nitrogen/nitrogen/generated/android/segmentsliderOnLoad.cpp +50 -0
  67. package/lib/nitrogen/nitrogen/generated/android/segmentsliderOnLoad.hpp +25 -0
  68. package/lib/nitrogen/nitrogen/generated/ios/SegmentSlider+autolinking.rb +60 -0
  69. package/lib/nitrogen/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Bridge.cpp +49 -0
  70. package/lib/nitrogen/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Bridge.hpp +116 -0
  71. package/lib/nitrogen/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Umbrella.hpp +44 -0
  72. package/lib/nitrogen/nitrogen/generated/ios/SegmentSliderAutolinking.mm +33 -0
  73. package/lib/nitrogen/nitrogen/generated/ios/SegmentSliderAutolinking.swift +25 -0
  74. package/lib/nitrogen/nitrogen/generated/ios/c++/HybridSegmentSliderSpecSwift.cpp +11 -0
  75. package/lib/nitrogen/nitrogen/generated/ios/c++/HybridSegmentSliderSpecSwift.hpp +208 -0
  76. package/lib/nitrogen/nitrogen/generated/ios/c++/views/HybridSegmentSliderComponent.mm +196 -0
  77. package/lib/nitrogen/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  78. package/lib/nitrogen/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
  79. package/lib/nitrogen/nitrogen/generated/ios/swift/HybridSegmentSliderSpec.swift +76 -0
  80. package/lib/nitrogen/nitrogen/generated/ios/swift/HybridSegmentSliderSpec_cxx.swift +423 -0
  81. package/lib/nitrogen/nitrogen/generated/shared/c++/HybridSegmentSliderSpec.cpp +62 -0
  82. package/lib/nitrogen/nitrogen/generated/shared/c++/HybridSegmentSliderSpec.hpp +105 -0
  83. package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridSegmentSliderComponent.cpp +327 -0
  84. package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridSegmentSliderComponent.hpp +128 -0
  85. package/lib/nitrogen/nitrogen/generated/shared/json/SegmentSliderConfig.json +30 -0
  86. package/lib/typescript/package.json +1 -0
  87. package/lib/typescript/src/SegmentSlider.nitro.d.ts +72 -0
  88. package/lib/typescript/src/SegmentSlider.nitro.d.ts.map +1 -0
  89. package/lib/typescript/src/index.d.ts +11 -0
  90. package/lib/typescript/src/index.d.ts.map +1 -0
  91. package/nitro.json +17 -0
  92. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  93. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  94. package/nitrogen/generated/android/c++/JHybridSegmentSliderSpec.cpp +265 -0
  95. package/nitrogen/generated/android/c++/JHybridSegmentSliderSpec.hpp +106 -0
  96. package/nitrogen/generated/android/c++/views/JHybridSegmentSliderStateUpdater.cpp +136 -0
  97. package/nitrogen/generated/android/c++/views/JHybridSegmentSliderStateUpdater.hpp +49 -0
  98. package/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/Func_void.kt +80 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/Func_void_double.kt +80 -0
  100. package/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/HybridSegmentSliderSpec.kt +203 -0
  101. package/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/segmentsliderOnLoad.kt +35 -0
  102. package/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/views/HybridSegmentSliderManager.kt +50 -0
  103. package/nitrogen/generated/android/kotlin/com/margelo/nitro/segmentslider/views/HybridSegmentSliderStateUpdater.kt +23 -0
  104. package/nitrogen/generated/android/segmentslider+autolinking.cmake +83 -0
  105. package/nitrogen/generated/android/segmentslider+autolinking.gradle +27 -0
  106. package/nitrogen/generated/android/segmentsliderOnLoad.cpp +50 -0
  107. package/nitrogen/generated/android/segmentsliderOnLoad.hpp +25 -0
  108. package/nitrogen/generated/ios/SegmentSlider+autolinking.rb +60 -0
  109. package/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Bridge.cpp +49 -0
  110. package/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Bridge.hpp +116 -0
  111. package/nitrogen/generated/ios/SegmentSlider-Swift-Cxx-Umbrella.hpp +44 -0
  112. package/nitrogen/generated/ios/SegmentSliderAutolinking.mm +33 -0
  113. package/nitrogen/generated/ios/SegmentSliderAutolinking.swift +25 -0
  114. package/nitrogen/generated/ios/c++/HybridSegmentSliderSpecSwift.cpp +11 -0
  115. package/nitrogen/generated/ios/c++/HybridSegmentSliderSpecSwift.hpp +208 -0
  116. package/nitrogen/generated/ios/c++/views/HybridSegmentSliderComponent.mm +196 -0
  117. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  118. package/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
  119. package/nitrogen/generated/ios/swift/HybridSegmentSliderSpec.swift +76 -0
  120. package/nitrogen/generated/ios/swift/HybridSegmentSliderSpec_cxx.swift +423 -0
  121. package/nitrogen/generated/shared/c++/HybridSegmentSliderSpec.cpp +62 -0
  122. package/nitrogen/generated/shared/c++/HybridSegmentSliderSpec.hpp +105 -0
  123. package/nitrogen/generated/shared/c++/views/HybridSegmentSliderComponent.cpp +327 -0
  124. package/nitrogen/generated/shared/c++/views/HybridSegmentSliderComponent.hpp +128 -0
  125. package/nitrogen/generated/shared/json/SegmentSliderConfig.json +30 -0
  126. package/package.json +170 -0
  127. package/src/SegmentSlider.nitro.ts +80 -0
  128. package/src/index.tsx +59 -0
@@ -0,0 +1,412 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Native segmented slider. Renders the track, fill bar, segment marks, thumb
5
+ /// and value bubble as CALayers and handles the drag gesture itself, so a drag
6
+ /// never crosses the JS bridge per frame. Mirrors the web variant
7
+ /// (`composite/SegmentSlider/index.tsx`) pixel-for-pixel.
8
+ final class HybridSegmentSlider: HybridSegmentSliderSpec {
9
+
10
+ // MARK: - Geometry (points), identical to the web constants
11
+ private let thumbSize: CGFloat = 16
12
+ private let markSize: CGFloat = 10
13
+ private let haloRing: CGFloat = 4 // translucent halo width beyond each mark
14
+ private var inset: CGFloat { thumbSize / 2 }
15
+
16
+ // MARK: - HybridView
17
+ var view: UIView = SegmentSliderUIView()
18
+
19
+ // MARK: - Layers
20
+ private let trackLayer = CALayer()
21
+ private let fillLayer = CALayer()
22
+ private let thumbLayer = CALayer()
23
+ private var markLayers: [CALayer] = []
24
+ private let bubbleLayer = CALayer()
25
+ private let bubbleTextLayer = CATextLayer()
26
+ // Translucent highlight shown around the segment node the thumb is over while
27
+ // dragging (mobile analogue of the web mark hover halo). One shared layer,
28
+ // repositioned to the active node; below the marks so the node dot stays crisp.
29
+ private let haloLayer = CALayer()
30
+
31
+ // MARK: - Cached colors
32
+ private var cFill = UIColor.clear.cgColor
33
+ private var cTrack = UIColor.clear.cgColor
34
+ private var cThumb = UIColor.white.cgColor
35
+ private var cThumbBorder = UIColor.gray.cgColor
36
+ private var cMarkActive = UIColor.clear.cgColor
37
+ private var cMarkInactive = UIColor.white.cgColor
38
+ private var cMarkBorder = UIColor.gray.cgColor
39
+ private var cBubble = UIColor.black.cgColor
40
+ private var cBubbleText = UIColor.white.cgColor
41
+
42
+ // MARK: - Interaction state
43
+ private var currentValue: Double = 0
44
+ private var lastEmitted: Double = .nan
45
+ private var isDragging = false
46
+ private var hasLaidOut = false
47
+ // Segment node the thumb currently sits on (-1 = none). Drives the halo +
48
+ // a light haptic tick fired once each time the thumb enters a new node.
49
+ private var hoverMarkIndex = -1
50
+ private lazy var haptic = UIImpactFeedbackGenerator(style: .light)
51
+
52
+ // MARK: - Props (required props are non-optional in the generated spec)
53
+ var value: Double = 0 {
54
+ didSet {
55
+ // While dragging, the finger owns the value — ignore controlled updates
56
+ // (web skips its useLayoutEffect when draggingRef.current is true).
57
+ if !isDragging {
58
+ currentValue = value
59
+ applyVisual(value)
60
+ }
61
+ }
62
+ }
63
+ var min: Double = 0 { didSet { scheduleLayout() } }
64
+ var max: Double = 100 { didSet { scheduleLayout() } }
65
+ var segments: Double = 0 { didSet { scheduleLayout() } }
66
+ var sliderHeight: Double = 4 { didSet { scheduleLayout() } }
67
+ var disabled: Bool = false {
68
+ didSet {
69
+ view.alpha = disabled ? 0.5 : 1
70
+ view.isUserInteractionEnabled = !disabled
71
+ }
72
+ }
73
+ var showBubble: Bool = true { didSet { scheduleLayout() } }
74
+ var centerOrigin: Bool = false { didSet { scheduleLayout() } }
75
+ var epoch: Double = 0 { didSet { scheduleLayout() } }
76
+
77
+ var fillColor: String = "" {
78
+ didSet { cFill = parse(fillColor); fillLayer.backgroundColor = cFill; scheduleLayout() }
79
+ }
80
+ var trackColor: String = "" {
81
+ didSet { cTrack = parse(trackColor); trackLayer.backgroundColor = cTrack }
82
+ }
83
+ var thumbColor: String = "" {
84
+ didSet { cThumb = parse(thumbColor); thumbLayer.backgroundColor = cThumb }
85
+ }
86
+ var thumbBorderColor: String = "" {
87
+ didSet { cThumbBorder = parse(thumbBorderColor); thumbLayer.borderColor = cThumbBorder }
88
+ }
89
+ var markActiveColor: String = "" {
90
+ didSet { cMarkActive = parse(markActiveColor); scheduleLayout() }
91
+ }
92
+ var markInactiveColor: String = "" {
93
+ didSet { cMarkInactive = parse(markInactiveColor); scheduleLayout() }
94
+ }
95
+ var markBorderColor: String = "" {
96
+ didSet { cMarkBorder = parse(markBorderColor); scheduleLayout() }
97
+ }
98
+ var bubbleColor: String = "" {
99
+ didSet { cBubble = parse(bubbleColor); bubbleLayer.backgroundColor = cBubble }
100
+ }
101
+ var bubbleTextColor: String = "" {
102
+ didSet { cBubbleText = parse(bubbleTextColor); bubbleTextLayer.foregroundColor = cBubbleText }
103
+ }
104
+
105
+ // MARK: - Callbacks
106
+ var onChange: ((Double) -> Void)?
107
+ var onSlideStart: (() -> Void)?
108
+ var onSlideComplete: (() -> Void)?
109
+
110
+ // MARK: - Init
111
+ override init() {
112
+ super.init()
113
+ view.backgroundColor = .clear
114
+ view.isUserInteractionEnabled = true
115
+
116
+ trackLayer.cornerRadius = CGFloat(sliderHeight) / 2
117
+ fillLayer.cornerRadius = CGFloat(sliderHeight) / 2
118
+
119
+ thumbLayer.cornerRadius = thumbSize / 2
120
+ thumbLayer.borderWidth = 1
121
+ thumbLayer.shadowColor = UIColor.black.cgColor
122
+ thumbLayer.shadowOpacity = 0.08
123
+ thumbLayer.shadowRadius = 1
124
+ thumbLayer.shadowOffset = CGSize(width: 0, height: 1)
125
+
126
+ bubbleLayer.cornerRadius = 4
127
+ bubbleLayer.opacity = 0
128
+ bubbleTextLayer.contentsScale = UIScreen.main.scale
129
+ bubbleTextLayer.alignmentMode = .center
130
+ bubbleTextLayer.fontSize = 12
131
+ bubbleTextLayer.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
132
+ bubbleLayer.addSublayer(bubbleTextLayer)
133
+
134
+ let haloSize = markSize + haloRing * 2
135
+ haloLayer.bounds = CGRect(x: 0, y: 0, width: haloSize, height: haloSize)
136
+ haloLayer.cornerRadius = haloSize / 2
137
+ haloLayer.opacity = 0
138
+
139
+ view.layer.addSublayer(trackLayer)
140
+ view.layer.addSublayer(fillLayer)
141
+ view.layer.addSublayer(thumbLayer)
142
+ view.layer.addSublayer(haloLayer)
143
+ view.layer.addSublayer(bubbleLayer)
144
+
145
+ if let v = view as? SegmentSliderUIView {
146
+ v.onLayout = { [weak self] in self?.performLayout() }
147
+ v.onTouchBegan = { [weak self] x in self?.handleTouchBegan(x) }
148
+ v.onTouchMoved = { [weak self] x in self?.handleTouchMoved(x) }
149
+ v.onTouchEnded = { [weak self] in self?.handleTouchEnded() }
150
+ }
151
+ }
152
+
153
+ // MARK: - Color helper
154
+ private func parse(_ s: String) -> CGColor {
155
+ SegmentSliderColorParser.parse(s).cgColor
156
+ }
157
+
158
+ private func scheduleLayout() {
159
+ view.setNeedsLayout()
160
+ }
161
+
162
+ // MARK: - Geometry helpers
163
+ private var trackWidth: CGFloat { Swift.max(view.bounds.width - thumbSize, 0) }
164
+ private var trackLeft: CGFloat { inset }
165
+ private var range: Double { let r = max - min; return r == 0 ? 1 : r }
166
+ private var markCount: Int { segments >= 1 ? Int(segments) + 1 : 0 }
167
+
168
+ /// value -> fraction 0..1
169
+ private func valueToFrac(_ v: Double) -> CGFloat {
170
+ let clamped = Swift.max(min, Swift.min(max, v))
171
+ return CGFloat((clamped - min) / range)
172
+ }
173
+
174
+ private func valueFromX(_ x: CGFloat) -> Double {
175
+ let w = trackWidth
176
+ if w <= 0 { return currentValue }
177
+ let clampedX = Swift.max(trackLeft, Swift.min(trackLeft + w, x))
178
+ let frac = Double((clampedX - trackLeft) / w)
179
+ return min + frac * (max - min)
180
+ }
181
+
182
+ // MARK: - Layout
183
+ private func performLayout() {
184
+ let bounds = view.bounds
185
+ guard bounds.width > 0 else { return }
186
+
187
+ syncMarkLayers(markCount)
188
+
189
+ let h = CGFloat(sliderHeight)
190
+ let trackTop = bounds.midY - h / 2
191
+
192
+ CATransaction.begin()
193
+ CATransaction.setDisableActions(true)
194
+
195
+ trackLayer.frame = CGRect(x: trackLeft, y: trackTop, width: trackWidth, height: h)
196
+ trackLayer.cornerRadius = h / 2
197
+ trackLayer.backgroundColor = cTrack
198
+ fillLayer.cornerRadius = h / 2
199
+
200
+ // Marks sit on the track line, centred vertically on it.
201
+ for i in 0..<markLayers.count {
202
+ let layer = markLayers[i]
203
+ let frac = markCount > 1 ? CGFloat(i) / CGFloat(markCount - 1) : 0
204
+ let cx = trackLeft + frac * trackWidth
205
+ layer.bounds = CGRect(x: 0, y: 0, width: markSize, height: markSize)
206
+ layer.position = CGPoint(x: cx, y: bounds.midY)
207
+ layer.cornerRadius = markSize / 2
208
+ layer.borderWidth = 1
209
+ }
210
+
211
+ // Thumb sizing (position set in applyVisual).
212
+ thumbLayer.bounds = CGRect(x: 0, y: 0, width: thumbSize, height: thumbSize)
213
+ thumbLayer.cornerRadius = thumbSize / 2
214
+ thumbLayer.backgroundColor = cThumb
215
+ thumbLayer.borderColor = cThumbBorder
216
+
217
+ CATransaction.commit()
218
+
219
+ hasLaidOut = true
220
+ applyVisual(currentValue)
221
+ }
222
+
223
+ private func syncMarkLayers(_ count: Int) {
224
+ if markLayers.count < count {
225
+ for _ in markLayers.count..<count {
226
+ let l = CALayer()
227
+ l.borderWidth = 1
228
+ // Marks render ABOVE the thumb + halo (web parity: mark zIndex 3 >
229
+ // thumb 2). So when the thumb sits on a segment node the node's dot
230
+ // shows through the thumb's centre — the "white ring + dark centre"
231
+ // bullseye look. Inserting above the halo keeps order
232
+ // track < fill < thumb < halo < marks < bubble.
233
+ view.layer.insertSublayer(l, above: haloLayer)
234
+ markLayers.append(l)
235
+ }
236
+ } else if markLayers.count > count {
237
+ for l in markLayers[count...] { l.removeFromSuperlayer() }
238
+ markLayers.removeLast(markLayers.count - count)
239
+ }
240
+ }
241
+
242
+ // MARK: - Visual application (no implicit animations)
243
+ private func applyVisual(_ v: Double) {
244
+ guard hasLaidOut, view.bounds.width > 0 else { return }
245
+ let bounds = view.bounds
246
+ let h = CGFloat(sliderHeight)
247
+ let trackTop = bounds.midY - h / 2
248
+ let frac = valueToFrac(v)
249
+ let thumbX = trackLeft + frac * trackWidth
250
+
251
+ CATransaction.begin()
252
+ CATransaction.setDisableActions(true)
253
+
254
+ // Fill bar.
255
+ if centerOrigin {
256
+ if v == 0 {
257
+ fillLayer.frame = CGRect(x: trackLeft, y: trackTop, width: 0, height: h)
258
+ } else {
259
+ let centerFrac = valueToFrac(0)
260
+ let leftFrac = Swift.min(frac, centerFrac)
261
+ let widthFrac = abs(frac - centerFrac)
262
+ fillLayer.frame = CGRect(
263
+ x: trackLeft + leftFrac * trackWidth, y: trackTop,
264
+ width: widthFrac * trackWidth, height: h)
265
+ }
266
+ } else {
267
+ fillLayer.frame = CGRect(
268
+ x: trackLeft, y: trackTop, width: frac * trackWidth, height: h)
269
+ }
270
+ fillLayer.backgroundColor = cFill
271
+
272
+ // Marks active states.
273
+ let centerFrac = valueToFrac(0)
274
+ for i in 0..<markLayers.count {
275
+ let markFrac = markLayers.count > 1 ? CGFloat(i) / CGFloat(markLayers.count - 1) : 0
276
+ var active: Bool
277
+ if centerOrigin {
278
+ if v == 0 {
279
+ active = false
280
+ } else if v > 0 {
281
+ active = markFrac > centerFrac && markFrac <= frac
282
+ } else {
283
+ active = markFrac >= frac && markFrac < centerFrac
284
+ }
285
+ } else {
286
+ active = markFrac <= frac
287
+ }
288
+ let layer = markLayers[i]
289
+ layer.backgroundColor = active ? cMarkActive : cMarkInactive
290
+ layer.borderColor = active ? cMarkActive : cMarkBorder
291
+ }
292
+
293
+ // Thumb position (keep current scale transform).
294
+ thumbLayer.position = CGPoint(x: thumbX, y: bounds.midY)
295
+
296
+ // Bubble.
297
+ if showBubble {
298
+ let text = "\(Int(v.rounded()))%"
299
+ bubbleTextLayer.string = text
300
+ let font = UIFont.systemFont(ofSize: 12, weight: .semibold)
301
+ let textW = (text as NSString)
302
+ .size(withAttributes: [.font: font]).width
303
+ let padH: CGFloat = 8
304
+ let bw = ceil(textW) + padH * 2
305
+ let bh: CGFloat = 20
306
+ let by = trackTop - 8 - bh
307
+ bubbleLayer.frame = CGRect(x: thumbX - bw / 2, y: by, width: bw, height: bh)
308
+ bubbleTextLayer.frame = CGRect(x: 0, y: 2, width: bw, height: 16)
309
+ }
310
+
311
+ CATransaction.commit()
312
+
313
+ updateHover(thumbX: thumbX)
314
+ }
315
+
316
+ // MARK: - Node halo + haptic (mobile analogue of the web mark hover)
317
+ /// Highlights the node the thumb is over and fires a light haptic tick once
318
+ /// per node entered. Only while dragging — controlled value syncs just keep
319
+ /// the halo hidden. Does NOT snap the value (parity with web/perp).
320
+ private func updateHover(thumbX: CGFloat) {
321
+ guard isDragging, markCount > 0 else {
322
+ if hoverMarkIndex != -1 { hoverMarkIndex = -1; setHalo(visible: false, index: 0) }
323
+ return
324
+ }
325
+ let threshold: CGFloat = 12 // half of the 24pt mark hit area
326
+ var best = -1
327
+ var bestDist = threshold
328
+ for i in 0..<markCount {
329
+ let frac = markCount > 1 ? CGFloat(i) / CGFloat(markCount - 1) : 0
330
+ let mx = trackLeft + frac * trackWidth
331
+ let d = abs(thumbX - mx)
332
+ if d <= bestDist { bestDist = d; best = i }
333
+ }
334
+ guard best != hoverMarkIndex else { return }
335
+ hoverMarkIndex = best
336
+ if best == -1 {
337
+ setHalo(visible: false, index: 0)
338
+ } else {
339
+ setHalo(visible: true, index: best)
340
+ haptic.impactOccurred(intensity: 0.6)
341
+ haptic.prepare()
342
+ }
343
+ }
344
+
345
+ private func setHalo(visible: Bool, index: Int) {
346
+ CATransaction.begin()
347
+ CATransaction.setDisableActions(true)
348
+ if visible {
349
+ let frac = markCount > 1 ? CGFloat(index) / CGFloat(markCount - 1) : 0
350
+ let mx = trackLeft + frac * trackWidth
351
+ haloLayer.position = CGPoint(x: mx, y: view.bounds.midY)
352
+ haloLayer.backgroundColor = UIColor(cgColor: cMarkActive)
353
+ .withAlphaComponent(0.25).cgColor
354
+ haloLayer.opacity = 1
355
+ } else {
356
+ haloLayer.opacity = 0
357
+ }
358
+ CATransaction.commit()
359
+ }
360
+
361
+ // MARK: - Touch handling
362
+ private func handleTouchBegan(_ x: CGFloat) {
363
+ guard !disabled else { return }
364
+ isDragging = true
365
+ haptic.prepare()
366
+ onSlideStart?()
367
+ animateThumbScale(1.15)
368
+ setBubbleVisible(true)
369
+ setValue(valueFromX(x))
370
+ }
371
+
372
+ private func handleTouchMoved(_ x: CGFloat) {
373
+ guard isDragging else { return }
374
+ setValue(valueFromX(x))
375
+ }
376
+
377
+ private func handleTouchEnded() {
378
+ guard isDragging else { return }
379
+ isDragging = false
380
+ hoverMarkIndex = -1
381
+ setHalo(visible: false, index: 0)
382
+ animateThumbScale(1.0)
383
+ setBubbleVisible(false)
384
+ onSlideComplete?()
385
+ }
386
+
387
+ private func setValue(_ raw: Double) {
388
+ let clamped = Swift.max(min, Swift.min(max, raw))
389
+ let rounded = clamped.rounded()
390
+ currentValue = rounded
391
+ applyVisual(rounded)
392
+ if rounded != lastEmitted {
393
+ lastEmitted = rounded
394
+ onChange?(rounded)
395
+ }
396
+ }
397
+
398
+ private func animateThumbScale(_ s: CGFloat) {
399
+ CATransaction.begin()
400
+ CATransaction.setAnimationDuration(0.12)
401
+ thumbLayer.transform = CATransform3DMakeScale(s, s, 1)
402
+ CATransaction.commit()
403
+ }
404
+
405
+ private func setBubbleVisible(_ visible: Bool) {
406
+ guard showBubble else { return }
407
+ CATransaction.begin()
408
+ CATransaction.setAnimationDuration(0.15)
409
+ bubbleLayer.opacity = visible ? 1 : 0
410
+ CATransaction.commit()
411
+ }
412
+ }
@@ -0,0 +1,152 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Parses a color string coming from JS. Supports:
5
+ /// - `#rgb`, `#rrggbb`, `#rrggbbaa`
6
+ /// - `rgb(r, g, b)` / `rgba(r, g, b, a)` where a is 0..1
7
+ /// Falls back to `fallback` on parse failure.
8
+ enum SegmentSliderColorParser {
9
+ static func parse(_ value: String?, fallback: UIColor = .clear) -> UIColor {
10
+ guard let raw = value?.trimmingCharacters(in: .whitespacesAndNewlines),
11
+ !raw.isEmpty else {
12
+ return fallback
13
+ }
14
+
15
+ if raw.hasPrefix("#") {
16
+ return parseHex(raw) ?? fallback
17
+ }
18
+ if raw.lowercased().hasPrefix("rgb") {
19
+ return parseRGB(raw) ?? fallback
20
+ }
21
+ return fallback
22
+ }
23
+
24
+ private static func parseHex(_ hex: String) -> UIColor? {
25
+ var s = hex
26
+ s.removeFirst() // '#'
27
+
28
+ // Expand shorthand #rgb -> #rrggbb
29
+ if s.count == 3 {
30
+ s = s.map { "\($0)\($0)" }.joined()
31
+ }
32
+
33
+ guard s.count == 6 || s.count == 8 else { return nil }
34
+ var int: UInt64 = 0
35
+ guard Scanner(string: s).scanHexInt64(&int) else { return nil }
36
+
37
+ let r, g, b, a: CGFloat
38
+ if s.count == 8 {
39
+ r = CGFloat((int >> 24) & 0xFF) / 255.0
40
+ g = CGFloat((int >> 16) & 0xFF) / 255.0
41
+ b = CGFloat((int >> 8) & 0xFF) / 255.0
42
+ a = CGFloat(int & 0xFF) / 255.0
43
+ } else {
44
+ r = CGFloat((int >> 16) & 0xFF) / 255.0
45
+ g = CGFloat((int >> 8) & 0xFF) / 255.0
46
+ b = CGFloat(int & 0xFF) / 255.0
47
+ a = 1.0
48
+ }
49
+ return UIColor(red: r, green: g, blue: b, alpha: a)
50
+ }
51
+
52
+ private static func parseRGB(_ value: String) -> UIColor? {
53
+ guard let open = value.firstIndex(of: "("),
54
+ let close = value.firstIndex(of: ")") else { return nil }
55
+ let inner = value[value.index(after: open)..<close]
56
+ let parts = inner.split(separator: ",").map {
57
+ $0.trimmingCharacters(in: .whitespaces)
58
+ }
59
+ guard parts.count >= 3,
60
+ let r = Double(parts[0]),
61
+ let g = Double(parts[1]),
62
+ let b = Double(parts[2]) else { return nil }
63
+ let a = parts.count >= 4 ? (Double(parts[3]) ?? 1.0) : 1.0
64
+ return UIColor(
65
+ red: CGFloat(r) / 255.0,
66
+ green: CGFloat(g) / 255.0,
67
+ blue: CGFloat(b) / 255.0,
68
+ alpha: CGFloat(a)
69
+ )
70
+ }
71
+ }
72
+
73
+ /// UIControl subclass that forwards `layoutSubviews` and raw touch positions to
74
+ /// closures so the Nitro HybridView can re-lay-out its CALayers and drive the
75
+ /// drag entirely on the native UI thread.
76
+ ///
77
+ /// It subclasses `UIControl` (not `UIView`) on purpose: an enclosing
78
+ /// `UIScrollView` returns `false` from `touchesShouldCancel(in:)` for
79
+ /// `UIControl` subviews, so a horizontal drag on the slider is NOT cancelled
80
+ /// and stolen by the scroll view's pan gesture. This is the same mechanism
81
+ /// `UISlider` relies on to stay draggable inside a scroll view.
82
+ final class SegmentSliderUIView: UIControl {
83
+ var onLayout: (() -> Void)?
84
+ /// Touch began at x (points, view coordinates).
85
+ var onTouchBegan: ((CGFloat) -> Void)?
86
+ /// Touch moved to x (points, view coordinates).
87
+ var onTouchMoved: ((CGFloat) -> Void)?
88
+ /// Touch ended or was cancelled.
89
+ var onTouchEnded: (() -> Void)?
90
+
91
+ override func layoutSubviews() {
92
+ super.layoutSubviews()
93
+ onLayout?()
94
+ }
95
+
96
+ // While the user is dragging the slider, NO ancestor pan gesture may act —
97
+ // not the enclosing scroll view's scroll, not the navigation stack's
98
+ // swipe-back, not a parent pager. UIKit's UIControl exemption
99
+ // (`touchesShouldCancelInContentView:`) does NOT help, because the scroll
100
+ // view passes its *direct* content subview to that method, not this
101
+ // deeply-nested view. So on touch-down we walk the ancestor chain and
102
+ // temporarily disable every pan-type recognizer (UIScrollView pan,
103
+ // UIScreenEdgePan / RNSPanGestureRecognizer back-swipe, pager pans — all are
104
+ // UIPanGestureRecognizer subclasses), restoring them on touch-up. This is the
105
+ // iOS analogue of Android's `requestDisallowInterceptTouchEvent(true)`.
106
+ private var disabledRecognizers: [UIGestureRecognizer] = []
107
+
108
+ private func lockAncestorGestures() {
109
+ var v: UIView? = superview
110
+ while let cur = v {
111
+ if let grs = cur.gestureRecognizers {
112
+ for gr in grs where gr is UIPanGestureRecognizer && gr.isEnabled {
113
+ gr.isEnabled = false // cancels it if mid-recognition, blocks it otherwise
114
+ disabledRecognizers.append(gr)
115
+ }
116
+ }
117
+ v = cur.superview
118
+ }
119
+ }
120
+
121
+ private func unlockAncestorGestures() {
122
+ for gr in disabledRecognizers {
123
+ gr.isEnabled = true
124
+ }
125
+ disabledRecognizers.removeAll()
126
+ }
127
+
128
+ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
129
+ super.touchesBegan(touches, with: event)
130
+ lockAncestorGestures()
131
+ guard let t = touches.first else { return }
132
+ onTouchBegan?(t.location(in: self).x)
133
+ }
134
+
135
+ override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
136
+ super.touchesMoved(touches, with: event)
137
+ guard let t = touches.first else { return }
138
+ onTouchMoved?(t.location(in: self).x)
139
+ }
140
+
141
+ override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
142
+ super.touchesEnded(touches, with: event)
143
+ unlockAncestorGestures()
144
+ onTouchEnded?()
145
+ }
146
+
147
+ override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
148
+ super.touchesCancelled(touches, with: event)
149
+ unlockAncestorGestures()
150
+ onTouchEnded?()
151
+ }
152
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=SegmentSlider.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["SegmentSlider.nitro.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ import { useCallback, useRef } from 'react';
4
+ import { callback, getHostComponent } from 'react-native-nitro-modules';
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ const SegmentSliderConfig = require('../nitrogen/generated/shared/json/SegmentSliderConfig.json');
7
+ const SegmentSliderHost = getHostComponent('SegmentSlider', () => SegmentSliderConfig);
8
+
9
+ /**
10
+ * Thin wrapper so consumers pass plain functions. Nitro view callbacks must be
11
+ * wrapped with `callback(...)` to cross the JSI boundary; we encapsulate that
12
+ * here behind refs so the underlying functions stay stable across re-renders
13
+ * (otherwise every parent render would re-create the native callback).
14
+ */
15
+ export function SegmentSliderView({
16
+ onChange,
17
+ onSlideStart,
18
+ onSlideComplete,
19
+ ...props
20
+ }) {
21
+ const onChangeRef = useRef(onChange);
22
+ onChangeRef.current = onChange;
23
+ const onSlideStartRef = useRef(onSlideStart);
24
+ onSlideStartRef.current = onSlideStart;
25
+ const onSlideCompleteRef = useRef(onSlideComplete);
26
+ onSlideCompleteRef.current = onSlideComplete;
27
+ const stableOnChange = useCallback(value => {
28
+ onChangeRef.current?.(value);
29
+ }, []);
30
+ const stableOnSlideStart = useCallback(() => {
31
+ onSlideStartRef.current?.();
32
+ }, []);
33
+ const stableOnSlideComplete = useCallback(() => {
34
+ onSlideCompleteRef.current?.();
35
+ }, []);
36
+ return /*#__PURE__*/_jsx(SegmentSliderHost, {
37
+ ...props,
38
+ onChange: onChange ? callback(stableOnChange) : undefined,
39
+ onSlideStart: onSlideStart ? callback(stableOnSlideStart) : undefined,
40
+ onSlideComplete: onSlideComplete ? callback(stableOnSlideComplete) : undefined
41
+ });
42
+ }
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useCallback","useRef","callback","getHostComponent","jsx","_jsx","SegmentSliderConfig","require","SegmentSliderHost","SegmentSliderView","onChange","onSlideStart","onSlideComplete","props","onChangeRef","current","onSlideStartRef","onSlideCompleteRef","stableOnChange","value","stableOnSlideStart","stableOnSlideComplete","undefined"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,WAAW,EAAEC,MAAM,QAAQ,OAAO;AAG3C,SAASC,QAAQ,EAAEC,gBAAgB,QAAQ,4BAA4B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAOxE,MAAMC,mBAAmB,GAAGC,OAAO,CAAC,4DAA4D,CAAC;AAIjG,MAAMC,iBAAiB,GAAGL,gBAAgB,CAGxC,eAAe,EAAE,MAAMG,mBAAmB,CAAC;;AAE7C;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,iBAAiBA,CAAC;EAChCC,QAAQ;EACRC,YAAY;EACZC,eAAe;EACf,GAAGC;AAC2B,CAAC,EAAE;EACjC,MAAMC,WAAW,GAAGb,MAAM,CAACS,QAAQ,CAAC;EACpCI,WAAW,CAACC,OAAO,GAAGL,QAAQ;EAC9B,MAAMM,eAAe,GAAGf,MAAM,CAACU,YAAY,CAAC;EAC5CK,eAAe,CAACD,OAAO,GAAGJ,YAAY;EACtC,MAAMM,kBAAkB,GAAGhB,MAAM,CAACW,eAAe,CAAC;EAClDK,kBAAkB,CAACF,OAAO,GAAGH,eAAe;EAE5C,MAAMM,cAAc,GAAGlB,WAAW,CAAEmB,KAAa,IAAK;IACpDL,WAAW,CAACC,OAAO,GAAGI,KAAK,CAAC;EAC9B,CAAC,EAAE,EAAE,CAAC;EACN,MAAMC,kBAAkB,GAAGpB,WAAW,CAAC,MAAM;IAC3CgB,eAAe,CAACD,OAAO,GAAG,CAAC;EAC7B,CAAC,EAAE,EAAE,CAAC;EACN,MAAMM,qBAAqB,GAAGrB,WAAW,CAAC,MAAM;IAC9CiB,kBAAkB,CAACF,OAAO,GAAG,CAAC;EAChC,CAAC,EAAE,EAAE,CAAC;EAEN,oBACEV,IAAA,CAACG,iBAAiB;IAAA,GACZK,KAAK;IACTH,QAAQ,EAAEA,QAAQ,GAAGR,QAAQ,CAACgB,cAAc,CAAC,GAAGI,SAAU;IAC1DX,YAAY,EAAEA,YAAY,GAAGT,QAAQ,CAACkB,kBAAkB,CAAC,GAAGE,SAAU;IACtEV,eAAe,EACbA,eAAe,GAAGV,QAAQ,CAACmB,qBAAqB,CAAC,GAAGC;EACrD,CACF,CAAC;AAEN","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,75 @@
1
+ ///
2
+ /// JFunc_void.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #include <fbjni/fbjni.h>
11
+ #include <functional>
12
+
13
+ #include <functional>
14
+ #include <NitroModules/JNICallable.hpp>
15
+
16
+ namespace margelo::nitro::segmentslider {
17
+
18
+ using namespace facebook;
19
+
20
+ /**
21
+ * Represents the Java/Kotlin callback `() -> Unit`.
22
+ * This can be passed around between C++ and Java/Kotlin.
23
+ */
24
+ struct JFunc_void: public jni::JavaClass<JFunc_void> {
25
+ public:
26
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/segmentslider/Func_void;";
27
+
28
+ public:
29
+ /**
30
+ * Invokes the function this `JFunc_void` instance holds through JNI.
31
+ */
32
+ void invoke() const {
33
+ static const auto method = javaClassStatic()->getMethod<void()>("invoke");
34
+ method(self());
35
+ }
36
+ };
37
+
38
+ /**
39
+ * An implementation of Func_void that is backed by a C++ implementation (using `std::function<...>`)
40
+ */
41
+ class JFunc_void_cxx final: public jni::HybridClass<JFunc_void_cxx, JFunc_void> {
42
+ public:
43
+ static jni::local_ref<JFunc_void::javaobject> fromCpp(const std::function<void()>& func) {
44
+ return JFunc_void_cxx::newObjectCxxArgs(func);
45
+ }
46
+
47
+ public:
48
+ /**
49
+ * Invokes the C++ `std::function<...>` this `JFunc_void_cxx` instance holds.
50
+ */
51
+ void invoke_cxx() {
52
+ _func();
53
+ }
54
+
55
+ public:
56
+ [[nodiscard]]
57
+ inline const std::function<void()>& getFunction() const {
58
+ return _func;
59
+ }
60
+
61
+ public:
62
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/segmentslider/Func_void_cxx;";
63
+ static void registerNatives() {
64
+ registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_cxx::invoke_cxx)});
65
+ }
66
+
67
+ private:
68
+ explicit JFunc_void_cxx(const std::function<void()>& func): _func(func) { }
69
+
70
+ private:
71
+ friend HybridBase;
72
+ std::function<void()> _func;
73
+ };
74
+
75
+ } // namespace margelo::nitro::segmentslider