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