freya_components/
slider.rs1use freya_core::prelude::*;
2use torin::prelude::*;
3
4use crate::{
5 get_theme,
6 theming::component_themes::SliderThemePartial,
7};
8
9#[derive(Debug, Default, PartialEq, Clone, Copy)]
10pub enum SliderStatus {
11 #[default]
12 Idle,
13 Hovering,
14}
15
16#[cfg_attr(feature = "docs",
38 doc = embed_doc_image::embed_image!("slider", "images/gallery_slider.png")
39)]
40#[derive(Clone, PartialEq)]
41pub struct Slider {
42 pub(crate) theme: Option<SliderThemePartial>,
43 value: f64,
44 on_moved: EventHandler<f64>,
45 size: Size,
46 direction: Direction,
47 enabled: bool,
48 key: DiffKey,
49}
50
51impl KeyExt for Slider {
52 fn write_key(&mut self) -> &mut DiffKey {
53 &mut self.key
54 }
55}
56
57impl Slider {
58 pub fn new(handler: impl FnMut(f64) + 'static) -> Self {
59 Self {
60 theme: None,
61 value: 0.0,
62 on_moved: EventHandler::new(handler),
63 size: Size::fill(),
64 direction: Direction::Horizontal,
65 enabled: true,
66 key: DiffKey::None,
67 }
68 }
69
70 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
71 self.enabled = enabled.into();
72 self
73 }
74
75 pub fn value(mut self, value: f64) -> Self {
76 self.value = value.clamp(0.0, 100.0);
77 self
78 }
79
80 pub fn theme(mut self, theme: SliderThemePartial) -> Self {
81 self.theme = Some(theme);
82 self
83 }
84
85 pub fn size(mut self, size: Size) -> Self {
86 self.size = size;
87 self
88 }
89
90 pub fn direction(mut self, direction: Direction) -> Self {
91 self.direction = direction;
92 self
93 }
94
95 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
96 self.key = key.into();
97 self
98 }
99}
100
101impl Render for Slider {
102 fn render(&self) -> impl IntoElement {
103 let theme = get_theme!(&self.theme, slider);
104 let focus = use_focus();
105 let focus_status = use_focus_status(focus);
106 let mut status = use_state(SliderStatus::default);
107 let mut clicking = use_state(|| false);
108 let mut size = use_state(Area::default);
109
110 let enabled = use_reactive(&self.enabled);
111 use_drop(move || {
112 if status() == SliderStatus::Hovering && enabled() {
113 Cursor::set(CursorIcon::default());
114 }
115 });
116
117 let direction_is_vertical = self.direction == Direction::Vertical;
118 let value = self.value.clamp(0.0, 100.0);
119 let on_moved = self.on_moved.clone();
120
121 let on_key_down = {
122 let on_moved = self.on_moved.clone();
123 move |e: Event<KeyboardEventData>| match e.key {
124 Key::ArrowLeft if !direction_is_vertical => {
125 e.stop_propagation();
126 on_moved.call((value - 4.0).clamp(0.0, 100.0));
127 }
128 Key::ArrowRight if !direction_is_vertical => {
129 e.stop_propagation();
130 on_moved.call((value + 4.0).clamp(0.0, 100.0));
131 }
132 Key::ArrowUp if direction_is_vertical => {
133 e.stop_propagation();
134 on_moved.call((value + 4.0).clamp(0.0, 100.0));
135 }
136 Key::ArrowDown if direction_is_vertical => {
137 e.stop_propagation();
138 on_moved.call((value - 4.0).clamp(0.0, 100.0));
139 }
140 _ => {}
141 }
142 };
143
144 let on_pointer_enter = move |_| {
145 *status.write() = SliderStatus::Hovering;
146 if enabled() {
147 Cursor::set(CursorIcon::Pointer);
148 } else {
149 Cursor::set(CursorIcon::NotAllowed);
150 }
151 };
152
153 let on_pointer_leave = move |_| {
154 Cursor::set(CursorIcon::default());
155 *status.write() = SliderStatus::Idle;
156 };
157
158 let on_pointer_down = {
159 let on_moved = self.on_moved.clone();
160 move |e: Event<PointerEventData>| {
161 focus.request_focus();
162 clicking.set(true);
163 e.stop_propagation();
164 let coordinates = e.element_location();
165 let percentage = if direction_is_vertical {
166 let y = coordinates.y - 8.0;
167 100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
168 } else {
169 let x = coordinates.x - 8.0;
170 x / (size.read().width() as f64 - 15.) * 100.0
171 };
172 let percentage = percentage.clamp(0.0, 100.0);
173
174 on_moved.call(percentage);
175 }
176 };
177
178 let on_global_mouse_up = move |_| {
179 clicking.set(false);
180 };
181
182 let on_global_mouse_move = move |e: Event<MouseEventData>| {
183 e.stop_propagation();
184 if *clicking.peek() {
185 let coordinates = e.global_location;
186 let percentage = if direction_is_vertical {
187 let y = coordinates.y - size.read().min_y() as f64 - 8.0;
188 100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
189 } else {
190 let x = coordinates.x - size.read().min_x() as f64 - 8.0;
191 x / (size.read().width() as f64 - 15.) * 100.0
192 };
193 let percentage = percentage.clamp(0.0, 100.0);
194
195 on_moved.call(percentage);
196 }
197 };
198
199 let border = if focus_status() == FocusStatus::Keyboard {
200 Border::new()
201 .fill(theme.border_fill)
202 .width(2.)
203 .alignment(BorderAlignment::Inner)
204 } else {
205 Border::new()
206 .fill(Color::TRANSPARENT)
207 .width(0.)
208 .alignment(BorderAlignment::Inner)
209 };
210
211 let (
212 slider_width,
213 slider_height,
214 track_width,
215 track_height,
216 thumb_offset_x,
217 thumb_offset_y,
218 thumb_main_align,
219 padding,
220 ) = if direction_is_vertical {
221 (
222 Size::px(6.),
223 self.size.clone(),
224 Size::px(6.),
225 Size::func_data(
226 move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
227 &(value as i32),
228 ),
229 -6.,
230 3.,
231 Alignment::end(),
232 (0., 8.),
233 )
234 } else {
235 (
236 self.size.clone(),
237 Size::px(6.),
238 Size::func_data(
239 move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
240 &(value as i32),
241 ),
242 Size::px(6.),
243 -3.,
244 -6.,
245 Alignment::start(),
246 (8., 0.),
247 )
248 };
249
250 let thumb = rect()
251 .width(Size::fill())
252 .offset_x(thumb_offset_x)
253 .offset_y(thumb_offset_y)
254 .child(
255 rect()
256 .width(Size::px(18.))
257 .height(Size::px(18.))
258 .corner_radius(50.)
259 .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
260 .padding(4.)
261 .child(
262 rect()
263 .width(Size::fill())
264 .height(Size::fill())
265 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
266 .corner_radius(50.),
267 ),
268 );
269
270 let track = rect()
271 .width(track_width)
272 .height(track_height)
273 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
274 .corner_radius(50.);
275
276 rect()
277 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
278 .maybe(self.enabled, |rect| {
279 rect.on_key_down(on_key_down)
280 .on_pointer_down(on_pointer_down)
281 .on_global_mouse_move(on_global_mouse_move)
282 .on_global_mouse_up(on_global_mouse_up)
283 })
284 .on_pointer_enter(on_pointer_enter)
285 .on_pointer_leave(on_pointer_leave)
286 .a11y_id(focus.a11y_id())
287 .a11y_focusable(self.enabled)
288 .border(border)
289 .corner_radius(50.)
290 .padding(padding)
291 .child(
292 rect()
293 .width(slider_width)
294 .height(slider_height)
295 .background(theme.background.mul_if(!self.enabled, 0.85))
296 .corner_radius(50.)
297 .direction(self.direction)
298 .main_align(thumb_main_align)
299 .children(if direction_is_vertical {
300 vec![thumb.into(), track.into()]
301 } else {
302 vec![track.into(), thumb.into()]
303 }),
304 )
305 }
306
307 fn render_key(&self) -> DiffKey {
308 self.key.clone().or(self.default_key())
309 }
310}