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/BsGUILayoutY.h"
4#include "GUI/BsGUIElement.h"
5#include "GUI/BsGUISpace.h"
6#include "Math/BsMath.h"
7#include "Math/BsVector2I.h"
8
9namespace bs
10{
11 GUILayoutY::GUILayoutY(const GUIDimensions& dimensions)
12 : GUILayout(dimensions)
13 { }
14
15 LayoutSizeRange GUILayoutY::_calculateLayoutSizeRange() const
16 {
17 Vector2I optimalSize;
18 Vector2I minSize;
19
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.x = sizeRange.min.x = 0;
29
30 UINT32 paddingX = child->_getPadding().left + child->_getPadding().right;
31 UINT32 paddingY = child->_getPadding().top + child->_getPadding().bottom;
32
33 optimalSize.y += sizeRange.optimal.y + paddingY;
34 optimalSize.x = std::max((UINT32)optimalSize.x, sizeRange.optimal.x + paddingX);
35
36 minSize.y += sizeRange.min.y + paddingY;
37 minSize.x = std::max((UINT32)minSize.x, sizeRange.min.x + paddingX);
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 GUILayoutY::_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.x = 0;
69 childSizeRange.min.x = 0;
70 }
71
72 UINT32 paddingX = child->_getPadding().left + child->_getPadding().right;
73 UINT32 paddingY = child->_getPadding().top + child->_getPadding().bottom;
74
75 optimalSize.y += childSizeRange.optimal.y + paddingY;
76 optimalSize.x = std::max((UINT32)optimalSize.x, childSizeRange.optimal.x + paddingX);
77
78 minSize.y += childSizeRange.min.y + paddingY;
79 minSize.x = std::max((UINT32)minSize.x, childSizeRange.min.x + paddingX);
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 GUILayoutY::_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.y;
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].height = sizeRanges[childIdx].optimal.y;
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.fixedHeight())
139 processedElements[childIdx] = true;
140 else
141 {
142 if (elementAreas[childIdx].height > 0)
143 {
144 numNonClampedElements++;
145 totalNonClampedSize += elementAreas[childIdx].height;
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.height > totalOptimalSize)
157 {
158 UINT32 extraSize = layoutArea.height - 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 extraHeight = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize);
176 UINT32 elementHeight = elementAreas[childIdx].height + extraHeight;
177
178 // Clamp if needed
179 if (child->_getType() == GUIElementBase::Type::FlexibleSpace)
180 {
181 processedElements[childIdx] = true;
182 numNonClampedElements--;
183 elementAreas[childIdx].height = elementHeight;
184
185 remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraHeight);
186 }
187
188 childIdx++;
189 }
190
191 totalOptimalSize = layoutArea.height;
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].height;
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.height)
215 {
216 UINT32 extraSize = totalOptimalSize - layoutArea.height;
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 extraHeight = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize);
237 UINT32 elementHeight = (UINT32)std::max(0, (INT32)elementAreas[childIdx].height - (INT32)extraHeight);
238
239 // Clamp if needed
240 switch (child->_getType())
241 {
242 case GUIElementBase::Type::FlexibleSpace:
243 elementAreas[childIdx].height = 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 (elementHeight == 0)
254 {
255 processedElements[childIdx] = true;
256 numNonClampedElements--;
257 }
258 else if (childSizeRange.min.y > 0 && (INT32)elementHeight < childSizeRange.min.y)
259 {
260 elementHeight = childSizeRange.min.y;
261
262 processedElements[childIdx] = true;
263 numNonClampedElements--;
264 }
265
266 extraHeight = elementAreas[childIdx].height - elementHeight;
267 elementAreas[childIdx].height = elementHeight;
268 remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraHeight);
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.height - 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 extraHeight = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize);
301 UINT32 elementHeight = elementAreas[childIdx].height + extraHeight;
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 (elementHeight == 0)
317 {
318 processedElements[childIdx] = true;
319 numNonClampedElements--;
320 }
321 else if (childSizeRange.max.y > 0 && (INT32)elementHeight > childSizeRange.max.y)
322 {
323 elementHeight = childSizeRange.max.y;
324
325 processedElements[childIdx] = true;
326 numNonClampedElements--;
327 }
328
329 extraHeight = elementHeight - elementAreas[childIdx].height;
330 elementAreas[childIdx].height = elementHeight;
331 remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraHeight);
332 }
333 break;
334 case GUIElementBase::Type::FixedSpace:
335 break;
336 }
337
338 childIdx++;
339 }
340 }
341 }
342
343 if (elementScaleWeights != nullptr)
344 bs_stack_free(elementScaleWeights);
345
346 if (processedElements != nullptr)
347 bs_stack_free(processedElements);
348
349 // Compute offsets and width
350 UINT32 yOffset = 0;
351 childIdx = 0;
352
353 for (auto& child : mChildren)
354 {
355 UINT32 elemHeight = elementAreas[childIdx].height;
356 yOffset += child->_getPadding().top;
357
358 const LayoutSizeRange& sizeRange = sizeRanges[childIdx];
359 UINT32 elemWidth = (UINT32)sizeRanges[childIdx].optimal.x;
360 const GUIDimensions& dimensions = child->_getDimensions();
361 if (!dimensions.fixedWidth())
362 {
363 elemWidth = layoutArea.width;
364 if (sizeRange.min.x > 0 && elemWidth < (UINT32)sizeRange.min.x)
365 elemWidth = (UINT32)sizeRange.min.x;
366
367 if (sizeRange.max.x > 0 && elemWidth > (UINT32)sizeRange.max.x)
368 elemWidth = (UINT32)sizeRange.max.x;
369 }
370
371 elementAreas[childIdx].width = elemWidth;
372
373 if (child->_getType() == GUIElementBase::Type::Element)
374 {
375 GUIElement* element = static_cast<GUIElement*>(child);
376
377 UINT32 xPadding = element->_getPadding().left + element->_getPadding().right;
378 INT32 xOffset = Math::ceilToInt((INT32)(layoutArea.width - (INT32)(elemWidth + xPadding)) * 0.5f);
379 xOffset = std::max(0, xOffset);
380
381 elementAreas[childIdx].x = layoutArea.x + xOffset;
382 elementAreas[childIdx].y = layoutArea.y + yOffset;
383 }
384 else
385 {
386 elementAreas[childIdx].x = layoutArea.x;
387 elementAreas[childIdx].y = layoutArea.y + yOffset;
388 }
389
390 yOffset += elemHeight + child->_getPadding().bottom;
391 childIdx++;
392 }
393 }
394
395 void GUILayoutY::_updateLayoutInternal(const GUILayoutData& data)
396 {
397 UINT32 numElements = (UINT32)mChildren.size();
398 Rect2I* elementAreas = nullptr;
399
400 if (numElements > 0)
401 elementAreas = bs_stack_new<Rect2I>(numElements);
402
403 _getElementAreas(data.area, elementAreas, numElements, mChildSizeRanges, mSizeRange);
404
405 // Now that we have all the areas, actually assign them
406 UINT32 childIdx = 0;
407
408 GUILayoutData childData = data;
409 for(auto& child : mChildren)
410 {
411 if (child->_isActive())
412 {
413 childData.area = elementAreas[childIdx];
414 childData.clipRect = childData.area;
415 childData.clipRect.clip(data.clipRect);
416
417 child->_setLayoutData(childData);
418 child->_updateLayoutInternal(childData);
419 }
420
421 childIdx++;
422 }
423
424 if (elementAreas != nullptr)
425 bs_stack_free(elementAreas);
426 }
427
428 GUILayoutY* GUILayoutY::create()
429 {
430 return bs_new<GUILayoutY>();
431 }
432
433 GUILayoutY* GUILayoutY::create(const GUIOptions& options)
434 {
435 return bs_new<GUILayoutY>(GUIDimensions::create(options));
436 }
437}