Skip to main content

fui/controls/
slider.rs

1use super::internal::slider_presenter::{
2    create_default_slider_presenter, SliderPresenter, SliderTemplate, SliderVisualState,
3};
4use super::*;
5use crate::logger;
6use crate::node::WeakFlexBox;
7use crate::{focus_adorner, focus_visibility};
8
9const DEFAULT_SLIDER_LENGTH: f32 = 180.0;
10
11fn clamp(value: f32, min: f32, max: f32) -> f32 {
12    if value < min {
13        return min;
14    }
15    if value > max {
16        return max;
17    }
18    value
19}
20
21fn create_slider_presenter(
22    template: Option<Rc<dyn SliderTemplate>>,
23    sizing: Option<SliderSizing>,
24) -> Rc<dyn SliderPresenter> {
25    if let Some(template) = template {
26        return template.create(sizing);
27    }
28    if let Some(template_set) = get_control_templates() {
29        if let Some(template) = template_set.slider {
30            return template.create(sizing);
31        }
32    }
33    create_default_slider_presenter(sizing)
34}
35
36#[derive(Clone)]
37pub struct Slider {
38    root: FlexBox,
39    slider_presenter: Rc<RefCell<Rc<dyn SliderPresenter>>>,
40    template_override: Rc<RefCell<Option<Rc<dyn SliderTemplate>>>>,
41    sizing_value: Rc<Cell<Option<SliderSizing>>>,
42    colors_value: Rc<Cell<Option<SliderColors>>>,
43    min: Rc<Cell<f32>>,
44    max: Rc<Cell<f32>>,
45    step: Rc<Cell<f32>>,
46    value: Rc<Cell<f32>>,
47    length: Rc<Cell<f32>>,
48    orientation: Rc<Cell<Orientation>>,
49    changed: Rc<RefCell<Option<SliderChangedCallback>>>,
50    hovered_state: Rc<Cell<bool>>,
51    dragging_state: Rc<Cell<bool>>,
52    focused_state: Rc<Cell<bool>>,
53    weak_root: Rc<WeakNodeRef>,
54    weak_flex_root: WeakFlexBox,
55}
56
57impl Default for Slider {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl Slider {
64    pub fn new() -> Self {
65        let root = flex_box();
66        let slider_presenter = create_slider_presenter(None, None);
67        let presenter_root = slider_presenter.root();
68        presenter_root.position_type(PositionType::Absolute);
69
70        root.interactive(true)
71            .focusable(true, 0)
72            .semantic_role(SemanticRole::Slider)
73            .reflect_semantic_disabled_from_enabled()
74            .semantic_orientation(Orientation::Horizontal)
75            .semantic_value_range(0.0, 0.0, 100.0)
76            .default_semantic_label("Slider")
77            .cursor(CursorStyle::Pointer)
78            .child(&presenter_root);
79
80        let control = Self {
81            weak_root: Rc::new(root.node_ref().downgrade()),
82            weak_flex_root: root.downgrade(),
83            root,
84            slider_presenter: Rc::new(RefCell::new(slider_presenter)),
85            template_override: Rc::new(RefCell::new(None)),
86            sizing_value: Rc::new(Cell::new(None)),
87            colors_value: Rc::new(Cell::new(None)),
88            min: Rc::new(Cell::new(0.0)),
89            max: Rc::new(Cell::new(100.0)),
90            step: Rc::new(Cell::new(1.0)),
91            value: Rc::new(Cell::new(0.0)),
92            length: Rc::new(Cell::new(DEFAULT_SLIDER_LENGTH)),
93            orientation: Rc::new(Cell::new(Orientation::Horizontal)),
94            changed: Rc::new(RefCell::new(None)),
95            hovered_state: Rc::new(Cell::new(false)),
96            dragging_state: Rc::new(Cell::new(false)),
97            focused_state: Rc::new(Cell::new(false)),
98        };
99        control.install_visual_subscriptions();
100        control.bind_events();
101        let target = SliderEventTarget::from_slider(&control);
102        control.persist_state(crate::persisted::persisted_value_adapter(
103            "slider-value",
104            crate::persisted::PersistedFloat32Codec,
105            1,
106            {
107                let value = control.value.clone();
108                move || Some(value.get())
109            },
110            move |value| {
111                target.apply_persisted_value(value);
112            },
113        ));
114        control.set_value_inner(0.0, false, false);
115        control.handle_theme_changed();
116        control
117    }
118
119    fn bind_events(&self) {
120        let target = SliderEventTarget::from_slider(self);
121        self.root.on_pointer_enter(move |_event| {
122            target.handle_pointer_enter();
123        });
124
125        let target = SliderEventTarget::from_slider(self);
126        self.root.on_pointer_leave(move |_event| {
127            target.handle_pointer_leave();
128        });
129
130        let target = SliderEventTarget::from_slider(self);
131        self.root.on_pointer_down(move |event| {
132            target.handle_pointer_down(event);
133        });
134
135        let target = SliderEventTarget::from_slider(self);
136        self.root.on_pointer_move(move |event| {
137            target.handle_pointer_move(event);
138        });
139
140        let target = SliderEventTarget::from_slider(self);
141        self.root.on_pointer_up(move |event| {
142            target.handle_pointer_up(event);
143        });
144
145        let target = SliderEventTarget::from_slider(self);
146        self.root.on_key_down(move |event| {
147            target.sync_focus_chrome();
148            target.key_value(event);
149        });
150
151        let target = SliderEventTarget::from_slider(self);
152        self.root.on_focus_changed(move |event| {
153            if target.focused_state.get() != event.focused {
154                target.focused_state.set(event.focused);
155                target.handle_theme_changed();
156            }
157        });
158    }
159
160    pub fn min(&self, value: f32) -> &Self {
161        self.min.set(value);
162        if self.max.get() < value {
163            self.max.set(value);
164        }
165        self.set_value_inner(self.value.get(), true, false);
166        self
167    }
168
169    pub fn max(&self, value: f32) -> &Self {
170        self.max.set(value);
171        if self.min.get() > value {
172            self.min.set(value);
173        }
174        self.set_value_inner(self.value.get(), true, false);
175        self
176    }
177
178    pub fn step(&self, value: f32) -> &Self {
179        if value <= 0.0 {
180            logger::warn(
181                "Layout",
182                &format!("Slider.step() received {value}; clamping to 1.0."),
183            );
184        }
185        self.step.set(if value > 0.0 { value } else { 1.0 });
186        self.set_value_inner(self.value.get(), true, false);
187        self
188    }
189
190    pub fn value(&self, value: f32) -> &Self {
191        self.set_value_inner(value, true, false);
192        self
193    }
194
195    pub fn length(&self, value: f32) -> &Self {
196        let thumb_size = self.slider_presenter.borrow().metrics().thumb_size;
197        if value <= thumb_size {
198            logger::warn(
199                "Layout",
200                &format!(
201                    "Slider.length() received {value}; clamping to a value above the thumb size."
202                ),
203            );
204        }
205        self.length.set(if value > thumb_size {
206            value
207        } else {
208            thumb_size + 1.0
209        });
210        self.sync_presentation();
211        self
212    }
213
214    pub fn orientation(&self, orientation: Orientation) -> &Self {
215        let orientation = if orientation == Orientation::Vertical {
216            Orientation::Vertical
217        } else {
218            Orientation::Horizontal
219        };
220        self.orientation.set(orientation);
221        self.root.semantic_orientation(orientation);
222        self.sync_semantic_label();
223        self.sync_presentation();
224        self
225    }
226
227    pub fn sizing(&self, sizing: SliderSizing) -> &Self {
228        self.set_sizing(Some(sizing))
229    }
230
231    pub fn clear_sizing(&self) -> &Self {
232        self.set_sizing(None)
233    }
234
235    fn set_sizing(&self, sizing: Option<SliderSizing>) -> &Self {
236        self.sizing_value.set(sizing);
237        self.replace_presenter(create_slider_presenter(
238            self.template_override.borrow().clone(),
239            self.sizing_value.get(),
240        ));
241        let thumb_size = self.slider_presenter.borrow().metrics().thumb_size;
242        if self.length.get() <= thumb_size {
243            self.length.set(thumb_size + 1.0);
244        }
245        self.sync_presentation();
246        self
247    }
248
249    pub fn colors(&self, colors: SliderColors) -> &Self {
250        self.set_colors(Some(colors))
251    }
252
253    pub fn clear_colors(&self) -> &Self {
254        self.set_colors(None)
255    }
256
257    fn set_colors(&self, colors: Option<SliderColors>) -> &Self {
258        self.colors_value.set(colors);
259        self.sync_presentation();
260        self
261    }
262
263    pub fn template(&self, template: Rc<dyn SliderTemplate>) -> &Self {
264        self.set_template(Some(template))
265    }
266
267    pub fn clear_template(&self) -> &Self {
268        self.set_template(None)
269    }
270
271    fn set_template(&self, template: Option<Rc<dyn SliderTemplate>>) -> &Self {
272        self.template_override.replace(template.clone());
273        self.replace_presenter(create_slider_presenter(template, self.sizing_value.get()));
274        let thumb_size = self.slider_presenter.borrow().metrics().thumb_size;
275        if self.length.get() <= thumb_size {
276            logger::warn(
277                "Layout",
278                "Slider.template() increased the thumb size beyond the current slider length; clamping length to stay interactive.",
279            );
280            self.length.set(thumb_size + 1.0);
281        }
282        self.sync_presentation();
283        self
284    }
285
286    pub fn enabled(&self, enabled: bool) -> &Self {
287        self.root.enabled(enabled);
288        if !enabled && self.dragging_state.replace(false) {
289            if let Some(handle) = upgraded_handle(&self.weak_root) {
290                crate::event::release_pointer(handle);
291                unsafe { crate::ffi::fui_release_pointer_capture() };
292            }
293        }
294        self.handle_theme_changed();
295        self
296    }
297
298    pub fn on_changed(&self, handler: impl Fn(SliderChangedEventArgs) + 'static) -> &Self {
299        *self.changed.borrow_mut() = Some(Rc::new(handler));
300        self
301    }
302
303    pub fn current_value(&self) -> f32 {
304        self.value.get()
305    }
306
307    fn install_visual_subscriptions(&self) {
308        let target = SliderEventTarget::from_slider(self);
309        let theme_guard = subscribe(move |_theme| {
310            target.handle_theme_changed();
311        });
312        self.root
313            .retained_node_ref()
314            .retain_attachment(Rc::new(theme_guard));
315
316        let target = SliderEventTarget::from_slider(self);
317        let focus_guard = focus_visibility::subscribe(move |_visible| {
318            target.sync_focus_chrome();
319        });
320        self.root
321            .retained_node_ref()
322            .retain_attachment(Rc::new(focus_guard));
323    }
324
325    fn set_value_inner(&self, value: f32, emit: bool, announce: bool) {
326        SliderEventTarget::from_slider(self).set_value_inner(value, emit, announce);
327    }
328
329    fn create_visual_state(&self) -> SliderVisualState {
330        create_slider_visual_state(
331            self.value.get(),
332            self.min.get(),
333            self.max.get(),
334            self.orientation.get(),
335            self.hovered_state.get(),
336            self.dragging_state.get(),
337            self.focused_state.get(),
338            self.is_enabled(),
339        )
340    }
341
342    fn sync_presentation(&self) {
343        let state = self.create_visual_state();
344        sync_slider_geometry_with_state(
345            &self.root,
346            self.slider_presenter.borrow().as_ref(),
347            state,
348            self.length.get(),
349        );
350        sync_slider_visual_state_with_state(
351            &self.root,
352            self.slider_presenter.borrow().as_ref(),
353            state,
354            self.colors_value.get(),
355        );
356    }
357
358    fn sync_semantic_label(&self) {
359        self.root
360            .default_semantic_label(if self.orientation.get() == Orientation::Vertical {
361                "Vertical slider"
362            } else {
363                "Slider"
364            });
365    }
366
367    fn handle_theme_changed(&self) {
368        self.root.cursor(if self.is_enabled() {
369            CursorStyle::Pointer
370        } else {
371            CursorStyle::Default
372        });
373        self.sync_presentation();
374        self.sync_focus_chrome();
375    }
376
377    fn is_enabled(&self) -> bool {
378        self.root.retained_node_ref().is_enabled_for_routing()
379    }
380
381    fn sync_focus_chrome(&self) {
382        sync_slider_focus_chrome(&self.root, self.focused_state.get(), self.is_enabled());
383    }
384
385    fn replace_presenter(&self, next_presenter: Rc<dyn SliderPresenter>) {
386        let previous = self.slider_presenter.borrow().clone();
387        if Rc::ptr_eq(&previous, &next_presenter) {
388            return;
389        }
390        let previous_root = previous.root();
391        let next_root = next_presenter.root();
392        next_root.position_type(PositionType::Absolute);
393        self.root.remove_child(&previous_root);
394        self.root.child(&next_root);
395        previous_root.dispose();
396        self.slider_presenter.replace(next_presenter);
397    }
398}
399
400impl Node for Slider {
401    fn retained_node_ref(&self) -> NodeRef {
402        self.root.retained_node_ref()
403    }
404
405    fn build_self(&self) {
406        self.sync_presentation();
407        self.sync_focus_chrome();
408        self.root.build_self();
409    }
410}
411
412impl HasFlexBoxRoot for Slider {
413    fn flex_box_root(&self) -> &FlexBox {
414        &self.root
415    }
416}
417
418impl ThemeBindable for Slider {
419    fn theme_binding_node(&self) -> NodeRef {
420        self.root.retained_node_ref()
421    }
422
423    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
424        let target = SliderEventTarget::from_slider(self);
425        Box::new(move || target.upgrade())
426    }
427}
428
429#[derive(Clone)]
430struct SliderEventTarget {
431    weak_root: Rc<WeakNodeRef>,
432    weak_flex_root: WeakFlexBox,
433    slider_presenter: Rc<RefCell<Rc<dyn SliderPresenter>>>,
434    template_override: Rc<RefCell<Option<Rc<dyn SliderTemplate>>>>,
435    sizing_value: Rc<Cell<Option<SliderSizing>>>,
436    min: Rc<Cell<f32>>,
437    max: Rc<Cell<f32>>,
438    step: Rc<Cell<f32>>,
439    value: Rc<Cell<f32>>,
440    length: Rc<Cell<f32>>,
441    orientation: Rc<Cell<Orientation>>,
442    changed: Rc<RefCell<Option<SliderChangedCallback>>>,
443    focused_state: Rc<Cell<bool>>,
444    hovered_state: Rc<Cell<bool>>,
445    dragging_state: Rc<Cell<bool>>,
446    colors_value: Rc<Cell<Option<SliderColors>>>,
447}
448
449impl SliderEventTarget {
450    fn from_slider(slider: &Slider) -> Self {
451        Self {
452            weak_root: slider.weak_root.clone(),
453            weak_flex_root: slider.weak_flex_root.clone(),
454            slider_presenter: slider.slider_presenter.clone(),
455            template_override: slider.template_override.clone(),
456            sizing_value: slider.sizing_value.clone(),
457            min: slider.min.clone(),
458            max: slider.max.clone(),
459            step: slider.step.clone(),
460            value: slider.value.clone(),
461            length: slider.length.clone(),
462            orientation: slider.orientation.clone(),
463            changed: slider.changed.clone(),
464            focused_state: slider.focused_state.clone(),
465            hovered_state: slider.hovered_state.clone(),
466            dragging_state: slider.dragging_state.clone(),
467            colors_value: slider.colors_value.clone(),
468        }
469    }
470
471    fn upgrade(&self) -> Option<Slider> {
472        Some(Slider {
473            root: self.weak_flex_root.upgrade()?,
474            slider_presenter: self.slider_presenter.clone(),
475            template_override: self.template_override.clone(),
476            sizing_value: self.sizing_value.clone(),
477            colors_value: self.colors_value.clone(),
478            min: self.min.clone(),
479            max: self.max.clone(),
480            step: self.step.clone(),
481            value: self.value.clone(),
482            length: self.length.clone(),
483            orientation: self.orientation.clone(),
484            changed: self.changed.clone(),
485            hovered_state: self.hovered_state.clone(),
486            dragging_state: self.dragging_state.clone(),
487            focused_state: self.focused_state.clone(),
488            weak_root: self.weak_root.clone(),
489            weak_flex_root: self.weak_flex_root.clone(),
490        })
491    }
492
493    fn is_enabled(&self) -> bool {
494        self.weak_flex_root
495            .upgrade()
496            .map(|root| root.retained_node_ref().is_enabled_for_routing())
497            .unwrap_or(false)
498    }
499
500    fn handle_pointer_enter(&self) {
501        if !self.is_enabled() {
502            return;
503        }
504        self.hovered_state.set(true);
505        self.sync_presentation();
506    }
507
508    fn handle_pointer_leave(&self) {
509        if !self.is_enabled() {
510            return;
511        }
512        self.hovered_state.set(false);
513        if !self.dragging_state.get() {
514            self.sync_presentation();
515        }
516    }
517
518    fn handle_pointer_down(&self, event: &mut PointerEventArgs) {
519        if !self.is_enabled() {
520            return;
521        }
522        self.hovered_state.set(true);
523        self.dragging_state.set(true);
524        self.update_value_from_pointer(event.scene_x, event.scene_y, true, true);
525        self.sync_presentation();
526        event.capture_pointer();
527        event.handled = true;
528    }
529
530    fn handle_pointer_move(&self, event: &mut PointerEventArgs) {
531        if !self.is_enabled() || !self.dragging_state.get() || event.buttons.is_empty() {
532            return;
533        }
534        self.update_value_from_pointer(event.scene_x, event.scene_y, true, true);
535        event.handled = true;
536    }
537
538    fn handle_pointer_up(&self, event: &mut PointerEventArgs) {
539        if !self.is_enabled() || !self.dragging_state.get() {
540            return;
541        }
542        self.update_value_from_pointer(event.scene_x, event.scene_y, true, true);
543        self.dragging_state.set(false);
544        self.sync_presentation();
545        event.release_pointer_capture();
546        event.handled = true;
547    }
548
549    fn key_value(&self, event: &mut KeyEventArgs) {
550        if !self.is_enabled() || event.modifiers != 0 {
551            return;
552        }
553        let mut next = self.value.get();
554        match event.key.as_str() {
555            "Home" => next = self.min.get(),
556            "End" => next = self.max.get(),
557            "ArrowUp" if self.orientation.get() == Orientation::Vertical => next += self.step.get(),
558            "ArrowDown" if self.orientation.get() == Orientation::Vertical => {
559                next -= self.step.get()
560            }
561            "ArrowRight" if self.orientation.get() == Orientation::Horizontal => {
562                next += self.step.get()
563            }
564            "ArrowLeft" if self.orientation.get() == Orientation::Horizontal => {
565                next -= self.step.get()
566            }
567            _ => return,
568        }
569        self.set_value_inner(next, true, true);
570        event.handled = true;
571    }
572
573    fn apply_persisted_value(&self, value: f32) {
574        self.set_value_inner(value, true, false);
575    }
576
577    fn set_value_inner(&self, next: f32, emit: bool, announce: bool) {
578        let normalized =
579            normalize_slider_value(next, self.min.get(), self.max.get(), self.step.get());
580        if (normalized - self.value.get()).abs() <= f32::EPSILON {
581            self.sync_presentation();
582            return;
583        }
584        self.value.set(normalized);
585        if let Some(root) = self.weak_flex_root.upgrade() {
586            root.semantic_value_range(normalized, self.min.get(), self.max.get());
587            if announce && root.has_built_handle() {
588                ui::request_semantic_announcement(root.handle().raw());
589            }
590        }
591        self.sync_presentation();
592        if emit {
593            if let Some(callback) = self.changed.borrow().clone() {
594                callback(SliderChangedEventArgs { value: normalized });
595            }
596        }
597    }
598
599    fn update_value_from_pointer(&self, x: f32, y: f32, emit: bool, announce: bool) {
600        let thumb_size = self.slider_presenter.borrow().metrics().thumb_size;
601        let available = self.length.get() - thumb_size;
602        if available <= 0.0 {
603            return;
604        }
605        let (mut local_x, mut local_y) = (x, y);
606        if let Some(handle) = upgraded_handle(&self.weak_root) {
607            if let Some(bounds) = ui::get_bounds(handle.raw()) {
608                local_x = x - bounds[0];
609                local_y = y - bounds[1];
610            }
611        }
612        let leading_inset = SLIDER_OUTER_INSET + (thumb_size * 0.5);
613        let offset = if self.orientation.get() == Orientation::Vertical {
614            available - clamp(local_y - leading_inset, 0.0, available)
615        } else {
616            clamp(local_x - leading_inset, 0.0, available)
617        };
618        let fraction = clamp(offset / available, 0.0, 1.0);
619        let next = self.min.get() + ((self.max.get() - self.min.get()) * fraction);
620        self.set_value_inner(next, emit, announce);
621    }
622
623    fn create_visual_state(&self) -> SliderVisualState {
624        create_slider_visual_state(
625            self.value.get(),
626            self.min.get(),
627            self.max.get(),
628            self.orientation.get(),
629            self.hovered_state.get(),
630            self.dragging_state.get(),
631            self.focused_state.get(),
632            self.is_enabled(),
633        )
634    }
635
636    fn sync_presentation(&self) {
637        let Some(root) = self.weak_flex_root.upgrade() else {
638            return;
639        };
640        let presenter = self.slider_presenter.borrow().clone();
641        let state = self.create_visual_state();
642        sync_slider_geometry_with_state(&root, presenter.as_ref(), state, self.length.get());
643        sync_slider_visual_state_with_state(
644            &root,
645            presenter.as_ref(),
646            state,
647            self.colors_value.get(),
648        );
649    }
650
651    fn handle_theme_changed(&self) {
652        let Some(root) = self.weak_flex_root.upgrade() else {
653            return;
654        };
655        root.cursor(if self.is_enabled() {
656            CursorStyle::Pointer
657        } else {
658            CursorStyle::Default
659        });
660        self.sync_presentation();
661        self.sync_focus_chrome();
662    }
663
664    fn sync_focus_chrome(&self) {
665        let Some(root) = self.weak_flex_root.upgrade() else {
666            return;
667        };
668        sync_slider_focus_chrome(
669            &root,
670            self.focused_state.get(),
671            root.retained_node_ref().is_enabled_for_routing(),
672        );
673    }
674}
675
676#[allow(clippy::too_many_arguments)]
677fn create_slider_visual_state(
678    value: f32,
679    min: f32,
680    max: f32,
681    orientation: Orientation,
682    hovered: bool,
683    dragging: bool,
684    focused: bool,
685    enabled: bool,
686) -> SliderVisualState {
687    let normalized_value = if max > min {
688        clamp((value - min) / (max - min), 0.0, 1.0)
689    } else {
690        0.0
691    };
692    SliderVisualState::new(
693        value,
694        min,
695        max,
696        normalized_value,
697        orientation,
698        hovered,
699        dragging,
700        focused,
701        enabled,
702    )
703}
704
705fn sync_slider_geometry_with_state(
706    root: &FlexBox,
707    presenter: &dyn SliderPresenter,
708    state: SliderVisualState,
709    length: f32,
710) {
711    let metrics = presenter.metrics();
712    if state.orientation == Orientation::Vertical {
713        root.width(
714            metrics.thumb_size + (SLIDER_OUTER_INSET * 2.0) + metrics.cross_axis_extra,
715            Unit::Pixel,
716        )
717        .height(length + (SLIDER_OUTER_INSET * 2.0), Unit::Pixel);
718    } else {
719        root.width(length + (SLIDER_OUTER_INSET * 2.0), Unit::Pixel)
720            .height(
721                metrics.thumb_size + (SLIDER_OUTER_INSET * 2.0) + metrics.cross_axis_extra,
722                Unit::Pixel,
723            );
724    }
725    presenter
726        .root()
727        .position_type(PositionType::Absolute)
728        .position(SLIDER_CHILD_INSET, SLIDER_CHILD_INSET);
729    presenter.layout(state, length);
730    root.semantic_value_range(state.value, state.min, state.max);
731    root.default_semantic_label(if state.orientation == Orientation::Vertical {
732        "Vertical slider"
733    } else {
734        "Slider"
735    });
736}
737
738fn sync_slider_visual_state_with_state(
739    root: &FlexBox,
740    presenter: &dyn SliderPresenter,
741    state: SliderVisualState,
742    colors: Option<SliderColors>,
743) {
744    let theme = current_theme();
745    root.corner_radius(theme.spacing.sm)
746        .border(SLIDER_FOCUS_BORDER_WIDTH, theme.colors.background)
747        .padding(
748            SLIDER_PADDING,
749            SLIDER_PADDING,
750            SLIDER_PADDING,
751            SLIDER_PADDING,
752        )
753        .opacity(if state.enabled { 1.0 } else { 0.6 });
754    presenter.apply(theme, state, colors);
755}
756
757fn sync_slider_focus_chrome(root: &FlexBox, focused: bool, enabled: bool) {
758    if focused && enabled && focus_visibility::keyboard_focus_visible() {
759        focus_adorner::show_standard(root, current_theme().spacing.sm);
760        return;
761    }
762    focus_adorner::hide_owner(root);
763}