Skip to main content

zng_wgt_input/
focus.rs

1//! Keyboard focus properties, [`tab_index`](fn@tab_index), [`focusable`](fn@focusable),
2//! [`on_focus`](fn@on_focus), [`is_focused`](fn@is_focused) and more.
3
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use zng_app::widget::info::WIDGET_TREE_CHANGED_EVENT;
8use zng_ext_input::focus::*;
9use zng_ext_input::gesture::{CLICK_EVENT, GESTURES};
10use zng_ext_window::WINDOWS;
11use zng_wgt::node::bind_state_init;
12use zng_wgt::prelude::*;
13
14/// Makes the widget focusable when set to `true`.
15#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
16pub fn focusable(child: impl IntoUiNode, focusable: impl IntoVar<bool>) -> UiNode {
17    let focusable = focusable.into_var();
18    match_node(child, move |_, op| match op {
19        UiNodeOp::Init => {
20            WIDGET.sub_var_info(&focusable);
21        }
22        UiNodeOp::Info { info } => {
23            FocusInfoBuilder::new(info).focusable(focusable.get());
24        }
25        _ => {}
26    })
27}
28
29/// Customizes the widget order during TAB navigation.
30#[property(CONTEXT, default(TabIndex::default()))]
31pub fn tab_index(child: impl IntoUiNode, tab_index: impl IntoVar<TabIndex>) -> UiNode {
32    let tab_index = tab_index.into_var();
33    match_node(child, move |_, op| match op {
34        UiNodeOp::Init => {
35            WIDGET.sub_var_info(&tab_index);
36        }
37        UiNodeOp::Info { info } => {
38            FocusInfoBuilder::new(info).tab_index(tab_index.get());
39        }
40        _ => {}
41    })
42}
43
44/// Makes the widget into a focus scope when set to `true`.
45#[property(CONTEXT, default(false))]
46pub fn focus_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
47    focus_scope_impl(child, is_scope, false)
48}
49/// Widget is the ALT focus scope.
50///
51/// ALT focus scopes are also, `TabIndex::SKIP`, `skip_directional_nav`, `TabNav::Cycle` and `DirectionalNav::Cycle` by default.
52///
53/// Also see [`focus_click_behavior`] that can be used to return focus automatically when any widget inside the ALT scope
54/// handles a click.
55///
56/// [`focus_click_behavior`]: fn@focus_click_behavior
57#[property(CONTEXT, default(false))]
58pub fn alt_focus_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
59    focus_scope_impl(child, is_scope, true)
60}
61
62fn focus_scope_impl(child: impl IntoUiNode, is_scope: impl IntoVar<bool>, is_alt: bool) -> UiNode {
63    let is_scope = is_scope.into_var();
64    match_node(child, move |_, op| match op {
65        UiNodeOp::Init => {
66            WIDGET.sub_var_info(&is_scope);
67        }
68        UiNodeOp::Info { info } => {
69            let mut info = FocusInfoBuilder::new(info);
70            if is_alt {
71                info.alt_scope(is_scope.get());
72            } else {
73                info.scope(is_scope.get());
74            }
75        }
76        UiNodeOp::Deinit if is_alt && FOCUS.is_focus_within(WIDGET.id()).get() => {
77            // focus auto recovery can't return focus if the entire scope is missing.
78            FOCUS.focus_exit(false);
79        }
80        _ => {}
81    })
82}
83
84/// Behavior of a focus scope when it receives direct focus.
85#[property(CONTEXT, default(FocusScopeOnFocus::default()))]
86pub fn focus_scope_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> UiNode {
87    let behavior = behavior.into_var();
88    match_node(child, move |_, op| match op {
89        UiNodeOp::Init => {
90            WIDGET.sub_var_info(&behavior);
91        }
92        UiNodeOp::Info { info } => {
93            FocusInfoBuilder::new(info).on_focus(behavior.get());
94        }
95        _ => {}
96    })
97}
98
99/// Tab navigation within this focus scope.
100#[property(CONTEXT, default(TabNav::Continue))]
101pub fn tab_nav(child: impl IntoUiNode, tab_nav: impl IntoVar<TabNav>) -> UiNode {
102    let tab_nav = tab_nav.into_var();
103    match_node(child, move |_, op| match op {
104        UiNodeOp::Init => {
105            WIDGET.sub_var_info(&tab_nav);
106        }
107        UiNodeOp::Info { info } => {
108            FocusInfoBuilder::new(info).tab_nav(tab_nav.get());
109        }
110        _ => {}
111    })
112}
113
114/// Keyboard arrows navigation within this focus scope.
115#[property(CONTEXT, default(DirectionalNav::Continue))]
116pub fn directional_nav(child: impl IntoUiNode, directional_nav: impl IntoVar<DirectionalNav>) -> UiNode {
117    let directional_nav = directional_nav.into_var();
118    match_node(child, move |_, op| match op {
119        UiNodeOp::Init => {
120            WIDGET.sub_var_info(&directional_nav);
121        }
122        UiNodeOp::Info { info } => {
123            FocusInfoBuilder::new(info).directional_nav(directional_nav.get());
124        }
125        _ => {}
126    })
127}
128
129/// Keyboard shortcuts that focus this widget or its first focusable descendant or its first focusable parent.
130#[property(CONTEXT, default(Shortcuts::default()))]
131pub fn focus_shortcut(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>) -> UiNode {
132    let shortcuts = shortcuts.into_var();
133    let mut _handle = None;
134    match_node(child, move |_, op| match op {
135        UiNodeOp::Init => {
136            WIDGET.sub_var(&shortcuts);
137            let s = shortcuts.get();
138            _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
139        }
140        UiNodeOp::Update { .. } => {
141            if let Some(s) = shortcuts.get_new() {
142                _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
143            }
144        }
145        _ => {}
146    })
147}
148
149/// If directional navigation from outside this widget skips over it and its descendants.
150///
151/// Setting this to `true` is the directional navigation equivalent of setting `tab_index` to `SKIP`.
152#[property(CONTEXT, default(false))]
153pub fn skip_directional(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
154    let enabled = enabled.into_var();
155    match_node(child, move |_, op| match op {
156        UiNodeOp::Init => {
157            WIDGET.sub_var_info(&enabled);
158        }
159        UiNodeOp::Info { info } => {
160            FocusInfoBuilder::new(info).skip_directional(enabled.get());
161        }
162        _ => {}
163    })
164}
165
166/// Behavior of a widget when a click event is send to it or a descendant.
167///
168/// See [`focus_click_behavior`] for more details.
169///
170/// [`focus_click_behavior`]: fn@focus_click_behavior
171#[derive(Clone, Copy, PartialEq, Eq)]
172pub enum FocusClickBehavior {
173    /// Click event always ignored.
174    Ignore,
175    /// Exit focus if a click event was send to the widget or descendant.
176    Exit {
177        /// If exiting from an ALT scope recursively seek the return widget that is not inside any ALT scope.
178        recursive_alt: bool,
179        /// Exit only if click targets an enabled widget.
180        enabled: bool,
181        /// Exit only if the click event propagation was stopped.
182        handled: bool,
183    },
184}
185impl std::fmt::Debug for FocusClickBehavior {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        if f.alternate() {
188            write!(f, "FocusClickBehavior::")?;
189        }
190        match &self {
191            Self::Ignore => write!(f, "Ignore"),
192            Self::Exit {
193                recursive_alt,
194                enabled,
195                handled,
196            } => f
197                .debug_struct("Exit")
198                .field("recursive_alt", recursive_alt)
199                .field("enabled", enabled)
200                .field("handled", handled)
201                .finish(),
202        }
203    }
204}
205impl FocusClickBehavior {
206    /// Default behavior for menu item buttons.
207    ///
208    /// Only if the item is enabled, exits entire menu.
209    pub fn menu_item() -> Self {
210        FocusClickBehavior::Exit {
211            recursive_alt: true,
212            enabled: true,
213            handled: false,
214        }
215    }
216}
217
218/// Behavior of a widget when a click event is send to it or a descendant.
219///
220/// When a click event targets the widget or descendant the `behavior` closest to the target is applied,
221/// that is if `Exit` is set in a parent, but `Ignore` is set on the target than the click is ignored.
222/// This can be used to create a effects like a menu that closes on click for command items, but not for clicks
223/// in sub-menu items.
224///
225/// Note that this property does not subscribe to any event, it only observes events flowing trough.
226#[property(CONTEXT, default(FocusClickBehavior::Ignore))]
227pub fn focus_click_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusClickBehavior>) -> UiNode {
228    let behavior = behavior.into_var();
229    match_node(child, move |c, op| {
230        if let UiNodeOp::Update { updates } = op {
231            let mut on_click = || {
232                if let Some(ctx) = &*FOCUS_CLICK_HANDLED_CTX.get() {
233                    // a parent also sets `focus_click_behavior`
234
235                    c.update(updates);
236
237                    // signal that we will handle this event
238                    ctx.swap(true, Ordering::Relaxed)
239                } else {
240                    // no parent sets `focus_click_behavior`, setup context
241
242                    let mut ctx = Some(Arc::new(Some(AtomicBool::new(false))));
243                    FOCUS_CLICK_HANDLED_CTX.with_context(&mut ctx, || c.update(updates));
244
245                    // get if any child already handled this event
246                    let ctx = ctx.unwrap();
247                    (*ctx).as_ref().unwrap().load(Ordering::Relaxed)
248                }
249            };
250
251            CLICK_EVENT.each_update(true, |args| {
252                let focus_click_handled_by_inner = on_click();
253                if !focus_click_handled_by_inner
254                    && let FocusClickBehavior::Exit {
255                        recursive_alt,
256                        enabled,
257                        handled,
258                    } = behavior.get()
259                {
260                    if enabled && !args.target.interactivity().is_enabled() {
261                        return;
262                    }
263                    if handled && !args.propagation.is_stopped() {
264                        return;
265                    }
266
267                    tracing::trace!("focus_exit by focus_click_behavior");
268                    FOCUS.focus_exit(recursive_alt);
269                }
270            });
271        }
272    })
273}
274context_local! {
275    static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
276}
277
278event_property! {
279    /// Focus changed in the widget or its descendants.
280    #[property(EVENT)]
281    pub fn on_focus_changed<on_pre_focus_changed>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
282        const PRE: bool;
283        EventNodeBuilder::new(FOCUS_CHANGED_EVENT).build::<PRE>(child, handler)
284    }
285
286    /// Widget got direct keyboard focus.
287    #[property(EVENT)]
288    pub fn on_focus<on_pre_focus>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
289        const PRE: bool;
290        EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
291            .filter(|| {
292                let id = WIDGET.id();
293                move |args| args.is_focus(id)
294            })
295            .build::<PRE>(child, handler)
296    }
297
298    /// Widget lost direct keyboard focus.
299    #[property(EVENT)]
300    pub fn on_blur<on_pre_blur>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
301        const PRE: bool;
302        EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
303            .filter(|| {
304                let id = WIDGET.id();
305                move |args| args.is_blur(id)
306            })
307            .build::<PRE>(child, handler)
308    }
309
310    /// Widget or one of its descendants got focus.
311    #[property(EVENT)]
312    pub fn on_focus_enter<on_pre_focus_enter>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
313        const PRE: bool;
314        EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
315            .filter(|| {
316                let id = WIDGET.id();
317                move |args| args.is_focus_enter(id)
318            })
319            .build::<PRE>(child, handler)
320    }
321
322    /// Widget or one of its descendants lost focus.
323    #[property(EVENT)]
324    pub fn on_focus_leave<on_pre_focus_leave>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
325        const PRE: bool;
326        EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
327            .filter(|| {
328                let id = WIDGET.id();
329                move |args| args.is_focus_leave(id)
330            })
331            .build::<PRE>(child, handler)
332    }
333}
334
335/// If the widget has keyboard focus.
336///
337/// This is only `true` if the widget itself is focused.
338/// Use [`is_focus_within`] to include focused widgets inside this one.
339///
340/// # Highlighting
341///
342/// This property is always `true` when the widget has focus, independent of what device moved the focus,
343/// usually when the keyboard is used a special visual indicator is rendered, a dotted line border is common,
344/// this state is called *highlighting* and is tracked by the focus manager. To implement such a visual you can use the
345/// [`is_focused_hgl`] property.
346///
347/// # Return Focus
348///
349/// Usually widgets that have a visual state for this property also have one for [`is_return_focus`], a common example is the
350/// *text-input* widget that shows an emphasized border and blinking cursor when focused and still shows the
351/// emphasized border without cursor when a menu is open and it is only the return focus.
352///
353/// [`is_focus_within`]: fn@is_focus_within
354/// [`is_focused_hgl`]: fn@is_focused_hgl
355/// [`is_return_focus`]: fn@is_return_focus
356#[property(EVENT, widget_impl(FocusableMix<P>))]
357pub fn is_focused(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
358    bind_state_init(child, state, |s| {
359        let id = WIDGET.id();
360        s.set(FOCUS.focused().with(|p| matches!(p, Some(p) if p.widget_id() == id)));
361        FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
362            if args.is_focus(id) {
363                Some(true)
364            } else if args.is_blur(id) {
365                Some(false)
366            } else {
367                None
368            }
369        })
370    })
371}
372
373/// If the widget or one of its descendants has keyboard focus.
374///
375/// To check if only the widget has keyboard focus use [`is_focused`].
376///
377/// To track *highlighted* focus within use [`is_focus_within_hgl`] property.
378///
379/// [`is_focused`]: fn@is_focused
380/// [`is_focus_within_hgl`]: fn@is_focus_within_hgl
381#[property(EVENT, widget_impl(FocusableMix<P>))]
382pub fn is_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
383    bind_state_init(child, state, |s| {
384        let id = WIDGET.id();
385        s.set(FOCUS.focused().with(|p| matches!(p, Some(p) if p.contains(id))));
386        FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
387            if args.is_focus_enter(id) {
388                Some(true)
389            } else if args.is_focus_leave(id) {
390                Some(false)
391            } else {
392                None
393            }
394        })
395    })
396}
397
398/// If the widget has keyboard focus and the user is using the keyboard to navigate.
399///
400/// This is only `true` if the widget itself is focused and the focus was acquired by keyboard navigation.
401/// You can use [`is_focus_within_hgl`] to include widgets inside this one.
402///
403/// # Highlighting
404///
405/// Usually when the keyboard is used to move the focus a special visual indicator is rendered, a dotted line border is common,
406/// this state is called *highlighting* and is tracked by the focus manager, this property is only `true`.
407///
408/// [`is_focus_within_hgl`]: fn@is_focus_within_hgl
409/// [`is_focused`]: fn@is_focused
410#[property(EVENT, widget_impl(FocusableMix<P>))]
411pub fn is_focused_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
412    bind_state_init(child, state, |s| {
413        let id = WIDGET.id();
414        s.set(FOCUS.is_highlighting().get() && FOCUS.focused().with(|p| matches!(p, Some(p) if p.widget_id() == id)));
415        FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
416            if args.is_focus(id) {
417                Some(args.highlight)
418            } else if args.is_blur(id) {
419                Some(false)
420            } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
421                Some(args.highlight)
422            } else {
423                None
424            }
425        })
426    })
427}
428
429/// If the widget or one of its descendants has keyboard focus and the user is using the keyboard to navigate.
430///
431/// To check if only the widget has keyboard focus use [`is_focused_hgl`].
432///
433/// Also see [`is_focus_within`] to check if the widget has focus within regardless of highlighting.
434///
435/// [`is_focused_hgl`]: fn@is_focused_hgl
436/// [`is_focus_within`]: fn@is_focus_within
437#[property(EVENT, widget_impl(FocusableMix<P>))]
438pub fn is_focus_within_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
439    bind_state_init(child, state, |s| {
440        let id = WIDGET.id();
441        s.set(FOCUS.is_highlighting().get() && FOCUS.focused().with(|p| matches!(p, Some(p) if p.contains(id))));
442        FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
443            if args.is_focus_enter(id) {
444                Some(args.highlight)
445            } else if args.is_focus_leave(id) {
446                Some(false)
447            } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
448                Some(args.highlight)
449            } else {
450                None
451            }
452        })
453    })
454}
455
456/// If the widget will be focused when a parent scope is focused.
457///
458/// Focus scopes can remember the last focused widget inside, the focus *returns* to
459/// this widget when the scope receives focus. Alt scopes also remember the widget from which the *alt* focus happened
460/// and can also return focus back to that widget.
461///
462/// Usually input widgets that have a visual state for [`is_focused`] also have a visual for this, a common example is the
463/// *text-input* widget that shows an emphasized border and blinking cursor when focused and still shows the
464/// emphasized border without cursor when a menu is open and it is only the return focus.
465///
466/// Note that a widget can be [`is_focused`] and `is_return_focus`, this property is `true` if any focus scope considers the
467/// widget its return focus, you probably want to declare the widget visual states in such a order that [`is_focused`] overrides
468/// the state of this property.
469///
470/// [`is_focused`]: fn@is_focused_hgl
471/// [`is_focused_hgl`]: fn@is_focused_hgl
472#[property(EVENT, widget_impl(FocusableMix<P>))]
473pub fn is_return_focus(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
474    bind_state_init(child, state, |s| {
475        let id = WIDGET.id();
476        RETURN_FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
477            if args.is_return_focus(id) {
478                Some(true)
479            } else if args.was_return_focus(id) {
480                Some(false)
481            } else {
482                None
483            }
484        })
485    })
486}
487
488/// If the widget or one of its descendants will be focused when a focus scope is focused.
489///
490/// To check if only the widget is the return focus use [`is_return_focus`].
491///
492/// [`is_return_focus`]: fn@is_return_focus
493#[property(EVENT, widget_impl(FocusableMix<P>))]
494pub fn is_return_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
495    bind_state_init(child, state, |s| {
496        let id = WIDGET.id();
497        RETURN_FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
498            if args.is_return_focus_enter(id) {
499                Some(true)
500            } else if args.is_return_focus_leave(id) {
501                Some(false)
502            } else {
503                None
504            }
505        })
506    })
507}
508
509/// If the widget is focused on info init.
510///
511/// When the widget is inited and present in the info tree a [`FOCUS.focus_widget_or_related`] request is made for the widget.
512///
513/// [`FOCUS.focus_widget_or_related`]: FOCUS::focus_widget_or_related
514#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
515pub fn focus_on_init(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
516    let enabled = enabled.into_var();
517
518    enum State {
519        WaitInfo,
520        InfoInited,
521        Done,
522    }
523    let mut state = State::WaitInfo;
524
525    match_node(child, move |_, op| match op {
526        UiNodeOp::Init => {
527            if enabled.get() {
528                state = State::WaitInfo;
529            } else {
530                state = State::Done;
531            }
532        }
533        UiNodeOp::Info { .. } => {
534            if let State::WaitInfo = &state {
535                state = State::InfoInited;
536                // next update will be after the info is in tree.
537                WIDGET.update();
538            }
539        }
540        UiNodeOp::Update { .. } => {
541            if let State::InfoInited = &state {
542                state = State::Done;
543                FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
544            }
545        }
546        _ => {}
547    })
548}
549
550/// If the widget return focus to the previous focus when it inited.
551///
552/// This can be used with the [`modal`] property to declare *modal dialogs* that return the focus
553/// to the widget that opens the dialog.
554///
555/// Consider using [`focus_click_behavior`] if the widget is also an ALT focus scope.
556///
557/// [`modal`]: fn@zng_wgt::modal
558/// [`focus_click_behavior`]: fn@focus_click_behavior
559#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
560pub fn return_focus_on_deinit(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
561    let enabled = enabled.into_var();
562    let mut return_focus = None;
563    match_node(child, move |_, op| match op {
564        UiNodeOp::Init => {
565            return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
566        }
567        UiNodeOp::Deinit => {
568            if let Some(id) = return_focus.take()
569                && enabled.get()
570            {
571                if let Some(w) = zng_ext_window::WINDOWS.widget_info(id)
572                    && w.into_focusable(false, false).is_some()
573                {
574                    // can focus on the next update
575                    FOCUS.focus_widget(id, false);
576                    return;
577                }
578                // try focus after info rebuild.
579                let win_id = WINDOW.id();
580                WIDGET_TREE_CHANGED_EVENT
581                    .hook(move |args| {
582                        if args.tree.window_id() != win_id {
583                            // some other window also rebuilt, retain if parent still open
584                            return WINDOWS.widget_tree(win_id).is_some();
585                        }
586                        FOCUS.focus_widget(id, false);
587                        false
588                    })
589                    .perm();
590
591                // ensure info rebuilds to clear the event at least
592                WIDGET.update_info();
593            }
594        }
595        _ => {}
596    })
597}
598
599/// Focusable widget mixin. Enables keyboard focusing on the widget and adds a focused highlight visual.
600#[widget_mixin]
601pub struct FocusableMix<P>(P);
602impl<P: WidgetImpl> FocusableMix<P> {
603    fn widget_intrinsic(&mut self) {
604        widget_set! {
605            self;
606            focusable = true;
607            when *#is_focused_hgl {
608                zng_wgt_fill::foreground_highlight = {
609                    offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
610                    widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
611                    sides: FOCUS_HIGHLIGHT_SIDES_VAR,
612                };
613            }
614        }
615    }
616}
617
618context_var! {
619    /// Padding offsets of the foreground highlight when the widget is focused.
620    pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
621    /// Border widths of the foreground highlight when the widget is focused.
622    pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
623    /// Border sides of the foreground highlight when the widget is focused.
624    pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
625}
626
627/// Sets the foreground highlight values used when the widget is focused and highlighted.
628#[property(
629    CONTEXT,
630    default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
631    widget_impl(FocusableMix<P>)
632)]
633pub fn focus_highlight(
634    child: impl IntoUiNode,
635    offsets: impl IntoVar<SideOffsets>,
636    widths: impl IntoVar<SideOffsets>,
637    sides: impl IntoVar<BorderSides>,
638) -> UiNode {
639    let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
640    let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
641    with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
642}