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, MessageDirection, UiMessage},
32 scroll_viewer::{ScrollViewer, ScrollViewerMessage},
33 widget::{Widget, WidgetBuilder, WidgetMessage},
34 BuildContext, Control, UiNode, UserInterface,
35};
36
37use fyrox_graph::SceneGraph;
38use std::ops::{Deref, DerefMut};
39
40/// A widget, that handles keyboard navigation on its descendant widgets using Tab key. It should
41/// be used as a root widget for an hierarchy, that should support Tab key navigation:
42///
43/// ```rust
44/// use fyrox_ui::{
45/// button::ButtonBuilder, navigation::NavigationLayerBuilder, stack_panel::StackPanelBuilder,
46/// text::TextBuilder, widget::WidgetBuilder, BuildContext,
47/// };
48///
49/// fn create_navigation_layer(ctx: &mut BuildContext) {
50/// NavigationLayerBuilder::new(
51/// WidgetBuilder::new().with_child(
52/// StackPanelBuilder::new(
53/// WidgetBuilder::new()
54/// .with_child(
55/// // This widget won't participate in Tab key navigation.
56/// TextBuilder::new(WidgetBuilder::new())
57/// .with_text("Do something?")
58/// .build(ctx),
59/// )
60/// // The keyboard focus for the following two buttons can be cycled using Tab/Shift+Tab.
61/// .with_child(
62/// ButtonBuilder::new(WidgetBuilder::new().with_tab_index(Some(0)))
63/// .with_text("OK")
64/// .build(ctx),
65/// )
66/// .with_child(
67/// ButtonBuilder::new(WidgetBuilder::new().with_tab_index(Some(1)))
68/// .with_text("Cancel")
69/// .build(ctx),
70/// ),
71/// )
72/// .build(ctx),
73/// ),
74/// )
75/// .build(ctx);
76/// }
77/// ```
78///
79/// This example shows how to create a simple confirmation dialog, that allows a user to use Tab key
80/// to cycle from one button to another. A focused button then can be "clicked" using Enter key.
81#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
82#[type_uuid(id = "135d347b-5019-4743-906c-6df5c295a3be")]
83#[reflect(derived_type = "UiNode")]
84pub struct NavigationLayer {
85 /// Base widget of the navigation layer.
86 pub widget: Widget,
87 /// A flag, that defines whether the navigation layer should search for a [`crate::scroll_viewer::ScrollViewer`]
88 /// parent widget and send [`crate::scroll_viewer::ScrollViewerMessage::BringIntoView`] message
89 /// to a newly focused widget.
90 pub bring_into_view: InheritableVariable<bool>,
91}
92
93crate::define_widget_deref!(NavigationLayer);
94
95#[derive(Debug)]
96struct OrderedHandle {
97 tab_index: usize,
98 handle: Handle<UiNode>,
99}
100
101impl Control for NavigationLayer {
102 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
103 self.widget.handle_routed_message(ui, message);
104
105 if let Some(WidgetMessage::KeyDown(KeyCode::Tab)) = message.data() {
106 // Collect all descendant widgets, that supports Tab navigation.
107 let mut tab_list = Vec::new();
108 for &child in self.children() {
109 for (descendant_handle, descendant_ref) in ui.traverse_iter(child) {
110 if !*descendant_ref.tab_stop && descendant_ref.is_globally_visible() {
111 if let Some(tab_index) = *descendant_ref.tab_index {
112 tab_list.push(OrderedHandle {
113 tab_index,
114 handle: descendant_handle,
115 });
116 }
117 }
118 }
119 }
120
121 if !tab_list.is_empty() {
122 tab_list.sort_by_key(|entry| entry.tab_index);
123
124 let focused_index = tab_list
125 .iter()
126 .position(|entry| entry.handle == ui.keyboard_focus_node)
127 .unwrap_or_default();
128
129 let next_focused_node_index = if ui.keyboard_modifiers.shift {
130 let count = tab_list.len() as isize;
131 let mut prev = (focused_index as isize).saturating_sub(1);
132 if prev < 0 {
133 prev += count;
134 }
135 (prev % count) as usize
136 } else {
137 focused_index.saturating_add(1) % tab_list.len()
138 };
139
140 if let Some(entry) = tab_list.get(next_focused_node_index) {
141 ui.send_message(WidgetMessage::focus(
142 entry.handle,
143 MessageDirection::ToWidget,
144 ));
145
146 if *self.bring_into_view {
147 // Find a parent scroll viewer.
148 if let Some((scroll_viewer, _)) =
149 ui.find_component_up::<ScrollViewer>(entry.handle)
150 {
151 ui.send_message(ScrollViewerMessage::bring_into_view(
152 scroll_viewer,
153 MessageDirection::ToWidget,
154 entry.handle,
155 ));
156 }
157 }
158 }
159 }
160 }
161 }
162}
163
164/// Navigation layer builder creates new [`NavigationLayer`] widget instances and adds them to the user interface.
165pub struct NavigationLayerBuilder {
166 widget_builder: WidgetBuilder,
167 bring_into_view: bool,
168}
169
170impl NavigationLayerBuilder {
171 /// Creates new builder instance.
172 pub fn new(widget_builder: WidgetBuilder) -> Self {
173 Self {
174 widget_builder,
175 bring_into_view: true,
176 }
177 }
178
179 /// Finishes navigation layer widget building and adds the instance to the user interface and
180 /// returns its handle.
181 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
182 let navigation_layer = NavigationLayer {
183 widget: self.widget_builder.build(ctx),
184 bring_into_view: self.bring_into_view.into(),
185 };
186 ctx.add_node(UiNode::new(navigation_layer))
187 }
188}
189
190#[cfg(test)]
191mod test {
192 use crate::navigation::NavigationLayerBuilder;
193 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
194
195 #[test]
196 fn test_deletion() {
197 test_widget_deletion(|ctx| NavigationLayerBuilder::new(WidgetBuilder::new()).build(ctx));
198 }
199}