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/BsGUIScrollArea.h"
4#include "GUI/BsGUIElementStyle.h"
5#include "GUI/BsGUIDimensions.h"
6#include "GUI/BsGUILayoutY.h"
7#include "GUI/BsGUIScrollBarVert.h"
8#include "GUI/BsGUIScrollBarHorz.h"
9#include "GUI/BsGUIMouseEvent.h"
10#include "GUI/BsGUILayoutUtility.h"
11
12using namespace std::placeholders;
13
14namespace bs
15{
16 const UINT32 GUIScrollArea::ScrollBarWidth = 16;
17 const UINT32 GUIScrollArea::WheelScrollAmount = 50;
18
19 GUIScrollArea::GUIScrollArea(ScrollBarType vertBarType, ScrollBarType horzBarType,
20 const String& scrollBarStyle, const String& scrollAreaStyle, const GUIDimensions& dimensions)
21 : GUIElementContainer(dimensions), mVertBarType(vertBarType), mHorzBarType(horzBarType)
22 , mScrollBarStyle(scrollBarStyle), mVertScroll(nullptr), mHorzScroll(nullptr), mVertOffset(0), mHorzOffset(0)
23 , mRecalculateVertOffset(false), mRecalculateHorzOffset(false)
24 {
25 mContentLayout = GUILayoutY::create();
26 _registerChildElement(mContentLayout);
27
28 mHorzScroll = GUIScrollBarHorz::create(mScrollBarStyle);
29 mVertScroll = GUIScrollBarVert::create(mScrollBarStyle);
30
31 _registerChildElement(mHorzScroll);
32 _registerChildElement(mVertScroll);
33
34 mHorzScroll->onScrollOrResize.connect(std::bind(&GUIScrollArea::horzScrollUpdate, this, _1));
35 mVertScroll->onScrollOrResize.connect(std::bind(&GUIScrollArea::vertScrollUpdate, this, _1));
36 }
37
38 void GUIScrollArea::updateClippedBounds()
39 {
40 mClippedBounds = mLayoutData.area;
41 mClippedBounds.clip(mLayoutData.clipRect);
42 }
43
44 Vector2I GUIScrollArea::_getOptimalSize() const
45 {
46 Vector2I optimalSize = mContentLayout->_getOptimalSize();
47
48 // Provide 10x10 in case underlying layout is empty because
49 // 0 doesn't work well with the layout system
50 optimalSize.x = std::max(10, optimalSize.x);
51 optimalSize.y = std::max(10, optimalSize.y);
52
53 return optimalSize;
54 }
55
56 LayoutSizeRange GUIScrollArea::_calculateLayoutSizeRange() const
57 {
58 // I'm ignoring scroll bars here since if the content layout fits
59 // then they're not needed and the range is valid. And if it doesn't
60 // fit the area will get clipped anyway and including the scroll bars
61 // won't change the size much, but it would complicate this method significantly.
62 if (mContentLayout->_isActive())
63 return mDimensions.calculateSizeRange(_getOptimalSize());
64
65 return mDimensions.calculateSizeRange(Vector2I());
66 }
67
68 LayoutSizeRange GUIScrollArea::_getLayoutSizeRange() const
69 {
70 return mSizeRange;
71 }
72
73 void GUIScrollArea::_updateOptimalLayoutSizes()
74 {
75 // Update all children first, otherwise we can't determine our own optimal size
76 GUIElementBase::_updateOptimalLayoutSizes();
77
78 if (mChildren.size() != mChildSizeRanges.size())
79 mChildSizeRanges.resize(mChildren.size());
80
81 UINT32 childIdx = 0;
82 for (auto& child : mChildren)
83 {
84 if (child->_isActive())
85 mChildSizeRanges[childIdx] = child->_getLayoutSizeRange();
86 else
87 mChildSizeRanges[childIdx] = LayoutSizeRange();
88
89 childIdx++;
90 }
91
92 mSizeRange = mDimensions.calculateSizeRange(_getOptimalSize());
93 }
94
95 void GUIScrollArea::_getElementAreas(const Rect2I& layoutArea, Rect2I* elementAreas, UINT32 numElements,
96 const Vector<LayoutSizeRange>& sizeRanges, const LayoutSizeRange& mySizeRange) const
97 {
98 Vector2I visibleSize, contentSize;
99 _getElementAreas(layoutArea, elementAreas, numElements, sizeRanges, visibleSize, contentSize);
100 }
101
102 void GUIScrollArea::_getElementAreas(const Rect2I& layoutArea, Rect2I* elementAreas, UINT32 numElements,
103 const Vector<LayoutSizeRange>& sizeRanges, Vector2I& visibleSize, Vector2I& contentSize) const
104 {
105 assert(mChildren.size() == numElements && numElements == 3);
106
107 UINT32 layoutIdx = 0;
108 UINT32 horzScrollIdx = 0;
109 UINT32 vertScrollIdx = 0;
110 UINT32 idx = 0;
111 for (auto& child : mChildren)
112 {
113 if (child == mContentLayout)
114 layoutIdx = idx;
115
116 if (child == mHorzScroll)
117 horzScrollIdx = idx;
118
119 if (child == mVertScroll)
120 vertScrollIdx = idx;
121
122 idx++;
123 }
124
125 // Calculate content layout bounds
126
127 //// We want elements to use their optimal height, since scroll area
128 //// technically provides "infinite" space
129 UINT32 optimalContentWidth = layoutArea.width;
130 if (mHorzBarType != ScrollBarType::NeverShow)
131 optimalContentWidth = sizeRanges[layoutIdx].optimal.x;
132
133 UINT32 optimalContentHeight = layoutArea.height;
134 if (mVertBarType != ScrollBarType::NeverShow)
135 optimalContentHeight = sizeRanges[layoutIdx].optimal.y;
136
137 UINT32 layoutWidth = std::max(optimalContentWidth, (UINT32)layoutArea.width);
138 UINT32 layoutHeight = std::max(optimalContentHeight, (UINT32)layoutArea.height);
139
140 contentSize = GUILayoutUtility::calcActualSize(layoutWidth, layoutHeight, mContentLayout, false);
141 visibleSize = Vector2I(layoutArea.width, layoutArea.height);
142
143 bool addHorzScrollbar = (mHorzBarType == ScrollBarType::ShowIfDoesntFit && contentSize.x > visibleSize.x) ||
144 mHorzBarType == ScrollBarType::AlwaysShow;
145
146 bool hasHorzScrollbar = false;
147 bool hasVertScrollbar = false;
148 if (addHorzScrollbar)
149 {
150 // Make room for scrollbar
151 visibleSize.y = (UINT32)std::max(0, (INT32)layoutArea.height - (INT32)ScrollBarWidth);
152 optimalContentHeight = (UINT32)std::max(0, (INT32)optimalContentHeight - (INT32)ScrollBarWidth);
153
154 if (sizeRanges[layoutIdx].min.y > 0)
155 optimalContentHeight = std::max((UINT32)sizeRanges[layoutIdx].min.y, optimalContentHeight);
156
157 layoutHeight = std::max(optimalContentHeight, (UINT32)visibleSize.y); // Never go below optimal size
158
159 contentSize = GUILayoutUtility::calcActualSize(layoutWidth, layoutHeight, mContentLayout, true);
160 hasHorzScrollbar = true;
161 }
162
163 bool addVertScrollbar = (mVertBarType == ScrollBarType::ShowIfDoesntFit && contentSize.y > visibleSize.y) ||
164 mVertBarType == ScrollBarType::AlwaysShow;
165
166 if (addVertScrollbar)
167 {
168 // Make room for scrollbar
169 visibleSize.x = (UINT32)std::max(0, (INT32)layoutArea.width - (INT32)ScrollBarWidth);
170 optimalContentWidth = (UINT32)std::max(0, (INT32)optimalContentWidth - (INT32)ScrollBarWidth);
171
172 if (sizeRanges[layoutIdx].min.x > 0)
173 optimalContentWidth = std::max((UINT32)sizeRanges[layoutIdx].min.x, optimalContentWidth);
174
175 layoutWidth = std::max(optimalContentWidth, (UINT32)visibleSize.x); // Never go below optimal size
176
177 contentSize = GUILayoutUtility::calcActualSize(layoutWidth, layoutHeight, mContentLayout, true);
178 hasVertScrollbar = true;
179
180 if (!hasHorzScrollbar) // Since width has been reduced, we need to check if we require the horizontal scrollbar
181 {
182 addHorzScrollbar = (mHorzBarType == ScrollBarType::ShowIfDoesntFit && contentSize.x > visibleSize.x) && mHorzBarType != ScrollBarType::NeverShow;
183
184 if (addHorzScrollbar)
185 {
186 // Make room for scrollbar
187 visibleSize.y = (UINT32)std::max(0, (INT32)layoutArea.height - (INT32)ScrollBarWidth);
188 optimalContentHeight = (UINT32)std::max(0, (INT32)optimalContentHeight - (INT32)ScrollBarWidth);
189
190 if (sizeRanges[layoutIdx].min.y > 0)
191 optimalContentHeight = std::max((UINT32)sizeRanges[layoutIdx].min.y, optimalContentHeight);
192
193 layoutHeight = std::max(optimalContentHeight, (UINT32)visibleSize.y); // Never go below optimal size
194
195 contentSize = GUILayoutUtility::calcActualSize(layoutWidth, layoutHeight, mContentLayout, true);
196 hasHorzScrollbar = true;
197 }
198 }
199 }
200
201 elementAreas[layoutIdx] = Rect2I(layoutArea.x, layoutArea.y, layoutWidth, layoutHeight);
202
203 // Calculate vertical scrollbar bounds
204 if (hasVertScrollbar)
205 {
206 INT32 scrollBarOffset = (UINT32)std::max(0, (INT32)layoutArea.width - (INT32)ScrollBarWidth);
207 UINT32 scrollBarHeight = layoutArea.height;
208 if (hasHorzScrollbar)
209 scrollBarHeight = (UINT32)std::max(0, (INT32)scrollBarHeight - (INT32)ScrollBarWidth);
210
211 elementAreas[vertScrollIdx] = Rect2I(layoutArea.x + scrollBarOffset, layoutArea.y, ScrollBarWidth, scrollBarHeight);
212 }
213 else
214 {
215 elementAreas[vertScrollIdx] = Rect2I(layoutArea.x + layoutWidth, layoutArea.y, 0, 0);
216 }
217
218 // Calculate horizontal scrollbar bounds
219 if (hasHorzScrollbar)
220 {
221 INT32 scrollBarOffset = (UINT32)std::max(0, (INT32)layoutArea.height - (INT32)ScrollBarWidth);
222 UINT32 scrollBarWidth = layoutArea.width;
223 if (hasVertScrollbar)
224 scrollBarWidth = (UINT32)std::max(0, (INT32)scrollBarWidth - (INT32)ScrollBarWidth);
225
226 elementAreas[horzScrollIdx] = Rect2I(layoutArea.x, layoutArea.y + scrollBarOffset, scrollBarWidth, ScrollBarWidth);
227 }
228 else
229 {
230 elementAreas[horzScrollIdx] = Rect2I(layoutArea.x, layoutArea.y + layoutHeight, 0, 0);
231 }
232 }
233
234 void GUIScrollArea::_updateLayoutInternal(const GUILayoutData& data)
235 {
236 UINT32 numElements = (UINT32)mChildren.size();
237 Rect2I* elementAreas = nullptr;
238
239 if (numElements > 0)
240 elementAreas = bs_stack_new<Rect2I>(numElements);
241
242 UINT32 layoutIdx = 0;
243 UINT32 horzScrollIdx = 0;
244 UINT32 vertScrollIdx = 0;
245 for (UINT32 i = 0; i < numElements; i++)
246 {
247 GUIElementBase* child = _getChild(i);
248
249 if (child == mContentLayout)
250 layoutIdx = i;
251
252 if (child == mHorzScroll)
253 horzScrollIdx = i;
254
255 if (child == mVertScroll)
256 vertScrollIdx = i;
257 }
258
259 _getElementAreas(data.area, elementAreas, numElements, mChildSizeRanges, mVisibleSize, mContentSize);
260
261 Rect2I& layoutBounds = elementAreas[layoutIdx];
262 Rect2I& horzScrollBounds = elementAreas[horzScrollIdx];
263 Rect2I& vertScrollBounds = elementAreas[vertScrollIdx];
264
265 // Recalculate offsets in case scroll percent got updated externally (this needs to be delayed to this point because
266 // at the time of the update content and visible sizes might be out of date).
267 UINT32 scrollableHeight = (UINT32)std::max(0, INT32(mContentSize.y) - INT32(mVisibleSize.y));
268 if (mRecalculateVertOffset)
269 {
270 mVertOffset = scrollableHeight * Math::clamp01(mVertScroll->getScrollPos());
271
272 mRecalculateVertOffset = false;
273 }
274
275 UINT32 scrollableWidth = (UINT32)std::max(0, INT32(mContentSize.x) - INT32(mVisibleSize.x));
276 if (mRecalculateHorzOffset)
277 {
278 mHorzOffset = scrollableWidth * Math::clamp01(mHorzScroll->getScrollPos());
279
280 mRecalculateHorzOffset = false;
281 }
282
283 // Reset offset in case layout size changed so everything fits
284 mVertOffset = Math::clamp(mVertOffset, 0.0f, (float)scrollableHeight);
285 mHorzOffset = Math::clamp(mHorzOffset, 0.0f, (float)scrollableWidth);
286
287 // Layout
288 if (mContentLayout->_isActive())
289 {
290 layoutBounds.x -= Math::floorToInt(mHorzOffset);
291 layoutBounds.y -= Math::floorToInt(mVertOffset);
292
293 Rect2I layoutClipRect = data.clipRect;
294 layoutClipRect.width = (UINT32)mVisibleSize.x;
295 layoutClipRect.height = (UINT32)mVisibleSize.y;
296 layoutClipRect.clip(data.clipRect);
297
298 GUILayoutData layoutData = data;
299 layoutData.area = layoutBounds;
300 layoutData.clipRect = layoutClipRect;
301
302 mContentLayout->_setLayoutData(layoutData);
303 mContentLayout->_updateLayoutInternal(layoutData);
304 }
305
306 // Vertical scrollbar
307 {
308 GUILayoutData vertScrollData = data;
309 vertScrollData.area = vertScrollBounds;
310
311 vertScrollData.clipRect = vertScrollBounds;
312 vertScrollData.clipRect.clip(data.clipRect);
313
314 mVertScroll->_setLayoutData(vertScrollData);
315 mVertScroll->_updateLayoutInternal(vertScrollData);
316
317 // Set new handle size and update position to match the new size
318 UINT32 scrollableHeight = (UINT32)std::max(0, INT32(mContentSize.y) - INT32(vertScrollBounds.height));
319 float newScrollPct = 0.0f;
320
321 if (scrollableHeight > 0)
322 newScrollPct = mVertOffset / scrollableHeight;
323
324 mVertScroll->_setHandleSize(vertScrollBounds.height / (float)mContentSize.y);
325 mVertScroll->_setScrollPos(newScrollPct);
326 }
327
328 // Horizontal scrollbar
329 {
330 GUILayoutData horzScrollData = data;
331 horzScrollData.area = horzScrollBounds;
332
333 horzScrollData.clipRect = horzScrollBounds;
334 horzScrollData.clipRect.clip(data.clipRect);
335
336 mHorzScroll->_setLayoutData(horzScrollData);
337 mHorzScroll->_updateLayoutInternal(horzScrollData);
338
339 // Set new handle size and update position to match the new size
340 UINT32 scrollableWidth = (UINT32)std::max(0, INT32(mContentSize.x) - INT32(horzScrollBounds.width));
341 float newScrollPct = 0.0f;
342
343 if (scrollableWidth > 0)
344 newScrollPct = mHorzOffset / scrollableWidth;
345
346 mHorzScroll->_setHandleSize(horzScrollBounds.width / (float)mContentSize.x);
347 mHorzScroll->_setScrollPos(newScrollPct);
348 }
349
350 if (elementAreas != nullptr)
351 bs_stack_free(elementAreas);
352 }
353
354 void GUIScrollArea::vertScrollUpdate(float scrollPos)
355 {
356 UINT32 scrollableHeight = (UINT32)std::max(0, INT32(mContentSize.y) - INT32(mVisibleSize.y));
357 mVertOffset = scrollableHeight * Math::clamp01(scrollPos);
358
359 _markLayoutAsDirty();
360 }
361
362 void GUIScrollArea::horzScrollUpdate(float scrollPos)
363 {
364 UINT32 scrollableWidth = (UINT32)std::max(0, INT32(mContentSize.x) - INT32(mVisibleSize.x));
365 mHorzOffset = scrollableWidth * Math::clamp01(scrollPos);
366
367 _markLayoutAsDirty();
368 }
369
370 void GUIScrollArea::scrollToVertical(float pct)
371 {
372 mVertScroll->_setScrollPos(pct);
373 mRecalculateVertOffset = true;
374
375 _markLayoutAsDirty();
376 }
377
378 void GUIScrollArea::scrollToHorizontal(float pct)
379 {
380 mHorzScroll->_setScrollPos(pct);
381 mRecalculateHorzOffset = true;
382
383 _markLayoutAsDirty();
384 }
385
386 float GUIScrollArea::getVerticalScroll() const
387 {
388 if (mVertScroll != nullptr)
389 return mVertScroll->getScrollPos();
390
391 return 0.0f;
392 }
393
394 float GUIScrollArea::getHorizontalScroll() const
395 {
396 if (mHorzScroll != nullptr)
397 return mHorzScroll->getScrollPos();
398
399 return 0.0f;
400 }
401
402 Rect2I GUIScrollArea::getContentBounds()
403 {
404 Rect2I bounds = getBounds();
405
406 if (mHorzScroll)
407 bounds.height -= ScrollBarWidth;
408
409 if (mVertScroll)
410 bounds.width -= ScrollBarWidth;
411
412 return bounds;
413 }
414
415 void GUIScrollArea::scrollUpPx(UINT32 pixels)
416 {
417 if(mVertScroll != nullptr)
418 {
419 UINT32 scrollableSize = (UINT32)std::max(0, INT32(mContentSize.y) - INT32(mVisibleSize.y));
420
421 float offset = 0.0f;
422 if(scrollableSize > 0)
423 offset = pixels / (float)scrollableSize;
424
425 mVertScroll->scroll(offset);
426 }
427 }
428
429 void GUIScrollArea::scrollDownPx(UINT32 pixels)
430 {
431 if(mVertScroll != nullptr)
432 {
433 UINT32 scrollableSize = (UINT32)std::max(0, INT32(mContentSize.y) - INT32(mVisibleSize.y));
434
435 float offset = 0.0f;
436 if(scrollableSize > 0)
437 offset = pixels / (float)scrollableSize;
438
439 mVertScroll->scroll(-offset);
440 }
441 }
442
443 void GUIScrollArea::scrollLeftPx(UINT32 pixels)
444 {
445 if(mHorzScroll != nullptr)
446 {
447 UINT32 scrollableSize = (UINT32)std::max(0, INT32(mContentSize.x) - INT32(mVisibleSize.x));
448
449 float offset = 0.0f;
450 if(scrollableSize > 0)
451 offset = pixels / (float)scrollableSize;
452
453 mHorzScroll->scroll(offset);
454 }
455 }
456
457 void GUIScrollArea::scrollRightPx(UINT32 pixels)
458 {
459 if(mHorzScroll != nullptr)
460 {
461 UINT32 scrollableSize = (UINT32)std::max(0, INT32(mContentSize.x) - INT32(mVisibleSize.x));
462
463 float offset = 0.0f;
464 if(scrollableSize > 0)
465 offset = pixels / (float)scrollableSize;
466
467 mHorzScroll->scroll(-offset);
468 }
469 }
470
471 void GUIScrollArea::scrollUpPct(float percent)
472 {
473 if(mVertScroll != nullptr)
474 mVertScroll->scroll(percent);
475 }
476
477 void GUIScrollArea::scrollDownPct(float percent)
478 {
479 if(mVertScroll != nullptr)
480 mVertScroll->scroll(-percent);
481 }
482
483 void GUIScrollArea::scrollLeftPct(float percent)
484 {
485 if(mHorzScroll != nullptr)
486 mHorzScroll->scroll(percent);
487 }
488
489 void GUIScrollArea::scrollRightPct(float percent)
490 {
491 if(mHorzScroll != nullptr)
492 mHorzScroll->scroll(-percent);
493 }
494
495 bool GUIScrollArea::_mouseEvent(const GUIMouseEvent& ev)
496 {
497 if(ev.getType() == GUIMouseEventType::MouseWheelScroll)
498 {
499 // Mouse wheel only scrolls on the Y axis
500 if(mVertScroll != nullptr)
501 {
502 UINT32 scrollableHeight = (UINT32)std::max(0, INT32(mContentSize.y) - INT32(mVisibleSize.y));
503 float additionalScroll = (float)WheelScrollAmount / scrollableHeight;
504
505 mVertScroll->scroll(additionalScroll * ev.getWheelScrollAmount());
506 return true;
507 }
508 }
509
510 return false;
511 }
512
513 GUIScrollArea* GUIScrollArea::create(ScrollBarType vertBarType, ScrollBarType horzBarType,
514 const String& scrollBarStyle, const String& scrollAreaStyle)
515 {
516 return new (bs_alloc<GUIScrollArea>()) GUIScrollArea(vertBarType, horzBarType, scrollBarStyle,
517 getStyleName<GUIScrollArea>(scrollAreaStyle), GUIDimensions::create());
518 }
519
520 GUIScrollArea* GUIScrollArea::create(const GUIOptions& options, const String& scrollBarStyle,
521 const String& scrollAreaStyle)
522 {
523 return new (bs_alloc<GUIScrollArea>()) GUIScrollArea(ScrollBarType::ShowIfDoesntFit,
524 ScrollBarType::ShowIfDoesntFit, scrollBarStyle, getStyleName<GUIScrollArea>(scrollAreaStyle), GUIDimensions::create(options));
525 }
526
527 GUIScrollArea* GUIScrollArea::create(const String& scrollBarStyle, const String& scrollAreaStyle)
528 {
529 return new (bs_alloc<GUIScrollArea>()) GUIScrollArea(ScrollBarType::ShowIfDoesntFit, ScrollBarType::ShowIfDoesntFit, scrollBarStyle,
530 getStyleName<GUIScrollArea>(scrollAreaStyle), GUIDimensions::create());
531 }
532
533 GUIScrollArea* GUIScrollArea::create(ScrollBarType vertBarType,
534 ScrollBarType horzBarType, const GUIOptions& options, const String& scrollBarStyle,
535 const String& scrollAreaStyle)
536 {
537 return new (bs_alloc<GUIScrollArea>()) GUIScrollArea(vertBarType, horzBarType, scrollBarStyle,
538 getStyleName<GUIScrollArea>(scrollAreaStyle), GUIDimensions::create(options));
539 }
540
541 const String& GUIScrollArea::getGUITypeName()
542 {
543 static String typeName = "ScrollArea";
544 return typeName;
545 }
546}