1use azul_core::{
15 callbacks::{CoreCallbackData, Update},
16 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
17 refany::RefAny,
18};
19use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
20use azul_css::{
21 props::{
22 basic::{color::ColorU, *},
23 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight, LayoutMarginLeft},
24 property::{CssProperty, *},
25 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleCursor},
26 },
27 impl_option_inner, AzString,
28};
29
30use crate::callbacks::{Callback, CallbackInfo};
31
32static SLIDER_TRACK_CLASS: &[IdOrClass] =
33 &[Class(AzString::from_const_str("__azul-native-slider"))];
34static SLIDER_THUMB_CLASS: &[IdOrClass] =
35 &[Class(AzString::from_const_str("__azul-native-slider-thumb"))];
36
37pub type SliderOnValueChangeCallbackType =
39 extern "C" fn(RefAny, CallbackInfo, SliderState) -> Update;
40impl_widget_callback!(
41 SliderOnValueChange,
42 OptionSliderOnValueChange,
43 SliderOnValueChangeCallback,
44 SliderOnValueChangeCallbackType
45);
46
47azul_core::impl_managed_callback! {
48 wrapper: SliderOnValueChangeCallback,
49 info_ty: CallbackInfo,
50 return_ty: Update,
51 default_ret: Update::DoNothing,
52 invoker_static: SLIDER_ON_VALUE_CHANGE_INVOKER,
53 invoker_ty: AzSliderOnValueChangeCallbackInvoker,
54 thunk_fn: az_slider_on_value_change_callback_thunk,
55 setter_fn: AzApp_setSliderOnValueChangeCallbackInvoker,
56 from_handle_fn: AzSliderOnValueChangeCallback_createFromHostHandle,
57 extra_args: [ state: SliderState ],
58}
59
60#[derive(Debug, Clone, PartialEq)]
62#[repr(C)]
63pub struct Slider {
64 pub slider_state: SliderStateWrapper,
65 pub track_style: CssPropertyWithConditionsVec,
67 pub thumb_style: CssPropertyWithConditionsVec,
69}
70
71#[derive(Debug, Default, Clone, PartialEq)]
72#[repr(C)]
73pub struct SliderStateWrapper {
74 pub on_value_change: OptionSliderOnValueChange,
76 pub inner: SliderState,
78 pub dragging: bool,
81}
82
83#[derive(Debug, Copy, Clone, PartialEq)]
85#[repr(C)]
86pub struct SliderState {
87 pub value: f32,
89 pub min: f32,
91 pub max: f32,
93}
94
95impl Default for SliderState {
96 fn default() -> Self {
97 Self {
98 value: 0.0,
99 min: 0.0,
100 max: 100.0,
101 }
102 }
103}
104
105const TRACK_WIDTH: isize = 200;
107const TRACK_HEIGHT: isize = 16;
108const TRACK_RADIUS: isize = 8;
109const THUMB_SIZE: isize = 16;
110const THUMB_RADIUS: isize = 8;
111
112const RAIL_COLOR: ColorU = ColorU {
115 r: 204,
116 g: 204,
117 b: 204,
118 a: 255,
119};
120const THUMB_COLOR: ColorU = ColorU {
122 r: 13,
123 g: 110,
124 b: 253,
125 a: 255,
126};
127
128const RAIL_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(RAIL_COLOR)];
129const RAIL_BG: StyleBackgroundContentVec =
130 StyleBackgroundContentVec::from_const_slice(RAIL_BG_ITEMS);
131const THUMB_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(THUMB_COLOR)];
132const THUMB_BG: StyleBackgroundContentVec =
133 StyleBackgroundContentVec::from_const_slice(THUMB_BG_ITEMS);
134
135static SLIDER_TRACK_STYLE: &[CssPropertyWithConditions] = &[
137 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
138 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
139 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
140 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Center)),
141 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
142 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(TRACK_WIDTH))),
143 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
144 TRACK_HEIGHT,
145 ))),
146 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
147 StyleBorderTopLeftRadius::const_px(TRACK_RADIUS),
148 )),
149 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
150 StyleBorderTopRightRadius::const_px(TRACK_RADIUS),
151 )),
152 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
153 StyleBorderBottomLeftRadius::const_px(TRACK_RADIUS),
154 )),
155 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
156 StyleBorderBottomRightRadius::const_px(TRACK_RADIUS),
157 )),
158 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
159 CssPropertyWithConditions::simple(CssProperty::const_background_content(RAIL_BG)),
160];
161
162fn value_to_fraction(value: f32, min: f32, max: f32) -> f32 {
164 if max <= min {
165 0.0
166 } else {
167 ((value - min) / (max - min)).clamp(0.0, 1.0)
168 }
169}
170
171#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn build_thumb_style(fraction: f32) -> CssPropertyWithConditionsVec {
175 let margin = (fraction * (TRACK_WIDTH - THUMB_SIZE) as f32).round() as isize;
176 CssPropertyWithConditionsVec::from_vec(alloc::vec![
177 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
178 THUMB_SIZE,
179 ))),
180 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
181 THUMB_SIZE,
182 ))),
183 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
184 0,
185 ))),
186 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
187 StyleBorderTopLeftRadius::const_px(THUMB_RADIUS),
188 )),
189 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
190 StyleBorderTopRightRadius::const_px(THUMB_RADIUS),
191 )),
192 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
193 StyleBorderBottomLeftRadius::const_px(THUMB_RADIUS),
194 )),
195 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
196 StyleBorderBottomRightRadius::const_px(THUMB_RADIUS),
197 )),
198 CssPropertyWithConditions::simple(CssProperty::const_background_content(THUMB_BG)),
199 CssPropertyWithConditions::simple(CssProperty::const_margin_left(
200 LayoutMarginLeft::const_px(margin),
201 )),
202 ])
203}
204
205impl Slider {
206 #[must_use] pub fn create(value: f32, min: f32, max: f32) -> Self {
208 let value = value.clamp(min, max);
209 Self {
210 slider_state: SliderStateWrapper {
211 inner: SliderState { value, min, max },
212 ..Default::default()
213 },
214 track_style: CssPropertyWithConditionsVec::from_const_slice(SLIDER_TRACK_STYLE),
215 thumb_style: build_thumb_style(value_to_fraction(value, min, max)),
216 }
217 }
218
219 #[inline]
221 pub fn set_value(&mut self, value: f32) {
222 let min = self.slider_state.inner.min;
223 let max = self.slider_state.inner.max;
224 let value = value.clamp(min, max);
225 self.slider_state.inner.value = value;
226 self.thumb_style = build_thumb_style(value_to_fraction(value, min, max));
227 }
228
229 #[inline]
231 #[must_use] pub fn with_value(mut self, value: f32) -> Self {
232 self.set_value(value);
233 self
234 }
235
236 #[inline]
237 #[must_use] pub fn swap_with_default(&mut self) -> Self {
238 let mut s = Self::create(0.0, 0.0, 100.0);
239 core::mem::swap(&mut s, self);
240 s
241 }
242
243 #[inline]
244 pub fn set_on_value_change<C: Into<SliderOnValueChangeCallback>>(
245 &mut self,
246 data: RefAny,
247 on_value_change: C,
248 ) {
249 self.slider_state.on_value_change = Some(SliderOnValueChange {
250 callback: on_value_change.into(),
251 refany: data,
252 })
253 .into();
254 }
255
256 #[inline]
257 #[must_use] pub fn with_on_value_change<C: Into<SliderOnValueChangeCallback>>(
258 mut self,
259 data: RefAny,
260 on_value_change: C,
261 ) -> Self {
262 self.set_on_value_change(data, on_value_change);
263 self
264 }
265
266 #[inline]
267 #[must_use] pub fn dom(self) -> Dom {
268 use azul_core::{
269 callbacks::CoreCallback,
270 dom::{EventFilter, HoverEventFilter},
271 refany::OptionRefAny,
272 };
273
274 let state = RefAny::new(self.slider_state);
278 let mk = |event: EventFilter, cb: usize| CoreCallbackData {
279 event,
280 callback: CoreCallback {
281 cb,
282 ctx: OptionRefAny::None,
283 },
284 refany: state.clone(),
285 };
286 let callbacks = vec![
287 mk(
288 EventFilter::Hover(HoverEventFilter::MouseDown),
289 on_slider_pointer_down as usize,
290 ),
291 mk(
292 EventFilter::Hover(HoverEventFilter::MouseOver),
293 on_slider_pointer_move as usize,
294 ),
295 mk(
296 EventFilter::Hover(HoverEventFilter::MouseUp),
297 on_slider_pointer_up as usize,
298 ),
299 mk(
300 EventFilter::Hover(HoverEventFilter::MouseLeave),
301 on_slider_pointer_up as usize,
302 ),
303 mk(
304 EventFilter::Hover(HoverEventFilter::TouchStart),
305 on_slider_pointer_down as usize,
306 ),
307 mk(
308 EventFilter::Hover(HoverEventFilter::TouchMove),
309 on_slider_pointer_move as usize,
310 ),
311 mk(
312 EventFilter::Hover(HoverEventFilter::TouchEnd),
313 on_slider_pointer_up as usize,
314 ),
315 ];
316
317 Dom::create_div()
318 .with_ids_and_classes(IdOrClassVec::from_const_slice(SLIDER_TRACK_CLASS))
319 .with_css_props(self.track_style)
320 .with_callbacks(callbacks.into())
321 .with_tab_index(TabIndex::Auto)
322 .with_children(
323 vec![Dom::create_div()
324 .with_ids_and_classes(IdOrClassVec::from_const_slice(SLIDER_THUMB_CLASS))
325 .with_css_props(self.thumb_style)]
326 .into(),
327 )
328 }
329}
330
331impl Default for Slider {
332 fn default() -> Self {
333 Self::create(0.0, 0.0, 100.0)
334 }
335}
336
337#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn apply_cursor_value(slider: &mut SliderStateWrapper, info: &mut CallbackInfo) -> Update {
341 let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
342 return Update::DoNothing;
343 };
344 let width = info
346 .get_hit_node_rect()
347 .map(|r| r.size.width)
348 .filter(|w| *w > 0.0)
349 .unwrap_or(TRACK_WIDTH as f32);
350
351 let fraction = (pos.x / width).clamp(0.0, 1.0);
352 let min = slider.inner.min;
353 let max = slider.inner.max;
354 slider.inner.value = fraction.mul_add(max - min, min);
355
356 let track_id = info.get_hit_node();
358 if let Some(thumb_id) = info.get_first_child(track_id) {
359 let margin = (fraction * (width - THUMB_SIZE as f32)).round() as isize;
360 info.set_css_property(
361 thumb_id,
362 CssProperty::const_margin_left(LayoutMarginLeft::const_px(margin)),
363 );
364 }
365
366 let inner = slider.inner;
367 match slider.on_value_change.as_mut() {
368 Some(SliderOnValueChange { callback, refany }) => (callback.cb)(refany.clone(), *info, inner),
369 None => Update::DoNothing,
370 }
371}
372
373extern "C" fn on_slider_pointer_down(mut data: RefAny, mut info: CallbackInfo) -> Update {
375 let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() else {
376 return Update::DoNothing;
377 };
378 slider.dragging = true;
379 apply_cursor_value(&mut slider, &mut info)
380}
381
382extern "C" fn on_slider_pointer_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
384 let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() else {
385 return Update::DoNothing;
386 };
387 if !slider.dragging {
388 return Update::DoNothing;
389 }
390 apply_cursor_value(&mut slider, &mut info)
391}
392
393extern "C" fn on_slider_pointer_up(mut data: RefAny, _info: CallbackInfo) -> Update {
395 if let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() {
396 slider.dragging = false;
397 }
398 Update::DoNothing
399}
400
401impl From<Slider> for Dom {
402 fn from(s: Slider) -> Self {
403 s.dom()
404 }
405}