kas_core/event/cx/
nav.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Event context: navigation focus
7
8use super::{EventCx, EventState};
9use crate::event::{Event, FocusSource};
10use crate::{Action, Id, Node};
11#[allow(unused)] use crate::{Tile, event::Command};
12
13/// Action of Widget::_nav_next
14#[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
15#[cfg_attr(docsrs, doc(cfg(internal_doc)))]
16#[derive(Copy, Clone, Debug, PartialEq, Eq)]
17pub enum NavAdvance {
18    /// Match only `focus` if navigable
19    None,
20    /// Walk children forwards, self first
21    ///
22    /// Parameter: whether this can match self (in addition to other widgets).
23    Forward(bool),
24    /// Walk children backwards, self last
25    ///
26    /// Parameter: whether this can match self (in addition to other widgets).
27    Reverse(bool),
28}
29
30#[crate::impl_default(PendingNavFocus::None)]
31pub(super) enum PendingNavFocus {
32    None,
33    Set {
34        target: Option<Id>,
35        source: FocusSource,
36    },
37    Next {
38        target: Option<Id>,
39        advance: NavAdvance,
40        source: FocusSource,
41    },
42}
43
44impl PendingNavFocus {
45    #[inline]
46    pub(super) fn is_some(&self) -> bool {
47        !matches!(self, PendingNavFocus::None)
48    }
49}
50
51impl EventState {
52    /// Get whether this widget has navigation focus
53    #[inline]
54    pub fn has_nav_focus(&self, w_id: &Id) -> bool {
55        *w_id == self.nav_focus
56    }
57
58    /// Get the current navigation focus, if any
59    ///
60    /// This is the widget selected by navigating the UI with the Tab key.
61    ///
62    /// Note: changing navigation focus (e.g. via [`Self::clear_nav_focus`],
63    /// [`Self::request_nav_focus`] or [`Self::next_nav_focus`]) does not
64    /// immediately affect the result of this method.
65    #[inline]
66    pub fn nav_focus(&self) -> Option<&Id> {
67        self.nav_focus.as_ref()
68    }
69
70    /// Clear navigation focus
71    pub fn clear_nav_focus(&mut self) {
72        self.pending_nav_focus = PendingNavFocus::Set {
73            target: None,
74            source: FocusSource::Synthetic,
75        };
76    }
77
78    pub(super) fn clear_nav_focus_on(&mut self, target: &Id) {
79        if let Some(id) = self.nav_focus.as_ref()
80            && target.is_ancestor_of(id)
81        {
82            if matches!(&self.pending_nav_focus, PendingNavFocus::Set { target, .. } if target.as_ref() == Some(id))
83            {
84                self.pending_nav_focus = PendingNavFocus::None;
85            }
86
87            if matches!(self.pending_nav_focus, PendingNavFocus::None) {
88                self.pending_nav_focus = PendingNavFocus::Set {
89                    target: None,
90                    source: FocusSource::Synthetic,
91                };
92            }
93        }
94    }
95
96    /// Request navigation focus directly
97    ///
98    /// If `id` already has navigation focus or navigation focus is disabled
99    /// globally then nothing happens. If widget `id` supports
100    /// [navigation focus](Tile::navigable), then it should receive
101    /// [`Event::NavFocus`]; if not then the first supporting ancestor will
102    /// receive focus.
103    pub fn request_nav_focus(&mut self, id: Id, source: FocusSource) {
104        self.pending_nav_focus = PendingNavFocus::Next {
105            target: Some(id),
106            advance: NavAdvance::None,
107            source,
108        };
109    }
110
111    /// Set navigation focus directly
112    ///
113    /// If `id` already has navigation focus or navigation focus is disabled
114    /// globally then nothing happens, otherwise widget `id` should receive
115    /// [`Event::NavFocus`].
116    ///
117    /// Normally, [`Tile::navigable`] will return true for widget `id` but this
118    /// is not checked or required. For example, a `ScrollLabel` can receive
119    /// focus on text selection with the mouse.
120    pub(crate) fn set_nav_focus(&mut self, id: Id, source: FocusSource) {
121        self.pending_nav_focus = PendingNavFocus::Set {
122            target: Some(id),
123            source,
124        };
125    }
126
127    /// Advance the navigation focus
128    ///
129    /// If `target == Some(id)`, this looks for the next widget from `id`
130    /// (inclusive) which is [navigable](Tile::navigable). Otherwise where
131    /// some widget `id` has [`nav_focus`](Self::nav_focus) this looks for the
132    /// next navigable widget *excluding* `id`. If no reference is available,
133    /// this instead looks for the first navigable widget.
134    ///
135    /// If `reverse`, instead search for the previous or last navigable widget.
136    pub fn next_nav_focus(
137        &mut self,
138        target: impl Into<Option<Id>>,
139        reverse: bool,
140        source: FocusSource,
141    ) {
142        let target = target.into();
143        let advance = match reverse {
144            false => NavAdvance::Forward(target.is_some()),
145            true => NavAdvance::Reverse(target.is_some()),
146        };
147        self.pending_nav_focus = PendingNavFocus::Next {
148            target,
149            advance,
150            source,
151        };
152    }
153
154    /// Sets the fallback recipient of [`Event::Command`]
155    ///
156    /// Where a key-press translates to a [`Command`], this is first sent to
157    /// widgets with applicable key, selection and/or navigation focus as an
158    /// [`Event::Command`]. If this event goes unhandled and a fallback
159    /// recipient is set using this method, then this fallback recipient will
160    /// be sent the same event.
161    ///
162    /// There may be one fallback recipient per window; do not use an [`Id`]
163    /// from another window. If this method is called multiple times, the last
164    /// such call succeeds.
165    pub fn register_nav_fallback(&mut self, id: Id) {
166        if self.nav_fallback.is_none() {
167            log::debug!(target: "kas_core::event","register_nav_fallback: id={id}");
168            self.nav_fallback = Some(id);
169        }
170    }
171}
172
173impl<'a> EventCx<'a> {
174    // Call Widget::_nav_next
175    #[inline]
176    pub(super) fn nav_next(
177        &mut self,
178        mut widget: Node<'_>,
179        focus: Option<&Id>,
180        advance: NavAdvance,
181    ) -> Option<Id> {
182        log::trace!(target: "kas_core::event", "nav_next: focus={focus:?}, advance={advance:?}");
183
184        widget._nav_next(&mut self.config_cx(), focus, advance)
185    }
186
187    pub(super) fn handle_pending_nav_focus(&mut self, widget: Node<'_>) {
188        match std::mem::take(&mut self.pending_nav_focus) {
189            PendingNavFocus::None => (),
190            PendingNavFocus::Set { target, source } => {
191                self.set_nav_focus_impl(widget, target, source)
192            }
193            PendingNavFocus::Next {
194                target,
195                advance,
196                source,
197            } => self.next_nav_focus_impl(widget, target, advance, source),
198        }
199    }
200
201    /// Set navigation focus immediately
202    pub(super) fn set_nav_focus_impl(
203        &mut self,
204        mut widget: Node,
205        target: Option<Id>,
206        source: FocusSource,
207    ) {
208        if target == self.nav_focus || !self.config.nav_focus {
209            return;
210        }
211
212        self.clear_key_focus();
213
214        if let Some(old) = self.nav_focus.take() {
215            self.action(&old, Action::REDRAW);
216            self.send_event(widget.re(), old, Event::LostNavFocus);
217        }
218
219        self.nav_focus = target.clone();
220        log::debug!(target: "kas_core::event", "nav_focus = {target:?}");
221        if let Some(id) = target {
222            self.action(&id, Action::REDRAW);
223            self.send_event(widget, id, Event::NavFocus(source));
224        }
225    }
226
227    /// Advance the keyboard navigation focus immediately
228    pub(super) fn next_nav_focus_impl(
229        &mut self,
230        mut widget: Node,
231        target: Option<Id>,
232        advance: NavAdvance,
233        source: FocusSource,
234    ) {
235        if !self.config.nav_focus || (target.is_some() && target == self.nav_focus) {
236            return;
237        }
238
239        if let Some(id) = self
240            .popups
241            .last()
242            .filter(|popup| popup.is_sized)
243            .map(|state| state.desc.id.clone())
244        {
245            if id.is_ancestor_of(widget.id_ref()) {
246                // do nothing
247            } else if let Some(r) = widget.find_node(&id, |node| {
248                self.next_nav_focus_impl(node, target, advance, source)
249            }) {
250                return r;
251            } else {
252                log::warn!(
253                    target: "kas_core::event",
254                    "next_nav_focus: have open pop-up which is not a child of widget",
255                );
256                return;
257            }
258        }
259
260        let focus = target.or_else(|| self.nav_focus.clone());
261
262        // Whether to restart from the beginning on failure
263        let restart = focus.is_some();
264
265        let mut opt_id = self.nav_next(widget.re(), focus.as_ref(), advance);
266        if restart && opt_id.is_none() {
267            opt_id = self.nav_next(widget.re(), None, advance);
268        }
269
270        self.set_nav_focus_impl(widget, opt_id, source);
271    }
272}