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