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 a11y_id.request_focus();
181 clicking.set(true);
182 e.stop_propagation();
183 let coordinates = e.element_location();
184 on_moved.call(calc_percentage(coordinates.x, coordinates.y));
185 }
186 };
187
188 let on_global_pointer_press = move |_: Event<PointerEventData>| {
189 clicking.set(false);
190 };
191
192 let on_global_pointer_move = move |e: Event<PointerEventData>| {
193 e.stop_propagation();
194 if *clicking.peek() {
195 let coordinates = e.global_location();
196 on_moved.call(calc_percentage(
197 coordinates.x - size.read().min_x() as f64,
198 coordinates.y - size.read().min_y() as f64,
199 ));
200 }
201 };
202
203 let on_wheel = {
204 let on_moved = self.on_moved.clone();
205 move |e: Event<WheelEventData>| {
206 if e.delta_y == 0.0 {
207 return;
208 }
209 e.stop_propagation();
210 on_moved.call((value + e.delta_y * 0.1).clamp(0.0, 100.0));
211 }
212 };
213
214 let border = if focus() == Focus::Keyboard {
215 Border::new()
216 .fill(theme.border_fill)
217 .width(2.)
218 .alignment(BorderAlignment::Inner)
219 } else {
220 Border::new()
221 .fill(Color::TRANSPARENT)
222 .width(0.)
223 .alignment(BorderAlignment::Inner)
224 };
225
226 let (slider_width, slider_height) = if direction_is_vertical {
227 (Size::px(6.), self.size.clone())
228 } else {
229 (self.size.clone(), Size::px(6.))
230 };
231
232 let track_size = Size::func_data(
233 move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
234 &(value as i32),
235 );
236
237 let (track_width, track_height) = if direction_is_vertical {
238 (Size::px(6.), track_size)
239 } else {
240 (track_size, Size::px(6.))
241 };
242
243 let (thumb_offset_x, thumb_offset_y) = if direction_is_vertical {
244 (-6., 3.)
245 } else {
246 (-3., -6.)
247 };
248
249 let thumb_main_align = if direction_is_vertical {
250 Alignment::end()
251 } else {
252 Alignment::start()
253 };
254
255 let padding = if direction_is_vertical {
256 (0., 8.)
257 } else {
258 (8., 0.)
259 };
260
261 let thumb = rect()
262 .width(Size::fill())
263 .offset_x(thumb_offset_x)
264 .offset_y(thumb_offset_y)
265 .child(
266 rect()
267 .width(Size::px(18.))
268 .height(Size::px(18.))
269 .corner_radius(50.)
270 .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
271 .padding(4.)
272 .child(
273 rect()
274 .width(Size::fill())
275 .height(Size::fill())
276 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
277 .corner_radius(50.),
278 ),
279 );
280
281 let track = rect()
282 .width(track_width)
283 .height(track_height)
284 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
285 .corner_radius(50.);
286
287 rect()
288 .a11y_id(a11y_id)
289 .a11y_focusable(self.enabled)
290 .a11y_role(AccessibilityRole::Slider)
291 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
292 .maybe(self.enabled, |rect| {
293 rect.on_key_down(on_key_down)
294 .on_pointer_down(on_pointer_down)
295 .on_global_pointer_move(on_global_pointer_move)
296 .on_global_pointer_press(on_global_pointer_press)
297 .on_wheel(on_wheel)
298 })
299 .on_pointer_enter(on_pointer_enter)
300 .on_pointer_leave(on_pointer_leave)
301 .border(border)
302 .corner_radius(50.)
303 .padding(padding)
304 .child(
305 rect()
306 .width(slider_width)
307 .height(slider_height)
308 .background(theme.background.mul_if(!self.enabled, 0.85))
309 .corner_radius(50.)
310 .direction(self.direction)
311 .main_align(thumb_main_align)
312 .children(if direction_is_vertical {
313 vec![thumb.into(), track.into()]
314 } else {
315 vec![track.into(), thumb.into()]
316 }),
317 )
318 }
319
320 fn render_key(&self) -> DiffKey {
321 self.key.clone().or(self.default_key())
322 }
323}