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}