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/BsGUILayoutX.h"
4#include "GUI/BsGUIElement.h"
5#include "GUI/BsGUISpace.h"
6#include "Math/BsMath.h"
7#include "Math/BsVector2I.h"
8#include "Profiling/BsProfilerCPU.h"
9
10namespace bs
11{
12 GUILayoutX::GUILayoutX(const GUIDimensions& dimensions)
13 : GUILayout(dimensions)
14 { }
15
16 LayoutSizeRange GUILayoutX::_calculateLayoutSizeRange() const
17 {
18 Vector2I optimalSize;
19 Vector2I minSize;
20 for (auto& child : mChildren)
21 {
22 if (!child->_isActive())
23 continue;
24
25 LayoutSizeRange sizeRange = child->_calculateLayoutSizeRange();
26
27 if (child->_getType() == GUIElementBase::Type::FixedSpace)
28 sizeRange.optimal.y = sizeRange.min.y = 0;
29
30 UINT32 paddingX = child->_getPadding().left + child->_getPadding().right;
31 UINT32 paddingY = child->_getPadding().top + child->_getPadding().bottom;
32
33 optimalSize.x += sizeRange.optimal.x + paddingX;
34 optimalSize.y = std::max((UINT32)optimalSize.y, sizeRange.optimal.y + paddingY);
35
36 minSize.x += sizeRange.min.x + paddingX;
37 minSize.y = std::max((UINT32)minSize.y, sizeRange.min.y + paddingY);
38 }
39
40 LayoutSizeRange sizeRange = _getDimensions().calculateSizeRange(optimalSize);
41 sizeRange.min.x = std::max(sizeRange.min.x, minSize.x);
42 sizeRange.min.y = std::max(sizeRange.min.y, minSize.y);
43
44 return sizeRange;
45 }
46
47 void GUILayoutX::_updateOptimalLayoutSizes()
48 {
49 // Update all children first, otherwise we can't determine our own optimal size
50 GUIElementBase::_updateOptimalLayoutSizes();
51
52 if(mChildren.size() != mChildSizeRanges.size())
53 mChildSizeRanges.resize(mChildren.size());
54
55 Vector2I optimalSize;
56 Vector2I minSize;
57
58 UINT32 childIdx = 0;
59 for(auto& child : mChildren)
60 {
61 LayoutSizeRange& childSizeRange = mChildSizeRanges[childIdx];
62
63 if (child->_isActive())
64 {
65 childSizeRange = child->_getLayoutSizeRange();
66 if (child->_getType() == GUIElementBase::Type::FixedSpace)
67 {
68 childSizeRange.optimal.y = 0;
69 childSizeRange.min.y = 0;
70 }
71
72 UINT32 paddingX = child->_getPadding().left + child->_getPadding().right;
73 UINT32 paddingY = child->_getPadding().top + child->_getPadding().bottom;
74
75 optimalSize.x += childSizeRange.optimal.x + paddingX;
76 optimalSize.y = std::max((UINT32)optimalSize.y, childSizeRange.optimal.y + paddingY);
77
78 minSize.x += childSizeRange.min.x + paddingX;
79 minSize.y = std::max((UINT32)minSize.y, childSizeRange.min.y + paddingY);
80 }
81 else
82 childSizeRange = LayoutSizeRange();
83
84 childIdx++;
85 }
86
87 mSizeRange = _getDimensions().calculateSizeRange(optimalSize);
88 mSizeRange.min.x = std::max(mSizeRange.min.x, minSize.x);
89 mSizeRange.min.y = std::max(mSizeRange.min.y, minSize.y);
90 }
91
92 void GUILayoutX::_getElementAreas(const Rect2I& layoutArea, Rect2I* elementAreas, UINT32 numElements,
93 const Vector<LayoutSizeRange>& sizeRanges, const LayoutSizeRange& mySizeRange) const
94 {
95 assert(mChildren.size() == numElements);
96
97 UINT32 totalOptimalSize = mySizeRange.optimal.x;
98 UINT32 totalNonClampedSize = 0;
99 UINT32 numNonClampedElements = 0;
100 UINT32 numFlexibleSpaces = 0;
101
102 bool* processedElements = nullptr;
103 float* elementScaleWeights = nullptr;
104
105 if (mChildren.size() > 0)
106 {
107 processedElements = bs_stack_alloc<bool>((UINT32)mChildren.size());
108 memset(processedElements, 0, mChildren.size() * sizeof(bool));
109
110 elementScaleWeights = bs_stack_alloc<float>((UINT32)mChildren.size());
111 memset(elementScaleWeights, 0, mChildren.size() * sizeof(float));
112 }
113
114 // Set initial sizes, count number of children per type and mark fixed elements as already processed
115 UINT32 childIdx = 0;
116 for (auto& child : mChildren)
117 {
118 elementAreas[childIdx].width = sizeRanges[childIdx].optimal.x;
119
120 if (child->_getType() == GUIElementBase::Type::FixedSpace)
121 {
122 processedElements[childIdx] = true;
123 }
124 else if (child->_getType() == GUIElementBase::Type::FlexibleSpace)
125 {
126 if (child->_isActive())
127 {
128 numFlexibleSpaces++;
129 numNonClampedElements++;
130 }
131 else
132 processedElements[childIdx] = true;
133 }
134 else
135 {
136 const GUIDimensions& dimensions = child->_getDimensions();
137
138 if (dimensions.fixedWidth())
139 processedElements[childIdx] = true;
140 else
141 {
142 if (elementAreas[childIdx].width > 0)
143 {
144 numNonClampedElements++;
145 totalNonClampedSize += elementAreas[childIdx].width;
146 }
147 else
148 processedElements[childIdx] = true;
149 }
150 }
151
152 childIdx++;
153 }
154
155 // If there is some room left, calculate flexible space sizes (since they will fill up all that extra room)
156 if ((UINT32)layoutArea.width > totalOptimalSize)
157 {
158 UINT32 extraSize = layoutArea.width - totalOptimalSize;
159 UINT32 remainingSize = extraSize;
160
161 // Flexible spaces always expand to fill up all unused space
162 if (numFlexibleSpaces > 0)
163 {
164 float avgSize = remainingSize / (float)numFlexibleSpaces;
165
166 childIdx = 0;
167 for (auto& child : mChildren)
168 {
169 if (processedElements[childIdx])
170 {
171 childIdx++;
172 continue;
173 }
174
175 UINT32 extraWidth = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize);
176 UINT32 elementWidth = elementAreas[childIdx].width + extraWidth;
177
178 // Clamp if needed
179 if (child->_getType() == GUIElementBase::Type::FlexibleSpace)
180 {
181 processedElements[childIdx] = true;
182 numNonClampedElements--;
183 elementAreas[childIdx].width = elementWidth;
184
185 remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraWidth);
186 }
187
188 childIdx++;
189 }
190
191 totalOptimalSize = layoutArea.width;
192 }
193 }
194
195 // Determine weight scale for every element. When scaling elements up/down they will be scaled based on this weight.
196 // Weight is to ensure all elements are scaled fairly, so elements that are large will get effected more than smaller elements.
197 childIdx = 0;
198 float invOptimalSize = 1.0f / totalNonClampedSize;
199 UINT32 childCount = (UINT32)mChildren.size();
200 for (UINT32 i = 0; i < childCount; i++)
201 {
202 if (processedElements[childIdx])
203 {
204 childIdx++;
205 continue;
206 }
207
208 elementScaleWeights[childIdx] = invOptimalSize * elementAreas[childIdx].width;
209
210 childIdx++;
211 }
212
213 // Our optimal size is larger than maximum allowed, so we need to reduce size of some elements
214 if (totalOptimalSize > (UINT32)layoutArea.width)
215 {
216 UINT32 extraSize = totalOptimalSize - layoutArea.width;
217 UINT32 remainingSize = extraSize;
218
219 // Iterate until we reduce everything so it fits, while maintaining
220 // equal average sizes using the weights we calculated earlier
221 while (remainingSize > 0 && numNonClampedElements > 0)
222 {
223 UINT32 totalRemainingSize = remainingSize;
224
225 childIdx = 0;
226 for (auto& child : mChildren)
227 {
228 if (processedElements[childIdx])
229 {
230 childIdx++;
231 continue;
232 }
233
234 float avgSize = totalRemainingSize * elementScaleWeights[childIdx];
235
236 UINT32 extraWidth = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize);
237 UINT32 elementWidth = (UINT32)std::max(0, (INT32)elementAreas[childIdx].width - (INT32)extraWidth);
238
239 // Clamp if needed
240 switch (child->_getType())
241 {
242 case GUIElementBase::Type::FlexibleSpace:
243 elementAreas[childIdx].width = 0;
244 processedElements[childIdx] = true;
245 numNonClampedElements--;
246 break;
247 case GUIElementBase::Type::Element:
248 case GUIElementBase::Type::Layout:
249 case GUIElementBase::Type::Panel:
250 {
251 const LayoutSizeRange& childSizeRange = sizeRanges[childIdx];
252
253 if (elementWidth == 0)
254 {
255 processedElements[childIdx] = true;
256 numNonClampedElements--;
257 }
258 else if (childSizeRange.min.x > 0 && (INT32)elementWidth < childSizeRange.min.x)
259 {
260 elementWidth = childSizeRange.min.x;
261
262 processedElements[childIdx] = true;
263 numNonClampedElements--;
264 }
265
266 extraWidth = elementAreas[childIdx].width - elementWidth;
267 elementAreas[childIdx].width = elementWidth;
268 remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraWidth);
269 }
270 break;
271 case GUIElementBase::Type::FixedSpace:
272 break;
273 }
274
275 childIdx++;
276 }
277 }
278 }
279 else // We are smaller than the allowed maximum, so try to expand some elements
280 {
281 UINT32 extraSize = layoutArea.width - totalOptimalSize;
282 UINT32 remainingSize = extraSize;
283
284 // Iterate until we reduce everything so it fits, while maintaining
285 // equal average sizes using the weights we calculated earlier
286 while (remainingSize > 0 && numNonClampedElements > 0)
287 {
288 UINT32 totalRemainingSize = remainingSize;
289
290 childIdx = 0;
291 for (auto& child : mChildren)
292 {
293 if (processedElements[childIdx])
294 {
295 childIdx++;
296 continue;
297 }
298
299 float avgSize = totalRemainingSize * elementScaleWeights[childIdx];
300 UINT32 extraWidth = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize);
301 UINT32 elementWidth = elementAreas[childIdx].width + extraWidth;
302
303 // Clamp if needed
304 switch (child->_getType())
305 {
306 case GUIElementBase::Type::FlexibleSpace:
307 processedElements[childIdx] = true;
308 numNonClampedElements--;
309 break;
310 case GUIElementBase::Type::Element:
311 case GUIElementBase::Type::Layout:
312 case GUIElementBase::Type::Panel:
313 {
314 const LayoutSizeRange& childSizeRange = sizeRanges[childIdx];
315
316 if (elementWidth == 0)
317 {
318 processedElements[childIdx] = true;
319 numNonClampedElements--;
320 }
321 else if (childSizeRange.max.x > 0 && (INT32)elementWidth > childSizeRange.max.x)
322 {
323 elementWidth = childSizeRange.max.x;
324
325 processedElements[childIdx] = true;
326 numNonClampedElements--;
327 }
328
329 extraWidth = elementWidth - elementAreas[childIdx].width;
330 elementAreas[childIdx].width = elementWidth;
331 remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraWidth);
332 }
333 break;
334 case GUIElementBase::Type::FixedSpace:
335 break;
336 }
337
338 childIdx++;
339 }
340 }
341 }
342
343 // Compute offsets and height
344 UINT32 xOffset = 0;
345 childIdx = 0;
346
347 for (auto& child : mChildren)
348 {
349 UINT32 elemWidth = elementAreas[childIdx].width;
350 xOffset += child->_getPadding().left;
351
352 const LayoutSizeRange& sizeRange = sizeRanges[childIdx];
353 UINT32 elemHeight = (UINT32)sizeRange.optimal.y;
354 const GUIDimensions& dimensions = child->_getDimensions();
355 if (!dimensions.fixedHeight())
356 {
357 elemHeight = layoutArea.height;
358 if (sizeRange.min.y > 0 && elemHeight < (UINT32)sizeRange.min.y)
359 elemHeight = (UINT32)sizeRange.min.y;
360
361 if (sizeRange.max.y > 0 && elemHeight > (UINT32)sizeRange.max.y)
362 elemHeight = (UINT32)sizeRange.max.y;
363 }
364 elementAreas[childIdx].height = elemHeight;
365
366 if (child->_getType() == GUIElementBase::Type::Element)
367 {
368 GUIElement* element = static_cast<GUIElement*>(child);
369
370 UINT32 yPadding = element->_getPadding().top + element->_getPadding().bottom;
371 INT32 yOffset = Math::ceilToInt(((INT32)layoutArea.height - (INT32)(elemHeight + yPadding)) * 0.5f);
372 yOffset = std::max(0, yOffset);
373
374 elementAreas[childIdx].x = layoutArea.x + xOffset;
375 elementAreas[childIdx].y = layoutArea.y + yOffset;
376 }
377 else
378 {
379 elementAreas[childIdx].x = layoutArea.x + xOffset;
380 elementAreas[childIdx].y = layoutArea.y;
381 }
382
383 xOffset += elemWidth + child->_getPadding().right;
384 childIdx++;
385 }
386
387 if (elementScaleWeights != nullptr)
388 bs_stack_free(elementScaleWeights);
389
390 if (processedElements != nullptr)
391 bs_stack_free(processedElements);
392 }
393
394 void GUILayoutX::_updateLayoutInternal(const GUILayoutData& data)
395 {
396 UINT32 numElements = (UINT32)mChildren.size();
397 Rect2I* elementAreas = nullptr;
398
399 if (numElements > 0)
400 elementAreas = bs_stack_new<Rect2I>(numElements);
401
402 _getElementAreas(data.area, elementAreas, numElements, mChildSizeRanges, mSizeRange);
403
404 // Now that we have all the areas, actually assign them
405 UINT32 childIdx = 0;
406
407 GUILayoutData childData = data;
408 for(auto& child : mChildren)
409 {
410 if (child->_isActive())
411 {
412 childData.area = elementAreas[childIdx];
413 childData.clipRect = childData.area;
414 childData.clipRect.clip(data.clipRect);
415
416 child->_setLayoutData(childData);
417 child->_updateLayoutInternal(childData);
418 }
419
420 childIdx++;
421 }
422
423 if(elementAreas != nullptr)
424 bs_stack_free(elementAreas);
425 }
426
427 GUILayoutX* GUILayoutX::create()
428 {
429 return bs_new<GUILayoutX>();
430 }
431
432 GUILayoutX* GUILayoutX::create(const GUIOptions& options)
433 {
434 return bs_new<GUILayoutX>(GUIDimensions::create(options));
435 }
436}