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