impulse_thaw/slider/range_slider/
mod.rs1use 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 #[prop(default = 0f64.into(), into)]
13 min: Signal<f64>,
14 #[prop(default = 100f64.into(), into)]
16 max: Signal<f64>,
17 #[prop(optional, into)]
19 step: MaybeProp<f64>,
20 #[prop(default = true.into(), into)]
22 show_stops: Signal<bool>,
23 #[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}