freya_components/
slider.rs1use freya_core::prelude::*;
2use torin::prelude::*;
3
4use crate::{
5 define_theme,
6 get_theme,
7};
8
9define_theme! {
10 %[component]
11 pub Slider {
12 %[fields]
13 background: Color,
14 thumb_background: Color,
15 thumb_inner_background: Color,
16 border_fill: Color,
17 }
18}
19
20#[cfg_attr(feature = "docs",
42 doc = embed_doc_image::embed_image!("slider", "images/gallery_slider.png")
43)]
44#[derive(Clone, PartialEq)]
45pub struct Slider {
46 pub(crate) theme: Option<SliderThemePartial>,
47 value: f64,
48 on_moved: EventHandler<f64>,
49 size: Size,
50 direction: Direction,
51 enabled: bool,
52 scroll_enabled: bool,
53 cursor_icon: CursorIcon,
54 step: Option<f64>,
55 key: DiffKey,
56}
57
58impl KeyExt for Slider {
59 fn write_key(&mut self) -> &mut DiffKey {
60 &mut self.key
61 }
62}
63
64impl Slider {
65 pub fn new(on_moved: impl Into<EventHandler<f64>>) -> Self {
66 Self {
67 theme: None,
68 value: 0.0,
69 on_moved: on_moved.into(),
70 size: Size::fill(),
71 direction: Direction::Horizontal,
72 enabled: true,
73 scroll_enabled: true,
74 cursor_icon: CursorIcon::default(),
75 step: None,
76 key: DiffKey::None,
77 }
78 }
79
80 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
81 self.enabled = enabled.into();
82 self
83 }
84
85 pub fn scroll_enabled(mut self, scroll_enabled: impl Into<bool>) -> Self {
89 self.scroll_enabled = scroll_enabled.into();
90 self
91 }
92
93 pub fn value(mut self, value: f64) -> Self {
94 self.value = value.clamp(0.0, 100.0);
95 self
96 }
97
98 pub fn theme(mut self, theme: SliderThemePartial) -> Self {
99 self.theme = Some(theme);
100 self
101 }
102
103 pub fn size(mut self, size: Size) -> Self {
104 self.size = size;
105 self
106 }
107
108 pub fn direction(mut self, direction: Direction) -> Self {
109 self.direction = direction;
110 self
111 }
112
113 pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
115 self.cursor_icon = cursor_icon.into();
116 self
117 }
118
119 pub fn step(mut self, step: f64) -> Self {
121 self.step = Some(step);
122 self
123 }
124}
125
126impl Component for Slider {
127 fn render(&self) -> impl IntoElement {
128 let theme = get_theme!(&self.theme, SliderThemePreference, "slider");
129 let a11y_id = use_a11y();
130 let focus = use_focus(a11y_id);
131 let mut clicking = use_state(|| false);
132 let mut size = use_state(Area::default);
133
134 let direction_is_vertical = self.direction == Direction::Vertical;
135 let value = self.value;
136 let on_moved = self.on_moved.clone();
137
138 let step = self.step.filter(|step| *step > 0.0);
139 let snap = move |value: f64| -> f64 {
140 let value = value.clamp(0.0, 100.0);
141 match step {
142 Some(step) => ((value / step).round() * step).min(100.0),
143 None => value,
144 }
145 };
146 let keyboard_step = step.unwrap_or(4.0);
147
148 let on_key_down = {
149 let on_moved = self.on_moved.clone();
150 move |e: Event<KeyboardEventData>| match e.key {
151 Key::Named(NamedKey::ArrowLeft) if !direction_is_vertical => {
152 e.stop_propagation();
153 on_moved.call(snap(value - keyboard_step));
154 }
155 Key::Named(NamedKey::ArrowRight) if !direction_is_vertical => {
156 e.stop_propagation();
157 on_moved.call(snap(value + keyboard_step));
158 }
159 Key::Named(NamedKey::ArrowUp) if direction_is_vertical => {
160 e.stop_propagation();
161 on_moved.call(snap(value + keyboard_step));
162 }
163 Key::Named(NamedKey::ArrowDown) if direction_is_vertical => {
164 e.stop_propagation();
165 on_moved.call(snap(value - keyboard_step));
166 }
167 _ => {}
168 }
169 };
170
171 let calc_percentage = move |x: f64, y: f64| -> f64 {
172 let pct = if direction_is_vertical {
173 let y = y - 8.0;
174 100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
175 } else {
176 let x = x - 8.0;
177 x / (size.read().width() as f64 - 15.) * 100.0
178 };
179 snap(pct)
180 };
181
182 let on_pointer_down = {
183 let on_moved = self.on_moved.clone();
184 move |e: Event<PointerEventData>| {
185 if !e.data().is_primary() {
186 return;
187 }
188 a11y_id.request_focus();
189 clicking.set(true);
190 e.stop_propagation();
191 let coordinates = e.element_location();
192 on_moved.call(calc_percentage(coordinates.x, coordinates.y));
193 }
194 };
195
196 let on_global_pointer_press = move |_: Event<PointerEventData>| {
197 clicking.set(false);
198 };
199
200 let on_global_pointer_move = move |e: Event<PointerEventData>| {
201 e.stop_propagation();
202 if *clicking.peek() {
203 let coordinates = e.global_location();
204 on_moved.call(calc_percentage(
205 coordinates.x - size.read().min_x() as f64,
206 coordinates.y - size.read().min_y() as f64,
207 ));
208 }
209 };
210
211 let on_wheel = {
212 let on_moved = self.on_moved.clone();
213 move |e: Event<WheelEventData>| {
214 if e.delta_y == 0.0 {
215 return;
216 }
217 e.stop_propagation();
218 let delta = match step {
219 Some(step) => step * e.delta_y.signum(),
220 None => e.delta_y * 0.1,
221 };
222 on_moved.call(snap(value + delta));
223 }
224 };
225
226 let border = if focus() == Focus::Keyboard {
227 Border::new()
228 .fill(theme.border_fill)
229 .width(2.)
230 .alignment(BorderAlignment::Inner)
231 } else {
232 Border::new()
233 .fill(Color::TRANSPARENT)
234 .width(0.)
235 .alignment(BorderAlignment::Inner)
236 };
237
238 let (slider_width, slider_height) = if direction_is_vertical {
239 (Size::auto(), self.size.clone())
240 } else {
241 (self.size.clone(), Size::auto())
242 };
243
244 let (inner_slider_width, inner_slider_height) = if direction_is_vertical {
245 (Size::px(6.), self.size.clone())
246 } else {
247 (self.size.clone(), Size::px(6.))
248 };
249
250 let track_size = Size::func_data(
251 move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
252 &(value as i32),
253 );
254
255 let (track_width, track_height) = if direction_is_vertical {
256 (Size::px(6.), track_size)
257 } else {
258 (track_size, Size::px(6.))
259 };
260
261 let (thumb_offset_x, thumb_offset_y) = if direction_is_vertical {
262 (-6., 3.)
263 } else {
264 (-3., -6.)
265 };
266
267 let thumb_main_align = if direction_is_vertical {
268 Alignment::end()
269 } else {
270 Alignment::start()
271 };
272
273 let padding = if direction_is_vertical {
274 (0., 8.)
275 } else {
276 (8., 0.)
277 };
278
279 let thumb = rect()
280 .width(Size::fill())
281 .offset_x(thumb_offset_x)
282 .offset_y(thumb_offset_y)
283 .child(
284 rect()
285 .width(Size::px(18.))
286 .height(Size::px(18.))
287 .corner_radius(50.)
288 .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
289 .padding(4.)
290 .child(
291 rect()
292 .expanded()
293 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
294 .corner_radius(50.),
295 ),
296 );
297
298 let track = rect()
299 .width(track_width)
300 .height(track_height)
301 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
302 .corner_radius(50.);
303
304 rect()
305 .a11y_id(a11y_id)
306 .a11y_focusable(self.enabled)
307 .a11y_role(AccessibilityRole::Slider)
308 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
309 .maybe(self.enabled, |rect| {
310 rect.on_key_down(on_key_down)
311 .on_pointer_down(on_pointer_down)
312 .on_global_pointer_move(on_global_pointer_move)
313 .on_global_pointer_press(on_global_pointer_press)
314 .maybe(self.scroll_enabled, |el| el.on_wheel(on_wheel))
315 })
316 .cursor(if self.enabled {
317 self.cursor_icon
318 } else {
319 CursorIcon::NotAllowed
320 })
321 .border(border)
322 .corner_radius(50.)
323 .padding(padding)
324 .width(slider_width)
325 .height(slider_height)
326 .child(
327 rect()
328 .width(inner_slider_width)
329 .height(inner_slider_height)
330 .background(theme.background.mul_if(!self.enabled, 0.85))
331 .corner_radius(50.)
332 .direction(self.direction)
333 .main_align(thumb_main_align)
334 .children(if direction_is_vertical {
335 vec![thumb, track]
336 } else {
337 vec![track, thumb]
338 }),
339 )
340 }
341
342 fn render_key(&self) -> DiffKey {
343 self.key.clone().or(self.default_key())
344 }
345}