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