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