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::px(6.), self.size.clone())
231 } else {
232 (self.size.clone(), Size::px(6.))
233 };
234
235 let track_size = Size::func_data(
236 move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
237 &(value as i32),
238 );
239
240 let (track_width, track_height) = if direction_is_vertical {
241 (Size::px(6.), track_size)
242 } else {
243 (track_size, Size::px(6.))
244 };
245
246 let (thumb_offset_x, thumb_offset_y) = if direction_is_vertical {
247 (-6., 3.)
248 } else {
249 (-3., -6.)
250 };
251
252 let thumb_main_align = if direction_is_vertical {
253 Alignment::end()
254 } else {
255 Alignment::start()
256 };
257
258 let padding = if direction_is_vertical {
259 (0., 8.)
260 } else {
261 (8., 0.)
262 };
263
264 let thumb = rect()
265 .width(Size::fill())
266 .offset_x(thumb_offset_x)
267 .offset_y(thumb_offset_y)
268 .child(
269 rect()
270 .width(Size::px(18.))
271 .height(Size::px(18.))
272 .corner_radius(50.)
273 .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
274 .padding(4.)
275 .child(
276 rect()
277 .width(Size::fill())
278 .height(Size::fill())
279 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
280 .corner_radius(50.),
281 ),
282 );
283
284 let track = rect()
285 .width(track_width)
286 .height(track_height)
287 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
288 .corner_radius(50.);
289
290 rect()
291 .a11y_id(a11y_id)
292 .a11y_focusable(self.enabled)
293 .a11y_role(AccessibilityRole::Slider)
294 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
295 .maybe(self.enabled, |rect| {
296 rect.on_key_down(on_key_down)
297 .on_pointer_down(on_pointer_down)
298 .on_global_pointer_move(on_global_pointer_move)
299 .on_global_pointer_press(on_global_pointer_press)
300 .on_wheel(on_wheel)
301 })
302 .on_pointer_enter(on_pointer_enter)
303 .on_pointer_leave(on_pointer_leave)
304 .border(border)
305 .corner_radius(50.)
306 .padding(padding)
307 .child(
308 rect()
309 .width(slider_width)
310 .height(slider_height)
311 .background(theme.background.mul_if(!self.enabled, 0.85))
312 .corner_radius(50.)
313 .direction(self.direction)
314 .main_align(thumb_main_align)
315 .children(if direction_is_vertical {
316 vec![thumb.into(), track.into()]
317 } else {
318 vec![track.into(), thumb.into()]
319 }),
320 )
321 }
322
323 fn render_key(&self) -> DiffKey {
324 self.key.clone().or(self.default_key())
325 }
326}