1//************************************ bs::framework - Copyright 2018 Marko Pintera **************************************//
2//*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********//
3#include "GUI/BsGUINavGroup.h"
4#include "GUI/BsGUIElement.h"
5#include "GUI/BsGUIManager.h"
6
7namespace bs
8{
9 SPtr<GUINavGroup> GUINavGroup::create()
10 {
11 return bs_shared_ptr_new<GUINavGroup>();
12 }
13
14 void GUINavGroup::focusFirst()
15 {
16 if(mOrderedElements.empty())
17 return;
18
19 // Find first element with an explicit index, if one exists
20 auto iterStart = mOrderedElements.begin();
21 if(iterStart->first != 0)
22 {
23 iterStart->second->setFocus(true, true);
24 return;
25 }
26
27 // Otherwise look for top-left element without an explicit index
28 focusTopLeft();
29 }
30
31 void GUINavGroup::focusNext(GUIElement* anchor)
32 {
33 // Nothing currently in focus
34 if(!anchor)
35 {
36 focusFirst();
37 return;
38 }
39
40 const INT32 tabIdx = mElements[anchor];
41
42 // Find next element using the explicit index
43 if(tabIdx != 0)
44 {
45 auto iterFind = mOrderedElements.lower_bound(tabIdx);
46 while(iterFind->second != anchor)
47 ++iterFind;
48
49 ++iterFind;
50
51 // Reached the end, wrap around
52 if(iterFind == mOrderedElements.end())
53 return focusFirst();
54
55 // If a next element with an explicit index exists, select it
56 if(iterFind->first != 0)
57 {
58 iterFind->second->setFocus(true, true);
59 return;
60 }
61
62 // Select top-left element with no tab index
63 focusTopLeft();
64 return;
65 }
66
67 // Find next element to focus on
68 {
69 const Rect2I focusedElemBounds = anchor->_getClippedBounds();
70
71 // We look for the element to the right of the current element, within some Y range (a 'row').
72 //// We search by rows in order to make the navigation perceptually nicer. Sometimes elements appear to be
73 //// in the same row, but might be off by a few pixels, in which case the simpler approach would 'jump'
74 //// over an element.
75 constexpr static INT32 ROW_HEIGHT = 5;
76
77 const auto unindexedRange = mOrderedElements.equal_range(0);
78 bs_frame_mark();
79 {
80 struct YCompare
81 {
82 bool operator()(const GUIElement* lhs, const GUIElement* rhs) const
83 {
84 const Rect2I boundsLHS = lhs->_getClippedBounds();
85 const Rect2I boundsRHS = rhs->_getClippedBounds();
86
87 if(boundsLHS.y != boundsRHS.y)
88 return boundsLHS.y < boundsRHS.y;
89
90 return lhs < rhs;
91 }
92 };
93
94 // Build a list of relevant elements, ordered by height
95 FrameSet<GUIElement*, YCompare> elements;
96 for (auto iter = unindexedRange.first; iter != unindexedRange.second; ++iter)
97 {
98 GUIElement* element = iter->second;
99 const bool acceptsKeyFocus = element->getOptionFlags().isSet(GUIElementOption::AcceptsKeyFocus);
100 if (!acceptsKeyFocus || !element->_isVisible() || element->_isDisabled())
101 continue;
102
103 const Rect2I elemBounds = element->_getClippedBounds();
104 const bool isFullyClipped = elemBounds.width == 0 || elemBounds.height == 0;
105
106 if (isFullyClipped)
107 continue;
108
109 elements.insert(element);
110 }
111
112 // Find the row the currently selected element is part of
113 auto iterElem = elements.begin();
114 auto iterRowStart = iterElem;
115
116 INT32 firstRowY = 0;
117 INT32 rowY = 0;
118 for(; iterElem != elements.end(); ++iterElem)
119 {
120 GUIElement* element = *iterElem;
121
122 const Rect2I elemBounds = element->_getClippedBounds();
123 if(iterElem == elements.begin())
124 {
125 firstRowY = elemBounds.y;
126 rowY = elemBounds.y;
127 }
128 else
129 {
130 const INT32 yDiff = elemBounds.y - rowY;
131
132 // New row
133 if (yDiff >= ROW_HEIGHT)
134 {
135 iterRowStart = iterElem;
136 rowY = elemBounds.y;
137 }
138 }
139
140 if(element == anchor)
141 break;
142 }
143
144 const bool foundRow = iterElem != elements.end();
145 if(!foundRow)
146 rowY = firstRowY;
147
148 // Try to find the next element in the current row (to the right of the current one)
149 GUIElement* nextElement = nullptr;
150 INT32 nearestX = std::numeric_limits<INT32>::max();
151 iterElem = iterRowStart;
152 for(; iterElem != elements.end(); ++iterElem)
153 {
154 GUIElement* element = *iterElem;
155 if(element == anchor)
156 continue;
157
158 const Rect2I elemBounds = element->_getClippedBounds();
159 const INT32 yDiff = elemBounds.y - rowY;
160
161 // New row
162 if(yDiff >= ROW_HEIGHT)
163 {
164 rowY = elemBounds.y;
165 break;
166 }
167
168 // Note: We're purposely ignoring elements at the same exact position, as the tab focus would then just
169 // ping-pong between the two elements, and we'd have to keep a list of previously visited elements in
170 // order to avoid the issue.
171 if(elemBounds.x > focusedElemBounds.x)
172 {
173 const INT32 xDiff = elemBounds.x - focusedElemBounds.x;
174 if (xDiff < nearestX)
175 {
176 nearestX = xDiff;
177 nextElement = element;
178 }
179 }
180 }
181
182 // If no element in the current row, find the left-most element in the next row
183 if(!nextElement)
184 {
185 nearestX = std::numeric_limits<INT32>::max();
186 for (; iterElem != elements.end(); ++iterElem)
187 {
188 GUIElement* element = *iterElem;
189
190 const Rect2I elemBounds = element->_getClippedBounds();
191 const INT32 yDiff = elemBounds.y - rowY;
192
193 // New row
194 if (yDiff >= ROW_HEIGHT)
195 break;
196
197 if (elemBounds.x < nearestX)
198 {
199 nearestX = elemBounds.x;
200 nextElement = element;
201 }
202 }
203 }
204
205 if (nextElement)
206 {
207 nextElement->setFocus(true, true);
208 return;
209 }
210
211 }
212 bs_frame_clear();
213
214 // No more elements with no tab index. Check elements with positive tab index
215 const auto iterAfterUnindexed = unindexedRange.second;
216 if(iterAfterUnindexed != mOrderedElements.end())
217 {
218 iterAfterUnindexed->second->setFocus(true, true);
219 return;
220 }
221
222 // Reached the end, wrap around
223 focusFirst();
224 }
225 }
226
227 void GUINavGroup::focusTopLeft()
228 {
229 UINT32 lowestDist = std::numeric_limits<UINT32>::max();
230 GUIElement* topLeftElement = nullptr;
231
232 // Grab only elements without an explicit index
233 const auto unindexedRange = mOrderedElements.equal_range(0);
234 for(auto iter = unindexedRange.first; iter != unindexedRange.second; ++iter)
235 {
236 GUIElement* element = iter->second;
237
238 // Ignore elements that are hidden, disabled or just don't accept input focus
239 const bool acceptsKeyFocus = element->getOptionFlags().isSet(GUIElementOption::AcceptsKeyFocus);
240 if(!acceptsKeyFocus || !element->_isVisible() || element->_isDisabled())
241 continue;
242
243 // Ignore elements that have been fully clipped
244 const Rect2I elemBounds = element->_getClippedBounds();
245 if(elemBounds.width == 0 || elemBounds.height == 0)
246 continue;
247
248 Vector2I elementPos(elemBounds.x, elemBounds.y);
249
250 const UINT32 dist = elementPos.squaredLength();
251 if (dist < lowestDist)
252 {
253 lowestDist = dist;
254 topLeftElement = element;
255 }
256 }
257
258 if (topLeftElement)
259 topLeftElement->setFocus(true, true);
260 }
261
262 void GUINavGroup::registerElement(GUIElement* element, INT32 tabIdx)
263 {
264 mElements[element] = tabIdx;
265 mOrderedElements.insert(std::make_pair(tabIdx, element));
266 }
267
268 void GUINavGroup::setIndex(GUIElement* element, INT32 tabIdx)
269 {
270 const auto iterFind = mElements.find(element);
271 assert(iterFind != mElements.end());
272
273 const INT32 existingTabIdx = iterFind->second;
274 mElements[element] = tabIdx;
275
276 const auto iterPair = mOrderedElements.equal_range(existingTabIdx);
277 for(auto iter = iterPair.first; iter != iterPair.second; ++iter)
278 {
279 if(iter->second == element)
280 {
281 mOrderedElements.erase(iter);
282 break;
283 }
284 }
285
286 mOrderedElements.insert(std::make_pair(tabIdx, element));
287 }
288
289 void GUINavGroup::unregisterElement(GUIElement* element)
290 {
291 const auto iterFind = mElements.find(element);
292 if(iterFind == mElements.end())
293 return;
294
295 const INT32 existingTabIdx = iterFind->second;
296 const auto iterPair = mOrderedElements.equal_range(existingTabIdx);
297 for(auto iter = iterPair.first; iter != iterPair.second; ++iter)
298 {
299 if(iter->second == element)
300 {
301 mOrderedElements.erase(iter);
302 break;
303 }
304 }
305
306 mElements.erase(element);
307 }
308}
309