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) = if direction_is_vertical {
207 (Size::px(6.), self.size.clone())
208 } else {
209 (self.size.clone(), Size::px(6.))
210 };
211
212 let track_size = Size::func_data(
213 move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
214 &(value as i32),
215 );
216
217 let (track_width, track_height) = if direction_is_vertical {
218 (Size::px(6.), track_size)
219 } else {
220 (track_size, Size::px(6.))
221 };
222
223 let (thumb_offset_x, thumb_offset_y) = if direction_is_vertical {
224 (-6., 3.)
225 } else {
226 (-3., -6.)
227 };
228
229 let thumb_main_align = if direction_is_vertical {
230 Alignment::end()
231 } else {
232 Alignment::start()
233 };
234
235 let padding = if direction_is_vertical {
236 (0., 8.)
237 } else {
238 (8., 0.)
239 };
240
241 let thumb = rect()
242 .width(Size::fill())
243 .offset_x(thumb_offset_x)
244 .offset_y(thumb_offset_y)
245 .child(
246 rect()
247 .width(Size::px(18.))
248 .height(Size::px(18.))
249 .corner_radius(50.)
250 .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
251 .padding(4.)
252 .child(
253 rect()
254 .width(Size::fill())
255 .height(Size::fill())
256 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
257 .corner_radius(50.),
258 ),
259 );
260
261 let track = rect()
262 .width(track_width)
263 .height(track_height)
264 .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
265 .corner_radius(50.);
266
267 rect()
268 .a11y_id(focus.a11y_id())
269 .a11y_focusable(self.enabled)
270 .a11y_role(AccessibilityRole::Slider)
271 .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
272 .maybe(self.enabled, |rect| {
273 rect.on_key_down(on_key_down)
274 .on_pointer_down(on_pointer_down)
275 .on_global_pointer_move(on_global_pointer_move)
276 .on_global_pointer_press(on_global_pointer_press)
277 })
278 .on_pointer_enter(on_pointer_enter)
279 .on_pointer_leave(on_pointer_leave)
280 .border(border)
281 .corner_radius(50.)
282 .padding(padding)
283 .child(
284 rect()
285 .width(slider_width)
286 .height(slider_height)
287 .background(theme.background.mul_if(!self.enabled, 0.85))
288 .corner_radius(50.)
289 .direction(self.direction)
290 .main_align(thumb_main_align)
291 .children(if direction_is_vertical {
292 vec![thumb.into(), track.into()]
293 } else {
294 vec![track.into(), thumb.into()]
295 }),
296 )
297 }
298
299 fn render_key(&self) -> DiffKey {
300 self.key.clone().or(self.default_key())
301 }
302}