1use crate::components::number_utils::{clamp, round_with_precision};
7use dioxus::prelude::Key;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
11pub enum SliderOrientation {
12 #[default]
13 Horizontal,
14 Vertical,
15}
16
17impl SliderOrientation {
18 pub fn is_vertical(self) -> bool {
19 matches!(self, SliderOrientation::Vertical)
20 }
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct SliderMath {
26 pub min: f64,
27 pub max: f64,
28 pub step: Option<f64>,
30 pub precision: Option<u32>,
32 pub reverse: bool,
34 pub orientation: SliderOrientation,
35}
36
37impl Default for SliderMath {
38 fn default() -> Self {
39 Self {
40 min: 0.0,
41 max: 100.0,
42 step: Some(1.0),
43 precision: None,
44 reverse: false,
45 orientation: SliderOrientation::Horizontal,
46 }
47 }
48}
49
50impl SliderMath {
51 pub fn normalized(self) -> Self {
53 if self.max >= self.min {
54 self
55 } else {
56 Self {
57 min: self.max,
58 max: self.min,
59 ..self
60 }
61 }
62 }
63
64 pub fn range(&self) -> f64 {
65 (self.max - self.min).abs().max(f64::EPSILON)
66 }
67}
68
69pub fn snap_value(value: f64, math: &SliderMath) -> f64 {
71 let math = math.normalized();
72 let clamped = clamp(
73 value,
74 &crate::components::number_utils::NumberRules {
75 min: Some(math.min),
76 max: Some(math.max),
77 step: None,
78 precision: None,
79 },
80 );
81
82 let snapped = if let Some(step) = math.step {
84 let positive_step = step.abs();
85 if positive_step <= f64::EPSILON {
86 clamped
87 } else {
88 let steps = ((clamped - math.min) / positive_step).round();
89 let candidate = math.min + steps * positive_step;
90 candidate.min(math.max)
92 }
93 } else {
94 clamped
95 };
96
97 round_with_precision(snapped, math.precision)
98}
99
100pub fn value_to_ratio(value: f64, math: &SliderMath) -> f64 {
102 let math = math.normalized();
103 let snapped = snap_value(value, &math);
104 let ratio = (snapped - math.min) / math.range();
105 if math.reverse { 1.0 - ratio } else { ratio }.clamp(0.0, 1.0)
106}
107
108pub fn ratio_to_value(ratio: f64, math: &SliderMath) -> f64 {
110 let math = math.normalized();
111 let mut normalized = ratio.clamp(0.0, 1.0);
112 if math.reverse {
113 normalized = 1.0 - normalized;
114 }
115 let raw = math.min + normalized * math.range();
116 snap_value(raw, &math)
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum KeyboardAction {
122 Step(i32),
124 ToMin,
126 ToMax,
128}
129
130pub fn keyboard_action_for_key(key: &Key, reverse: bool) -> Option<KeyboardAction> {
132 let direction = |positive: KeyboardAction, negative: KeyboardAction| {
133 if reverse { negative } else { positive }
134 };
135 match key {
136 Key::ArrowRight | Key::ArrowUp => {
137 Some(direction(KeyboardAction::Step(1), KeyboardAction::Step(-1)))
138 }
139 Key::ArrowLeft | Key::ArrowDown => {
140 Some(direction(KeyboardAction::Step(-1), KeyboardAction::Step(1)))
141 }
142 Key::PageUp => Some(direction(
143 KeyboardAction::Step(10),
144 KeyboardAction::Step(-10),
145 )),
146 Key::PageDown => Some(direction(
147 KeyboardAction::Step(-10),
148 KeyboardAction::Step(10),
149 )),
150 Key::Home => Some(KeyboardAction::ToMin),
151 Key::End => Some(KeyboardAction::ToMax),
152 _ => None,
153 }
154}
155
156pub fn apply_keyboard_action(current: f64, action: KeyboardAction, math: &SliderMath) -> f64 {
158 match action {
159 KeyboardAction::Step(delta) => {
160 let step = math.step.unwrap_or(1.0).abs();
161 let next = current + (delta as f64) * step;
162 snap_value(next, math)
163 }
164 KeyboardAction::ToMin => snap_value(math.normalized().min, math),
165 KeyboardAction::ToMax => snap_value(math.normalized().max, math),
166 }
167}
168
169#[cfg(target_arch = "wasm32")]
171pub fn ratio_from_pointer_event(
172 event: &web_sys::PointerEvent,
173 rect: &web_sys::DomRect,
174 math: &SliderMath,
175) -> Option<f64> {
176 let axis_size = if math.orientation.is_vertical() {
177 rect.height()
178 } else {
179 rect.width()
180 };
181 if axis_size <= 0.0 {
182 return None;
183 }
184
185 let origin = if math.orientation.is_vertical() {
186 rect.y()
187 } else {
188 rect.x()
189 };
190 let position = if math.orientation.is_vertical() {
191 event.client_y() as f64
192 } else {
193 event.client_x() as f64
194 };
195
196 let mut ratio = (position - origin) / axis_size;
197 ratio = ratio.clamp(0.0, 1.0);
198 if math.reverse {
199 ratio = 1.0 - ratio;
200 }
201 Some(ratio)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn snap_respects_step_and_precision() {
210 let math = SliderMath {
211 min: 0.0,
212 max: 5.0,
213 step: Some(0.3),
214 precision: Some(2),
215 reverse: false,
216 orientation: SliderOrientation::Horizontal,
217 };
218 assert_eq!(snap_value(1.11, &math), 1.2);
219 assert_eq!(snap_value(5.5, &math), 5.0);
220 }
221
222 #[test]
223 fn ratio_conversion_respects_reverse() {
224 let math = SliderMath {
225 min: 0.0,
226 max: 100.0,
227 step: Some(1.0),
228 precision: None,
229 reverse: true,
230 orientation: SliderOrientation::Horizontal,
231 };
232 assert!((value_to_ratio(50.0, &math) - 0.5).abs() < 1e-6);
234 assert!(value_to_ratio(10.0, &math) > value_to_ratio(90.0, &math));
236 }
237
238 #[test]
239 fn ratio_roundtrip() {
240 let math = SliderMath {
241 min: -10.0,
242 max: 10.0,
243 step: Some(0.5),
244 precision: Some(1),
245 reverse: false,
246 orientation: SliderOrientation::Horizontal,
247 };
248 let ratio = value_to_ratio(2.5, &math);
249 let back = ratio_to_value(ratio, &math);
250 assert!((back - 2.5).abs() < 1e-6);
251 }
252
253 #[test]
254 fn keyboard_actions_apply_steps() {
255 let math = SliderMath {
256 min: 0.0,
257 max: 10.0,
258 step: Some(1.0),
259 precision: None,
260 reverse: false,
261 orientation: SliderOrientation::Horizontal,
262 };
263 let next = apply_keyboard_action(5.0, KeyboardAction::Step(1), &math);
264 assert_eq!(next, 6.0);
265 let prev = apply_keyboard_action(0.2, KeyboardAction::Step(-1), &math);
266 assert_eq!(prev, 0.0);
267 let maxed = apply_keyboard_action(3.0, KeyboardAction::ToMax, &math);
268 assert_eq!(maxed, 10.0);
269 }
270
271 #[test]
272 fn keyboard_action_for_key_respects_reverse() {
273 let forward = keyboard_action_for_key(&Key::ArrowRight, false).unwrap();
274 assert_eq!(forward, KeyboardAction::Step(1));
275 let reversed = keyboard_action_for_key(&Key::ArrowRight, true).unwrap();
276 assert_eq!(reversed, KeyboardAction::Step(-1));
277 }
278}