Skip to main content

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::{ConfigCx, EventCx, EventState};
9use crate::event::{Event, FocusSource};
10use crate::{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
44#[derive(Default)]
45pub(super) struct NavFocus {
46    focus: Option<Id>,
47    pub(super) fallback: Option<Id>,
48    pending_focus: PendingNavFocus,
49}
50
51impl NavFocus {
52    #[inline]
53    pub(super) fn has_pending_changes(&self) -> bool {
54        !matches!(self.pending_focus, PendingNavFocus::None)
55    }
56}
57
58impl EventState {
59    /// Get whether this widget has navigation focus
60    #[inline]
61    pub fn has_nav_focus(&self, w_id: &Id) -> bool {
62        *w_id == self.nav.focus
63    }
64
65    /// Get the current navigation focus, if any
66    ///
67    /// This is the widget selected by navigating the UI with the Tab key.
68    ///
69    /// Note: changing navigation focus (e.g. via [`EventCx::clear_nav_focus`],
70    /// [`EventCx::request_nav_focus`] or [`EventCx::next_nav_focus`]) does not
71    /// immediately affect the result of this method.
72    #[inline]
73    pub fn nav_focus(&self) -> Option<&Id> {
74        self.nav.focus.as_ref()
75    }
76
77    pub(super) fn clear_nav_focus_on(&mut self, target: &Id) {
78        if let Some(id) = self.nav.focus.as_ref()
79            && target.is_ancestor_of(id)
80        {
81            if matches!(&self.nav.pending_focus, PendingNavFocus::Set { target, .. } if target.as_ref() == Some(id))
82            {
83                self.nav.pending_focus = PendingNavFocus::None;
84            }
85
86            if matches!(self.nav.pending_focus, PendingNavFocus::None) {
87                self.nav.pending_focus = PendingNavFocus::Set {
88                    target: None,
89                    source: FocusSource::Synthetic,
90                };
91            }
92        }
93    }
94
95    /// Set navigation focus directly
96    ///
97    /// If `id` already has navigation focus or navigation focus is disabled
98    /// globally then nothing happens, otherwise widget `id` should receive
99    /// [`Event::NavFocus`].
100    ///
101    /// Normally, [`Tile::navigable`] will return true for widget `id` but this
102    /// is not checked or required. For example, a `ScrollLabel` can receive
103    /// focus on text selection with the mouse.
104    pub(crate) fn set_nav_focus(&mut self, id: Id, source: FocusSource) {
105        self.nav.pending_focus = PendingNavFocus::Set {
106            target: Some(id),
107            source,
108        };
109    }
110}
111
112impl<'a> ConfigCx<'a> {
113    /// Sets the fallback recipient of [`Event::Command`]
114    ///
115    /// Where a key-press translates to a [`Command`], this is first sent to
116    /// widgets with applicable key, selection and/or navigation focus as an
117    /// [`Event::Command`]. If this event goes unhandled and a fallback
118    /// recipient is set using this method, then this fallback recipient will
119    /// be sent the same event.
120    ///
121    /// There may be one fallback recipient per window; do not use an [`Id`]
122    /// from another window. If this method is called multiple times, the last
123    /// such call succeeds.
124    pub fn register_nav_fallback(&mut self, id: Id) {
125        if self.nav.fallback.is_none() {
126            log::debug!(target: "kas_core::event","register_nav_fallback: id={id}");
127            self.nav.fallback = Some(id);
128        }
129    }
130}
131
132impl<'a> EventCx<'a> {
133    /// Clear navigation focus
134    pub fn clear_nav_focus(&mut self) {
135        self.nav.pending_focus = PendingNavFocus::Set {
136            target: None,
137            source: FocusSource::Synthetic,
138        };
139    }
140
141    /// Request navigation focus directly
142    ///
143    /// If `id` already has navigation focus or navigation focus is disabled
144    /// globally then nothing happens. If widget `id` supports
145    /// [navigation focus](Tile::navigable), then it should receive
146    /// [`Event::NavFocus`]; if not then the first supporting ancestor will
147    /// receive focus.
148    pub fn request_nav_focus(&mut self, id: Id, source: FocusSource) {
149        self.nav.pending_focus = PendingNavFocus::Next {
150            target: Some(id),
151            advance: NavAdvance::None,
152            source,
153        };
154    }
155
156    /// Advance the navigation focus
157    ///
158    /// If `target == Some(id)`, this looks for the next widget from `id`
159    /// (inclusive) which is [navigable](Tile::navigable). Otherwise where
160    /// some widget `id` has [`nav_focus`](EventState::nav_focus) this looks for the
161    /// next navigable widget *excluding* `id`. If no reference is available,
162    /// this instead looks for the first navigable widget.
163    ///
164    /// If `reverse`, instead search for the previous or last navigable widget.
165    pub fn next_nav_focus(
166        &mut self,
167        target: impl Into<Option<Id>>,
168        reverse: bool,
169        source: FocusSource,
170    ) {
171        let target = target.into();
172        let advance = match reverse {
173            false => NavAdvance::Forward(target.is_some()),
174            true => NavAdvance::Reverse(target.is_some()),
175        };
176        self.nav.pending_focus = PendingNavFocus::Next {
177            target,
178            advance,
179            source,
180        };
181    }
182
183    // Call Widget::_nav_next
184    #[inline]
185    pub(super) fn nav_next(
186        &mut self,
187        tile: &dyn Tile,
188        focus: Option<&Id>,
189        advance: NavAdvance,
190    ) -> Option<Id> {
191        log::trace!(target: "kas_core::event", "nav_next: focus={focus:?}, advance={advance:?}");
192
193        tile._nav_next(self, focus, advance)
194    }
195
196    pub(super) fn handle_pending_nav_focus(&mut self, widget: Node<'_>) {
197        match std::mem::take(&mut self.nav.pending_focus) {
198            PendingNavFocus::None => (),
199            PendingNavFocus::Set { target, source } => {
200                self.set_nav_focus_impl(widget, target, source)
201            }
202            PendingNavFocus::Next {
203                target,
204                advance,
205                source,
206            } => self.next_nav_focus_impl(widget, target, advance, source),
207        }
208    }
209
210    /// Set navigation focus immediately
211    pub(super) fn set_nav_focus_impl(
212        &mut self,
213        mut widget: Node,
214        target: Option<Id>,
215        source: FocusSource,
216    ) {
217        if target == self.nav.focus || !self.config.nav_focus {
218            return;
219        }
220
221        if let Some(id) = self.input.sel_focus().cloned()
222            && id != target
223        {
224            self.input.clear_sel_socus_on(&id);
225        }
226
227        if let Some(old) = self.nav.focus.take() {
228            self.redraw();
229            self.send_event(widget.re(), old, Event::LostNavFocus);
230        }
231
232        self.nav.focus = target.clone();
233        log::debug!(target: "kas_core::event", "nav_focus = {target:?}");
234        if let Some(id) = target {
235            self.redraw();
236            self.send_event(widget, id, Event::NavFocus(source));
237        }
238    }
239
240    /// Advance the keyboard navigation focus immediately
241    pub(super) fn next_nav_focus_impl(
242        &mut self,
243        mut widget: Node,
244        target: Option<Id>,
245        advance: NavAdvance,
246        source: FocusSource,
247    ) {
248        if !self.config.nav_focus || (target.is_some() && target == self.nav.focus) {
249            return;
250        }
251
252        if let Some(id) = self
253            .popups
254            .last()
255            .filter(|popup| popup.is_sized)
256            .map(|state| state.desc.id.clone())
257        {
258            if id.is_ancestor_of(widget.id_ref()) {
259                // do nothing
260            } else if let Some(r) = widget.find_node(&id, |node| {
261                self.next_nav_focus_impl(node, target, advance, source)
262            }) {
263                return r;
264            } else {
265                log::warn!(
266                    target: "kas_core::event",
267                    "next_nav_focus: have open pop-up which is not a child of widget",
268                );
269                return;
270            }
271        }
272
273        let focus = target.or_else(|| self.nav.focus.clone());
274
275        // Whether to restart from the beginning on failure
276        let restart = focus.is_some();
277
278        let mut opt_id = self.nav_next(widget.as_tile(), focus.as_ref(), advance);
279        if restart && opt_id.is_none() {
280            opt_id = self.nav_next(widget.as_tile(), None, advance);
281        }
282
283        self.set_nav_focus_impl(widget, opt_id, source);
284    }
285}