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