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 | |
10 | namespace 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 = 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 = 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 = 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 = 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 = 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 = 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 | } |