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