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 cursor_icon: CursorIcon,
53 key: DiffKey,
54}
55
56impl KeyExt for Slider {
57 fn write_key(&mut self) -> &mut DiffKey {
58 &mut self.key
59 }
60}
61
62impl Slider {
63 pub fn new(on_moved: impl Into<EventHandler<f64>>) -> Self {
64 Self {
65 theme: None,
66 value: 0.0,
67 on_moved: on_moved.into(),
68 size: Size::fill(),
69 direction: Direction::Horizontal,
70 enabled: true,
71 cursor_icon: CursorIcon::default(),
72 key: DiffKey::None,
73 }
74 }
75
76 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
77 self.enabled = enabled.into();
78 self
79 }
80
81 pub fn value(mut self, value: f64) -> Self {
82 self.value = value.clamp(0.0, 100.0);
83 self
84 }
85
86 pub fn theme(mut self, theme: SliderThemePartial) -> Self {
87 self.theme = Some(theme);
88 self
89 }
90
91 pub fn size(mut self, size: Size) -> Self {
92 self.size = size;
93 self
94 }
95
96 pub fn direction(mut self, direction: Direction) -> Self {
97 self.direction = direction;
98 self
99 }
100
101 pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
103 self.cursor_icon = cursor_icon.into();
104 self
105 }
106}
107
108impl Component for Slider {
109 fn render(&self) -> impl IntoElement {
110 let theme = get_theme!(&self.theme, SliderThemePreference, "slider");
111 let a11y_id = use_a11y();
112 let focus = use_focus(a11y_id);
113 let mut hovering = use_state(|| false);
114 let mut clicking = use_state(|| false);
115 let mut size = use_state(Area::default);
116
117 let enabled = use_reactive(&self.enabled);
118 let cursor_icon = self.cursor_icon;
119 use_drop(move || {
120 if hovering() {
121 Cursor::set(CursorIcon::default());
122 }
123 });
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 on_key_down = {
130 let on_moved = self.on_moved.clone();
131 move |e: Event<KeyboardEventData>| match e.key {
132 Key::Named(NamedKey::ArrowLeft) if !direction_is_vertical => {
133 e.stop_propagation();
134 on_moved.call((value - 4.0).clamp(0.0, 100.0));
135 }
136 Key::Named(NamedKey::ArrowRight) if !direction_is_vertical => {
137 e.stop_propagation();
138 on_moved.call((value + 4.0).clamp(0.0, 100.0));
139 }
140 Key::Named(NamedKey::ArrowUp) if direction_is_vertical => {
141 e.stop_propagation();
142 on_moved.call((value + 4.0).clamp(0.0, 100.0));
143 }
144 Key::Named(NamedKey::ArrowDown) if direction_is_vertical => {
145 e.stop_propagation();
146 on_moved.call((value - 4.0).clamp(0.0, 100.0));
147 }
148 _ => {}
149 }
150 };
151
152 let on_pointer_enter = move |_| {
153 hovering.set(true);
154 if enabled() {
155 Cursor::set(cursor_icon);
156 } else {
157 Cursor::set(CursorIcon::NotAllowed);
158 }
159 };
160
161 let on_pointer_leave = move |_| {
162 Cursor::set(CursorIcon::default());
163 hovering.set(false);
164 };
165
166 let calc_percentage = move |x: f64, y: f64| -> f64 {
167 let pct = if direction_is_vertical {
168 let y = y - 8.0;
169 100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
170 } else {
171 let x = x - 8.0;
172 x / (size.read().width() as f64 - 15.) * 100.0
173 };
174 pct.clamp(0.0, 100.0)
175 };
176
177 let on_pointer_down = {
178 let on_moved = self.on_moved.clone();
179 move |e: Event<PointerEventData>| {
180 if !e.data().is_primary() {
181 return;
182 }
183 a11y_id.request_focus();
184 clicking.set(true);
185 e.stop_propagation();
186 let coordinates = e.element_location();
187 on_moved.call(calc_percentage(coordinates.x, coordinates.y));
188 }
189 };
190
191 let on_global_pointer_press = move |_: Event<PointerEventData>| {
192 clicking.set(false);
193 };
194
195 let on_global_pointer_move = move |e: Event<PointerEventData>| {
196 e.stop_propagation();
197 if *clicking.peek() {
198 let coordinates = e.global_location();
199 on_moved.call(calc_percentage(
200 coordinates.x - size.read().min_x() as f64,
201 coordinates.y - size.read().min_y() as f64,
202 ));
203 }
204 };
205
206 let on_wheel = {
207 let on_moved = self.on_moved.clone();
208 move |e: Event<WheelEventData>| {
209 if e.delta_y == 0.0 {
210 return;
211 }
212 e.stop_propagation();
213 on_moved.call((value + e.delta_y * 0.1).clamp(0.0, 100.0));
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_focusable(self.enabled)
298 .a11y_role(AccessibilityRole::Slider)
299 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
300 .maybe(self.enabled, |rect| {
301 rect.on_key_down(on_key_down)
302 .on_pointer_down(on_pointer_down)
303 .on_global_pointer_move(on_global_pointer_move)
304 .on_global_pointer_press(on_global_pointer_press)
305 .on_wheel(on_wheel)
306 })
307 .on_pointer_enter(on_pointer_enter)
308 .on_pointer_leave(on_pointer_leave)
309 .border(border)
310 .corner_radius(50.)
311 .padding(padding)
312 .width(slider_width)
313 .height(slider_height)
314 .child(
315 rect()
316 .width(inner_slider_width)
317 .height(inner_slider_height)
318 .background(theme.background.mul_if(!self.enabled, 0.85))
319 .corner_radius(50.)
320 .direction(self.direction)
321 .main_align(thumb_main_align)
322 .children(if direction_is_vertical {
323 vec![thumb.into(), track.into()]
324 } else {
325 vec![track.into(), thumb.into()]
326 }),
327 )
328 }
329
330 fn render_key(&self) -> DiffKey {
331 self.key.clone().or(self.default_key())
332 }
333}