You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2970 lines
112 KiB
C++
2970 lines
112 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#include "UiDynamicScrollBoxComponent.h"
|
|
|
|
#include "UiElementComponent.h"
|
|
#include "UiNavigationHelpers.h"
|
|
#include "UiLayoutHelpers.h"
|
|
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
#include <AzCore/Serialization/EditContext.h>
|
|
#include <AzCore/RTTI/BehaviorContext.h>
|
|
#include <AzCore/Component/ComponentApplicationBus.h>
|
|
|
|
#include <LyShine/Bus/UiTransform2dBus.h>
|
|
#include <LyShine/Bus/UiCanvasBus.h>
|
|
#include <LyShine/Bus/UiLayoutCellBus.h>
|
|
#include <LyShine/Bus/UiLayoutCellDefaultBus.h>
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//! UiDynamicScrollBoxDataBus Behavior context handler class
|
|
class BehaviorUiDynamicScrollBoxDataBusHandler
|
|
: public UiDynamicScrollBoxDataBus::Handler
|
|
, public AZ::BehaviorEBusHandler
|
|
{
|
|
public:
|
|
AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiDynamicScrollBoxDataBusHandler, "{74FA95AB-D4C2-40B8-8568-1B174BF577C0}", AZ::SystemAllocator,
|
|
GetNumElements, GetElementWidth, GetElementHeight, GetNumSections, GetNumElementsInSection,
|
|
GetElementInSectionWidth, GetElementInSectionHeight, GetSectionHeaderWidth, GetSectionHeaderHeight);
|
|
|
|
int GetNumElements() override
|
|
{
|
|
int numElements = 0;
|
|
CallResult(numElements, FN_GetNumElements);
|
|
return numElements;
|
|
}
|
|
|
|
float GetElementWidth(int index) override
|
|
{
|
|
float width = 0.0f;
|
|
CallResult(width, FN_GetElementWidth, index);
|
|
return width;
|
|
}
|
|
|
|
float GetElementHeight(int index) override
|
|
{
|
|
float height = 0.0f;
|
|
CallResult(height, FN_GetElementHeight, index);
|
|
return height;
|
|
}
|
|
|
|
int GetNumSections() override
|
|
{
|
|
int numSections = 0;
|
|
CallResult(numSections, FN_GetNumSections);
|
|
return numSections;
|
|
}
|
|
|
|
int GetNumElementsInSection(int sectionIndex) override
|
|
{
|
|
int numElementsInSection = 0;
|
|
CallResult(numElementsInSection, FN_GetNumElementsInSection, sectionIndex);
|
|
return numElementsInSection;
|
|
}
|
|
|
|
float GetElementInSectionWidth(int sectionIndex, int index) override
|
|
{
|
|
float width = 0.0f;
|
|
CallResult(width, FN_GetElementInSectionWidth, sectionIndex, index);
|
|
return width;
|
|
}
|
|
|
|
float GetElementInSectionHeight(int sectionIndex, int index) override
|
|
{
|
|
float height = 0.0f;
|
|
CallResult(height, FN_GetElementInSectionHeight, sectionIndex, index);
|
|
return height;
|
|
}
|
|
|
|
float GetSectionHeaderWidth(int sectionIndex) override
|
|
{
|
|
float width = 0.0f;
|
|
CallResult(width, FN_GetSectionHeaderWidth, sectionIndex);
|
|
return width;
|
|
}
|
|
|
|
float GetSectionHeaderHeight(int sectionIndex) override
|
|
{
|
|
float height = 0.0f;
|
|
CallResult(height, FN_GetSectionHeaderHeight, sectionIndex);
|
|
return height;
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//! UiDynamicScrollBoxElementNotificationBus Behavior context handler class
|
|
class BehaviorUiDynamicScrollBoxElementNotificationBusHandler
|
|
: public UiDynamicScrollBoxElementNotificationBus::Handler
|
|
, public AZ::BehaviorEBusHandler
|
|
{
|
|
public:
|
|
AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiDynamicScrollBoxElementNotificationBusHandler, "{4D166273-4D12-45A4-BC42-A7FF59A2092E}", AZ::SystemAllocator,
|
|
OnElementBecomingVisible, OnPrepareElementForSizeCalculation,
|
|
OnElementInSectionBecomingVisible, OnPrepareElementInSectionForSizeCalculation,
|
|
OnSectionHeaderBecomingVisible, OnPrepareSectionHeaderForSizeCalculation);
|
|
|
|
void OnElementBecomingVisible(AZ::EntityId entityId, int index) override
|
|
{
|
|
Call(FN_OnElementBecomingVisible, entityId, index);
|
|
}
|
|
|
|
void OnPrepareElementForSizeCalculation(AZ::EntityId entityId, int index) override
|
|
{
|
|
Call(FN_OnPrepareElementForSizeCalculation, entityId, index);
|
|
}
|
|
|
|
void OnElementInSectionBecomingVisible(AZ::EntityId entityId, int sectionIndex, int index) override
|
|
{
|
|
Call(FN_OnElementInSectionBecomingVisible, entityId, sectionIndex, index);
|
|
}
|
|
|
|
void OnPrepareElementInSectionForSizeCalculation(AZ::EntityId entityId, int sectionIndex, int index) override
|
|
{
|
|
Call(FN_OnPrepareElementInSectionForSizeCalculation, entityId, sectionIndex, index);
|
|
}
|
|
|
|
void OnSectionHeaderBecomingVisible(AZ::EntityId entityId, int sectionIndex) override
|
|
{
|
|
Call(FN_OnSectionHeaderBecomingVisible, entityId, sectionIndex);
|
|
}
|
|
|
|
void OnPrepareSectionHeaderForSizeCalculation(AZ::EntityId entityId, int sectionIndex) override
|
|
{
|
|
Call(FN_OnPrepareSectionHeaderForSizeCalculation, entityId, sectionIndex);
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PUBLIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDynamicScrollBoxComponent::CachedElementInfo::CachedElementInfo()
|
|
: m_size(-1.0f)
|
|
, m_accumulatedSize(-1.0f)
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDynamicScrollBoxComponent::UiDynamicScrollBoxComponent()
|
|
: m_autoRefreshOnPostActivate(true)
|
|
, m_defaultNumElements(0)
|
|
, m_variableItemElementSize(false)
|
|
, m_autoCalculateItemElementSize(true)
|
|
, m_estimatedItemElementSize(0.0f)
|
|
, m_hasSections(false)
|
|
, m_defaultNumSections(1)
|
|
, m_stickyHeaders(false)
|
|
, m_variableHeaderElementSize(false)
|
|
, m_autoCalculateHeaderElementSize(true)
|
|
, m_estimatedHeaderElementSize(0.0f)
|
|
, m_averageElementSize(0.0f)
|
|
, m_numElementsUsedForAverage(0)
|
|
, m_lastCalculatedVisibleContentOffset(0.0f)
|
|
, m_isVertical(true)
|
|
, m_firstDisplayedElementIndex(-1)
|
|
, m_lastDisplayedElementIndex(-1)
|
|
, m_firstVisibleElementIndex(-1)
|
|
, m_lastVisibleElementIndex(-1)
|
|
, m_numElements(0)
|
|
, m_listPreparedForDisplay(false)
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDynamicScrollBoxComponent::~UiDynamicScrollBoxComponent()
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::RefreshContent()
|
|
{
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
PrepareListForDisplay();
|
|
}
|
|
|
|
ResizeContentToFitElements();
|
|
|
|
ClearDisplayedElements();
|
|
|
|
bool keepAtEndIfWasAtEnd = false;
|
|
if (AnyElementTypesHaveEstimatedSizes())
|
|
{
|
|
// Check if the content's pivot is at the end (bottom or right)
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
if (contentEntityId.IsValid())
|
|
{
|
|
AZ::Vector2 pivot(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);
|
|
|
|
if (m_isVertical)
|
|
{
|
|
keepAtEndIfWasAtEnd = pivot.GetY() == 1.0f;
|
|
}
|
|
else
|
|
{
|
|
keepAtEndIfWasAtEnd = pivot.GetX() == 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateElementVisibility(keepAtEndIfWasAtEnd);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::AddElementsToEnd(int numElementsToAdd, bool scrollToEndIfWasAtEnd)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", m_listPreparedForDisplay, "AddElementsToEnd() is only supported after the first content refresh");
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_hasSections, "AddElementsToEnd() can only be used on lists that are not divided into sections");
|
|
|
|
if (numElementsToAdd > 0 && !m_hasSections)
|
|
{
|
|
m_numElements += numElementsToAdd;
|
|
|
|
// Calculate new content size
|
|
float sizeDiff = 0.0f;
|
|
if (!m_variableElementSize[ElementType::Item])
|
|
{
|
|
sizeDiff = numElementsToAdd * m_prototypeElementSize[ElementType::Item];
|
|
}
|
|
else
|
|
{
|
|
// Add cache entries for the new elements
|
|
m_cachedElementInfo.insert(m_cachedElementInfo.end(), numElementsToAdd, CachedElementInfo());
|
|
|
|
for (int i = m_numElements - numElementsToAdd; i < m_numElements; i++)
|
|
{
|
|
sizeDiff += GetAndCacheVariableElementSize(i);
|
|
}
|
|
|
|
if (m_autoCalculateElementSize[ElementType::Item])
|
|
{
|
|
DisableElementsForAutoSizeCalculation();
|
|
}
|
|
|
|
UpdateAverageElementSize(numElementsToAdd, sizeDiff);
|
|
}
|
|
|
|
bool scrollToEnd = scrollToEndIfWasAtEnd && IsScrolledToEnd();
|
|
if (scrollToEnd)
|
|
{
|
|
float scrollDiff = CalculateContentEndDeltaAfterSizeChange(sizeDiff);
|
|
AdjustContentSizeAndScrollOffsetByDelta(sizeDiff, scrollDiff);
|
|
|
|
if (!IsScrolledToEnd())
|
|
{
|
|
ScrollToEnd();
|
|
}
|
|
else
|
|
{
|
|
UpdateElementVisibility(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float scrollDiff = CalculateContentBeginningDeltaAfterSizeChange(sizeDiff);
|
|
AdjustContentSizeAndScrollOffsetByDelta(sizeDiff, scrollDiff);
|
|
|
|
UpdateElementVisibility();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::RemoveElementsFromFront(int numElementsToRemove)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", m_listPreparedForDisplay, "RemoveElementsFromFront() is only supported after the first content refresh");
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_hasSections, "RemoveElementsFromFront() can only be used on lists that are not divided into sections");
|
|
|
|
if (numElementsToRemove > 0 && !m_hasSections)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", numElementsToRemove <= m_numElements, "attempting to remove more elements than are in the list");
|
|
|
|
numElementsToRemove = AZ::GetClamp(numElementsToRemove, 0, m_numElements);
|
|
|
|
float sizeDiff = 0.0f;
|
|
if (!m_variableElementSize[ElementType::Item])
|
|
{
|
|
sizeDiff = numElementsToRemove * m_prototypeElementSize[ElementType::Item];
|
|
}
|
|
else
|
|
{
|
|
// Get accumulated size being removed
|
|
sizeDiff = GetVariableSizeElementOffset(numElementsToRemove - 1) + GetVariableElementSize(numElementsToRemove - 1);
|
|
|
|
// Update cached element info
|
|
m_cachedElementInfo.erase(m_cachedElementInfo.begin(), m_cachedElementInfo.begin() + numElementsToRemove);
|
|
|
|
// Update accumulated sizes
|
|
int newElementCount = m_numElements - numElementsToRemove;
|
|
for (int i = 0; i < newElementCount; i++)
|
|
{
|
|
if (m_cachedElementInfo[i].m_accumulatedSize >= 0.0f)
|
|
{
|
|
m_cachedElementInfo[i].m_accumulatedSize -= sizeDiff;
|
|
}
|
|
}
|
|
}
|
|
sizeDiff = -sizeDiff;
|
|
|
|
m_numElements -= numElementsToRemove;
|
|
|
|
if (numElementsToRemove > 0)
|
|
{
|
|
ClearDisplayedElements();
|
|
|
|
float scrollDiff = CalculateContentBeginningDeltaAfterSizeChange(sizeDiff) - sizeDiff;
|
|
AdjustContentSizeAndScrollOffsetByDelta(sizeDiff, scrollDiff);
|
|
|
|
UpdateElementVisibility();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::ScrollToEnd()
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", m_listPreparedForDisplay, "ScrollToEnd() is only supported after the first content refresh");
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get content's parent
|
|
AZ::EntityId contentParentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
|
|
if (!contentParentEntityId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get content's rect in canvas space
|
|
UiTransformInterface::Rect contentRect;
|
|
EBUS_EVENT_ID(contentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentRect);
|
|
|
|
// Get content parent's rect in canvas space
|
|
UiTransformInterface::Rect parentRect;
|
|
EBUS_EVENT_ID(contentParentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect);
|
|
|
|
float scrollDelta = 0.0f;
|
|
if (m_isVertical)
|
|
{
|
|
if (contentRect.bottom > parentRect.bottom)
|
|
{
|
|
scrollDelta = parentRect.bottom - contentRect.bottom;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (contentRect.right > parentRect.right)
|
|
{
|
|
scrollDelta = parentRect.right - contentRect.right;
|
|
}
|
|
}
|
|
|
|
if (scrollDelta != 0.0f)
|
|
{
|
|
AdjustContentSizeAndScrollOffsetByDelta(0.0f, scrollDelta);
|
|
|
|
UpdateElementVisibility(true);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::GetElementIndexOfChild(AZ::EntityId childElement)
|
|
{
|
|
AZ::EntityId immediateChild = GetImmediateContentChildFromDescendant(childElement);
|
|
|
|
for (const auto& e : m_displayedElements)
|
|
{
|
|
if (e.m_element == immediateChild)
|
|
{
|
|
if (!m_hasSections)
|
|
{
|
|
return e.m_elementIndex;
|
|
}
|
|
else
|
|
{
|
|
return e.m_indexInfo.m_itemIndexInSection;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::GetSectionIndexOfChild(AZ::EntityId childElement)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", m_hasSections, "GetSectionIndexOfChild() can only be used on lists that are divided into sections");
|
|
|
|
if (m_hasSections)
|
|
{
|
|
AZ::EntityId immediateChild = GetImmediateContentChildFromDescendant(childElement);
|
|
|
|
for (const auto& e : m_displayedElements)
|
|
{
|
|
if (e.m_element == immediateChild)
|
|
{
|
|
return e.m_indexInfo.m_sectionIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetChildAtElementIndex(int index)
|
|
{
|
|
AZ::EntityId elementId;
|
|
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_hasSections, "GetChildAtElementIndex() can only be used on lists that are not divided into sections");
|
|
|
|
if (!m_hasSections)
|
|
{
|
|
elementId = FindDisplayedElementWithIndex(index);
|
|
}
|
|
|
|
return elementId;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetChildAtSectionAndElementIndex(int sectionIndex, int index)
|
|
{
|
|
AZ::EntityId elementId;
|
|
|
|
AZ_Warning("UiDynamicScrollBoxComponent", m_hasSections, "GetChildElementAtSectionAndLocationIndex() can only be used on lists that are divided into sections");
|
|
|
|
if (m_hasSections)
|
|
{
|
|
for (const auto& e : m_displayedElements)
|
|
{
|
|
if (e.m_indexInfo.m_sectionIndex == sectionIndex && e.m_indexInfo.m_itemIndexInSection == index)
|
|
{
|
|
elementId = e.m_element;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return elementId;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetAutoRefreshOnPostActivate()
|
|
{
|
|
return m_autoRefreshOnPostActivate;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetAutoRefreshOnPostActivate(bool autoRefresh)
|
|
{
|
|
m_autoRefreshOnPostActivate = autoRefresh;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetPrototypeElement()
|
|
{
|
|
return m_itemPrototypeElement;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetPrototypeElement(AZ::EntityId prototypeElement)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_itemPrototypeElement = prototypeElement;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetElementsVaryInSize()
|
|
{
|
|
return m_variableItemElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetElementsVaryInSize(bool varyInSize)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_variableItemElementSize = varyInSize;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetAutoCalculateVariableElementSize()
|
|
{
|
|
return m_autoCalculateItemElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetAutoCalculateVariableElementSize(bool autoCalculateSize)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_autoCalculateItemElementSize = autoCalculateSize;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetEstimatedVariableElementSize()
|
|
{
|
|
return m_estimatedItemElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetEstimatedVariableElementSize(float estimatedSize)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_estimatedItemElementSize = AZ::GetMax(estimatedSize, 0.0f);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetSectionsEnabled()
|
|
{
|
|
return m_hasSections;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetSectionsEnabled(bool sectionsEnabled)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_hasSections = sectionsEnabled;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetPrototypeHeader()
|
|
{
|
|
return m_headerPrototypeElement;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetPrototypeHeader(AZ::EntityId prototypeHeader)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_headerPrototypeElement = prototypeHeader;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetHeadersSticky()
|
|
{
|
|
return m_stickyHeaders;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetHeadersSticky(bool stickyHeaders)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_stickyHeaders = stickyHeaders;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetHeadersVaryInSize()
|
|
{
|
|
return m_variableHeaderElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetHeadersVaryInSize(bool varyInSize)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_variableHeaderElementSize = varyInSize;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::GetAutoCalculateVariableHeaderSize()
|
|
{
|
|
return m_autoCalculateHeaderElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetAutoCalculateVariableHeaderSize(bool autoCalculateSize)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_autoCalculateHeaderElementSize = autoCalculateSize;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetEstimatedVariableHeaderSize()
|
|
{
|
|
return m_estimatedHeaderElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetEstimatedVariableHeaderSize(float estimatedSize)
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");
|
|
|
|
if (!m_listPreparedForDisplay)
|
|
{
|
|
m_estimatedHeaderElementSize = AZ::GetMax(estimatedSize, 0.0f);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::OnScrollOffsetChanging([[maybe_unused]] AZ::Vector2 newScrollOffset)
|
|
{
|
|
UpdateElementVisibility();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::OnScrollOffsetChanged([[maybe_unused]] AZ::Vector2 newScrollOffset)
|
|
{
|
|
UpdateElementVisibility();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::InGamePostActivate()
|
|
{
|
|
if (m_autoRefreshOnPostActivate)
|
|
{
|
|
RefreshContent();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::OnCanvasSpaceRectChanged([[maybe_unused]] AZ::EntityId entityId, const UiTransformInterface::Rect& oldRect, const UiTransformInterface::Rect& newRect)
|
|
{
|
|
// If old rect equals new rect, size changed due to initialization
|
|
bool sizeChanged = (oldRect == newRect) || (!oldRect.GetSize().IsClose(newRect.GetSize(), 0.05f));
|
|
|
|
if (sizeChanged)
|
|
{
|
|
UpdateElementVisibility();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::OnUiElementBeingDestroyed()
|
|
{
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
if (m_prototypeElement[i].IsValid())
|
|
{
|
|
EBUS_EVENT_ID(m_prototypeElement[i], UiElementBus, DestroyElement);
|
|
m_prototypeElement[i].SetInvalid();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PUBLIC STATIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void UiDynamicScrollBoxComponent::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
|
|
|
|
if (serializeContext)
|
|
{
|
|
serializeContext->Class<UiDynamicScrollBoxComponent, AZ::Component>()
|
|
->Version(1)
|
|
->Field("AutoRefreshOnPostActivate", &UiDynamicScrollBoxComponent::m_autoRefreshOnPostActivate)
|
|
->Field("PrototypeElement", &UiDynamicScrollBoxComponent::m_itemPrototypeElement)
|
|
->Field("VariableElementSize", &UiDynamicScrollBoxComponent::m_variableItemElementSize)
|
|
->Field("AutoCalcElementSize", &UiDynamicScrollBoxComponent::m_autoCalculateItemElementSize)
|
|
->Field("EstimatedElementSize", &UiDynamicScrollBoxComponent::m_estimatedItemElementSize)
|
|
->Field("DefaultNumElements", &UiDynamicScrollBoxComponent::m_defaultNumElements)
|
|
->Field("HasSections", &UiDynamicScrollBoxComponent::m_hasSections)
|
|
->Field("HeaderPrototypeElement", &UiDynamicScrollBoxComponent::m_headerPrototypeElement)
|
|
->Field("StickyHeaders", &UiDynamicScrollBoxComponent::m_stickyHeaders)
|
|
->Field("VariableHeaderSize", &UiDynamicScrollBoxComponent::m_variableHeaderElementSize)
|
|
->Field("AutoCalcHeaderSize", &UiDynamicScrollBoxComponent::m_autoCalculateHeaderElementSize)
|
|
->Field("EstimatedHeaderSize", &UiDynamicScrollBoxComponent::m_estimatedHeaderElementSize)
|
|
->Field("DefaultNumSections", &UiDynamicScrollBoxComponent::m_defaultNumSections);
|
|
|
|
AZ::EditContext* ec = serializeContext->GetEditContext();
|
|
if (ec)
|
|
{
|
|
auto editInfo = ec->Class<UiDynamicScrollBoxComponent>("DynamicScrollBox",
|
|
"A component that dynamically sets up scroll box content as a horizontal or vertical list of elements that\n"
|
|
"are cloned from a prototype element. Only the minimum number of elements are created for efficient scrolling.\n"
|
|
"The scroll box's content element's first child acts as the prototype element.");
|
|
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
|
->Attribute(AZ::Edit::Attributes::Category, "UI")
|
|
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiDynamicScrollBox.png")
|
|
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiDynamicScrollBox.png")
|
|
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_autoRefreshOnPostActivate, "Refresh on activate",
|
|
"Whether the list should automatically prepare and refresh its content post activation.");
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_itemPrototypeElement, "Prototype element",
|
|
"The prototype element to be used for the elements in the list. If empty, the prototype element will default to the first child of the content element.");
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_variableItemElementSize, "Variable element size",
|
|
"Whether elements in the list can vary in size. If not, the element size is fixed and is determined by the prototype element.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_autoCalculateItemElementSize, "Auto calc element size",
|
|
"Whether element sizes should be auto calculated or whether they should be requested.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_variableItemElementSize);
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_estimatedItemElementSize, "Estimated element size",
|
|
"The element size to use as an estimate before the element appears in the view. If set to 0, sizes will be calculated up front.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_variableItemElementSize)
|
|
->Attribute(AZ::Edit::Attributes::Min, 0.0f);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiDynamicScrollBoxComponent::m_defaultNumElements, "Default num elements",
|
|
"The default number of elements in the list.")
|
|
->Attribute(AZ::Edit::Attributes::Min, 0);
|
|
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Sections")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_hasSections, "Enabled",
|
|
"Whether the list should be divided into sections with headers.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_headerPrototypeElement, "Prototype header",
|
|
"The prototype element to be used for the section headers in the list.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections);
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_stickyHeaders, "Sticky headers",
|
|
"Whether headers should stick to the beginning of the visible list area.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections);
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_variableHeaderElementSize, "Variable header size",
|
|
"Whether headers in the list can vary in size. If not, the header size is fixed and is determined by the prototype element.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections)
|
|
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_autoCalculateHeaderElementSize, "Auto calc header size",
|
|
"Whether header sizes should be auto calculated or whether they should be requested.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::HeadersHaveVariableSizes);
|
|
|
|
editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_estimatedHeaderElementSize, "Estimated header size",
|
|
"The header size to use as an estimate before the header appears in the view. If set to 0, sizes will be calculated up front.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::HeadersHaveVariableSizes)
|
|
->Attribute(AZ::Edit::Attributes::Min, 0.0f);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiDynamicScrollBoxComponent::m_defaultNumSections, "Default num sections",
|
|
"The default number of sections in the list.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections)
|
|
->Attribute(AZ::Edit::Attributes::Min, 1);
|
|
}
|
|
}
|
|
|
|
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
|
|
if (behaviorContext)
|
|
{
|
|
behaviorContext->EBus<UiDynamicScrollBoxBus>("UiDynamicScrollBoxBus")
|
|
->Event("RefreshContent", &UiDynamicScrollBoxBus::Events::RefreshContent)
|
|
->Event("AddElementsToEnd", &UiDynamicScrollBoxBus::Events::AddElementsToEnd)
|
|
->Event("RemoveElementsFromFront", &UiDynamicScrollBoxBus::Events::RemoveElementsFromFront)
|
|
->Event("ScrollToEnd", &UiDynamicScrollBoxBus::Events::ScrollToEnd)
|
|
->Event("GetElementIndexOfChild", &UiDynamicScrollBoxBus::Events::GetElementIndexOfChild)
|
|
->Event("GetSectionIndexOfChild", &UiDynamicScrollBoxBus::Events::GetSectionIndexOfChild)
|
|
->Event("GetChildAtElementIndex", &UiDynamicScrollBoxBus::Events::GetChildAtElementIndex)
|
|
->Event("GetChildAtSectionAndElementIndex", &UiDynamicScrollBoxBus::Events::GetChildAtSectionAndElementIndex)
|
|
->Event("GetAutoRefreshOnPostActivate", &UiDynamicScrollBoxBus::Events::GetAutoRefreshOnPostActivate)
|
|
->Event("SetAutoRefreshOnPostActivate", &UiDynamicScrollBoxBus::Events::SetAutoRefreshOnPostActivate)
|
|
->Event("GetPrototypeElement", &UiDynamicScrollBoxBus::Events::GetPrototypeElement)
|
|
->Event("SetPrototypeElement", &UiDynamicScrollBoxBus::Events::SetPrototypeElement)
|
|
->Event("GetElementsVaryInSize", &UiDynamicScrollBoxBus::Events::GetElementsVaryInSize)
|
|
->Event("SetElementsVaryInSize", &UiDynamicScrollBoxBus::Events::SetElementsVaryInSize)
|
|
->Event("GetAutoCalculateVariableElementSize", &UiDynamicScrollBoxBus::Events::GetAutoCalculateVariableElementSize)
|
|
->Event("SetAutoCalculateVariableElementSize", &UiDynamicScrollBoxBus::Events::SetAutoCalculateVariableElementSize)
|
|
->Event("GetEstimatedVariableElementSize", &UiDynamicScrollBoxBus::Events::GetEstimatedVariableElementSize)
|
|
->Event("SetEstimatedVariableElementSize", &UiDynamicScrollBoxBus::Events::SetEstimatedVariableElementSize)
|
|
->Event("GetSectionsEnabled", &UiDynamicScrollBoxBus::Events::GetSectionsEnabled)
|
|
->Event("SetSectionsEnabled", &UiDynamicScrollBoxBus::Events::SetSectionsEnabled)
|
|
->Event("GetPrototypeHeader", &UiDynamicScrollBoxBus::Events::GetPrototypeHeader)
|
|
->Event("SetPrototypeHeader", &UiDynamicScrollBoxBus::Events::SetPrototypeHeader)
|
|
->Event("GetHeadersSticky", &UiDynamicScrollBoxBus::Events::GetHeadersSticky)
|
|
->Event("SetHeadersSticky", &UiDynamicScrollBoxBus::Events::SetHeadersSticky)
|
|
->Event("GetHeadersVaryInSize", &UiDynamicScrollBoxBus::Events::GetHeadersVaryInSize)
|
|
->Event("SetHeadersVaryInSize", &UiDynamicScrollBoxBus::Events::SetHeadersVaryInSize)
|
|
->Event("GetAutoCalculateVariableHeaderSize", &UiDynamicScrollBoxBus::Events::GetAutoCalculateVariableHeaderSize)
|
|
->Event("SetAutoCalculateVariableHeaderSize", &UiDynamicScrollBoxBus::Events::SetAutoCalculateVariableHeaderSize)
|
|
->Event("GetEstimatedVariableHeaderSize", &UiDynamicScrollBoxBus::Events::GetEstimatedVariableHeaderSize)
|
|
->Event("SetEstimatedVariableHeaderSize", &UiDynamicScrollBoxBus::Events::SetEstimatedVariableHeaderSize)
|
|
;
|
|
|
|
behaviorContext->EBus<UiDynamicScrollBoxDataBus>("UiDynamicScrollBoxDataBus")
|
|
->Handler<BehaviorUiDynamicScrollBoxDataBusHandler>();
|
|
|
|
behaviorContext->EBus<UiDynamicScrollBoxElementNotificationBus>("UiDynamicScrollBoxElementNotificationBus")
|
|
->Handler<BehaviorUiDynamicScrollBoxElementNotificationBusHandler>();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PROTECTED MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::Activate()
|
|
{
|
|
UiDynamicScrollBoxBus::Handler::BusConnect(GetEntityId());
|
|
UiInitializationBus::Handler::BusConnect(GetEntityId());
|
|
UiElementNotificationBus::Handler::BusConnect(GetEntityId());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::Deactivate()
|
|
{
|
|
UiDynamicScrollBoxBus::Handler::BusDisconnect();
|
|
UiInitializationBus::Handler::BusDisconnect();
|
|
if (UiTransformChangeNotificationBus::Handler::BusIsConnected())
|
|
{
|
|
UiTransformChangeNotificationBus::Handler::BusDisconnect();
|
|
}
|
|
if (UiScrollBoxNotificationBus::Handler::BusIsConnected())
|
|
{
|
|
UiScrollBoxNotificationBus::Handler::BusDisconnect();
|
|
}
|
|
UiElementNotificationBus::Handler::BusDisconnect();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::PrepareListForDisplay()
|
|
{
|
|
if (m_listPreparedForDisplay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Set whether list is vertical or horizontal
|
|
m_isVertical = true;
|
|
EBUS_EVENT_ID_RESULT(m_isVertical, GetEntityId(), UiScrollBoxBus, GetIsVerticalScrollingEnabled);
|
|
|
|
m_variableElementSize[ElementType::Item] = m_variableItemElementSize;
|
|
m_autoCalculateElementSize[ElementType::Item] = m_variableItemElementSize ? m_autoCalculateItemElementSize : false;
|
|
m_estimatedElementSize[ElementType::Item] = m_variableItemElementSize ? m_estimatedItemElementSize : 0.0f;
|
|
m_variableElementSize[ElementType::SectionHeader] = m_hasSections ? m_variableHeaderElementSize : false;
|
|
m_autoCalculateElementSize[ElementType::SectionHeader] = (m_hasSections && m_variableHeaderElementSize) ? m_autoCalculateHeaderElementSize : false;
|
|
m_estimatedElementSize[ElementType::SectionHeader] = (m_hasSections && m_variableHeaderElementSize) ? m_estimatedHeaderElementSize : 0.0f;
|
|
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
m_prototypeElement[i].SetInvalid();
|
|
}
|
|
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
int numChildren = 0;
|
|
EBUS_EVENT_ID_RESULT(numChildren, contentEntityId, UiElementBus, GetNumChildElements);
|
|
|
|
// Make sure the item prototype element isn't pointing to itself (the dynamic scroll box) or an ancestor,
|
|
// otherwise this scroll box will spawn scroll boxes recursively ad infinitum.
|
|
if (IsValidPrototype(m_itemPrototypeElement))
|
|
{
|
|
m_prototypeElement[ElementType::Item] = m_itemPrototypeElement;
|
|
}
|
|
else
|
|
{
|
|
if (m_itemPrototypeElement.IsValid())
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", false,
|
|
"The prototype element is not safe for cloning. "
|
|
"This scroll box's prototype element contains the scroll box itself which can result in recursively spawning scroll boxes. "
|
|
"Please change the prototype element to a nonancestral entity.");
|
|
}
|
|
|
|
// Find the prototype element as the first child of the content element
|
|
if (numChildren > 0)
|
|
{
|
|
AZ::EntityId prototypeEntityId;
|
|
EBUS_EVENT_ID_RESULT(prototypeEntityId, contentEntityId, UiElementBus, GetChildEntityId, 0);
|
|
m_prototypeElement[ElementType::Item] = prototypeEntityId;
|
|
}
|
|
}
|
|
|
|
if (m_hasSections)
|
|
{
|
|
if (IsValidPrototype(m_headerPrototypeElement))
|
|
{
|
|
// Prototype header element is defined in properties
|
|
m_prototypeElement[ElementType::SectionHeader] = m_headerPrototypeElement;
|
|
}
|
|
else if(m_headerPrototypeElement.IsValid())
|
|
{
|
|
AZ_Warning("UiDynamicScrollBoxComponent", false,
|
|
"The selected prototype header is not safe for cloning. "
|
|
"This scroll box's prototype header contains the scroll box itself which can result in recursively spawning scroll boxes. "
|
|
"Please change the header to a nonancestral entity.");
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
m_isPrototypeElementNavigable[i] = false;
|
|
m_prototypeElementSize[i] = 0.0f;
|
|
|
|
if (m_prototypeElement[i].IsValid())
|
|
{
|
|
m_isPrototypeElementNavigable[i] = UiNavigationHelpers::IsElementInteractableAndNavigable(m_prototypeElement[i]);
|
|
|
|
// Store the size of the item prototype element for future content element size calculations
|
|
AZ::Vector2 prototypeElementSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(prototypeElementSize, m_prototypeElement[i], UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
m_prototypeElementSize[i] = (m_isVertical ? prototypeElementSize.GetY() : prototypeElementSize.GetX());
|
|
|
|
// Set anchors to top or left
|
|
SetElementAnchors(m_prototypeElement[i]);
|
|
}
|
|
}
|
|
|
|
AZ::Entity* contentEntity = GetContentEntity();
|
|
if (contentEntity)
|
|
{
|
|
// Get the content entity's element component
|
|
UiElementComponent* elementComponent = contentEntity->FindComponent<UiElementComponent>();
|
|
AZ_Assert(elementComponent, "entity has no UiElementComponent");
|
|
|
|
if (elementComponent)
|
|
{
|
|
// Remove any extra elements
|
|
for (int i = numChildren - 1; i >= 0; i--)
|
|
{
|
|
AZ::EntityId entityId;
|
|
EBUS_EVENT_ID_RESULT(entityId, contentEntityId, UiElementBus, GetChildEntityId, i);
|
|
|
|
// Remove the child element
|
|
elementComponent->RemoveChild(entityId);
|
|
|
|
if (!IsPrototypeElement(entityId))
|
|
{
|
|
EBUS_EVENT_ID(entityId, UiElementBus, DestroyElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the content's parent
|
|
AZ::EntityId contentParentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
|
|
|
|
// Create an entity that will be used as the sticky header
|
|
m_currentStickyHeader.m_elementIndex = -1;
|
|
m_currentStickyHeader.m_indexInfo.m_sectionIndex = -1;
|
|
m_currentStickyHeader.m_indexInfo.m_itemIndexInSection = -1;
|
|
m_currentStickyHeader.m_type = ElementType::SectionHeader;
|
|
if (m_hasSections && m_stickyHeaders && contentParentEntityId.IsValid())
|
|
{
|
|
m_currentStickyHeader.m_element = ClonePrototypeElement(ElementType::SectionHeader, contentParentEntityId);
|
|
EBUS_EVENT_ID(m_currentStickyHeader.m_element, UiElementBus, SetIsEnabled, false);
|
|
}
|
|
|
|
// Listen for canvas space rect changes of the content's parent
|
|
if (contentParentEntityId.IsValid())
|
|
{
|
|
UiTransformChangeNotificationBus::Handler::BusConnect(contentParentEntityId);
|
|
}
|
|
|
|
// Listen to scrollbox scrolling events
|
|
UiScrollBoxNotificationBus::Handler::BusConnect(GetEntityId());
|
|
}
|
|
|
|
m_listPreparedForDisplay = true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Entity* UiDynamicScrollBoxComponent::GetContentEntity() const
|
|
{
|
|
AZ::Entity* contentEntity = nullptr;
|
|
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (contentEntityId.IsValid())
|
|
{
|
|
EBUS_EVENT_RESULT(contentEntity, AZ::ComponentApplicationBus, FindEntity, contentEntityId);
|
|
}
|
|
|
|
return contentEntity;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::ClonePrototypeElement(ElementType elementType, AZ::EntityId parentEntityId) const
|
|
{
|
|
AZ::EntityId element;
|
|
|
|
// Clone the prototype element and add it as a child of the specified parent (defaults to content entity)
|
|
AZ::Entity* prototypeEntity = nullptr;
|
|
EBUS_EVENT_RESULT(prototypeEntity, AZ::ComponentApplicationBus, FindEntity, m_prototypeElement[elementType]);
|
|
if (prototypeEntity)
|
|
{
|
|
if (!parentEntityId.IsValid())
|
|
{
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
parentEntityId = contentEntityId;
|
|
}
|
|
|
|
// Find the parent entity
|
|
AZ::Entity* parentEntity = nullptr;
|
|
EBUS_EVENT_RESULT(parentEntity, AZ::ComponentApplicationBus, FindEntity, parentEntityId);
|
|
|
|
if (parentEntity)
|
|
{
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
|
|
AZ::Entity* clonedElement = nullptr;
|
|
EBUS_EVENT_ID_RESULT(clonedElement, canvasEntityId, UiCanvasBus, CloneElement, prototypeEntity, parentEntity);
|
|
|
|
if (clonedElement)
|
|
{
|
|
element = clonedElement->GetId();
|
|
}
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::IsPrototypeElement(AZ::EntityId entityId) const
|
|
{
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
if (m_prototypeElement[i] == entityId)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::AllPrototypeElementsValid() const
|
|
{
|
|
return (m_prototypeElement[ElementType::Item].IsValid() && (!m_hasSections || m_prototypeElement[ElementType::SectionHeader].IsValid()));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::AnyPrototypeElementsNavigable() const
|
|
{
|
|
return (m_isPrototypeElementNavigable[ElementType::Item] || (m_hasSections && m_isPrototypeElementNavigable[ElementType::SectionHeader]));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::AnyElementTypesHaveVariableSize() const
|
|
{
|
|
return (m_variableElementSize[ElementType::Item] || (m_hasSections && m_variableElementSize[ElementType::SectionHeader]));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::AnyElementTypesHaveEstimatedSizes() const
|
|
{
|
|
return (m_estimatedElementSize[ElementType::Item] > 0.0f || (m_hasSections && m_estimatedElementSize[ElementType::SectionHeader] > 0.0f));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::AllElementTypesHaveEstimatedSizes() const
|
|
{
|
|
return (m_estimatedElementSize[ElementType::Item] > 0.0f && (!m_hasSections || m_estimatedElementSize[ElementType::SectionHeader] > 0.0f));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::StickyHeadersEnabled() const
|
|
{
|
|
return (m_hasSections && m_stickyHeaders && m_currentStickyHeader.m_element.IsValid());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::ResizeContentToFitElements()
|
|
{
|
|
if (!AllPrototypeElementsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the number of elements in the list
|
|
if (!m_hasSections)
|
|
{
|
|
m_sections.clear();
|
|
|
|
m_numElements = m_defaultNumElements;
|
|
EBUS_EVENT_ID_RESULT(m_numElements, GetEntityId(), UiDynamicScrollBoxDataBus, GetNumElements);
|
|
}
|
|
else
|
|
{
|
|
int numSections = m_defaultNumSections;
|
|
EBUS_EVENT_ID_RESULT(numSections, GetEntityId(), UiDynamicScrollBoxDataBus, GetNumSections);
|
|
numSections = AZ::GetMax(numSections, 1);
|
|
|
|
m_sections.clear();
|
|
m_sections.reserve(numSections);
|
|
m_numElements = 0;
|
|
for (int i = 0; i < numSections; i++)
|
|
{
|
|
int numItems = m_defaultNumElements;
|
|
EBUS_EVENT_ID_RESULT(numItems, GetEntityId(), UiDynamicScrollBoxDataBus, GetNumElementsInSection, i);
|
|
|
|
Section section;
|
|
section.m_index = i;
|
|
section.m_numItems = numItems;
|
|
section.m_headerElementIndex = m_numElements;
|
|
m_sections.push_back(section);
|
|
|
|
m_numElements += 1 + section.m_numItems;
|
|
}
|
|
}
|
|
|
|
// Calculate new content size
|
|
float newSize = 0.0f;
|
|
if (!AnyElementTypesHaveVariableSize())
|
|
{
|
|
if (!m_hasSections)
|
|
{
|
|
newSize = m_numElements * m_prototypeElementSize[ElementType::Item];
|
|
}
|
|
else
|
|
{
|
|
int numHeaders = static_cast<int>(m_sections.size());
|
|
int numItems = m_numElements - numHeaders;
|
|
newSize = numHeaders * m_prototypeElementSize[ElementType::SectionHeader] + numItems * m_prototypeElementSize[ElementType::Item];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Some element types have variable element sizes
|
|
|
|
// Reset cached element info
|
|
m_cachedElementInfo.clear();
|
|
m_cachedElementInfo.reserve(m_numElements);
|
|
m_cachedElementInfo.insert(m_cachedElementInfo.end(), m_numElements, CachedElementInfo());
|
|
|
|
if (AllElementTypesHaveEstimatedSizes())
|
|
{
|
|
if (!m_hasSections)
|
|
{
|
|
newSize = m_numElements * m_estimatedElementSize[ElementType::Item];
|
|
}
|
|
else
|
|
{
|
|
int numHeaders = static_cast<int>(m_sections.size());
|
|
int numItems = m_numElements - numHeaders;
|
|
newSize = numHeaders * m_estimatedElementSize[ElementType::SectionHeader] + numItems * m_estimatedElementSize[ElementType::Item];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < m_numElements; i++)
|
|
{
|
|
newSize += GetAndCacheVariableElementSize(i);
|
|
}
|
|
|
|
DisableElementsForAutoSizeCalculation();
|
|
}
|
|
|
|
m_averageElementSize = 0.0f;
|
|
m_numElementsUsedForAverage = 0;
|
|
|
|
UpdateAverageElementSize(m_numElements, newSize);
|
|
}
|
|
|
|
// Resize content element
|
|
ResizeContentElement(newSize);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::ResizeContentElement(float newSize) const
|
|
{
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get current content size
|
|
AZ::Vector2 curContentSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(curContentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
float curSize = m_isVertical ? curContentSize.GetY() : curContentSize.GetX();
|
|
|
|
if (newSize != curSize)
|
|
{
|
|
// Resize content element
|
|
|
|
UiTransform2dInterface::Offsets offsets;
|
|
EBUS_EVENT_ID_RESULT(offsets, contentEntityId, UiTransform2dBus, GetOffsets);
|
|
|
|
AZ::Vector2 pivot;
|
|
EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);
|
|
|
|
float sizeDiff = newSize - curSize;
|
|
|
|
if (m_isVertical)
|
|
{
|
|
offsets.m_top -= sizeDiff * pivot.GetY();
|
|
offsets.m_bottom += sizeDiff * (1.0f - pivot.GetY());
|
|
}
|
|
else
|
|
{
|
|
offsets.m_left -= sizeDiff * pivot.GetX();
|
|
offsets.m_right += sizeDiff * (1.0f - pivot.GetX());
|
|
}
|
|
|
|
EBUS_EVENT_ID(contentEntityId, UiTransform2dBus, SetOffsets, offsets);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::AdjustContentSizeAndScrollOffsetByDelta(float sizeDelta, float scrollDelta) const
|
|
{
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get content size
|
|
AZ::Vector2 contentSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(contentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
if (sizeDelta != 0.0f)
|
|
{
|
|
if (m_isVertical)
|
|
{
|
|
contentSize.SetY(contentSize.GetY() + sizeDelta);
|
|
}
|
|
else
|
|
{
|
|
contentSize.SetX(contentSize.GetX() + sizeDelta);
|
|
}
|
|
}
|
|
|
|
// Get scroll offset
|
|
AZ::Vector2 scrollOffset(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(scrollOffset, GetEntityId(), UiScrollBoxBus, GetScrollOffset);
|
|
|
|
if (scrollDelta != 0.0f)
|
|
{
|
|
if (m_isVertical)
|
|
{
|
|
scrollOffset.SetY(scrollOffset.GetY() + scrollDelta);
|
|
}
|
|
else
|
|
{
|
|
scrollOffset.SetX(scrollOffset.GetX() + scrollDelta);
|
|
}
|
|
}
|
|
|
|
EBUS_EVENT_ID(GetEntityId(), UiScrollBoxBus, ChangeContentSizeAndScrollOffset, contentSize, scrollOffset);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::CalculateVariableElementSize(int index)
|
|
{
|
|
float size = 0.0f;
|
|
|
|
AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);
|
|
|
|
if (index < 0 || index >= m_numElements)
|
|
{
|
|
return size;
|
|
}
|
|
|
|
ElementType elementType = GetElementTypeAtIndex(index);
|
|
|
|
if (!m_autoCalculateElementSize[elementType])
|
|
{
|
|
if (m_isVertical)
|
|
{
|
|
if (!m_hasSections)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementHeight, index);
|
|
}
|
|
else
|
|
{
|
|
ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(index);
|
|
if (elementType == ElementType::Item)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementInSectionHeight, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
|
|
}
|
|
else if (elementType == ElementType::SectionHeader)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetSectionHeaderHeight, elementIndexInfo.m_sectionIndex);
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "unknown element type");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!m_hasSections)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementWidth, index);
|
|
}
|
|
else
|
|
{
|
|
ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(index);
|
|
if (elementType == ElementType::Item)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementInSectionWidth, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
|
|
}
|
|
else if (elementType == ElementType::SectionHeader)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetSectionHeaderWidth, elementIndexInfo.m_sectionIndex);
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "unknown element type");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AZ::EntityId elementForAutoSizeCalculation = GetElementForAutoSizeCalculation(elementType);
|
|
|
|
// Auto calculate the size of the element
|
|
AZ_Assert(elementForAutoSizeCalculation.IsValid(), "elementForAutoSizeCalculation is invalid");
|
|
|
|
// Notify listeners to setup this element for auto calculation
|
|
if (!m_hasSections)
|
|
{
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnPrepareElementForSizeCalculation, elementForAutoSizeCalculation, index);
|
|
}
|
|
else
|
|
{
|
|
ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(index);
|
|
if (elementType == ElementType::Item)
|
|
{
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnPrepareElementInSectionForSizeCalculation, elementForAutoSizeCalculation, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
|
|
}
|
|
else if (elementType == ElementType::SectionHeader)
|
|
{
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnPrepareSectionHeaderForSizeCalculation, elementForAutoSizeCalculation, elementIndexInfo.m_sectionIndex);
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "unknown element type");
|
|
}
|
|
}
|
|
size = AutoCalculateElementSize(elementForAutoSizeCalculation);
|
|
}
|
|
|
|
// Cache the calculated size
|
|
m_cachedElementInfo[index].m_size = size;
|
|
|
|
return size;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetAndCacheVariableElementSize(int index)
|
|
{
|
|
float size = 0.0f;
|
|
|
|
AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);
|
|
|
|
if (index < 0 || index >= m_numElements)
|
|
{
|
|
return size;
|
|
}
|
|
|
|
if (m_cachedElementInfo[index].m_size >= 0.0f)
|
|
{
|
|
// Use the cached size
|
|
size = m_cachedElementInfo[index].m_size;
|
|
}
|
|
else
|
|
{
|
|
ElementType elementType = GetElementTypeAtIndex(index);
|
|
|
|
if (!m_variableElementSize[elementType])
|
|
{
|
|
// Use the prototype element size
|
|
size = m_prototypeElementSize[elementType];
|
|
|
|
// Cache the calculated size
|
|
m_cachedElementInfo[index].m_size = size;
|
|
|
|
// Cache the accumulated size
|
|
m_cachedElementInfo[index].m_accumulatedSize = GetVariableSizeElementOffset(index) + size;
|
|
}
|
|
else if (m_estimatedElementSize[elementType] > 0.0f)
|
|
{
|
|
// Uses the estimated element size
|
|
size = m_estimatedElementSize[elementType];
|
|
}
|
|
else
|
|
{
|
|
size = CalculateVariableElementSize(index);
|
|
|
|
// Cache the accumulated size
|
|
m_cachedElementInfo[index].m_accumulatedSize = GetVariableSizeElementOffset(index) + size;
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetVariableElementSize(int index) const
|
|
{
|
|
float size = 0.0f;
|
|
|
|
AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);
|
|
|
|
if (index < 0 || index >= m_numElements)
|
|
{
|
|
return size;
|
|
}
|
|
|
|
if (m_cachedElementInfo[index].m_size >= 0.0f)
|
|
{
|
|
// Use the cached size
|
|
size = m_cachedElementInfo[index].m_size;
|
|
}
|
|
else
|
|
{
|
|
ElementType elementType = GetElementTypeAtIndex(index);
|
|
|
|
if (m_estimatedElementSize[elementType] > 0.0f)
|
|
{
|
|
// Uses the estimated element size
|
|
size = m_estimatedElementSize[elementType];
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "GetVariableElementSize is being called before size is known");
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::GetLastKnownAccumulatedSizeIndex(int index, int numElementsWithUnknownSizeOut[ElementType::NumElementTypes]) const
|
|
{
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
numElementsWithUnknownSizeOut[i] = 0;
|
|
}
|
|
|
|
for (int i = index - 1; i >= 0; i--)
|
|
{
|
|
if (m_cachedElementInfo[i].m_accumulatedSize >= 0.0f)
|
|
{
|
|
return i;
|
|
}
|
|
|
|
ElementType elementType = GetElementTypeAtIndex(i);
|
|
++numElementsWithUnknownSizeOut[elementType];
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetElementOffsetAtIndex(int index) const
|
|
{
|
|
float offset = 0.0f;
|
|
|
|
if (!AnyElementTypesHaveVariableSize())
|
|
{
|
|
offset = GetFixedSizeElementOffset(index);
|
|
}
|
|
else
|
|
{
|
|
offset = GetVariableSizeElementOffset(index);
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetFixedSizeElementOffset(int index) const
|
|
{
|
|
float offset = 0.0f;
|
|
|
|
if (!m_hasSections)
|
|
{
|
|
offset = m_prototypeElementSize[ElementType::Item] * index;
|
|
}
|
|
else
|
|
{
|
|
int numHeaders = 0;
|
|
int numItems = 0;
|
|
|
|
int numSections = static_cast<int>(m_sections.size());
|
|
if (numSections > 0)
|
|
{
|
|
if (index > m_sections[numSections - 1].m_headerElementIndex)
|
|
{
|
|
numHeaders = numSections;
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < numSections; i++)
|
|
{
|
|
if (index <= m_sections[i].m_headerElementIndex)
|
|
{
|
|
numHeaders = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
numItems = index - numHeaders;
|
|
}
|
|
|
|
offset = numHeaders * m_prototypeElementSize[ElementType::SectionHeader] + numItems * m_prototypeElementSize[ElementType::Item];
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetVariableSizeElementOffset(int index) const
|
|
{
|
|
float offset = 0.0f;
|
|
|
|
AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);
|
|
|
|
if (index < 0 || index >= m_numElements)
|
|
{
|
|
return offset;
|
|
}
|
|
|
|
if (index > 0)
|
|
{
|
|
if (m_cachedElementInfo[index - 1].m_accumulatedSize >= 0.0f)
|
|
{
|
|
offset = m_cachedElementInfo[index - 1].m_accumulatedSize;
|
|
}
|
|
else
|
|
{
|
|
// Calculate the accumulated size
|
|
int numElementsWithUnknownSizeOut[ElementType::NumElementTypes];
|
|
int lastKnownIndex = GetLastKnownAccumulatedSizeIndex(index, numElementsWithUnknownSizeOut);
|
|
|
|
offset = lastKnownIndex >= 0 ? m_cachedElementInfo[lastKnownIndex].m_accumulatedSize : 0.0f;
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
offset += numElementsWithUnknownSizeOut[i] * m_estimatedElementSize[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::UpdateAverageElementSize(int numAddedElements, float sizeDelta)
|
|
{
|
|
float curTotalSize = m_averageElementSize * m_numElementsUsedForAverage;
|
|
|
|
m_numElementsUsedForAverage += numAddedElements;
|
|
m_averageElementSize = (m_numElementsUsedForAverage > 0) ? (AZ::GetMax(curTotalSize + sizeDelta, 0.0f) / m_numElementsUsedForAverage) : 0.0f;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::ClearDisplayedElements()
|
|
{
|
|
for (const auto& e : m_displayedElements)
|
|
{
|
|
ElementType elementType = e.m_type;
|
|
m_recycledElements[elementType].push_front(e.m_element);
|
|
|
|
// Disable element
|
|
EBUS_EVENT_ID(e.m_element, UiElementBus, SetIsEnabled, false);
|
|
}
|
|
|
|
m_displayedElements.clear();
|
|
|
|
m_firstDisplayedElementIndex = -1;
|
|
m_lastDisplayedElementIndex = -1;
|
|
m_firstVisibleElementIndex = -1;
|
|
m_lastVisibleElementIndex = -1;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::FindDisplayedElementWithIndex(int index) const
|
|
{
|
|
AZ::EntityId elementId;
|
|
|
|
for (const auto& e : m_displayedElements)
|
|
{
|
|
if (e.m_elementIndex == index)
|
|
{
|
|
elementId = e.m_element;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return elementId;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::GetVisibleAreaSize() const
|
|
{
|
|
float visibleAreaSize = 0.0f;
|
|
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return visibleAreaSize;
|
|
}
|
|
|
|
// Get content's parent
|
|
AZ::EntityId contentParentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
|
|
if (!contentParentEntityId.IsValid())
|
|
{
|
|
return visibleAreaSize;
|
|
}
|
|
|
|
// Get content parent's size in canvas space
|
|
AZ::Vector2 contentParentSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(contentParentSize, contentParentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
if (m_isVertical)
|
|
{
|
|
visibleAreaSize = contentParentSize.GetY();
|
|
}
|
|
else
|
|
{
|
|
visibleAreaSize = contentParentSize.GetX();
|
|
}
|
|
|
|
return visibleAreaSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::AreAnyElementsVisible(AZ::Vector2& visibleContentBoundsOut) const
|
|
{
|
|
if (m_numElements == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get content's parent
|
|
AZ::EntityId contentParentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
|
|
if (!contentParentEntityId.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get content's rect in canvas space
|
|
UiTransformInterface::Rect contentRect;
|
|
EBUS_EVENT_ID(contentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentRect);
|
|
|
|
// Get content parent's rect in canvas space
|
|
UiTransformInterface::Rect parentRect;
|
|
EBUS_EVENT_ID(contentParentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect);
|
|
|
|
// Check if any items are visible
|
|
AZ::Vector2 minA(contentRect.left, contentRect.top);
|
|
AZ::Vector2 maxA(contentRect.right, contentRect.bottom);
|
|
AZ::Vector2 minB(parentRect.left, parentRect.top);
|
|
AZ::Vector2 maxB(parentRect.right, parentRect.bottom);
|
|
|
|
bool boxesIntersect = true;
|
|
|
|
if (maxA.GetX() < minB.GetX() || // a is left of b
|
|
minA.GetX() > maxB.GetX() || // a is right of b
|
|
maxA.GetY() < minB.GetY() || // a is above b
|
|
minA.GetY() > maxB.GetY()) // a is below b
|
|
{
|
|
boxesIntersect = false; // no overlap
|
|
}
|
|
|
|
if (boxesIntersect)
|
|
{
|
|
// Set visible content bounds
|
|
if (m_isVertical)
|
|
{
|
|
// Set top offset
|
|
visibleContentBoundsOut.SetX(AZ::GetMax(parentRect.top - contentRect.top, 0.0f));
|
|
|
|
// Set bottom offset
|
|
visibleContentBoundsOut.SetY(AZ::GetMin(parentRect.bottom, contentRect.bottom) - contentRect.top);
|
|
}
|
|
else
|
|
{
|
|
// Set left offset
|
|
visibleContentBoundsOut.SetX(AZ::GetMax(parentRect.left - contentRect.left, 0.0f));
|
|
|
|
// Set right offset
|
|
visibleContentBoundsOut.SetY(AZ::GetMin(parentRect.right, contentRect.right) - contentRect.left);
|
|
}
|
|
}
|
|
|
|
return boxesIntersect;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::UpdateElementVisibility(bool keepAtEndIfWasAtEnd)
|
|
{
|
|
// Calculate which elements are visible
|
|
int firstVisibleElementIndex = -1;
|
|
int lastVisibleElementIndex = -1;
|
|
int firstDisplayedElementIndex = -1;
|
|
int lastDisplayedElementIndex = -1;
|
|
int firstDisplayedElementIndexWithSizeChange = -1;
|
|
float totalElementSizeChange = 0.0f;
|
|
float scrollChange = 0.0f;
|
|
|
|
AZ::Vector2 visibleContentBounds(0.0f, 0.0f);
|
|
bool elementsVisible = AreAnyElementsVisible(visibleContentBounds);
|
|
|
|
if (elementsVisible)
|
|
{
|
|
CalculateVisibleElementIndices(keepAtEndIfWasAtEnd,
|
|
visibleContentBounds,
|
|
firstVisibleElementIndex,
|
|
lastVisibleElementIndex,
|
|
firstDisplayedElementIndex,
|
|
lastDisplayedElementIndex,
|
|
firstDisplayedElementIndexWithSizeChange,
|
|
totalElementSizeChange,
|
|
scrollChange);
|
|
}
|
|
|
|
m_lastCalculatedVisibleContentOffset = visibleContentBounds.GetX();
|
|
if (totalElementSizeChange != 0.0f)
|
|
{
|
|
m_lastCalculatedVisibleContentOffset += CalculateContentBeginningDeltaAfterSizeChange(totalElementSizeChange);
|
|
}
|
|
|
|
if (StickyHeadersEnabled())
|
|
{
|
|
UpdateStickyHeader(firstVisibleElementIndex, lastVisibleElementIndex, m_lastCalculatedVisibleContentOffset);
|
|
}
|
|
|
|
// Remove the elements that are no longer being displayed
|
|
m_displayedElements.remove_if(
|
|
[this, firstDisplayedElementIndex, lastDisplayedElementIndex](const DisplayedElement& e)
|
|
{
|
|
if ((firstDisplayedElementIndex < 0) || (e.m_elementIndex < firstDisplayedElementIndex) || (e.m_elementIndex > lastDisplayedElementIndex))
|
|
{
|
|
// This element is no longer being displayed, move it to the recycled elements list
|
|
m_recycledElements[e.m_type].push_front(e.m_element);
|
|
|
|
// Disable element
|
|
EBUS_EVENT_ID(e.m_element, UiElementBus, SetIsEnabled, false);
|
|
|
|
// Remove element from the displayed element list
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Add the newly displayed elements
|
|
if (firstDisplayedElementIndex >= 0)
|
|
{
|
|
for (int i = firstDisplayedElementIndex; i <= lastDisplayedElementIndex; i++)
|
|
{
|
|
if (!IsElementDisplayedAtIndex(i))
|
|
{
|
|
ElementType elementType = GetElementTypeAtIndex(i);
|
|
ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(i);
|
|
|
|
AZ::EntityId element = GetElementForDisplay(elementType);
|
|
DisplayedElement elementEntry;
|
|
elementEntry.m_element = element;
|
|
elementEntry.m_elementIndex = i;
|
|
elementEntry.m_indexInfo = elementIndexInfo;
|
|
elementEntry.m_type = elementType;
|
|
|
|
m_displayedElements.push_front(elementEntry);
|
|
|
|
if (m_variableElementSize[elementType])
|
|
{
|
|
SizeVariableElementAtIndex(element, i);
|
|
}
|
|
|
|
PositionElementAtIndex(element, i);
|
|
|
|
// Notify listeners that this element is about to be displayed
|
|
if (!m_hasSections)
|
|
{
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnElementBecomingVisible, element, i);
|
|
}
|
|
else
|
|
{
|
|
if (elementType == ElementType::Item)
|
|
{
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnElementInSectionBecomingVisible, element, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
|
|
}
|
|
else if (elementType == ElementType::SectionHeader)
|
|
{
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnSectionHeaderBecomingVisible, element, elementIndexInfo.m_sectionIndex);
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "unknown element type");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (firstDisplayedElementIndexWithSizeChange >= 0 && firstDisplayedElementIndexWithSizeChange <= i)
|
|
{
|
|
AZ::EntityId element = FindDisplayedElementWithIndex(i);
|
|
PositionElementAtIndex(element, i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_firstVisibleElementIndex = firstVisibleElementIndex;
|
|
m_lastVisibleElementIndex = lastVisibleElementIndex;
|
|
m_firstDisplayedElementIndex = firstDisplayedElementIndex;
|
|
m_lastDisplayedElementIndex = lastDisplayedElementIndex;
|
|
|
|
if (totalElementSizeChange != 0.0f || scrollChange != 0.0f)
|
|
{
|
|
AdjustContentSizeAndScrollOffsetByDelta(totalElementSizeChange, scrollChange);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::CalculateVisibleElementIndices(bool keepAtEndIfWasAtEnd,
|
|
const AZ::Vector2& visibleContentBounds,
|
|
int& firstVisibleElementIndexOut,
|
|
int& lastVisibleElementIndexOut,
|
|
int& firstDisplayedElementIndexOut,
|
|
int& lastDisplayedElementIndexOut,
|
|
int& firstDisplayedElementIndexWithSizeChangeOut,
|
|
float& totalElementSizeChangeOut,
|
|
float& scrollChangeOut)
|
|
{
|
|
firstVisibleElementIndexOut = -1;
|
|
lastVisibleElementIndexOut = -1;
|
|
firstDisplayedElementIndexOut = -1;
|
|
lastDisplayedElementIndexOut = -1;
|
|
firstDisplayedElementIndexWithSizeChangeOut = -1;
|
|
totalElementSizeChangeOut = 0.0f;
|
|
scrollChangeOut = 0.0f;
|
|
|
|
if (!AllPrototypeElementsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool addedExtraElementsForNavigation = false;
|
|
|
|
if (!AnyElementTypesHaveVariableSize())
|
|
{
|
|
// All elements are the same size
|
|
FindVisibleElementIndicesForFixedSizes(visibleContentBounds, firstVisibleElementIndexOut, lastVisibleElementIndexOut);
|
|
}
|
|
else
|
|
{
|
|
// Elements vary in size
|
|
|
|
if (AnyElementTypesHaveEstimatedSizes())
|
|
{
|
|
// We may not have the real sizes of all the elements yet
|
|
|
|
// Find the first elment index that's visible and that will remain in the same position
|
|
int visibleElementIndex = -1;
|
|
|
|
bool keepAtEnd = keepAtEndIfWasAtEnd && IsScrolledToEnd();
|
|
if (keepAtEnd)
|
|
{
|
|
visibleElementIndex = m_numElements - 1;
|
|
}
|
|
else
|
|
{
|
|
visibleElementIndex = FindVisibleElementIndexToRemainInPlace(visibleContentBounds);
|
|
}
|
|
|
|
// Calculate the first and last visible elements without moving the beginning (top or left) of the specified visible element index
|
|
CalculateVisibleElementIndicesFromVisibleElementIndex(visibleElementIndex,
|
|
visibleContentBounds,
|
|
keepAtEnd,
|
|
firstVisibleElementIndexOut,
|
|
lastVisibleElementIndexOut,
|
|
firstDisplayedElementIndexOut,
|
|
lastDisplayedElementIndexOut,
|
|
firstDisplayedElementIndexWithSizeChangeOut,
|
|
totalElementSizeChangeOut,
|
|
scrollChangeOut);
|
|
|
|
addedExtraElementsForNavigation = true;
|
|
}
|
|
else
|
|
{
|
|
// We have the real sizes of all the elements
|
|
|
|
// Estimate a first visible element index
|
|
int estimatedFirstVisibleElementIndex = EstimateFirstVisibleElementIndex(visibleContentBounds);
|
|
|
|
// Look for the real new first visible element index
|
|
float curElementEnd = 0.0f;
|
|
firstVisibleElementIndexOut = FindFirstVisibleElementIndex(estimatedFirstVisibleElementIndex, visibleContentBounds, curElementEnd);
|
|
|
|
// Now find the last visible element index
|
|
lastVisibleElementIndexOut = firstVisibleElementIndexOut;
|
|
while (curElementEnd < visibleContentBounds.GetY() && lastVisibleElementIndexOut < m_numElements - 1)
|
|
{
|
|
++lastVisibleElementIndexOut;
|
|
curElementEnd += GetVariableElementSize(lastVisibleElementIndexOut);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!addedExtraElementsForNavigation)
|
|
{
|
|
firstDisplayedElementIndexOut = firstVisibleElementIndexOut;
|
|
lastDisplayedElementIndexOut = lastVisibleElementIndexOut;
|
|
|
|
AddExtraElementsForNavigation(firstDisplayedElementIndexOut, lastDisplayedElementIndexOut);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::UpdateStickyHeader(int firstVisibleElementIndex, int lastVisibleElementIndex, float visibleContentBeginning)
|
|
{
|
|
// Find which header should currently be sticky
|
|
if (firstVisibleElementIndex >= 0)
|
|
{
|
|
ElementIndexInfo firstVisibleElementIndexInfo = GetElementIndexInfoFromIndex(firstVisibleElementIndex);
|
|
int newStickyHeaderElementIndex = m_sections[firstVisibleElementIndexInfo.m_sectionIndex].m_headerElementIndex;
|
|
if (newStickyHeaderElementIndex != m_currentStickyHeader.m_elementIndex)
|
|
{
|
|
if (m_currentStickyHeader.m_elementIndex < 0)
|
|
{
|
|
EBUS_EVENT_ID(m_currentStickyHeader.m_element, UiElementBus, SetIsEnabled, true);
|
|
}
|
|
|
|
m_currentStickyHeader.m_elementIndex = newStickyHeaderElementIndex;
|
|
m_currentStickyHeader.m_indexInfo.m_sectionIndex = firstVisibleElementIndexInfo.m_sectionIndex;
|
|
|
|
if (m_variableElementSize[ElementType::SectionHeader])
|
|
{
|
|
SizeVariableElementAtIndex(m_currentStickyHeader.m_element, m_currentStickyHeader.m_elementIndex);
|
|
}
|
|
|
|
EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnSectionHeaderBecomingVisible, m_currentStickyHeader.m_element, m_currentStickyHeader.m_indexInfo.m_sectionIndex);
|
|
}
|
|
|
|
float stickyHeaderOffset = 0.0f;
|
|
|
|
// Check if the current sticky header is being pushed out of the way by another visible header
|
|
int firstVisibleHeaderIndex = FindFirstVisibleHeaderIndex(firstVisibleElementIndex, lastVisibleElementIndex, m_currentStickyHeader.m_elementIndex);
|
|
if (firstVisibleHeaderIndex >= 0)
|
|
{
|
|
// Get the beginning of the first visible header
|
|
float firstVisibleHeaderBeginning = GetElementOffsetAtIndex(firstVisibleHeaderIndex);
|
|
|
|
// Get the end of the current sticky header
|
|
float stickyHeaderSize = !m_variableElementSize[ElementType::SectionHeader] ? m_prototypeElementSize[ElementType::SectionHeader] : GetVariableElementSize(m_currentStickyHeader.m_elementIndex);
|
|
float stickyHeaderEnd = visibleContentBeginning + stickyHeaderSize;
|
|
|
|
// Adjust sticky header offset
|
|
if (firstVisibleHeaderBeginning < stickyHeaderEnd)
|
|
{
|
|
stickyHeaderOffset = firstVisibleHeaderBeginning - stickyHeaderEnd;
|
|
}
|
|
}
|
|
|
|
SetElementOffsets(m_currentStickyHeader.m_element, stickyHeaderOffset);
|
|
}
|
|
else
|
|
{
|
|
m_currentStickyHeader.m_elementIndex = -1;
|
|
m_currentStickyHeader.m_indexInfo.m_sectionIndex = -1;
|
|
|
|
// Hide the sticky header
|
|
EBUS_EVENT_ID(m_currentStickyHeader.m_element, UiElementBus, SetIsEnabled, false);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::FindFirstVisibleHeaderIndex(int firstVisibleElementIndex, int lastVisibleElementIndex, int excludeIndex)
|
|
{
|
|
int firstVisibleHeaderIndex = -1;
|
|
|
|
for (int i = firstVisibleElementIndex; i <= lastVisibleElementIndex; i++)
|
|
{
|
|
if (i != excludeIndex && GetElementTypeAtIndex(i) == ElementType::SectionHeader)
|
|
{
|
|
firstVisibleHeaderIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return firstVisibleHeaderIndex;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::FindVisibleElementIndicesForFixedSizes(const AZ::Vector2& visibleContentBounds,
|
|
int& firstVisibleElementIndexOut,
|
|
int& lastVisibleElementIndexOut) const
|
|
{
|
|
float itemSize = m_prototypeElementSize[ElementType::Item];
|
|
float beginningVisibleOffset = visibleContentBounds.GetX();
|
|
float endVisibleOffset = visibleContentBounds.GetY();
|
|
|
|
if (!m_hasSections)
|
|
{
|
|
if (itemSize > 0.0f)
|
|
{
|
|
// Calculate first visible element index
|
|
firstVisibleElementIndexOut = max(static_cast<int>(ceil(beginningVisibleOffset / itemSize)) - 1, 0);
|
|
|
|
// Calculate last visible element index
|
|
lastVisibleElementIndexOut = static_cast<int>(ceil(endVisibleOffset / itemSize)) - 1;
|
|
int lastElementIndex = max(m_numElements - 1, 0);
|
|
Limit(lastVisibleElementIndexOut, 0, lastElementIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float headerSize = m_prototypeElementSize[ElementType::SectionHeader];
|
|
|
|
if (itemSize > 0.0f || headerSize > 0.0f)
|
|
{
|
|
// Calculate first and last visible element indices
|
|
float curElementOffset = 0.0f;
|
|
int curSectionIndex = 0;
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
int& visibleElementIndex = (i == 0) ? firstVisibleElementIndexOut : lastVisibleElementIndexOut;
|
|
float visibleOffset = (i == 0) ? beginningVisibleOffset : endVisibleOffset;
|
|
|
|
for (; curSectionIndex < m_sections.size(); curSectionIndex++)
|
|
{
|
|
float headerElementEnd = curElementOffset + headerSize;
|
|
if (headerElementEnd >= visibleOffset)
|
|
{
|
|
visibleElementIndex = m_sections[curSectionIndex].m_headerElementIndex;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
float sectionEnd = headerElementEnd + itemSize * m_sections[curSectionIndex].m_numItems;
|
|
if (sectionEnd >= visibleOffset)
|
|
{
|
|
int numItems = 0;
|
|
if (itemSize > 0.0f)
|
|
{
|
|
numItems = static_cast<int>(ceil((visibleOffset - headerElementEnd) / itemSize));
|
|
}
|
|
visibleElementIndex = m_sections[curSectionIndex].m_headerElementIndex + numItems;
|
|
break;
|
|
}
|
|
else if (curSectionIndex == m_sections.size() - 1)
|
|
{
|
|
visibleElementIndex = m_sections[curSectionIndex].m_headerElementIndex + m_sections[curSectionIndex].m_numItems;
|
|
break;
|
|
}
|
|
|
|
curElementOffset = sectionEnd;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::FindVisibleElementIndexToRemainInPlace(const AZ::Vector2& visibleContentBounds) const
|
|
{
|
|
int visibleElementIndex = -1;
|
|
|
|
// Try and find the first previously visible element that's still visible
|
|
int firstPrevVisibleIndexStillVisible = -1;
|
|
if (m_firstVisibleElementIndex >= 0)
|
|
{
|
|
// Check if any of the previously visible elements are still visible
|
|
float prevFirstVisibleBeginning = GetVariableSizeElementOffset(m_firstVisibleElementIndex);
|
|
float prevLastVisibleEnd = GetVariableSizeElementOffset(m_lastVisibleElementIndex) + GetVariableElementSize(m_lastVisibleElementIndex);
|
|
|
|
if (!(prevFirstVisibleBeginning > visibleContentBounds.GetY() || prevLastVisibleEnd < visibleContentBounds.GetX()))
|
|
{
|
|
// Find the first previously visible element that's still visible
|
|
for (int index = m_firstVisibleElementIndex; index <= m_lastVisibleElementIndex; index++)
|
|
{
|
|
if (GetVariableSizeElementOffset(index) + GetVariableElementSize(index) >= visibleContentBounds.GetX())
|
|
{
|
|
firstPrevVisibleIndexStillVisible = index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (firstPrevVisibleIndexStillVisible >= 0)
|
|
{
|
|
visibleElementIndex = firstPrevVisibleIndexStillVisible;
|
|
}
|
|
else
|
|
{
|
|
// No previously visible elements are still visible, so find the first element that's about to become visible
|
|
|
|
// Estimate a first visible element index
|
|
int estimatedFirstVisibleElementIndex = EstimateFirstVisibleElementIndex(visibleContentBounds);
|
|
|
|
// Look for the real new first visible element index
|
|
float firstVisibleElementEnd = 0.0f;
|
|
visibleElementIndex = FindFirstVisibleElementIndex(estimatedFirstVisibleElementIndex, visibleContentBounds, firstVisibleElementEnd);
|
|
|
|
// We actually want the first visible element who's beginning (top or left) is visible if we don't know the first visible element's real size.
|
|
// This is so that we don't end up having to calculate the size of more elements if the real size of the first visible
|
|
// element ends up being smaller than the estimated size
|
|
if (m_cachedElementInfo[visibleElementIndex].m_size < 0.0f && visibleElementIndex < m_numElements - 1)
|
|
{
|
|
float firstVisibleElementBeginning = firstVisibleElementEnd - GetVariableElementSize(visibleElementIndex);
|
|
if (firstVisibleElementBeginning < visibleContentBounds.GetX() && firstVisibleElementEnd < visibleContentBounds.GetY())
|
|
{
|
|
++visibleElementIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
return visibleElementIndex;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::AddExtraElementsForNavigation(int& firstDisplayedElementIndexOut, int& lastDisplayedElementIndexOut) const
|
|
{
|
|
if (AnyPrototypeElementsNavigable())
|
|
{
|
|
if (firstDisplayedElementIndexOut > 0)
|
|
{
|
|
--firstDisplayedElementIndexOut;
|
|
if (m_hasSections)
|
|
{
|
|
int newFirstDisplayedElementIndex = firstDisplayedElementIndexOut;
|
|
while (!m_isPrototypeElementNavigable[GetElementTypeAtIndex(newFirstDisplayedElementIndex)] && newFirstDisplayedElementIndex >= 0)
|
|
{
|
|
--newFirstDisplayedElementIndex;
|
|
}
|
|
if (newFirstDisplayedElementIndex >= 0)
|
|
{
|
|
firstDisplayedElementIndexOut = newFirstDisplayedElementIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lastDisplayedElementIndexOut > -1 && lastDisplayedElementIndexOut < m_numElements - 1)
|
|
{
|
|
++lastDisplayedElementIndexOut;
|
|
if (m_hasSections)
|
|
{
|
|
int newLastDisplayedElementIndex = lastDisplayedElementIndexOut;
|
|
while (!m_isPrototypeElementNavigable[GetElementTypeAtIndex(newLastDisplayedElementIndex)] && newLastDisplayedElementIndex < m_numElements)
|
|
{
|
|
++newLastDisplayedElementIndex;
|
|
}
|
|
if (newLastDisplayedElementIndex < m_numElements)
|
|
{
|
|
lastDisplayedElementIndexOut = newLastDisplayedElementIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::EstimateFirstVisibleElementIndex(const AZ::Vector2& visibleContentBounds) const
|
|
{
|
|
// Estimate an index size that will be close to the new first visible element index
|
|
int estimatedElementIndex = 0;
|
|
|
|
if (m_averageElementSize > 0.0f)
|
|
{
|
|
if (m_firstVisibleElementIndex >= 0)
|
|
{
|
|
// Check how much scrolling has occurred
|
|
float scrollDelta = visibleContentBounds.GetX() - m_lastCalculatedVisibleContentOffset;
|
|
// Estimate the number of elements within the scroll delta
|
|
int estimatedElementIndexOffset = max(static_cast<int>(ceil(fabs(scrollDelta / m_averageElementSize))) - 1, 0);
|
|
estimatedElementIndex = m_firstVisibleElementIndex + (scrollDelta > 0.0f ? estimatedElementIndexOffset : -estimatedElementIndexOffset);
|
|
}
|
|
else
|
|
{
|
|
estimatedElementIndex = max(static_cast<int>(ceil(visibleContentBounds.GetX() / m_averageElementSize)) - 1, 0);
|
|
}
|
|
}
|
|
|
|
estimatedElementIndex = AZ::GetClamp(estimatedElementIndex, 0, m_numElements - 1);
|
|
|
|
return estimatedElementIndex;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::FindFirstVisibleElementIndex(int estimatedIndex, const AZ::Vector2& visibleContentBounds, float& firstVisibleElementEndOut) const
|
|
{
|
|
int curElementIndex = estimatedIndex;
|
|
float curElementPos = GetVariableSizeElementOffset(curElementIndex);
|
|
if (curElementPos <= visibleContentBounds.GetX())
|
|
{
|
|
// Traverse down to find the real new first visible element index
|
|
curElementPos += GetVariableElementSize(curElementIndex);
|
|
while (curElementPos < visibleContentBounds.GetX() && curElementIndex < m_numElements - 1)
|
|
{
|
|
++curElementIndex;
|
|
curElementPos += GetVariableElementSize(curElementIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Traverse up to find the real new first visible element index
|
|
while (curElementPos > visibleContentBounds.GetX() && curElementIndex > 0)
|
|
{
|
|
--curElementIndex;
|
|
curElementPos -= GetVariableElementSize(curElementIndex);
|
|
}
|
|
curElementPos += GetVariableElementSize(curElementIndex);
|
|
}
|
|
|
|
firstVisibleElementEndOut = curElementPos;
|
|
|
|
return curElementIndex;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::CalculateVisibleSpaceBeforeAndAfterElement(int visibleElementIndex,
|
|
bool keepAtEnd,
|
|
float visibleAreaBeginning,
|
|
float& spaceLeftBeforeOut,
|
|
float& spaceLeftAfterOut) const
|
|
{
|
|
float visibleAreaSize = GetVisibleAreaSize();
|
|
|
|
// Calculate space left in the visible area
|
|
spaceLeftBeforeOut = 0.0f;
|
|
spaceLeftAfterOut = 0.0f;
|
|
if (keepAtEnd)
|
|
{
|
|
spaceLeftAfterOut = 0.0f;
|
|
spaceLeftBeforeOut = AZ::GetMax(visibleAreaSize - GetVariableElementSize(visibleElementIndex), 0.0f);
|
|
}
|
|
else
|
|
{
|
|
float visibleElementBeginning = GetVariableSizeElementOffset(visibleElementIndex);
|
|
float visibleElementEnd = visibleElementBeginning + GetVariableElementSize(visibleElementIndex);
|
|
spaceLeftBeforeOut = AZ::GetMax(visibleElementBeginning - visibleAreaBeginning, 0.0f);
|
|
spaceLeftAfterOut = AZ::GetMax(visibleAreaSize - (visibleElementEnd - visibleAreaBeginning), 0.0f);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::CalculateVisibleElementIndicesFromVisibleElementIndex(int visibleElementIndex,
|
|
const AZ::Vector2& visibleContentBound,
|
|
bool keepAtEnd,
|
|
int& firstVisibleElementIndexOut,
|
|
int& lastVisibleElementIndexOut,
|
|
int& firstDisplayedElementIndexOut,
|
|
int& lastDisplayedElementIndexOut,
|
|
int& firstDisplayedElementIndexWithSizeChangeOut,
|
|
float& totalElementSizechangeOut,
|
|
float& scrollChangeOut)
|
|
{
|
|
// From the current element index that we know is going to be at least partly visible,
|
|
// traverse up and down to find the real first and last visible element indices
|
|
|
|
// Track the total change in element size
|
|
float totalSizeChange = 0.0f;
|
|
|
|
// Track the total change in size of elements that are positioned before the passed in
|
|
// visible element index who's beginning (top or left) will remain in the same position
|
|
float totalSizeChangeBeforeFixedVisibleElement = 0.0f;
|
|
|
|
// Keep track of the index of the first element who's size changed
|
|
firstDisplayedElementIndexWithSizeChangeOut = -1;
|
|
|
|
// Check if we need to calculate the real size for the known visible element index
|
|
if (m_cachedElementInfo[visibleElementIndex].m_size < 0.0f)
|
|
{
|
|
float prevSize = GetVariableElementSize(visibleElementIndex);
|
|
float newSize = CalculateVariableElementSize(visibleElementIndex);
|
|
|
|
totalSizeChange = newSize - prevSize;
|
|
firstDisplayedElementIndexWithSizeChangeOut = visibleElementIndex;
|
|
}
|
|
|
|
// Calculate visible space remaining
|
|
float spaceLeftBefore = 0.0f;
|
|
float spaceLeftAfter = 0.0f;
|
|
CalculateVisibleSpaceBeforeAndAfterElement(visibleElementIndex, keepAtEnd, visibleContentBound.GetX(), spaceLeftBefore, spaceLeftAfter);
|
|
|
|
firstVisibleElementIndexOut = visibleElementIndex;
|
|
lastVisibleElementIndexOut = visibleElementIndex;
|
|
firstDisplayedElementIndexOut = firstVisibleElementIndexOut;
|
|
lastDisplayedElementIndexOut = lastVisibleElementIndexOut;
|
|
|
|
bool extraElementsNeededForNavigation = AnyPrototypeElementsNavigable();
|
|
|
|
// Traverse up or left
|
|
bool hadSpaceLeft = true;
|
|
bool addedExtraElements = false;
|
|
while ((spaceLeftBefore > 0.0f || !addedExtraElements) && firstDisplayedElementIndexOut > 0)
|
|
{
|
|
if (spaceLeftBefore <= 0.0f)
|
|
{
|
|
if (hadSpaceLeft)
|
|
{
|
|
firstVisibleElementIndexOut = firstDisplayedElementIndexOut;
|
|
hadSpaceLeft = false;
|
|
}
|
|
|
|
if (!extraElementsNeededForNavigation)
|
|
{
|
|
break;
|
|
}
|
|
|
|
addedExtraElements = !m_hasSections || m_isPrototypeElementNavigable[GetElementTypeAtIndex(firstDisplayedElementIndexOut - 1)];
|
|
}
|
|
|
|
--firstDisplayedElementIndexOut;
|
|
if (m_cachedElementInfo[firstDisplayedElementIndexOut].m_size >= 0.0f)
|
|
{
|
|
spaceLeftBefore -= m_cachedElementInfo[firstDisplayedElementIndexOut].m_size;
|
|
}
|
|
else
|
|
{
|
|
// Calculate this element's size
|
|
float prevSize = GetVariableElementSize(firstDisplayedElementIndexOut);
|
|
float newSize = CalculateVariableElementSize(firstDisplayedElementIndexOut);
|
|
|
|
float sizeChange = newSize - prevSize;
|
|
totalSizeChange += sizeChange;
|
|
|
|
if (firstDisplayedElementIndexOut <= visibleElementIndex)
|
|
{
|
|
totalSizeChangeBeforeFixedVisibleElement += sizeChange;
|
|
}
|
|
|
|
spaceLeftBefore -= newSize;
|
|
|
|
if (firstDisplayedElementIndexWithSizeChangeOut < 0 || firstDisplayedElementIndexOut < firstDisplayedElementIndexWithSizeChangeOut)
|
|
{
|
|
firstDisplayedElementIndexWithSizeChangeOut = firstDisplayedElementIndexOut;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hadSpaceLeft)
|
|
{
|
|
firstVisibleElementIndexOut = firstDisplayedElementIndexOut;
|
|
}
|
|
|
|
// Traverse down or right
|
|
hadSpaceLeft = true;
|
|
addedExtraElements = false;
|
|
while ((spaceLeftAfter > 0.0f || !addedExtraElements) && lastDisplayedElementIndexOut < m_numElements - 1)
|
|
{
|
|
if (spaceLeftAfter <= 0.0f)
|
|
{
|
|
if (hadSpaceLeft)
|
|
{
|
|
lastVisibleElementIndexOut = lastDisplayedElementIndexOut;
|
|
hadSpaceLeft = false;
|
|
}
|
|
|
|
if (!extraElementsNeededForNavigation)
|
|
{
|
|
break;
|
|
}
|
|
|
|
addedExtraElements = !m_hasSections || m_isPrototypeElementNavigable[GetElementTypeAtIndex(lastDisplayedElementIndexOut + 1)];
|
|
}
|
|
|
|
++lastDisplayedElementIndexOut;
|
|
if (m_cachedElementInfo[lastDisplayedElementIndexOut].m_size >= 0.0f)
|
|
{
|
|
spaceLeftAfter -= m_cachedElementInfo[lastDisplayedElementIndexOut].m_size;
|
|
}
|
|
else
|
|
{
|
|
// Calculate this element's size
|
|
float prevSize = GetVariableElementSize(lastDisplayedElementIndexOut);
|
|
float newSize = CalculateVariableElementSize(lastDisplayedElementIndexOut);
|
|
|
|
float sizeChange = newSize - prevSize;
|
|
totalSizeChange += sizeChange;
|
|
|
|
if (lastDisplayedElementIndexOut <= visibleElementIndex)
|
|
{
|
|
totalSizeChangeBeforeFixedVisibleElement += sizeChange;
|
|
}
|
|
|
|
spaceLeftAfter -= newSize;
|
|
|
|
if (firstDisplayedElementIndexWithSizeChangeOut < 0 || lastDisplayedElementIndexOut < firstDisplayedElementIndexWithSizeChangeOut)
|
|
{
|
|
firstDisplayedElementIndexWithSizeChangeOut = lastDisplayedElementIndexOut;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hadSpaceLeft)
|
|
{
|
|
lastVisibleElementIndexOut = lastDisplayedElementIndexOut;
|
|
}
|
|
|
|
if (StickyHeadersEnabled())
|
|
{
|
|
// Check which header should currently be sticky and calculate its size if needed
|
|
if (firstVisibleElementIndexOut >= 0)
|
|
{
|
|
ElementIndexInfo firstVisibleElementIndexInfo = GetElementIndexInfoFromIndex(firstVisibleElementIndexOut);
|
|
int stickyHeaderElementIndex = m_sections[firstVisibleElementIndexInfo.m_sectionIndex].m_headerElementIndex;
|
|
|
|
if (m_cachedElementInfo[stickyHeaderElementIndex].m_size < 0.0f)
|
|
{
|
|
// Calculate this element's size
|
|
float prevSize = GetVariableElementSize(stickyHeaderElementIndex);
|
|
float newSize = CalculateVariableElementSize(stickyHeaderElementIndex);
|
|
|
|
float sizeChange = newSize - prevSize;
|
|
totalSizeChange += sizeChange;
|
|
|
|
// Cache the accumulated size
|
|
m_cachedElementInfo[stickyHeaderElementIndex].m_accumulatedSize = GetVariableSizeElementOffset(stickyHeaderElementIndex) + newSize;
|
|
|
|
// Update accumulated sizes for all elements after the sticky header and before the first displayed element who's size changed.
|
|
// The rest of the cache updates for the displayed elements who's size changed will be handled below
|
|
for (int index = stickyHeaderElementIndex + 1; index < AZ::GetMax(firstDisplayedElementIndexWithSizeChangeOut, firstDisplayedElementIndexOut); index++)
|
|
{
|
|
if (m_cachedElementInfo[index].m_accumulatedSize >= 0.0f)
|
|
{
|
|
m_cachedElementInfo[index].m_accumulatedSize += sizeChange;
|
|
}
|
|
}
|
|
|
|
if (stickyHeaderElementIndex <= visibleElementIndex)
|
|
{
|
|
totalSizeChangeBeforeFixedVisibleElement += sizeChange;
|
|
}
|
|
|
|
if (firstDisplayedElementIndexWithSizeChangeOut < 0 || stickyHeaderElementIndex < firstDisplayedElementIndexWithSizeChangeOut)
|
|
{
|
|
firstDisplayedElementIndexWithSizeChangeOut = stickyHeaderElementIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DisableElementsForAutoSizeCalculation();
|
|
|
|
// Update the cache info
|
|
if (firstDisplayedElementIndexWithSizeChangeOut >= 0)
|
|
{
|
|
// Cache the accumulated sizes for the displayed elements who's sizes were just calculated and cached
|
|
int startIndex = AZ::GetMax(firstDisplayedElementIndexWithSizeChangeOut, firstDisplayedElementIndexOut);
|
|
float curPos = GetVariableSizeElementOffset(startIndex);
|
|
for (int index = startIndex; index <= lastDisplayedElementIndexOut; index++)
|
|
{
|
|
curPos += m_cachedElementInfo[index].m_size;
|
|
m_cachedElementInfo[index].m_accumulatedSize = curPos;
|
|
}
|
|
|
|
// Update accumulated sizes for all elements after the last displayed element
|
|
for (int index = lastDisplayedElementIndexOut + 1; index < m_numElements; index++)
|
|
{
|
|
if (m_cachedElementInfo[index].m_accumulatedSize >= 0.0f)
|
|
{
|
|
m_cachedElementInfo[index].m_accumulatedSize += totalSizeChange;
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateAverageElementSize(0, totalSizeChange);
|
|
|
|
scrollChangeOut = 0.0f;
|
|
if (totalSizeChange != 0.0f)
|
|
{
|
|
if (keepAtEnd)
|
|
{
|
|
scrollChangeOut = CalculateContentEndDeltaAfterSizeChange(totalSizeChange);
|
|
}
|
|
else
|
|
{
|
|
scrollChangeOut = CalculateContentBeginningDeltaAfterSizeChange(totalSizeChange);
|
|
}
|
|
}
|
|
if (!keepAtEnd)
|
|
{
|
|
scrollChangeOut -= totalSizeChangeBeforeFixedVisibleElement;
|
|
}
|
|
|
|
totalElementSizechangeOut = totalSizeChange;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::CalculateContentBeginningDeltaAfterSizeChange(float contentSizeDelta) const
|
|
{
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
// Get current content size
|
|
AZ::Vector2 curContentSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(curContentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
UiTransform2dInterface::Offsets offsets;
|
|
EBUS_EVENT_ID_RESULT(offsets, contentEntityId, UiTransform2dBus, GetOffsets);
|
|
|
|
AZ::Vector2 pivot;
|
|
EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);
|
|
|
|
float beginningDelta = 0.0f;
|
|
if (m_isVertical)
|
|
{
|
|
beginningDelta = contentSizeDelta * pivot.GetY();
|
|
}
|
|
else
|
|
{
|
|
beginningDelta = contentSizeDelta * pivot.GetX();
|
|
}
|
|
|
|
return beginningDelta;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::CalculateContentEndDeltaAfterSizeChange(float contentSizeDelta) const
|
|
{
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
// Get current content size
|
|
AZ::Vector2 curContentSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(curContentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
UiTransform2dInterface::Offsets offsets;
|
|
EBUS_EVENT_ID_RESULT(offsets, contentEntityId, UiTransform2dBus, GetOffsets);
|
|
|
|
AZ::Vector2 pivot;
|
|
EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);
|
|
|
|
float endDelta = 0.0f;
|
|
if (m_isVertical)
|
|
{
|
|
// Restore end
|
|
endDelta = -contentSizeDelta * (1.0f - pivot.GetY());
|
|
}
|
|
else
|
|
{
|
|
// Restore end
|
|
endDelta = -contentSizeDelta * (1.0f - pivot.GetX());
|
|
}
|
|
|
|
return endDelta;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::IsScrolledToEnd() const
|
|
{
|
|
// Find the content element
|
|
AZ::EntityId contentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
|
|
|
|
if (!contentEntityId.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get content's parent
|
|
AZ::EntityId contentParentEntityId;
|
|
EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
|
|
if (!contentParentEntityId.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get content's rect in canvas space
|
|
UiTransformInterface::Rect contentRect;
|
|
EBUS_EVENT_ID(contentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentRect);
|
|
|
|
// Get content parent's rect in canvas space
|
|
UiTransformInterface::Rect parentRect;
|
|
EBUS_EVENT_ID(contentParentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect);
|
|
|
|
bool scrolledToEnd = false;
|
|
if (m_isVertical)
|
|
{
|
|
scrolledToEnd = parentRect.bottom >= contentRect.bottom;
|
|
}
|
|
else
|
|
{
|
|
scrolledToEnd = parentRect.right >= contentRect.right;
|
|
}
|
|
|
|
return scrolledToEnd;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::IsElementDisplayedAtIndex(int index) const
|
|
{
|
|
if (m_firstDisplayedElementIndex < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ((index >= m_firstDisplayedElementIndex) && (index <= m_lastDisplayedElementIndex));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetElementForDisplay(ElementType elementType)
|
|
{
|
|
AZ::EntityId element;
|
|
|
|
// Check if there is an existing element
|
|
if (!m_recycledElements[elementType].empty())
|
|
{
|
|
element = m_recycledElements[elementType].front();
|
|
m_recycledElements[elementType].pop_front();
|
|
|
|
// Enable element
|
|
EBUS_EVENT_ID(element, UiElementBus, SetIsEnabled, true);
|
|
}
|
|
else
|
|
{
|
|
element = ClonePrototypeElement(elementType);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetElementForAutoSizeCalculation(ElementType elementType)
|
|
{
|
|
if (!m_clonedElementForAutoSizeCalculation[elementType].IsValid())
|
|
{
|
|
m_clonedElementForAutoSizeCalculation[elementType] = ClonePrototypeElement(elementType);
|
|
}
|
|
else
|
|
{
|
|
// Enable element
|
|
EBUS_EVENT_ID(m_clonedElementForAutoSizeCalculation[elementType], UiElementBus, SetIsEnabled, true);
|
|
}
|
|
|
|
return m_clonedElementForAutoSizeCalculation[elementType];
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::DisableElementsForAutoSizeCalculation() const
|
|
{
|
|
for (int i = 0; i < ElementType::NumElementTypes; i++)
|
|
{
|
|
if (m_clonedElementForAutoSizeCalculation[i].IsValid())
|
|
{
|
|
// Disable element
|
|
EBUS_EVENT_ID(m_clonedElementForAutoSizeCalculation[i], UiElementBus, SetIsEnabled, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDynamicScrollBoxComponent::AutoCalculateElementSize(AZ::EntityId elementForAutoSizeCalculation) const
|
|
{
|
|
float size = 0.0f;
|
|
|
|
if (m_isVertical)
|
|
{
|
|
size = UiLayoutHelpers::GetLayoutElementTargetHeight(elementForAutoSizeCalculation);
|
|
}
|
|
else
|
|
{
|
|
size = UiLayoutHelpers::GetLayoutElementTargetWidth(elementForAutoSizeCalculation);
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SizeVariableElementAtIndex(AZ::EntityId element, int index) const
|
|
{
|
|
// Get current element size
|
|
AZ::Vector2 curElementSize(0.0f, 0.0f);
|
|
EBUS_EVENT_ID_RESULT(curElementSize, element, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);
|
|
|
|
float curSize = m_isVertical ? curElementSize.GetY() : curElementSize.GetX();
|
|
|
|
// Get new element size
|
|
float newSize = GetVariableElementSize(index);
|
|
|
|
if (newSize != curSize)
|
|
{
|
|
// Resize the element
|
|
|
|
UiTransform2dInterface::Offsets offsets;
|
|
EBUS_EVENT_ID_RESULT(offsets, element, UiTransform2dBus, GetOffsets);
|
|
|
|
AZ::Vector2 pivot;
|
|
EBUS_EVENT_ID_RESULT(pivot, element, UiTransformBus, GetPivot);
|
|
|
|
float sizeDiff = newSize - curSize;
|
|
|
|
if (m_isVertical)
|
|
{
|
|
offsets.m_top -= sizeDiff * pivot.GetY();
|
|
offsets.m_bottom += sizeDiff * (1.0f - pivot.GetY());
|
|
}
|
|
else
|
|
{
|
|
offsets.m_left -= sizeDiff * pivot.GetX();
|
|
offsets.m_right += sizeDiff * (1.0f - pivot.GetX());
|
|
}
|
|
|
|
EBUS_EVENT_ID(element, UiTransform2dBus, SetOffsets, offsets);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::PositionElementAtIndex(AZ::EntityId element, int index) const
|
|
{
|
|
// Position offsets based on index
|
|
float offset = GetElementOffsetAtIndex(index);
|
|
|
|
SetElementOffsets(element, offset);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetElementAnchors(AZ::EntityId element) const
|
|
{
|
|
// Get the element anchors
|
|
UiTransform2dInterface::Anchors anchors;
|
|
EBUS_EVENT_ID_RESULT(anchors, element, UiTransform2dBus, GetAnchors);
|
|
|
|
if (m_isVertical)
|
|
{
|
|
// Set anchors to top of parent
|
|
anchors.m_top = 0.0f;
|
|
anchors.m_bottom = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// Set anchors to left of parent
|
|
anchors.m_left = 0.0f;
|
|
anchors.m_right = 0.0f;
|
|
}
|
|
|
|
EBUS_EVENT_ID(element, UiTransform2dBus, SetAnchors, anchors, false, false);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDynamicScrollBoxComponent::SetElementOffsets(AZ::EntityId element, float offset) const
|
|
{
|
|
// Get the element offsets
|
|
UiTransform2dInterface::Offsets offsets;
|
|
EBUS_EVENT_ID_RESULT(offsets, element, UiTransform2dBus, GetOffsets);
|
|
|
|
if ((m_isVertical && offsets.m_top != offset) || (!m_isVertical && offsets.m_left != offset))
|
|
{
|
|
if (m_isVertical)
|
|
{
|
|
float height = offsets.m_bottom - offsets.m_top;
|
|
offsets.m_top = offset;
|
|
offsets.m_bottom = offsets.m_top + height;
|
|
}
|
|
else
|
|
{
|
|
float width = offsets.m_right - offsets.m_left;
|
|
offsets.m_left = offset;
|
|
offsets.m_right = offsets.m_left + width;
|
|
}
|
|
|
|
EBUS_EVENT_ID(element, UiTransform2dBus, SetOffsets, offsets);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDynamicScrollBoxComponent::ElementType UiDynamicScrollBoxComponent::GetElementTypeAtIndex(int index) const
|
|
{
|
|
ElementType elementType = ElementType::Item;
|
|
|
|
if (m_hasSections)
|
|
{
|
|
for (int i = 0; i < m_sections.size(); i++)
|
|
{
|
|
if (m_sections[i].m_headerElementIndex == index)
|
|
{
|
|
elementType = ElementType::SectionHeader;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return elementType;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDynamicScrollBoxComponent::ElementIndexInfo UiDynamicScrollBoxComponent::GetElementIndexInfoFromIndex(int index) const
|
|
{
|
|
ElementIndexInfo elementIndexInfo;
|
|
elementIndexInfo.m_sectionIndex = -1;
|
|
elementIndexInfo.m_itemIndexInSection = index;
|
|
|
|
if (m_hasSections)
|
|
{
|
|
for (int i = 0; i < m_sections.size(); i++)
|
|
{
|
|
if (index <= m_sections[i].m_headerElementIndex + m_sections[i].m_numItems)
|
|
{
|
|
elementIndexInfo.m_sectionIndex = i;
|
|
elementIndexInfo.m_itemIndexInSection = (index - m_sections[i].m_headerElementIndex) - 1; // for headers, this will be set to -1
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return elementIndexInfo;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiDynamicScrollBoxComponent::GetIndexFromElementIndexInfo(const ElementIndexInfo& elementIndexInfo) const
|
|
{
|
|
int index = elementIndexInfo.m_itemIndexInSection;
|
|
|
|
if (m_hasSections)
|
|
{
|
|
index += m_sections[elementIndexInfo.m_sectionIndex].m_headerElementIndex + 1;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDynamicScrollBoxComponent::GetImmediateContentChildFromDescendant(AZ::EntityId childElement) const
|
|
{
|
|
AZ::EntityId immediateChild;
|
|
|
|
AZ::Entity* contentEntity = GetContentEntity();
|
|
if (contentEntity)
|
|
{
|
|
immediateChild = childElement;
|
|
AZ::Entity* parent = nullptr;
|
|
EBUS_EVENT_ID_RESULT(parent, immediateChild, UiElementBus, GetParent);
|
|
while (parent && parent != contentEntity)
|
|
{
|
|
immediateChild = parent->GetId();
|
|
EBUS_EVENT_ID_RESULT(parent, immediateChild, UiElementBus, GetParent);
|
|
}
|
|
|
|
if (parent != contentEntity)
|
|
{
|
|
immediateChild.SetInvalid();
|
|
}
|
|
}
|
|
|
|
return immediateChild;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::HeadersHaveVariableSizes() const
|
|
{
|
|
return m_hasSections && m_variableHeaderElementSize;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDynamicScrollBoxComponent::IsValidPrototype(AZ::EntityId entityId) const
|
|
{
|
|
// Entities containing the scroll box itself are not safe to clone as they will respawn this
|
|
// scroll box and result in infinite recursive spawning.
|
|
if (!entityId.IsValid() || entityId == GetEntityId())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool isEntityAncestor;
|
|
UiElementBus::EventResult(isEntityAncestor, GetEntityId(), &UiElementBus::Events::IsAncestor, entityId);
|
|
return !isEntityAncestor;
|
|
}
|