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 UiNode, focusable: impl IntoVar<bool>) -> impl 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 UiNode, tab_index: impl IntoVar<TabIndex>) -> impl 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 UiNode, is_scope: impl IntoVar<bool>) -> impl 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 UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
58    focus_scope_impl(child, is_scope, true)
59}
60
61fn focus_scope_impl(child: impl UiNode, is_scope: impl IntoVar<bool>, is_alt: bool) -> impl 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 UiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> impl 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 UiNode, tab_nav: impl IntoVar<TabNav>) -> impl 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 UiNode, directional_nav: impl IntoVar<DirectionalNav>) -> impl 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 UiNode, shortcuts: impl IntoVar<Shortcuts>) -> impl 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 UiNode, enabled: impl IntoVar<bool>) -> impl 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 UiNode, behavior: impl IntoVar<FocusClickBehavior>) -> impl 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                if args.propagation().is_stopped() && !delegate() {
237                    // CLICK_EVENT not send if source mouse-input is already handled.
238
239                    let exit = match behavior.get() {
240                        FocusClickBehavior::Ignore => false,
241                        FocusClickBehavior::Exit => true,
242                        FocusClickBehavior::ExitEnabled => args.target.interactivity().is_enabled(),
243                        FocusClickBehavior::ExitHandled => true,
244                    };
245                    if exit {
246                        FOCUS.focus_exit();
247                    }
248                }
249            }
250        }
251    })
252}
253context_local! {
254    static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
255}
256
257event_property! {
258    /// Focus changed in the widget or its descendants.
259    pub fn focus_changed {
260        event: FOCUS_CHANGED_EVENT,
261        args: FocusChangedArgs,
262    }
263
264    /// Widget got direct keyboard focus.
265    pub fn focus {
266        event: FOCUS_CHANGED_EVENT,
267        args: FocusChangedArgs,
268        filter: |args| args.is_focus(WIDGET.id()),
269    }
270
271    /// Widget lost direct keyboard focus.
272    pub fn blur {
273        event: FOCUS_CHANGED_EVENT,
274        args: FocusChangedArgs,
275        filter: |args| args.is_blur(WIDGET.id()),
276    }
277
278    /// Widget or one of its descendants got focus.
279    pub fn focus_enter {
280        event: FOCUS_CHANGED_EVENT,
281        args: FocusChangedArgs,
282        filter: |args| args.is_focus_enter(WIDGET.id()),
283    }
284
285    /// Widget or one of its descendants lost focus.
286    pub fn focus_leave {
287        event: FOCUS_CHANGED_EVENT,
288        args: FocusChangedArgs,
289        filter: |args| args.is_focus_leave(WIDGET.id()),
290    }
291}
292
293/// If the widget has keyboard focus.
294///
295/// This is only `true` if the widget itself is focused.
296/// Use [`is_focus_within`] to include focused widgets inside this one.
297///
298/// # Highlighting
299///
300/// This property is always `true` when the widget has focus, independent of what device moved the focus,
301/// usually when the keyboard is used a special visual indicator is rendered, a dotted line border is common,
302/// this state is called *highlighting* and is tracked by the focus manager. To implement such a visual you can use the
303/// [`is_focused_hgl`] property.
304///
305/// # Return Focus
306///
307/// Usually widgets that have a visual state for this property also have one for [`is_return_focus`], a common example is the
308/// *text-input* widget that shows an emphasized border and blinking cursor when focused and still shows the
309/// emphasized border without cursor when a menu is open and it is only the return focus.
310///
311/// [`is_focus_within`]: fn@is_focus_within
312/// [`is_focused_hgl`]: fn@is_focused_hgl
313/// [`is_return_focus`]: fn@is_return_focus
314#[property(CONTEXT, widget_impl(FocusableMix<P>))]
315pub fn is_focused(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
316    event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
317        let id = WIDGET.id();
318        if args.is_focus(id) {
319            Some(true)
320        } else if args.is_blur(id) {
321            Some(false)
322        } else {
323            None
324        }
325    })
326}
327
328/// If the widget or one of its descendants has keyboard focus.
329///
330/// To check if only the widget has keyboard focus use [`is_focused`].
331///
332/// To track *highlighted* focus within use [`is_focus_within_hgl`] property.
333///
334/// [`is_focused`]: fn@is_focused
335/// [`is_focus_within_hgl`]: fn@is_focus_within_hgl
336#[property(CONTEXT, widget_impl(FocusableMix<P>))]
337pub fn is_focus_within(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
338    event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
339        let id = WIDGET.id();
340        if args.is_focus_enter(id) {
341            Some(true)
342        } else if args.is_focus_leave(id) {
343            Some(false)
344        } else {
345            None
346        }
347    })
348}
349
350/// If the widget has keyboard focus and the user is using the keyboard to navigate.
351///
352/// This is only `true` if the widget itself is focused and the focus was acquired by keyboard navigation.
353/// You can use [`is_focus_within_hgl`] to include widgets inside this one.
354///
355/// # Highlighting
356///
357/// Usually when the keyboard is used to move the focus a special visual indicator is rendered, a dotted line border is common,
358/// this state is called *highlighting* and is tracked by the focus manager, this property is only `true`.
359///
360/// [`is_focus_within_hgl`]: fn@is_focus_within_hgl
361/// [`is_focused`]: fn@is_focused
362#[property(CONTEXT, widget_impl(FocusableMix<P>))]
363pub fn is_focused_hgl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
364    event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
365        let id = WIDGET.id();
366        if args.is_focus(id) {
367            Some(args.highlight)
368        } else if args.is_blur(id) {
369            Some(false)
370        } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
371            Some(args.highlight)
372        } else {
373            None
374        }
375    })
376}
377
378/// If the widget or one of its descendants has keyboard focus and the user is using the keyboard to navigate.
379///
380/// To check if only the widget has keyboard focus use [`is_focused_hgl`].
381///
382/// Also see [`is_focus_within`] to check if the widget has focus within regardless of highlighting.
383///
384/// [`is_focused_hgl`]: fn@is_focused_hgl
385/// [`is_focus_within`]: fn@is_focus_within
386#[property(CONTEXT, widget_impl(FocusableMix<P>))]
387pub fn is_focus_within_hgl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
388    event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
389        let id = WIDGET.id();
390        if args.is_focus_enter(id) {
391            Some(args.highlight)
392        } else if args.is_focus_leave(id) {
393            Some(false)
394        } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
395            Some(args.highlight)
396        } else {
397            None
398        }
399    })
400}
401
402/// If the widget will be focused when a parent scope is focused.
403///
404/// Focus scopes can remember the last focused widget inside, the focus *returns* to
405/// this widget when the scope receives focus. Alt scopes also remember the widget from which the *alt* focus happened
406/// and can also return focus back to that widget.
407///
408/// Usually input widgets that have a visual state for [`is_focused`] also have a visual for this, a common example is the
409/// *text-input* widget that shows an emphasized border and blinking cursor when focused and still shows the
410/// emphasized border without cursor when a menu is open and it is only the return focus.
411///
412/// Note that a widget can be [`is_focused`] and `is_return_focus`, this property is `true` if any focus scope considers the
413/// widget its return focus, you probably want to declare the widget visual states in such a order that [`is_focused`] overrides
414/// the state of this property.
415///
416/// [`is_focused`]: fn@is_focused_hgl
417/// [`is_focused_hgl`]: fn@is_focused_hgl
418#[property(CONTEXT, widget_impl(FocusableMix<P>))]
419pub fn is_return_focus(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
420    event_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
421        let id = WIDGET.id();
422        if args.is_return_focus(id) {
423            Some(true)
424        } else if args.was_return_focus(id) {
425            Some(false)
426        } else {
427            None
428        }
429    })
430}
431
432/// If the widget or one of its descendants will be focused when a focus scope is focused.
433///
434/// To check if only the widget is the return focus use [`is_return_focus`].
435///
436/// [`is_return_focus`]: fn@is_return_focus
437#[property(CONTEXT, widget_impl(FocusableMix<P>))]
438pub fn is_return_focus_within(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
439    event_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
440        let id = WIDGET.id();
441        if args.is_return_focus_enter(id) {
442            Some(true)
443        } else if args.is_return_focus_leave(id) {
444            Some(false)
445        } else {
446            None
447        }
448    })
449}
450
451/// If the widget is focused on info init.
452///
453/// When the widget is inited and present in the info tree a [`FOCUS.focus_widget_or_related`] request is made for the widget.
454///
455/// [`FOCUS.focus_widget_or_related`]: FOCUS::focus_widget_or_related
456#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
457pub fn focus_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
458    let enabled = enabled.into_var();
459
460    enum State {
461        WaitInfo,
462        InfoInited,
463        Done,
464    }
465    let mut state = State::WaitInfo;
466
467    match_node(child, move |_, op| match op {
468        UiNodeOp::Init => {
469            if enabled.get() {
470                state = State::WaitInfo;
471            } else {
472                state = State::Done;
473            }
474        }
475        UiNodeOp::Info { .. } => {
476            if let State::WaitInfo = &state {
477                state = State::InfoInited;
478                // next update will be after the info is in tree.
479                WIDGET.update();
480            }
481        }
482        UiNodeOp::Update { .. } => {
483            if let State::InfoInited = &state {
484                state = State::Done;
485                FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
486            }
487        }
488        _ => {}
489    })
490}
491
492/// If the widget return focus to the previous focus when it inited.
493///
494/// This can be used with the [`modal`] property to declare *modal dialogs* that return the focus
495/// to the widget that opens the dialog.
496///
497/// Consider using [`focus_click_behavior`] if the widget is also an ALT focus scope.
498///
499/// [`modal`]: fn@zng_wgt::modal
500/// [`focus_click_behavior`]: fn@focus_click_behavior
501#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
502pub fn return_focus_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
503    let enabled = enabled.into_var();
504    let mut return_focus = None;
505    match_node(child, move |_, op| match op {
506        UiNodeOp::Init => {
507            return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
508        }
509        UiNodeOp::Deinit => {
510            if let Some(id) = return_focus.take() {
511                if enabled.get() {
512                    if let Some(w) = zng_ext_window::WINDOWS.widget_info(id) {
513                        if w.into_focusable(false, false).is_some() {
514                            // can focus on the next update
515                            FOCUS.focus_widget(id, false);
516                            return;
517                        }
518                    }
519                    // try focus after info rebuild.
520                    WIDGET_INFO_CHANGED_EVENT
521                        .on_pre_event(app_hn_once!(|_| {
522                            FOCUS.focus_widget(id, false);
523                        }))
524                        .perm();
525                    // ensure info rebuilds to clear the event at least
526                    WIDGET.update_info();
527                }
528            }
529        }
530        _ => {}
531    })
532}
533
534/// Focusable widget mixin. Enables keyboard focusing on the widget and adds a focused highlight visual.
535#[widget_mixin]
536pub struct FocusableMix<P>(P);
537impl<P: WidgetImpl> FocusableMix<P> {
538    fn widget_intrinsic(&mut self) {
539        widget_set! {
540            self;
541            focusable = true;
542            when *#is_focused_hgl {
543                zng_wgt_fill::foreground_highlight = {
544                    offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
545                    widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
546                    sides: FOCUS_HIGHLIGHT_SIDES_VAR,
547                };
548            }
549        }
550    }
551}
552
553context_var! {
554    /// Padding offsets of the foreground highlight when the widget is focused.
555    pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
556    /// Border widths of the foreground highlight when the widget is focused.
557    pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
558    /// Border sides of the foreground highlight when the widget is focused.
559    pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
560}
561
562/// Sets the foreground highlight values used when the widget is focused and highlighted.
563#[property(
564    CONTEXT,
565    default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
566    widget_impl(FocusableMix<P>)
567)]
568pub fn focus_highlight(
569    child: impl UiNode,
570    offsets: impl IntoVar<SideOffsets>,
571    widths: impl IntoVar<SideOffsets>,
572    sides: impl IntoVar<BorderSides>,
573) -> impl UiNode {
574    let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
575    let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
576    with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
577}