1use std::sync::Arc;
2
3#[derive(Debug, Clone)]
4pub struct SliderValuesUpdate {
5 pub values: Vec<f32>,
6 pub value_index_to_change: usize,
7}
8
9pub fn format_semantics_value(value: f32) -> Arc<str> {
15 if !value.is_finite() {
16 return Arc::from("NaN");
17 }
18 let rounded = value.round();
19 if (value - rounded).abs() < 1e-4 {
20 return Arc::from(format!("{}", rounded as i64).into_boxed_str());
21 }
22 Arc::from(format!("{value:.2}").into_boxed_str())
23}
24
25pub fn normalize_value(value: f32, min: f32, max: f32) -> f32 {
27 if !value.is_finite() || !min.is_finite() || !max.is_finite() {
28 return 0.0;
29 }
30 let span = max - min;
31 if !span.is_finite() || span.abs() <= f32::EPSILON {
32 return 0.0;
33 }
34 ((value - min) / span).clamp(0.0, 1.0)
35}
36
37fn decimal_count(step: f32) -> u32 {
38 let step = step.abs();
39 if !step.is_finite() || step <= 0.0 {
40 return 0;
41 }
42
43 let s = step.to_string();
44 let mut exp = 0i32;
45 let mut base = s.as_str();
46 if let Some(exp_at) = s.find(['e', 'E']) {
47 base = &s[..exp_at];
48 exp = s[exp_at + 1..].parse::<i32>().unwrap_or(0);
49 }
50
51 let base_decimals = base
52 .split_once('.')
53 .map(|(_, frac)| frac.len())
54 .unwrap_or(0) as i32;
55 let decimals = if exp < 0 {
56 base_decimals.saturating_add((-exp).min(38))
57 } else {
58 base_decimals.saturating_sub(exp)
59 };
60
61 decimals.max(0) as u32
62}
63
64fn round_to_step_decimals(value: f32, step: f32) -> f32 {
65 let decimals = decimal_count(step).min(10);
66 if decimals == 0 {
67 return value;
68 }
69 let factor = 10f64.powi(decimals as i32);
70 ((value as f64 * factor).round() / factor) as f32
71}
72
73pub fn snap_value(value: f32, min: f32, max: f32, step: f32) -> f32 {
75 if !value.is_finite() || !min.is_finite() || !max.is_finite() {
76 return min;
77 }
78 let mut out = value.clamp(min, max);
79 if step.is_finite() && step > 0.0 {
80 let steps = ((out - min) / step).round();
81 out = (min + steps * step).clamp(min, max);
82 out = round_to_step_decimals(out, step);
83 }
84 out
85}
86
87pub fn next_sorted_values(prev_values: &[f32], next_value: f32, at_index: usize) -> Vec<f32> {
91 if prev_values.is_empty() {
92 return vec![next_value];
93 }
94
95 let mut next_values = prev_values.to_vec();
96 let index = at_index.min(next_values.len().saturating_sub(1));
97 next_values[index] = next_value;
98 next_values.sort_by(|a, b| a.total_cmp(b));
99 next_values
100}
101
102pub fn closest_value_index(values: &[f32], next_value: f32) -> usize {
106 if values.len() <= 1 {
107 return 0;
108 }
109
110 let mut closest_index = 0;
111 let mut closest_distance = (values[0] - next_value).abs();
112 for (index, value) in values.iter().copied().enumerate().skip(1) {
113 let distance = (value - next_value).abs();
114 if distance < closest_distance {
115 closest_index = index;
116 closest_distance = distance;
117 }
118 }
119 closest_index
120}
121
122pub fn steps_between_values(values: &[f32]) -> Vec<f32> {
126 values.windows(2).map(|pair| pair[1] - pair[0]).collect()
127}
128
129pub fn has_min_steps_between_values(values: &[f32], min_steps_between_values: f32) -> bool {
133 if min_steps_between_values <= 0.0 {
134 return true;
135 }
136
137 let Some(min_delta) = steps_between_values(values).into_iter().reduce(f32::min) else {
138 return true;
139 };
140
141 min_delta >= min_steps_between_values
142}
143
144pub fn update_multi_thumb_values(
148 prev_values: &[f32],
149 raw_value: f32,
150 at_index: usize,
151 min: f32,
152 max: f32,
153 step: f32,
154 min_steps_between_thumbs: u32,
155) -> Option<SliderValuesUpdate> {
156 let step = if step.is_finite() && step > 0.0 {
157 step
158 } else {
159 1.0
160 };
161 let next_value = snap_value(raw_value, min, max, step);
162
163 let next_values = next_sorted_values(prev_values, next_value, at_index);
164 let min_steps_between_values = min_steps_between_thumbs as f32 * step;
165 if !has_min_steps_between_values(&next_values, min_steps_between_values) {
166 return None;
167 }
168
169 let value_index_to_change = next_values
170 .iter()
171 .position(|value| *value == next_value)
172 .unwrap_or(0);
173
174 Some(SliderValuesUpdate {
175 values: next_values,
176 value_index_to_change,
177 })
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn normalize_value_handles_degenerate_ranges() {
186 assert_eq!(normalize_value(5.0, 0.0, 0.0), 0.0);
187 assert_eq!(normalize_value(f32::NAN, 0.0, 1.0), 0.0);
188 assert_eq!(normalize_value(0.0, f32::NAN, 1.0), 0.0);
189 }
190
191 #[test]
192 fn normalize_value_clamps_to_unit_interval() {
193 assert_eq!(normalize_value(-1.0, 0.0, 10.0), 0.0);
194 assert_eq!(normalize_value(0.0, 0.0, 10.0), 0.0);
195 assert_eq!(normalize_value(5.0, 0.0, 10.0), 0.5);
196 assert_eq!(normalize_value(10.0, 0.0, 10.0), 1.0);
197 assert_eq!(normalize_value(999.0, 0.0, 10.0), 1.0);
198 }
199
200 #[test]
201 fn snap_value_snaps_to_nearest_step() {
202 assert_eq!(snap_value(0.0, 0.0, 10.0, 1.0), 0.0);
203 assert_eq!(snap_value(0.49, 0.0, 10.0, 1.0), 0.0);
204 assert_eq!(snap_value(0.51, 0.0, 10.0, 1.0), 1.0);
205 assert_eq!(snap_value(9.8, 0.0, 10.0, 1.0), 10.0);
206 assert_eq!(snap_value(5.3, 0.0, 10.0, 0.5), 5.5);
207 }
208
209 #[test]
210 fn snap_value_rounds_float_steps_like_radix() {
211 let v = snap_value(0.30000004, 0.0, 1.0, 0.1);
212 assert!((v - 0.3).abs() < 1e-6);
213
214 let v = snap_value(1.0000001, 0.0, 2.0, 0.25);
215 assert!((v - 1.0).abs() < 1e-6);
216 }
217
218 #[test]
219 fn format_semantics_value_uses_integer_when_close() {
220 assert_eq!(format_semantics_value(12.0).as_ref(), "12");
221 assert_eq!(format_semantics_value(12.00001).as_ref(), "12");
222 }
223
224 #[test]
225 fn closest_value_index_matches_radix_examples() {
226 assert_eq!(closest_value_index(&[10.0, 30.0], 25.0), 1);
227 assert_eq!(closest_value_index(&[10.0, 30.0], 11.0), 0);
228 }
229
230 #[test]
231 fn update_multi_thumb_values_sorts_and_updates_index() {
232 let update = update_multi_thumb_values(&[30.0, 10.0], 11.0, 1, 0.0, 100.0, 1.0, 0)
233 .expect("update should be allowed");
234 assert_eq!(update.values, vec![11.0, 30.0]);
235 assert_eq!(update.value_index_to_change, 0);
236 }
237
238 #[test]
239 fn update_multi_thumb_values_enforces_min_steps() {
240 let rejected = update_multi_thumb_values(&[10.0, 12.0], 11.0, 0, 0.0, 100.0, 1.0, 2);
241 assert!(rejected.is_none());
242
243 let allowed = update_multi_thumb_values(&[10.0, 13.0], 11.0, 0, 0.0, 100.0, 1.0, 2);
244 assert!(allowed.is_some());
245 }
246}