Skip to main content

fyrox_ui/
navigation.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! A widget, that handles keyboard navigation on its descendant widgets using Tab key. See [`NavigationLayer`]
22//! docs for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::{
27    core::{
28        pool::Handle, reflect::prelude::*, type_traits::prelude::*, variable::InheritableVariable,
29        visitor::prelude::*,
30    },
31    message::{KeyCode, UiMessage},
32    scroll_viewer::{ScrollViewer, ScrollViewerMessage},
33    widget::{Widget, WidgetBuilder, WidgetMessage},
34    BuildContext, Control, UiNode, UserInterface,
35};
36
37use fyrox_graph::SceneGraph;
38
39/// A widget, that handles keyboard navigation on its descendant widgets using Tab key. It should
40/// be used as a root widget for a hierarchy, that should support Tab key navigation:
41///
42/// ```rust
43/// use fyrox_ui::{
44///     button::ButtonBuilder, navigation::NavigationLayerBuilder, stack_panel::StackPanelBuilder,
45///     text::TextBuilder, widget::WidgetBuilder, BuildContext,
46/// };
47///
48/// fn create_navigation_layer(ctx: &mut BuildContext) {
49///     NavigationLayerBuilder::new(
50///         WidgetBuilder::new().with_child(
51///             StackPanelBuilder::new(
52///                 WidgetBuilder::new()
53///                     .with_child(
54///                         // This widget won't participate in Tab key navigation.
55///                         TextBuilder::new(WidgetBuilder::new())
56///                             .with_text("Do something?")
57///                             .build(ctx),
58///                     )
59///                     // The keyboard focus for the following two buttons can be cycled using Tab/Shift+Tab.
60///                     .with_child(
61///                         ButtonBuilder::new(WidgetBuilder::new().with_tab_index(Some(0)))
62///                             .with_text("OK")
63///                             .build(ctx),
64///                     )
65///                     .with_child(
66///                         ButtonBuilder::new(WidgetBuilder::new().with_tab_index(Some(1)))
67///                             .with_text("Cancel")
68///                             .build(ctx),
69///                     ),
70///             )
71///             .build(ctx),
72///         ),
73///     )
74///     .build(ctx);
75/// }
76/// ```
77///
78/// This example shows how to create a simple confirmation dialog, that allows a user to use Tab key
79/// to cycle from one button to another. A focused button then can be "clicked" using Enter key.
80#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
81#[type_uuid(id = "135d347b-5019-4743-906c-6df5c295a3be")]
82#[reflect(derived_type = "UiNode")]
83pub struct NavigationLayer {
84    /// Base widget of the navigation layer.
85    pub widget: Widget,
86    /// A flag, that defines whether the navigation layer should search for a [`crate::scroll_viewer::ScrollViewer`]
87    /// parent widget and send [`crate::scroll_viewer::ScrollViewerMessage::BringIntoView`] message
88    /// to a newly focused widget.
89    pub bring_into_view: InheritableVariable<bool>,
90}
91
92crate::define_widget_deref!(NavigationLayer);
93
94#[derive(Debug)]
95struct OrderedHandle {
96    tab_index: usize,
97    handle: Handle<UiNode>,
98}
99
100impl Control for NavigationLayer {
101    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
102        self.widget.handle_routed_message(ui, message);
103
104        if let Some(WidgetMessage::KeyDown(KeyCode::Tab)) = message.data() {
105            // Collect all descendant widgets, that supports Tab navigation.
106            let mut tab_list = Vec::new();
107            for &child in self.children() {
108                for (descendant_handle, descendant_ref) in ui.traverse_iter(child) {
109                    if !*descendant_ref.tab_stop && descendant_ref.is_globally_visible() {
110                        if let Some(tab_index) = *descendant_ref.tab_index {
111                            tab_list.push(OrderedHandle {
112                                tab_index,
113                                handle: descendant_handle,
114                            });
115                        }
116                    }
117                }
118            }
119
120            if !tab_list.is_empty() {
121                tab_list.sort_by_key(|entry| entry.tab_index);
122
123                let focused_index = tab_list
124                    .iter()
125                    .position(|entry| entry.handle == ui.keyboard_focus_node)
126                    .unwrap_or_default();
127
128                let next_focused_node_index = if ui.keyboard_modifiers.shift {
129                    let count = tab_list.len() as isize;
130                    let mut prev = (focused_index as isize).saturating_sub(1);
131                    if prev < 0 {
132                        prev += count;
133                    }
134                    (prev % count) as usize
135                } else {
136                    focused_index.saturating_add(1) % tab_list.len()
137                };
138
139                if let Some(entry) = tab_list.get(next_focused_node_index) {
140                    ui.send(entry.handle, WidgetMessage::Focus);
141
142                    if *self.bring_into_view {
143                        // Find a parent scroll viewer.
144                        if let Some((scroll_viewer, _)) =
145                            ui.find_component_up::<ScrollViewer>(entry.handle)
146                        {
147                            ui.send(
148                                scroll_viewer,
149                                ScrollViewerMessage::BringIntoView(entry.handle),
150                            );
151                        }
152                    }
153                }
154            }
155        }
156    }
157}
158
159/// Navigation layer builder creates new [`NavigationLayer`] widget instances and adds them to the user interface.
160pub struct NavigationLayerBuilder {
161    widget_builder: WidgetBuilder,
162    bring_into_view: bool,
163}
164
165impl NavigationLayerBuilder {
166    /// Creates new builder instance.
167    pub fn new(widget_builder: WidgetBuilder) -> Self {
168        Self {
169            widget_builder,
170            bring_into_view: true,
171        }
172    }
173
174    /// Finishes navigation layer widget building and adds the instance to the user interface and
175    /// returns its handle.
176    pub fn build(self, ctx: &mut BuildContext) -> Handle<NavigationLayer> {
177        let navigation_layer = NavigationLayer {
178            widget: self.widget_builder.build(ctx),
179            bring_into_view: self.bring_into_view.into(),
180        };
181        ctx.add(navigation_layer)
182    }
183}
184
185#[cfg(test)]
186mod test {
187    use crate::navigation::NavigationLayerBuilder;
188    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
189
190    #[test]
191    fn test_deletion() {
192        test_widget_deletion(|ctx| NavigationLayerBuilder::new(WidgetBuilder::new()).build(ctx));
193    }
194}