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 | |
7 | namespace 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 | |