impulse_thaw/slider/range_slider/
mod.rs

1use super::super::SliderInjection;
2use leptos::{context::Provider, ev, html, prelude::*};
3use thaw_components::OptionComp;
4use thaw_utils::{class_list, mount_style, Model};
5
6#[component]
7pub fn RangeSlider(
8    #[prop(optional, into)] class: MaybeProp<String>,
9    #[prop(optional, into)] style: MaybeProp<String>,
10    #[prop(optional, into)] value: Model<(f64, f64)>,
11    /// Min value of the slider.
12    #[prop(default = 0f64.into(), into)]
13    min: Signal<f64>,
14    /// Max value of the slider.
15    #[prop(default = 100f64.into(), into)]
16    max: Signal<f64>,
17    /// The step in which value is incremented.
18    #[prop(optional, into)]
19    step: MaybeProp<f64>,
20    /// Whether to display breakpoints.
21    #[prop(default = true.into(), into)]
22    show_stops: Signal<bool>,
23    /// Render the Slider in a vertical orientation, smallest value on the bottom.
24    #[prop(optional, into)]
25    vertical: Signal<bool>,
26    #[prop(optional)] children: Option<Children>,
27) -> impl IntoView {
28    mount_style("range-slider", include_str!("./range-slider.css"));
29
30    let rail_ref = NodeRef::<html::Div>::new();
31    let left_mousemove = StoredValue::new(false);
32    let right_mousemove = StoredValue::new(false);
33    let mousemove_handle = StoredValue::new(None::<WindowListenerHandle>);
34    let mouseup_handle = StoredValue::new(None::<WindowListenerHandle>);
35    let current_value = Memo::new(move |_| {
36        let (mut left, mut right) = value.get();
37        let min = min.get();
38        let max = max.get();
39        let step = step.get().unwrap_or_default();
40
41        left = closest_multiple(left, step, min, max);
42        right = closest_multiple(right, step, min, max);
43
44        (left, right)
45    });
46
47    let left_progress = Memo::new(move |_| {
48        let value = current_value.get().0;
49        let min = min.get();
50        let max = max.get();
51
52        value / (max - min) * 100.0
53    });
54
55    let right_progress = Memo::new(move |_| {
56        let value = current_value.get().1;
57        let min = min.get();
58        let max = max.get();
59
60        value / (max - min) * 100.0
61    });
62
63    let css_vars = move || {
64        let mut css_vars = String::new();
65
66        if vertical.get() {
67            css_vars.push_str("--thaw-slider--direction: 0deg;");
68        } else {
69            css_vars.push_str("--thaw-slider--direction: 90deg;");
70        }
71
72        if let Some(step) = step.get() {
73            if step > 0.0 && show_stops.get() {
74                let max = max.get();
75                let min = min.get();
76
77                css_vars.push_str(&format!(
78                    "--thaw-range-slider--steps-percent: {:.2}%;",
79                    step * 100.0 / (max - min)
80                ));
81            }
82        }
83
84        if let Some(style) = style.get() {
85            css_vars.push_str(&style);
86        }
87
88        css_vars
89    };
90
91    let rail_css_vars = move || {
92        let left = left_progress.get();
93        let right = right_progress.get();
94        let (left, right) = if left > right {
95            (right, left)
96        } else {
97            (left, right)
98        };
99        format!("--thaw-range-slider--left-progress: {left:.2}%; --thaw-range-slider--right-progress: {right:.2}%;")
100    };
101
102    let update_value = move |left, right| {
103        let min = min.get_untracked();
104        let max = max.get_untracked();
105        let step = step.get_untracked().unwrap_or_default();
106
107        value.set((
108            closest_multiple(left, step, min, max),
109            closest_multiple(right, step, min, max),
110        ));
111    };
112
113    let on_click = move |e: web_sys::MouseEvent| {
114        if let Some(rail_el) = rail_ref.get_untracked() {
115            let min = min.get_untracked();
116            let max = max.get_untracked();
117
118            let rail_rect = rail_el.get_bounding_client_rect();
119            let percentage = if vertical.get_untracked() {
120                let ev_y = f64::from(e.y());
121                let rail_height = rail_rect.height();
122                (rail_height + rail_rect.y() - ev_y) / rail_height * (max - min)
123            } else {
124                let ev_x = f64::from(e.x());
125                (ev_x - rail_rect.x()) / rail_rect.width() * (max - min)
126            };
127
128            let (left, right) = current_value.get();
129            let left_diff = (left - percentage).abs();
130            let right_diff = (right - percentage).abs();
131
132            if left_diff <= right_diff {
133                update_value(percentage, right);
134            } else {
135                update_value(left, percentage);
136            }
137        };
138    };
139
140    let cleanup_event = move || {
141        mousemove_handle.update_value(|handle| {
142            if let Some(handle) = handle.take() {
143                handle.remove();
144            }
145        });
146
147        mouseup_handle.update_value(|handle| {
148            if let Some(handle) = handle.take() {
149                handle.remove();
150            }
151        });
152    };
153
154    let on_mousemove = move || {
155        let mousemove = window_event_listener(ev::mousemove, move |e| {
156            if let Some(rail_el) = rail_ref.get_untracked() {
157                let min = min.get_untracked();
158                let max = max.get_untracked();
159
160                let rail_rect = rail_el.get_bounding_client_rect();
161                let percentage = if vertical.get_untracked() {
162                    let ev_y = f64::from(e.y());
163                    let rail_y = rail_rect.y();
164                    let rail_height = rail_rect.height();
165
166                    let length = if ev_y < rail_y {
167                        rail_height
168                    } else if ev_y > rail_y + rail_height {
169                        0.0
170                    } else {
171                        rail_y + rail_height - ev_y
172                    };
173
174                    length / rail_height * (max - min)
175                } else {
176                    let ev_x = f64::from(e.x());
177                    let rail_x = rail_rect.x();
178                    let rail_width = rail_rect.width();
179
180                    let length = if ev_x < rail_x {
181                        0.0
182                    } else if ev_x > rail_x + rail_width {
183                        rail_width
184                    } else {
185                        ev_x - rail_x
186                    };
187
188                    length / rail_width * (max - min)
189                };
190
191                if left_mousemove.get_value() {
192                    update_value(percentage, current_value.get_untracked().1);
193                    return;
194                } else if right_mousemove.get_value() {
195                    update_value(current_value.get_untracked().0, percentage);
196                    return;
197                }
198            }
199            cleanup_event();
200        });
201
202        let mouseup = window_event_listener(ev::mouseup, move |_| {
203            left_mousemove.set_value(false);
204            right_mousemove.set_value(false);
205            cleanup_event();
206        });
207
208        mousemove_handle.set_value(Some(mousemove));
209        mouseup_handle.set_value(Some(mouseup));
210    };
211
212    let on_left_mousedown = move |_| {
213        left_mousemove.set_value(true);
214        on_mousemove();
215    };
216
217    let on_right_mousedown = move |_| {
218        right_mousemove.set_value(true);
219        on_mousemove();
220    };
221
222    Owner::on_cleanup(cleanup_event);
223
224    view! {
225        <div
226            class=class_list![
227                "thaw-range-slider",
228                move || format!("thaw-range-slider--{}", if vertical.get() { "vertical" } else { "horizontal" }),
229                class
230            ]
231            on:click=on_click
232            style=css_vars
233        >
234            <div class="thaw-range-slider__rail" style=rail_css_vars node_ref=rail_ref></div>
235            <div
236                class="thaw-range-slider__thumb"
237                style=move || format!("--thaw-range-slider--progress: {:.2}%;", left_progress.get())
238                on:mousedown=on_left_mousedown
239            ></div>
240            <div
241                class="thaw-range-slider__thumb"
242                style=move || {
243                    format!("--thaw-range-slider--progress: {:.2}%;", right_progress.get())
244                }
245                on:mousedown=on_right_mousedown
246            ></div>
247            <OptionComp value=children let:children>
248                <Provider value=SliderInjection {
249                    max,
250                    min,
251                    vertical,
252                }>
253                    <div class="thaw-range-slider__datalist">{children()}</div>
254                </Provider>
255            </OptionComp>
256        </div>
257    }
258}
259
260fn closest_multiple(mut value: f64, multiple: f64, min: f64, max: f64) -> f64 {
261    if value < min {
262        return min;
263    }
264
265    if multiple <= 0.0 {
266        return if value > max { max } else { value };
267    }
268
269    let quotient = (value - min) / multiple;
270
271    let lower_multiple = quotient.floor() * multiple + min;
272    let upper_multiple = quotient.ceil() * multiple + min;
273
274    value = if (value - lower_multiple).abs() <= (value - upper_multiple).abs() {
275        lower_multiple
276    } else {
277        upper_multiple
278    };
279
280    while value > max {
281        value -= multiple;
282    }
283
284    if value < min {
285        min
286    } else {
287        value
288    }
289}