/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "LyShine_precompiled.h" #include "UiScrollBoxComponent.h" #include "Sprite.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "UiSerialize.h" //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiScrollBoxNotificationBus Behavior context handler class class BehaviorUiScrollBoxNotificationBusHandler : public UiScrollBoxNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiScrollBoxNotificationBusHandler, "{15CA0E45-F673-4E18-922F-D9DB1272CFEA}", AZ::SystemAllocator, OnScrollOffsetChanging, OnScrollOffsetChanged); void OnScrollOffsetChanging(AZ::Vector2 value) override { Call(FN_OnScrollOffsetChanging, value); } void OnScrollOffsetChanged(AZ::Vector2 value) override { Call(FN_OnScrollOffsetChanged, value); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiScrollableNotificationBus Behavior context handler class class BehaviorUiScrollableNotificationBusHandler : public UiScrollableNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiScrollableNotificationBusHandler, "{7F130E59-778C-4951-BB62-B2E57E530BC0}", AZ::SystemAllocator, OnScrollableValueChanging, OnScrollableValueChanged); void OnScrollableValueChanging(AZ::Vector2 value) override { Call(FN_OnScrollableValueChanging, value); } void OnScrollableValueChanged(AZ::Vector2 value) override { Call(FN_OnScrollableValueChanged, value); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::UiScrollBoxComponent() : m_scrollOffset(0.0f, 0.0f) , m_isHorizontalScrollingEnabled(true) , m_isVerticalScrollingEnabled(false) , m_isScrollingConstrained(true) , m_snapMode(SnapMode::None) , m_snapGrid(10.0f, 10.0f) , m_hScrollBarVisibility(ScrollBarVisibility::AlwaysShow) , m_vScrollBarVisibility(ScrollBarVisibility::AlwaysShow) , m_contentEntity() , m_hScrollBarEntity() , m_vScrollBarEntity() , m_onScrollOffsetChanged() , m_onScrollOffsetChanging() , m_scrollOffsetChangedActionName() , m_scrollOffsetChangingActionName() , m_isDragging(false) , m_isActive(false) , m_pressedScrollOffset(0.0f, 0.0f) , m_lastDragPoint(0.0f, 0.0f) { } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::~UiScrollBoxComponent() { } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::GetScrollOffset() { return m_scrollOffset; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetScrollOffset(AZ::Vector2 scrollOffset) { if (m_isScrollingConstrained) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { scrollOffset = ConstrainOffset(scrollOffset, contentParentEntity); } } if (scrollOffset != m_scrollOffset) { DoSetScrollOffset(scrollOffset); // Reset drag info if (m_isDragging) { m_pressedScrollOffset = m_scrollOffset; m_pressedPoint = m_lastDragPoint; } NotifyScrollersOnValueChanged(); DoChangedActions(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::GetNormalizedScrollValue() { AZ::Vector2 normalizedScrollValueOut(0.0f, 0.0f); bool result = ScrollOffsetToNormalizedScrollValue(m_scrollOffset, normalizedScrollValueOut); return normalizedScrollValueOut; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::ChangeContentSizeAndScrollOffset(AZ::Vector2 contentSize, AZ::Vector2 scrollOffset) { if (m_contentEntity.IsValid()) { AZ::Vector2 prevScrollOffset = m_scrollOffset; // Get current content size AZ::Vector2 prevContentSize(0.0f, 0.0f); EBUS_EVENT_ID_RESULT(prevContentSize, m_contentEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); // Resize content element if (prevContentSize != contentSize) { UiTransform2dInterface::Offsets offsets; EBUS_EVENT_ID_RESULT(offsets, m_contentEntity, UiTransform2dBus, GetOffsets); AZ::Vector2 pivot; EBUS_EVENT_ID_RESULT(pivot, m_contentEntity, UiTransformBus, GetPivot); AZ::Vector2 sizeDiff = contentSize - prevContentSize; if (sizeDiff.GetX() != 0.0f) { offsets.m_left -= sizeDiff.GetX() * pivot.GetX(); offsets.m_right += sizeDiff.GetX() * (1.0f - pivot.GetX()); } if (sizeDiff.GetY() != 0.0f) { offsets.m_top -= sizeDiff.GetY() * pivot.GetY(); offsets.m_bottom += sizeDiff.GetY() * (1.0f - pivot.GetY()); } EBUS_EVENT_ID(m_contentEntity, UiTransform2dBus, SetOffsets, offsets); } // Adjust scroll offset if (m_scrollOffset != scrollOffset) { DoSetScrollOffset(scrollOffset); } // Reset drag info if (m_isDragging) { m_pressedScrollOffset = m_scrollOffset; m_pressedPoint = m_lastDragPoint; } // Handle content size change which also handles snapping/constraining if (prevContentSize != contentSize) { ContentOrParentSizeChanged(); } else { if (prevScrollOffset != m_scrollOffset) { NotifyScrollersOnValueChanged(); } if (DoSnap()) { // Reset drag info if (m_isDragging) { m_pressedScrollOffset = m_scrollOffset; m_pressedPoint = m_lastDragPoint; } NotifyScrollersOnValueChanged(); DoChangedActions(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HasHorizontalContentToScroll() { bool hasContentToScroll = false; if (!m_isHorizontalScrollingEnabled) { hasContentToScroll = false; } else if (!m_isScrollingConstrained) { hasContentToScroll = true; } else { if (m_hScrollBarEntity.IsValid() && (m_hScrollBarVisibility != ScrollBarVisibility::AlwaysShow)) { bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, m_hScrollBarEntity, UiElementBus, IsEnabled); hasContentToScroll = isEnabled; } else { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { // Get content parent's size AZ::Vector2 parentSize; EBUS_EVENT_ID_RESULT(parentSize, contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); // Get content size UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); AZ::Vector2 contentSize = contentRect.GetSize(); hasContentToScroll = contentSize.GetX() > parentSize.GetX(); } } } return hasContentToScroll; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HasVerticalContentToScroll() { bool hasContentToScroll = false; if (!m_isVerticalScrollingEnabled) { hasContentToScroll = false; } else if (!m_isScrollingConstrained) { hasContentToScroll = true; } else { if (m_vScrollBarEntity.IsValid() && (m_vScrollBarVisibility != ScrollBarVisibility::AlwaysShow)) { bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, m_vScrollBarEntity, UiElementBus, IsEnabled); hasContentToScroll = isEnabled; } else { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { // Get content parent's size AZ::Vector2 parentSize; EBUS_EVENT_ID_RESULT(parentSize, contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); // Get content size UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); AZ::Vector2 contentSize = contentRect.GetSize(); hasContentToScroll = contentSize.GetY() > parentSize.GetY(); } } } return hasContentToScroll; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::GetIsHorizontalScrollingEnabled() { return m_isHorizontalScrollingEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetIsHorizontalScrollingEnabled(bool isEnabled) { m_isHorizontalScrollingEnabled = isEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::GetIsVerticalScrollingEnabled() { return m_isVerticalScrollingEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetIsVerticalScrollingEnabled(bool isEnabled) { m_isVerticalScrollingEnabled = isEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::GetIsScrollingConstrained() { return m_isScrollingConstrained; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetIsScrollingConstrained(bool isConstrained) { m_isScrollingConstrained = isConstrained; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxInterface::SnapMode UiScrollBoxComponent::GetSnapMode() { return m_snapMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetSnapMode(SnapMode snapMode) { m_snapMode = snapMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::GetSnapGrid() { return m_snapGrid; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetSnapGrid(AZ::Vector2 snapGrid) { m_snapGrid = snapGrid; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxInterface::ScrollBarVisibility UiScrollBoxComponent::GetHorizontalScrollBarVisibility() { return m_hScrollBarVisibility; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetHorizontalScrollBarVisibility(ScrollBarVisibility visibility) { m_hScrollBarVisibility = visibility; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxInterface::ScrollBarVisibility UiScrollBoxComponent::GetVerticalScrollBarVisibility() { return m_vScrollBarVisibility; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetVerticalScrollBarVisibility(ScrollBarVisibility visibility) { m_vScrollBarVisibility = visibility; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::ScrollOffsetChangeCallback UiScrollBoxComponent::GetScrollOffsetChangingCallback() { return m_onScrollOffsetChanging; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetScrollOffsetChangingCallback(ScrollOffsetChangeCallback onChange) { m_onScrollOffsetChanging = onChange; } //////////////////////////////////////////////////////////////////////////////////////////////////// const LyShine::ActionName& UiScrollBoxComponent::GetScrollOffsetChangingActionName() { return m_scrollOffsetChangingActionName; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetScrollOffsetChangingActionName(const LyShine::ActionName& actionName) { m_scrollOffsetChangingActionName = actionName; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::ScrollOffsetChangeCallback UiScrollBoxComponent::GetScrollOffsetChangedCallback() { return m_onScrollOffsetChanged; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetScrollOffsetChangedCallback(ScrollOffsetChangeCallback onChange) { m_onScrollOffsetChanged = onChange; } //////////////////////////////////////////////////////////////////////////////////////////////////// const LyShine::ActionName& UiScrollBoxComponent::GetScrollOffsetChangedActionName() { return m_scrollOffsetChangedActionName; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetScrollOffsetChangedActionName(const LyShine::ActionName& actionName) { m_scrollOffsetChangedActionName = actionName; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetContentEntity(AZ::EntityId entityId) { m_contentEntity = entityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiScrollBoxComponent::GetContentEntity() { return m_contentEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetHorizontalScrollBarEntity(AZ::EntityId entityId) { m_hScrollBarEntity = entityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiScrollBoxComponent::GetHorizontalScrollBarEntity() { return m_hScrollBarEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::SetVerticalScrollBarEntity(AZ::EntityId entityId) { m_vScrollBarEntity = entityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiScrollBoxComponent::GetVerticalScrollBarEntity() { return m_vScrollBarEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiScrollBoxComponent::FindClosestContentChildElement() { // if no content entity return an invalid entity id if (!m_contentEntity.IsValid()) { return AZ::EntityId(); } // Iterate over the children of the content element and find the one that has the smallest // offset from the content elements anchors to the child's pivot. // E.g. if the anchors are the center of the content (the default) and the chilren's pivots // are in their centers (the default) then we will find the child whose center is closest // to the center of the content element's parent (usually the mask element) LyShine::EntityArray children; EBUS_EVENT_ID_RESULT(children, m_contentEntity, UiElementBus, GetChildElements); float closestDistSq = FLT_MAX; AZ::EntityId closestChild; for (auto child : children) { AZ::Vector2 scrollOffsetToChild = ComputeCurrentOffsetToChild(child->GetId()); float distSq = scrollOffsetToChild.GetLengthSq(); if (distSq < closestDistSq) { closestChild = child->GetId(); closestDistSq = distSq; } } return closestChild; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiScrollBoxComponent::FindNextContentChildElement(UiNavigationHelpers::Command command) { // if no content entity return an invalid entity id if (!m_contentEntity.IsValid()) { return AZ::EntityId(); } // Iterate over the children of the content element and find the one who's pivot is closest to // the content element's anchors in the specified direction. LyShine::EntityArray children; EBUS_EVENT_ID_RESULT(children, m_contentEntity, UiElementBus, GetChildElements); float shortestDist = FLT_MAX; float shortestPerpendicularDist = FLT_MAX; AZ::EntityId closestChild; for (auto child : children) { AZ::Vector2 scrollOffsetToChild = ComputeCurrentOffsetToChild(child->GetId()); float dist = 0.0f; const float epsilon = 0.01f; if (command == UiNavigationHelpers::Command::Up) { dist = scrollOffsetToChild.GetY() < -epsilon ? -scrollOffsetToChild.GetY() : 0.0f; } else if (command == UiNavigationHelpers::Command::Down) { dist = scrollOffsetToChild.GetY() > epsilon ? scrollOffsetToChild.GetY() : 0.0f; } else if (command == UiNavigationHelpers::Command::Left) { dist = scrollOffsetToChild.GetX() < -epsilon ? -scrollOffsetToChild.GetX() : 0.0f; } else if (command == UiNavigationHelpers::Command::Right) { dist = scrollOffsetToChild.GetX() > epsilon ? scrollOffsetToChild.GetX() : 0.0f; } if (dist > 0.0f) { if (dist < shortestDist) { shortestDist = dist; shortestPerpendicularDist = fabs((command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down) ? scrollOffsetToChild.GetX() : scrollOffsetToChild.GetY()); closestChild = child->GetId(); } else if (dist == shortestDist) { float perpDist = fabs((command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down) ? scrollOffsetToChild.GetX() : scrollOffsetToChild.GetY()); if (perpDist < shortestPerpendicularDist) { shortestPerpendicularDist = perpDist; closestChild = child->GetId(); } } } } return closestChild; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::GetScrollableParentToContentRatio(AZ::Vector2& ratioOut) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { AZ::Vector2 parentSize; EBUS_EVENT_ID_RESULT(parentSize, contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); AZ::Vector2 contentSize = contentRect.GetSize(); ratioOut.SetX(contentSize.GetX() != 0.0f ? parentSize.GetX() / contentSize.GetX() : 1.0f); ratioOut.SetY(contentSize.GetY() != 0.0f ? parentSize.GetY() / contentSize.GetY() : 1.0f); return true; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::OnValueChangingByScroller(float value) { AZ::EntityId scroller = *UiScrollerToScrollableNotificationBus::GetCurrentBusId(); AZ::Vector2 newScrollOffsetOut; bool result = ScrollerValueToScrollOffsets(scroller, value, newScrollOffsetOut); if (result && m_scrollOffset != newScrollOffsetOut) { DoSetScrollOffset(newScrollOffsetOut); DoChangingActions(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::OnValueChangedByScroller(float value) { AZ::EntityId scroller = *UiScrollerToScrollableNotificationBus::GetCurrentBusId(); AZ::Vector2 newScrollOffsetOut; bool result = ScrollerValueToScrollOffsets(scroller, value, newScrollOffsetOut); if (result) { AZ::Vector2 prevScrollOffset = m_scrollOffset; if (m_scrollOffset != newScrollOffsetOut) { DoSetScrollOffset(newScrollOffsetOut); } if (DoSnap()) { // Snapping/constraining caused the scroll offsets to change, so notify scrollers NotifyScrollersOnValueChanged(); } if (m_scrollOffset != prevScrollOffset) { DoChangedActions(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::InGamePostActivate() { if (m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled) { // Set this entity as the scrollable entity of the scroller EBUS_EVENT_ID(m_hScrollBarEntity, UiScrollerBus, SetScrollableEntity, GetEntityId()); UiScrollerToScrollableNotificationBus::MultiHandler::BusConnect(m_hScrollBarEntity); } if (m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled) { // Set this entity as the scrollable entity of the scroller EBUS_EVENT_ID(m_vScrollBarEntity, UiScrollerBus, SetScrollableEntity, GetEntityId()); UiScrollerToScrollableNotificationBus::MultiHandler::BusConnect(m_vScrollBarEntity); } DoSetScrollOffset(m_scrollOffset); // Setup based on the size of the content and its parent ContentOrParentSizeChanged(); // Listen for canvas space rect changes from the content entity if (m_contentEntity.IsValid()) { UiTransformChangeNotificationBus::MultiHandler::BusConnect(m_contentEntity); // Listen for canvas space rect changes from the content entity's parent AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { UiTransformChangeNotificationBus::MultiHandler::BusConnect(contentParentEntity->GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive) { bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive); if (handled) { // clear the dragging flag, we are not dragging until we detect a drag m_isDragging = false; // record the scroll offset at the time of the press m_pressedScrollOffset = m_scrollOffset; } return handled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HandleReleased(AZ::Vector2 point) { if (m_isHandlingEvents) { // handle snapping DoSnap(); UiInteractableComponent::TriggerReleasedAction(); NotifyScrollersOnValueChanged(); // NOTE: when we have inertia/rubber-banding these actions should occur when snap is finished DoChangedActions(); } m_isPressed = false; m_isDragging = false; return m_isHandlingEvents; } ///////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HandleEnterPressed(bool& shouldStayActive) { bool handled = UiInteractableComponent::HandleEnterPressed(shouldStayActive); if (handled) { // the scrollbox will stay active after released shouldStayActive = true; m_isActive = true; } return handled; } ///////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HandleAutoActivation() { if (!m_isHandlingEvents) { return false; } m_isActive = true; return true; } ///////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::HandleKeyInputBegan(const AzFramework::InputChannel::Snapshot& inputSnapshot, AzFramework::ModifierKeyMask activeModifierKeys) { if (!m_isHandlingEvents) { return false; } // don't accept key input while in pressed state if (m_isPressed) { return false; } bool result = false; const UiNavigationHelpers::Command command = UiNavigationHelpers::MapInputChannelIdToUiNavigationCommand(inputSnapshot.m_channelId, activeModifierKeys); if (m_isHorizontalScrollingEnabled && ((command == UiNavigationHelpers::Command::Left || command == UiNavigationHelpers::Command::Right)) || (m_isVerticalScrollingEnabled && (command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down))) { AZ::Vector2 newScrollOffset = m_scrollOffset; if (m_snapMode == UiScrollBoxInterface::SnapMode::Children) { AZ::EntityId closestChild = FindNextContentChildElement(command); if (closestChild.IsValid()) { // want elastic animation eventually AZ::Vector2 deltaToSubtract = ComputeCurrentOffsetToChild(closestChild); // snapping should only move the content in the directions it is allowed to scroll if (!m_isHorizontalScrollingEnabled) { deltaToSubtract.SetX(0.0f); } else if (!m_isVerticalScrollingEnabled) { deltaToSubtract.SetY(0.0f); } newScrollOffset -= deltaToSubtract; // do constraining if (m_isScrollingConstrained) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); newScrollOffset = ConstrainOffset(newScrollOffset, contentParentEntity); } } } else if (m_snapMode == UiScrollBoxInterface::SnapMode::Grid) { if (command == UiNavigationHelpers::Command::Up) { newScrollOffset.SetY(newScrollOffset.GetY() + m_snapGrid.GetY()); } else if (command == UiNavigationHelpers::Command::Down) { newScrollOffset.SetY(newScrollOffset.GetY() - m_snapGrid.GetY()); } else if (command == UiNavigationHelpers::Command::Left) { newScrollOffset.SetX(newScrollOffset.GetX() + m_snapGrid.GetX()); } else if (command == UiNavigationHelpers::Command::Right) { newScrollOffset.SetX(newScrollOffset.GetX() - m_snapGrid.GetX()); } if (m_isScrollingConstrained) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); // Only scroll if constraining doesn't change the offset AZ::Vector2 constrainedScrollOffset = ConstrainOffset(newScrollOffset, contentParentEntity); if (constrainedScrollOffset != newScrollOffset) { newScrollOffset = m_scrollOffset; } } } else { // get content parent's rect in canvas space AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); const float keySteps = 10.0f; if (command == UiNavigationHelpers::Command::Left || command == UiNavigationHelpers::Command::Right) { float xStep = parentRect.GetSize().GetX() / keySteps; newScrollOffset.SetX(newScrollOffset.GetX() + (command == UiNavigationHelpers::Command::Left ? xStep : -xStep)); } else { float yStep = parentRect.GetSize().GetY() / keySteps; newScrollOffset.SetY(newScrollOffset.GetY() + (command == UiNavigationHelpers::Command::Up ? yStep : -yStep)); } // do constraining if (m_isScrollingConstrained) { newScrollOffset = ConstrainOffset(newScrollOffset, contentParentEntity); } } } if (newScrollOffset != m_scrollOffset) { DoSetScrollOffset(newScrollOffset); NotifyScrollersOnValueChanged(); DoChangingActions(); DoChangedActions(); } result = true; } return result; } ///////////////////////////////////////////////////////////////// void UiScrollBoxComponent::InputPositionUpdate(AZ::Vector2 point) { if (m_isPressed && m_contentEntity.IsValid()) { if (!m_isDragging) { CheckForDragOrHandOffToParent(point); } if (m_isDragging) { AZ::Vector2 dragVector = point - m_pressedPoint; AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); AZ::Matrix4x4 transform; if (contentParentEntity) { EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetTransformFromViewport, transform); } else { transform = AZ::Matrix4x4::CreateIdentity(); } // Transform the draw vector from viewport space to the local space of the parent of the content element // This means we can do all calculations in unrotated/unscaled space. AZ::Vector3 dragVector3(dragVector.GetX(), dragVector.GetY(), 0.0f); dragVector3 = transform.Multiply3x3(dragVector3); AZ::Vector2 dragVectorInParentSpace(dragVector3.GetX(), dragVector3.GetY()); if (!m_isHorizontalScrollingEnabled) { dragVectorInParentSpace.SetX(0.0f); } if (!m_isVerticalScrollingEnabled) { dragVectorInParentSpace.SetY(0.0f); } AZ::Vector2 newScrollOffset = m_pressedScrollOffset + dragVectorInParentSpace; // do constraining if (m_isScrollingConstrained) { newScrollOffset = ConstrainOffset(newScrollOffset, contentParentEntity); } m_lastDragPoint = point; if (newScrollOffset != m_scrollOffset) { DoSetScrollOffset(newScrollOffset); NotifyScrollersOnValueChanging(); DoChangingActions(); } } } } ///////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::DoesSupportDragHandOff(AZ::Vector2 startPoint) { // this component does support hand-off, so long as the start point is in its bounds bool isPointInRect = false; EBUS_EVENT_ID_RESULT(isPointInRect, GetEntityId(), UiTransformBus, IsPointInRect, startPoint); return isPointInRect; } ///////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::OfferDragHandOff(AZ::EntityId currentActiveInteractable, AZ::Vector2 startPoint, AZ::Vector2 currentPoint, float dragThreshold) { bool result = false; // This only gets called if this is not already the active interactable, check preconditions AZ_Assert(!m_isPressed && !m_isDragging, "ScrollBox is already active"); // get transform of content entity AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); AZ::Matrix4x4 transform; if (contentParentEntity) { EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetTransformFromViewport, transform); } else { transform = AZ::Matrix4x4::CreateIdentity(); } float validDragDistance = GetValidDragDistanceInPixels(startPoint, currentPoint); if (validDragDistance > dragThreshold) { // share this common code? m_isDragging = true; m_isPressed = true; m_pressedPoint = startPoint; m_pressedScrollOffset = m_scrollOffset; m_lastDragPoint = m_pressedPoint; // tell the canvas that this is now the active interacatable EBUS_EVENT_ID(currentActiveInteractable, UiInteractableActiveNotificationBus, ActiveChanged, GetEntityId(), false); result = true; } else { // The current drag movement is not over the threshhold to be dragging this interactable // look for a parent interactable that the start point of the drag is inside AZ::EntityId interactableContainer; EBUS_EVENT_ID_RESULT(interactableContainer, GetEntityId(), UiElementBus, FindParentInteractableSupportingDrag, startPoint); // if there was a parent interactable offer them the opportunity to become the active interactable EBUS_EVENT_ID_RESULT(result, interactableContainer, UiInteractableBus, OfferDragHandOff, currentActiveInteractable, startPoint, currentPoint, dragThreshold); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::LostActiveStatus() { UiInteractableComponent::LostActiveStatus(); if (m_isDragging) { if (m_isHandlingEvents) { // handle snapping DoSnap(); NotifyScrollersOnValueChanged(); // NOTE: when we have inertia/rubber-banding these actions should occur when snap is finished DoChangedActions(); } m_isDragging = false; } m_isActive = false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::HandleDescendantReceivedHoverByNavigation(AZ::EntityId descendantEntityId) { // Check if the content element is an ancestor of the descendant element bool isAncestor = false; if (m_contentEntity.IsValid()) { EBUS_EVENT_ID_RESULT(isAncestor, descendantEntityId, UiElementBus, IsAncestor, m_contentEntity); } if (isAncestor) { AZ::Vector2 newScrollOffset = m_scrollOffset; if (m_snapMode == UiScrollBoxInterface::SnapMode::Children) { // Find the descendant's ancestor that's a direct child of the content entity AZ::EntityId parent; EBUS_EVENT_ID_RESULT(parent, descendantEntityId, UiElementBus, GetParentEntityId); while (parent.IsValid()) { if (parent == m_contentEntity) { break; } descendantEntityId = parent; parent.SetInvalid(); EBUS_EVENT_ID_RESULT(parent, descendantEntityId, UiElementBus, GetParentEntityId); } if (descendantEntityId.IsValid()) { AZ::Vector2 offset = ComputeCurrentOffsetToChild(descendantEntityId); if (!m_isHorizontalScrollingEnabled) { offset.SetX(0.0f); } if (!m_isVerticalScrollingEnabled) { offset.SetY(0.0f); } newScrollOffset = m_scrollOffset - offset; } } else { // Check if the descendant element is visible in the viewport area AZ::EntityId contentParent; EBUS_EVENT_ID_RESULT(contentParent, m_contentEntity, UiElementBus, GetParentEntityId); if (contentParent.IsValid()) { UiTransformInterface::Rect contentParentRect; AZ::Matrix4x4 transformFromViewport; EBUS_EVENT_ID(contentParent, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentParentRect); EBUS_EVENT_ID(contentParent, UiTransformBus, GetTransformFromViewport, transformFromViewport); UiTransformInterface::RectPoints descendantPoints; EBUS_EVENT_ID(descendantEntityId, UiTransformBus, GetViewportSpacePoints, descendantPoints); descendantPoints = descendantPoints.Transform(transformFromViewport); UiTransformInterface::Rect descendantRect; descendantRect.left = descendantPoints.GetAxisAlignedTopLeft().GetX(); descendantRect.right = descendantPoints.GetAxisAlignedBottomRight().GetX(); descendantRect.top = descendantPoints.GetAxisAlignedTopLeft().GetY(); descendantRect.bottom = descendantPoints.GetAxisAlignedBottomRight().GetY(); bool descendantInsideH = (descendantRect.left >= contentParentRect.left && descendantRect.right <= contentParentRect.right); bool descendantInsideV = (descendantRect.top >= contentParentRect.top && descendantRect.bottom <= contentParentRect.bottom); if (!descendantInsideH || !descendantInsideV) { AZ::Vector2 offset(0.0f, 0.0f); // Scroll to make the descendant visible in the viewport area if (!descendantInsideH && m_isHorizontalScrollingEnabled) { float leftOffset = descendantRect.left - contentParentRect.left; float rightOffset = descendantRect.right - contentParentRect.right; bool shouldOffsetFromLeft = fabs(leftOffset) < fabs(rightOffset); offset.SetX(shouldOffsetFromLeft ? leftOffset : rightOffset); } if (!descendantInsideV && m_isVerticalScrollingEnabled) { float topOffset = descendantRect.top - contentParentRect.top; float bottomOffset = descendantRect.bottom - contentParentRect.bottom; bool shouldOffsetFromTop = fabs(topOffset) < fabs(bottomOffset); offset.SetY(shouldOffsetFromTop ? topOffset : bottomOffset); } newScrollOffset = m_scrollOffset - offset; if (m_snapMode == UiScrollBoxInterface::SnapMode::Grid) { // Make sure new offset is on the grid const float gridEpsilon = 0.00001f; if (m_snapGrid.GetX() >= gridEpsilon && m_isHorizontalScrollingEnabled) { float gridSteps = newScrollOffset.GetX() / m_snapGrid.GetX(); float roundedGridSteps = (offset.GetX() < 0.0f) ? ceil(gridSteps) : floor(gridSteps); newScrollOffset.SetX(roundedGridSteps * m_snapGrid.GetX()); } if (m_snapGrid.GetY() >= gridEpsilon && m_isVerticalScrollingEnabled) { float gridSteps = newScrollOffset.GetY() / m_snapGrid.GetY(); float roundedGridSteps = (offset.GetY() < 0.0f) ? ceil(gridSteps) : floor(gridSteps); newScrollOffset.SetY(roundedGridSteps * m_snapGrid.GetY()); } } } } } if (newScrollOffset != m_scrollOffset) { SetScrollOffset(newScrollOffset); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::OnCanvasSpaceRectChanged(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) { ContentOrParentSizeChanged(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::Activate() { UiInteractableComponent::Activate(); UiScrollBoxBus::Handler::BusConnect(GetEntityId()); UiScrollableBus::Handler::BusConnect(GetEntityId()); UiInitializationBus::Handler::BusConnect(GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::Deactivate() { UiInteractableComponent::Deactivate(); UiScrollBoxBus::Handler::BusDisconnect(GetEntityId()); UiScrollableBus::Handler::BusDisconnect(GetEntityId()); UiInitializationBus::Handler::BusDisconnect(GetEntityId()); UiTransformChangeNotificationBus::MultiHandler::BusDisconnect(); if (m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled) { UiScrollerToScrollableNotificationBus::MultiHandler::BusDisconnect(m_hScrollBarEntity); } if (m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled) { UiScrollerToScrollableNotificationBus::MultiHandler::BusDisconnect(m_vScrollBarEntity); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::IsAutoActivationSupported() { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiInteractableStatesInterface::State UiScrollBoxComponent::ComputeInteractableState() { UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal; if (!m_isHandlingEvents) { state = UiInteractableStatesInterface::StateDisabled; } else if (m_isPressed || m_isActive) { // Use pressed state regardless of mouse position state = UiInteractableStatesInterface::StatePressed; } else if (m_isHover) { state = UiInteractableStatesInterface::StateHover; } return state; } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { serializeContext->Class() ->Version(4, &VersionConverter) // Content group ->Field("ContentEntity", &UiScrollBoxComponent::m_contentEntity) ->Field("ScrollOffset", &UiScrollBoxComponent::m_scrollOffset) ->Field("ConstrainScrolling", &UiScrollBoxComponent::m_isScrollingConstrained) ->Field("SnapMode", &UiScrollBoxComponent::m_snapMode) ->Field("SnapGrid", &UiScrollBoxComponent::m_snapGrid) // Horizontal scrolling group ->Field("AllowHorizSrolling", &UiScrollBoxComponent::m_isHorizontalScrollingEnabled) ->Field("HScrollBarEntity", &UiScrollBoxComponent::m_hScrollBarEntity) ->Field("HScrollBarVisibility", &UiScrollBoxComponent::m_hScrollBarVisibility) // Vertical scrolling group ->Field("AllowVertScrolling", &UiScrollBoxComponent::m_isVerticalScrollingEnabled) ->Field("VScrollBarEntity", &UiScrollBoxComponent::m_vScrollBarEntity) ->Field("VScrollBarVisibility", &UiScrollBoxComponent::m_vScrollBarVisibility) // Actions group ->Field("ScrollOffsetChangingActionName", &UiScrollBoxComponent::m_scrollOffsetChangingActionName) ->Field("ScrollOffsetChangedActionName", &UiScrollBoxComponent::m_scrollOffsetChangedActionName); AZ::EditContext* ec = serializeContext->GetEditContext(); if (ec) { auto editInfo = ec->Class("ScrollBox", "An interactable component for scrolling a child element."); editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "UI") ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiScrollBox.png") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiScrollBox.png") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0)) ->Attribute(AZ::Edit::Attributes::AutoExpand, true); // Content group { editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Content") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiScrollBoxComponent::m_contentEntity, "Content element", "The child element that is the scrollable content.") ->Attribute(AZ::Edit::Attributes::EnumValues, &UiScrollBoxComponent::PopulateChildEntityList); editInfo->DataElement(0, &UiScrollBoxComponent::m_scrollOffset, "Initial scroll offset", "The initial offset of the scroll box content.") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Show); // needed because sub-elements are hidden editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiScrollBoxComponent::m_isScrollingConstrained, "Constrain scrolling", "Check this box to prevent the content from being scrolled beyond its edges."); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiScrollBoxComponent::m_snapMode, "Snap", "Sets the snapping behavior when the control is released.") ->EnumAttribute(UiScrollBoxInterface::SnapMode::None, "None") ->EnumAttribute(UiScrollBoxInterface::SnapMode::Children, "To children") ->EnumAttribute(UiScrollBoxInterface::SnapMode::Grid, "To grid") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c)); editInfo->DataElement(0, &UiScrollBoxComponent::m_snapGrid, "Grid spacing", "The scroll offset will be snapped to multiples of these values.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiScrollBoxComponent::IsSnapToGrid); } // Horizontal scrolling group { editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Horizontal scrolling") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiScrollBoxComponent::m_isHorizontalScrollingEnabled, "Enabled", "Check this box to allow the scroll box to be scrolled horizontally.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c)); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiScrollBoxComponent::m_hScrollBarEntity, "Scrollbar element", "The element that is the horizontal scrollbar.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiScrollBoxComponent::m_isHorizontalScrollingEnabled) ->Attribute(AZ::Edit::Attributes::EnumValues, &UiScrollBoxComponent::PopulateHScrollBarEntityList); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiScrollBoxComponent::m_hScrollBarVisibility, "Scrollbar visibility", "Sets visibility behavior of the horizontal scrollbar.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiScrollBoxComponent::m_isHorizontalScrollingEnabled) ->EnumAttribute(UiScrollBoxInterface::ScrollBarVisibility::AlwaysShow, "Always visible") ->EnumAttribute(UiScrollBoxInterface::ScrollBarVisibility::AutoHide, "Auto hide") ->EnumAttribute(UiScrollBoxInterface::ScrollBarVisibility::AutoHideAndResizeViewport, "Auto hide and resize view area"); } // Vertical scrolling group { editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Vertical scrolling") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiScrollBoxComponent::m_isVerticalScrollingEnabled, "Enabled", "Check this box to allow the scroll box to be scrolled vertically.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c)); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiScrollBoxComponent::m_vScrollBarEntity, "Scrollbar element", "The element that is the vertical scrollbar.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiScrollBoxComponent::m_isVerticalScrollingEnabled) ->Attribute(AZ::Edit::Attributes::EnumValues, &UiScrollBoxComponent::PopulateVScrollBarEntityList); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiScrollBoxComponent::m_vScrollBarVisibility, "Scrollbar visibility", "Sets visibility behavior of the vertical scrollbar.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiScrollBoxComponent::m_isVerticalScrollingEnabled) ->EnumAttribute(UiScrollBoxInterface::ScrollBarVisibility::AlwaysShow, "Always visible") ->EnumAttribute(UiScrollBoxInterface::ScrollBarVisibility::AutoHide, "Auto hide") ->EnumAttribute(UiScrollBoxInterface::ScrollBarVisibility::AutoHideAndResizeViewport, "Auto hide and resize view area"); } // Actions group { editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Actions") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(0, &UiScrollBoxComponent::m_scrollOffsetChangingActionName, "Change", "The action triggered while the offset is changing."); editInfo->DataElement(0, &UiScrollBoxComponent::m_scrollOffsetChangedActionName, "End change", "The action triggered when the offset is done changing."); } } } AZ::BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { behaviorContext->EBus("UiScrollBoxBus") ->Event("GetScrollOffset", &UiScrollBoxBus::Events::GetScrollOffset) ->Event("SetScrollOffset", &UiScrollBoxBus::Events::SetScrollOffset) ->Event("GetNormalizedScrollValue", &UiScrollBoxBus::Events::GetNormalizedScrollValue) ->Event("HasHorizontalContentToScroll", &UiScrollBoxBus::Events::HasHorizontalContentToScroll) ->Event("HasVerticalContentToScroll", &UiScrollBoxBus::Events::HasVerticalContentToScroll) ->Event("GetIsHorizontalScrollingEnabled", &UiScrollBoxBus::Events::GetIsHorizontalScrollingEnabled) ->Event("SetIsHorizontalScrollingEnabled", &UiScrollBoxBus::Events::SetIsHorizontalScrollingEnabled) ->Event("GetIsVerticalScrollingEnabled", &UiScrollBoxBus::Events::GetIsVerticalScrollingEnabled) ->Event("SetIsVerticalScrollingEnabled", &UiScrollBoxBus::Events::SetIsVerticalScrollingEnabled) ->Event("GetIsScrollingConstrained", &UiScrollBoxBus::Events::GetIsScrollingConstrained) ->Event("SetIsScrollingConstrained", &UiScrollBoxBus::Events::SetIsScrollingConstrained) ->Event("GetSnapMode", &UiScrollBoxBus::Events::GetSnapMode) ->Event("SetSnapMode", &UiScrollBoxBus::Events::SetSnapMode) ->Event("GetSnapGrid", &UiScrollBoxBus::Events::GetSnapGrid) ->Event("SetSnapGrid", &UiScrollBoxBus::Events::SetSnapGrid) ->Event("GetHorizontalScrollBarVisibility", &UiScrollBoxBus::Events::GetHorizontalScrollBarVisibility) ->Event("SetHorizontalScrollBarVisibility", &UiScrollBoxBus::Events::SetHorizontalScrollBarVisibility) ->Event("GetVerticalScrollBarVisibility", &UiScrollBoxBus::Events::GetVerticalScrollBarVisibility) ->Event("SetVerticalScrollBarVisibility", &UiScrollBoxBus::Events::SetVerticalScrollBarVisibility) ->Event("GetScrollOffsetChangingActionName", &UiScrollBoxBus::Events::GetScrollOffsetChangingActionName) ->Event("SetScrollOffsetChangingActionName", &UiScrollBoxBus::Events::SetScrollOffsetChangingActionName) ->Event("GetScrollOffsetChangedActionName", &UiScrollBoxBus::Events::GetScrollOffsetChangedActionName) ->Event("SetScrollOffsetChangedActionName", &UiScrollBoxBus::Events::SetScrollOffsetChangedActionName) ->Event("GetContentEntity", &UiScrollBoxBus::Events::GetContentEntity) ->Event("SetContentEntity", &UiScrollBoxBus::Events::SetContentEntity) ->Event("GetHorizontalScrollBarEntity", &UiScrollBoxBus::Events::GetHorizontalScrollBarEntity) ->Event("SetHorizontalScrollBarEntity", &UiScrollBoxBus::Events::SetHorizontalScrollBarEntity) ->Event("GetVerticalScrollBarEntity", &UiScrollBoxBus::Events::GetVerticalScrollBarEntity) ->Event("SetVerticalScrollBarEntity", &UiScrollBoxBus::Events::SetVerticalScrollBarEntity) ->Event("FindClosestContentChildElement", &UiScrollBoxBus::Events::FindClosestContentChildElement); behaviorContext->Enum<(int)UiScrollBoxInterface::SnapMode::None>("eUiScrollBoxSnapMode_None") ->Enum<(int)UiScrollBoxInterface::SnapMode::Children>("eUiScrollBoxSnapMode_Children") ->Enum<(int)UiScrollBoxInterface::SnapMode::Grid>("eUiScrollBoxSnapMode_Grid") ->Enum<(int)UiScrollBoxInterface::ScrollBarVisibility::AlwaysShow>("eUiScrollBoxScrollBarVisibility_AlwaysShow") ->Enum<(int)UiScrollBoxInterface::ScrollBarVisibility::AutoHide>("eUiScrollBoxScrollBarVisibility_AutoHide") ->Enum<(int)UiScrollBoxInterface::ScrollBarVisibility::AutoHideAndResizeViewport>("eUiScrollBoxScrollBarVisibility_AutoHideAndResizeViewport"); behaviorContext->EBus("UiScrollBoxNotificationBus") ->Handler(); behaviorContext->EBus("UiScrollableNotificationBus") ->Handler(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::EntityComboBoxVec UiScrollBoxComponent::PopulateChildEntityList() { EntityComboBoxVec result; // add a first entry for "None" result.push_back(AZStd::make_pair(AZ::EntityId(AZ::EntityId()), "")); // Get a list of all child elements LyShine::EntityArray matchingElements; EBUS_EVENT_ID(GetEntityId(), UiElementBus, FindDescendantElements, [](const AZ::Entity* entity) { return true; }, matchingElements); // add their names to the StringList and their IDs to the id list for (auto childEntity : matchingElements) { result.push_back(AZStd::make_pair(AZ::EntityId(childEntity->GetId()), childEntity->GetName())); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::EntityComboBoxVec UiScrollBoxComponent::PopulateHScrollBarEntityList() { return PopulateScrollBarEntityList(UiScrollerInterface::Orientation::Horizontal); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::EntityComboBoxVec UiScrollBoxComponent::PopulateVScrollBarEntityList() { return PopulateScrollBarEntityList(UiScrollerInterface::Orientation::Vertical); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiScrollBoxComponent::EntityComboBoxVec UiScrollBoxComponent::PopulateScrollBarEntityList(UiScrollerInterface::Orientation orientation) { EntityComboBoxVec result; // Add a first entry for "None" result.push_back(AZStd::make_pair(AZ::EntityId(), "")); // Get a list of all scrollbar elements AZ::EntityId canvasEntityId; EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId); LyShine::EntityArray scrollBarElements; EBUS_EVENT_ID(canvasEntityId, UiCanvasBus, FindElements, [this, orientation](const AZ::Entity* entity) { bool isScroller = false; if (entity->GetId() != GetEntityId()) { if (UiScrollerBus::FindFirstHandler(entity->GetId())) { // Check scrollbar's orientation UiScrollerInterface::Orientation entityOrientation; EBUS_EVENT_ID_RESULT(entityOrientation, entity->GetId(), UiScrollerBus, GetOrientation); isScroller = (entityOrientation == orientation); } } return isScroller; }, scrollBarElements); // Sort the elements by name AZStd::sort(scrollBarElements.begin(), scrollBarElements.end(), [](const AZ::Entity* e1, const AZ::Entity* e2) { return e1->GetName() < e2->GetName(); }); // Add their names to the StringList and their IDs to the id list for (auto scrollBarEntity : scrollBarElements) { result.push_back(AZStd::make_pair(scrollBarEntity->GetId(), scrollBarEntity->GetName())); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::IsSnapToGrid() const { return m_snapMode == SnapMode::Grid; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::ConstrainOffset(AZ::Vector2 proposedOffset, AZ::Entity* contentParentEntity) { AZ::Vector2 newScrollOffset = proposedOffset; if (contentParentEntity) { // get content parent's rect in canvas space UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); // get content's rect in canvas space UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); AZ::Vector2 latestOffsetDelta = newScrollOffset - m_scrollOffset; // add the requested scroll offset to the content rect to get the proposed position // The content has already need moved by the requested offset all but latestOffsetDelta UiTransformInterface::Rect origContentRect = contentRect; contentRect.MoveBy(latestOffsetDelta); if (contentRect.GetWidth() <= parentRect.GetWidth()) { newScrollOffset.SetX(0.0f); } else if (contentRect.left > parentRect.left) { newScrollOffset.SetX(newScrollOffset.GetX() - (contentRect.left - parentRect.left)); } else if (contentRect.right < parentRect.right) { newScrollOffset.SetX(newScrollOffset.GetX() + (parentRect.right - contentRect.right)); } if (contentRect.GetHeight() <= parentRect.GetHeight()) { newScrollOffset.SetY(0.0f); } else if (contentRect.top > parentRect.top) { newScrollOffset.SetY(newScrollOffset.GetY() - (contentRect.top - parentRect.top)); } else if (contentRect.bottom < parentRect.bottom) { newScrollOffset.SetY(newScrollOffset.GetY() + (parentRect.bottom - contentRect.bottom)); } } return newScrollOffset; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::DoSnap() { AZ::Vector2 newScrollOffset = m_scrollOffset; AZ::Vector2 deltaToSubtract(0.0f, 0.0f); if (m_snapMode == UiScrollBoxInterface::SnapMode::Children) { AZ::EntityId closestChild = FindClosestContentChildElement(); if (closestChild.IsValid()) { // want elastic animation eventually deltaToSubtract = ComputeCurrentOffsetToChild(closestChild); } } else if (m_snapMode == UiScrollBoxInterface::SnapMode::Grid) { deltaToSubtract = ComputeCurrentOffsetFromGrid(); } // snapping should only move the content in the directions it is allowed to scroll if (!m_isHorizontalScrollingEnabled) { deltaToSubtract.SetX(0.0f); } if (!m_isVerticalScrollingEnabled) { deltaToSubtract.SetY(0.0f); } newScrollOffset = m_scrollOffset - deltaToSubtract; if (m_isScrollingConstrained) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); newScrollOffset = ConstrainOffset(newScrollOffset, contentParentEntity); } if (newScrollOffset != m_scrollOffset) { DoSetScrollOffset(newScrollOffset); return true; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::ComputeCurrentOffsetToChild(AZ::EntityId child) { // Get the position of the child element's pivot in canvas space AZ::Vector2 childPivotPosition; EBUS_EVENT_ID_RESULT(childPivotPosition, child, UiTransformBus, GetCanvasSpacePivot); AZ::Vector2 anchorCenter = ComputeContentAnchorCenterInCanvasSpace(); // offset is the distance from the content anchors to the current child pivot position // (given the current scroll offset) AZ::Vector2 offsetToChild = childPivotPosition - anchorCenter; AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); AZ::Matrix4x4 transform; if (contentParentEntity) { EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetTransformFromCanvasSpace, transform); } else { transform = AZ::Matrix4x4::CreateIdentity(); } // Transform the offset from canvas space to the local space of the parent of the content element AZ::Vector3 offsetToChild3(offsetToChild.GetX(), offsetToChild.GetY(), 0.0f); offsetToChild3 = transform.Multiply3x3(offsetToChild3); offsetToChild = AZ::Vector2(offsetToChild3.GetX(), offsetToChild3.GetY()); return offsetToChild; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::ComputeCurrentOffsetFromGrid() { // offset is the delta to add to subtract from m_scrollOffset to put it on the grid AZ::Vector2 offsetToGrid; offsetToGrid.SetX(ComputeOffsetOfValueFromGrid(m_scrollOffset.GetX(), m_snapGrid.GetX())); offsetToGrid.SetY(ComputeOffsetOfValueFromGrid(m_scrollOffset.GetY(), m_snapGrid.GetY())); return offsetToGrid; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiScrollBoxComponent::ComputeContentAnchorCenterInCanvasSpace() const { // Get the position of the content elements anchors in canvas space AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (!contentParentEntity) { return AZ::Vector2(0.0f, 0.0f); } // get content parent's rect in canvas space UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); // Get the content anchor center in canvas space UiTransform2dInterface::Anchors anchors; EBUS_EVENT_ID_RESULT(anchors, m_contentEntity, UiTransform2dBus, GetAnchors); UiTransformInterface::Rect anchorRect; anchorRect.left = parentRect.left + anchors.m_left * parentRect.GetWidth(); anchorRect.right = parentRect.left + anchors.m_right * parentRect.GetWidth(); anchorRect.top = parentRect.top + anchors.m_top * parentRect.GetHeight(); anchorRect.bottom = parentRect.top + anchors.m_bottom * parentRect.GetHeight(); AZ::Vector2 anchorCenter = anchorRect.GetCenter(); AZ::Matrix4x4 transformToCanvasSpace; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetTransformToCanvasSpace, transformToCanvasSpace); AZ::Vector3 anchorCenter3(anchorCenter.GetX(), anchorCenter.GetY(), 0.0f); anchorCenter3 = transformToCanvasSpace * anchorCenter3; anchorCenter = AZ::Vector2(anchorCenter3.GetX(), anchorCenter3.GetY()); return anchorCenter; } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiScrollBoxComponent::ComputeOffsetOfValueFromGrid(float value, float gridStep) { const float gridEpsilon = 0.00001f; // compute offset to round to nearest point on grid float offsetFromGrid = 0.0f; if (gridStep >= gridEpsilon) { float roundedGridStep = roundf(value / gridStep); float targetValue = roundedGridStep * gridStep; offsetFromGrid = value - targetValue; } return offsetFromGrid; } //////////////////////////////////////////////////////////////////////////////////////////////////// // calculate how much we have dragged along the dragable axes of the ScrollBox float UiScrollBoxComponent::GetValidDragDistanceInPixels(AZ::Vector2 startPoint, AZ::Vector2 endPoint) { const float validDragRatio = 0.5f; AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (!contentParentEntity) { return 0.0f; } // convert the drag vector to local space AZ::Matrix4x4 transformFromViewport; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetTransformFromViewport, transformFromViewport); AZ::Vector2 dragVec = endPoint - startPoint; AZ::Vector3 dragVec3(dragVec.GetX(), dragVec.GetY(), 0.0f); AZ::Vector3 localDragVec = transformFromViewport.Multiply3x3(dragVec3); // constrain to the allowed movement directions if (!m_isHorizontalScrollingEnabled) { localDragVec.SetX(0.0f); } if (!m_isVerticalScrollingEnabled) { localDragVec.SetY(0.0f); } // convert back to viewport space AZ::Matrix4x4 transformToViewport; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetTransformToViewport, transformToViewport); AZ::Vector3 validDragVec = transformToViewport.Multiply3x3(localDragVec); float validDistance = validDragVec.GetLengthSq(); float totalDistance = dragVec.GetLengthSq(); // if they are not dragging mostly in a valid direction then ignore the drag if (validDistance / totalDistance < validDragRatio) { validDistance = 0.0f; } // return the valid drag distance return validDistance; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::CheckForDragOrHandOffToParent(AZ::Vector2 point) { AZ::EntityId parentDraggable; EBUS_EVENT_ID_RESULT(parentDraggable, GetEntityId(), UiElementBus, FindParentInteractableSupportingDrag, point); // if this interactable is inside another interactable that supports drag then we use // a threshold value before starting a drag on this interactable const float normalDragThreshold = 0.0f; const float containedDragThreshold = 5.0f; float dragThreshold = normalDragThreshold; if (parentDraggable.IsValid()) { dragThreshold = containedDragThreshold; } // calculate how much we have dragged in a valid direction float validDragDistance = GetValidDragDistanceInPixels(m_pressedPoint, point); if (validDragDistance > dragThreshold) { // we dragged above the threshold value along axis of slider m_isDragging = true; } else if (parentDraggable.IsValid()) { // offer the parent draggable the chance to become the active interactable bool handOff = false; EBUS_EVENT_ID_RESULT(handOff, parentDraggable, UiInteractableBus, OfferDragHandOff, GetEntityId(), m_pressedPoint, point, containedDragThreshold); if (handOff) { // interaction has been handed off to a container entity m_isPressed = false; } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::DoSetScrollOffset(AZ::Vector2 scrollOffset) { m_scrollOffset = scrollOffset; if (m_contentEntity.IsValid()) { // The scrollOffset is the distance from the content element's anchors to its pivot // Given the scrollOffset we adjust the offsets to make this so. UiTransform2dInterface::Offsets offsets; EBUS_EVENT_ID_RESULT(offsets, m_contentEntity, UiTransform2dBus, GetOffsets); AZ::Vector2 pivot; EBUS_EVENT_ID_RESULT(pivot, m_contentEntity, UiTransformBus, GetPivot); float width = offsets.m_right - offsets.m_left; float height = offsets.m_bottom - offsets.m_top; offsets.m_left = scrollOffset.GetX() - width * pivot.GetX(); offsets.m_right = offsets.m_left + width; offsets.m_top = scrollOffset.GetY() - height * pivot.GetY(); offsets.m_bottom = offsets.m_top + height; EBUS_EVENT_ID(m_contentEntity, UiTransform2dBus, SetOffsets, offsets); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::DoChangedActions() { if (m_onScrollOffsetChanged) { m_onScrollOffsetChanged(GetEntityId(), m_scrollOffset); } // Tell any action listeners about the event if (!m_scrollOffsetChangedActionName.empty()) { AZ::EntityId canvasEntityId; EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId); EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_scrollOffsetChangedActionName); } NotifyListenersOnScrollOffsetChanged(); NotifyListenersOnScrollValueChanged(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::DoChangingActions() { if (m_onScrollOffsetChanging) { m_onScrollOffsetChanging(GetEntityId(), m_scrollOffset); } // Tell any action listeners about the event if (!m_scrollOffsetChangingActionName.empty()) { AZ::EntityId canvasEntityId; EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId); EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_scrollOffsetChangingActionName); } NotifyListenersOnScrollOffsetChanging(); NotifyListenersOnScrollValueChanging(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::NotifyScrollersOnValueChanged() { AZ::Vector2 normalizedScrollValueOut; bool result = ScrollOffsetToNormalizedScrollValue(m_scrollOffset, normalizedScrollValueOut); if (result) { EBUS_EVENT_ID(GetEntityId(), UiScrollableToScrollerNotificationBus, OnValueChangedByScrollable, normalizedScrollValueOut); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::NotifyScrollersOnValueChanging() { AZ::Vector2 normalizedScrollValueOut; bool result = ScrollOffsetToNormalizedScrollValue(m_scrollOffset, normalizedScrollValueOut); if (result) { EBUS_EVENT_ID(GetEntityId(), UiScrollableToScrollerNotificationBus, OnValueChangingByScrollable, normalizedScrollValueOut); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::NotifyListenersOnScrollValueChanged() { AZ::Vector2 normalizedScrollValueOut; bool result = ScrollOffsetToNormalizedScrollValue(m_scrollOffset, normalizedScrollValueOut); if (result) { EBUS_EVENT_ID(GetEntityId(), UiScrollableNotificationBus, OnScrollableValueChanged, normalizedScrollValueOut); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::NotifyListenersOnScrollValueChanging() { AZ::Vector2 normalizedScrollValueOut; bool result = ScrollOffsetToNormalizedScrollValue(m_scrollOffset, normalizedScrollValueOut); if (result) { EBUS_EVENT_ID(GetEntityId(), UiScrollableNotificationBus, OnScrollableValueChanging, normalizedScrollValueOut); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::NotifyListenersOnScrollOffsetChanged() { EBUS_EVENT_ID(GetEntityId(), UiScrollBoxNotificationBus, OnScrollOffsetChanged, m_scrollOffset); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::NotifyListenersOnScrollOffsetChanging() { EBUS_EVENT_ID(GetEntityId(), UiScrollBoxNotificationBus, OnScrollOffsetChanging, m_scrollOffset); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiTransformInterface::Rect UiScrollBoxComponent::GetAxisAlignedContentRect() { UiTransformInterface::RectPoints points; EBUS_EVENT_ID(m_contentEntity, UiTransformBus, GetCanvasSpacePointsNoScaleRotate, points); AZ::Matrix4x4 transform; EBUS_EVENT_ID(m_contentEntity, UiTransformBus, GetLocalTransform, transform); points = points.Transform(transform); UiTransformInterface::Rect rect; rect.left = points.GetAxisAlignedTopLeft().GetX(); rect.right = points.GetAxisAlignedBottomRight().GetX(); rect.top = points.GetAxisAlignedTopLeft().GetY(); rect.bottom = points.GetAxisAlignedBottomRight().GetY(); return rect; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::ScrollOffsetToNormalizedScrollValue(AZ::Vector2 scrollOffset, AZ::Vector2& normalizedScrollValueOut) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); if (contentRect.GetWidth() <= parentRect.GetWidth()) { normalizedScrollValueOut.SetX(0.0f); } else { float minScrollOffset = scrollOffset.GetX() - (contentRect.left - parentRect.left); float maxScrollOffset = scrollOffset.GetX() - (contentRect.right - parentRect.right); normalizedScrollValueOut.SetX((scrollOffset.GetX() - minScrollOffset) / (maxScrollOffset - minScrollOffset)); } if (contentRect.GetHeight() <= parentRect.GetHeight()) { normalizedScrollValueOut.SetY(0.0f); } else { float minScrollOffset = scrollOffset.GetY() - (contentRect.top - parentRect.top); float maxScrollOffset = scrollOffset.GetY() - (contentRect.bottom - parentRect.bottom); normalizedScrollValueOut.SetY((scrollOffset.GetY() - minScrollOffset) / (maxScrollOffset - minScrollOffset)); } return true; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::NormalizedScrollValueToScrollOffset(UiScrollerInterface::Orientation orientation, float normalizedScrollValue, float& scrollOffsetOut) { if (orientation == UiScrollerInterface::Orientation::Horizontal || orientation == UiScrollerInterface::Orientation::Vertical) { AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); if (orientation == UiScrollerInterface::Orientation::Horizontal) { if (contentRect.GetWidth() <= parentRect.GetWidth()) { if (m_isScrollingConstrained) { scrollOffsetOut = 0.0f; } else { scrollOffsetOut = m_scrollOffset.GetX(); } } else { float minScrollOffset = m_scrollOffset.GetX() - (contentRect.left - parentRect.left); float maxScrollOffset = m_scrollOffset.GetX() - (contentRect.right - parentRect.right); scrollOffsetOut = minScrollOffset + (maxScrollOffset - minScrollOffset) * normalizedScrollValue; } } else // orientation == UiScrollerInterface::Orientation::Vertical { if (contentRect.GetHeight() <= parentRect.GetHeight()) { if (m_isScrollingConstrained) { scrollOffsetOut = 0.0f; } else { scrollOffsetOut = m_scrollOffset.GetY(); } } else { float minScrollOffset = m_scrollOffset.GetY() - (contentRect.top - parentRect.top); float maxScrollOffset = m_scrollOffset.GetY() - (contentRect.bottom - parentRect.bottom); scrollOffsetOut = minScrollOffset + (maxScrollOffset - minScrollOffset) * normalizedScrollValue; } } return true; } } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::ScrollerValueToScrollOffsets(AZ::EntityId scroller, float scrollerValue, AZ::Vector2& scrollOffsetsOut) { if (((scroller == m_hScrollBarEntity) && m_isHorizontalScrollingEnabled) || ((scroller == m_vScrollBarEntity) && m_isVerticalScrollingEnabled)) { float scrollOffsetOut; UiScrollerInterface::Orientation orientation = (scroller == m_hScrollBarEntity) ? UiScrollerInterface::Orientation::Horizontal : UiScrollerInterface::Orientation::Vertical; bool result = NormalizedScrollValueToScrollOffset(orientation, scrollerValue, scrollOffsetOut); if (result) { scrollOffsetsOut = m_scrollOffset; if (orientation == UiScrollerInterface::Orientation::Horizontal) { scrollOffsetsOut.SetX(scrollOffsetOut); } else // orientation == UiScrollerInterface::Orientation::Vertical { scrollOffsetsOut.SetY(scrollOffsetOut); } return true; } } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::IsVerticalScrollBarOnRight() { // Check if vertical scrollbar is on the right of the content's parent AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { // Get content parent rect in canvas space UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); // Get vertical scrollbar rect in canvas space UiTransformInterface::Rect vScrollBarRect; EBUS_EVENT_ID(m_vScrollBarEntity, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, vScrollBarRect); return (vScrollBarRect.GetCenter().GetX() > parentRect.GetCenter().GetX()); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::IsHorizontalScrollBarOnBottom() { // Check if horizontal scrollbar is on the bottom of the content's parent AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { // Get content parent rect in canvas space UiTransformInterface::Rect parentRect; EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); // Get horizontal scrollbar rect in canvas space UiTransformInterface::Rect hScrollBarRect; EBUS_EVENT_ID(m_hScrollBarEntity, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, hScrollBarRect); return (hScrollBarRect.GetCenter().GetY() > parentRect.GetCenter().GetY()); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::UpdateScrollBarVisiblity() { bool updateHorizontalScrollBar = m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled && (m_hScrollBarVisibility != ScrollBarVisibility::AlwaysShow); bool updateVerticalScrollBar = m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled && (m_vScrollBarVisibility != ScrollBarVisibility::AlwaysShow); if (updateHorizontalScrollBar || updateVerticalScrollBar) { // Set scrollbar visibility based on whether there is scrollable content AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { bool showHScrollBar = true; bool showVScrollBar = true; // Get content parent's size AZ::Vector2 parentSize; EBUS_EVENT_ID_RESULT(parentSize, contentParentEntity->GetId(), UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); // Get content size UiTransformInterface::Rect contentRect = GetAxisAlignedContentRect(); AZ::Vector2 contentSize = contentRect.GetSize(); // First check if none of the hideable scrollbars are needed bool needHScrollBar = false; bool needVScrollBar = false; if (updateHorizontalScrollBar) { needHScrollBar = (contentSize.GetX() > parentSize.GetX()); } if (updateVerticalScrollBar) { needVScrollBar = (contentSize.GetY() > parentSize.GetY()); } if (!needHScrollBar && !needVScrollBar) { showHScrollBar = false; showVScrollBar = false; } else { // Next, check if only a horizontal scrollbar is needed AZ::Vector2 supposedParentSize = parentSize; if (updateHorizontalScrollBar && (m_hScrollBarVisibility == ScrollBarVisibility::AutoHideAndResizeViewport)) { // Get height of horizontal scrollbar AZ::Vector2 hScrollBarSize; EBUS_EVENT_ID_RESULT(hScrollBarSize, m_hScrollBarEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); float hScrollBarHeight = hScrollBarSize.GetY(); supposedParentSize.SetY(supposedParentSize.GetY() - hScrollBarHeight); } if (contentSize.GetY() <= supposedParentSize.GetY() && contentSize.GetX() > supposedParentSize.GetX()) { showHScrollBar = true; showVScrollBar = false; } else { // Next, check if only a vertical scrollbar is needed supposedParentSize = parentSize; if (updateVerticalScrollBar && (m_vScrollBarVisibility == ScrollBarVisibility::AutoHideAndResizeViewport)) { // Get width of vertical scrollbar AZ::Vector2 vScrollBarSize; EBUS_EVENT_ID_RESULT(vScrollBarSize, m_vScrollBarEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); float vScrollBarWidth = vScrollBarSize.GetX(); supposedParentSize.SetX(supposedParentSize.GetX() - vScrollBarWidth); } if (contentSize.GetX() <= supposedParentSize.GetX() && contentSize.GetY() > supposedParentSize.GetY()) { showHScrollBar = false; showVScrollBar = true; } else { // Both scrollbars are needed showHScrollBar = true; showVScrollBar = true; } } } // Set enabled property on the scrollbars if (updateHorizontalScrollBar) { EBUS_EVENT_ID(m_hScrollBarEntity, UiElementBus, SetIsEnabled, showHScrollBar); } if (updateVerticalScrollBar) { EBUS_EVENT_ID(m_vScrollBarEntity, UiElementBus, SetIsEnabled, showVScrollBar); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::UpdateScrollBarAnchorsAndOffsets() { // Set scrollbar anchors and offsets based on the other scrollbar's visibility if (m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled && (m_hScrollBarVisibility != ScrollBarVisibility::AlwaysShow)) { // Set anchors UiTransform2dInterface::Anchors anchors; EBUS_EVENT_ID_RESULT(anchors, m_hScrollBarEntity, UiTransform2dBus, GetAnchors); anchors.m_left = 0.0f; anchors.m_right = 1.0f; EBUS_EVENT_ID(m_hScrollBarEntity, UiTransform2dBus, SetAnchors, anchors, false, false); // Set offsets UiTransform2dInterface::Offsets offsets; EBUS_EVENT_ID_RESULT(offsets, m_hScrollBarEntity, UiTransform2dBus, GetOffsets); bool isVScrollBarEnabled = false; if (m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled) { EBUS_EVENT_ID_RESULT(isVScrollBarEnabled, m_vScrollBarEntity, UiElementBus, IsEnabled); } if (isVScrollBarEnabled) { // Get width of vertical scrollbar AZ::Vector2 vScrollBarSize; EBUS_EVENT_ID_RESULT(vScrollBarSize, m_vScrollBarEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); if (IsVerticalScrollBarOnRight()) { offsets.m_left = 0.0f; offsets.m_right = -vScrollBarSize.GetX(); } else { offsets.m_left = vScrollBarSize.GetX(); offsets.m_right = 0.0f; } } else { offsets.m_left = 0.0f; offsets.m_right = 0.0f; } EBUS_EVENT_ID(m_hScrollBarEntity, UiTransform2dBus, SetOffsets, offsets); } if (m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled && (m_vScrollBarVisibility != ScrollBarVisibility::AlwaysShow)) { // Set anchors UiTransform2dInterface::Anchors anchors; EBUS_EVENT_ID_RESULT(anchors, m_vScrollBarEntity, UiTransform2dBus, GetAnchors); anchors.m_top = 0.0f; anchors.m_bottom = 1.0f; EBUS_EVENT_ID(m_vScrollBarEntity, UiTransform2dBus, SetAnchors, anchors, false, false); // Set offsets UiTransform2dInterface::Offsets offsets; EBUS_EVENT_ID_RESULT(offsets, m_vScrollBarEntity, UiTransform2dBus, GetOffsets); bool isHScrollBarEnabled = false; if (m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled) { EBUS_EVENT_ID_RESULT(isHScrollBarEnabled, m_hScrollBarEntity, UiElementBus, IsEnabled); } if (isHScrollBarEnabled) { // Get height of horizontal scrollbar AZ::Vector2 hScrollBarSize; EBUS_EVENT_ID_RESULT(hScrollBarSize, m_hScrollBarEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); if (IsHorizontalScrollBarOnBottom()) { offsets.m_top = 0.0f; offsets.m_bottom = -hScrollBarSize.GetY(); } else { offsets.m_top = hScrollBarSize.GetY(); offsets.m_bottom = 0.0f; } } else { offsets.m_top = 0.0f; offsets.m_bottom = 0.0f; } EBUS_EVENT_ID(m_vScrollBarEntity, UiTransform2dBus, SetOffsets, offsets); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::UpdateContentParentOffsets(bool checkScrollBarVisibility) { if ((m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled && (m_hScrollBarVisibility == ScrollBarVisibility::AutoHideAndResizeViewport)) || (m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled && (m_vScrollBarVisibility == ScrollBarVisibility::AutoHideAndResizeViewport))) { // Set content parent offsets based on scrollbar visibility AZ::Entity* contentParentEntity = nullptr; EBUS_EVENT_ID_RESULT(contentParentEntity, m_contentEntity, UiElementBus, GetParent); if (contentParentEntity) { UiTransform2dInterface::Offsets offsets; EBUS_EVENT_ID_RESULT(offsets, contentParentEntity->GetId(), UiTransform2dBus, GetOffsets); if (m_hScrollBarEntity.IsValid() && m_isHorizontalScrollingEnabled && (m_hScrollBarVisibility == ScrollBarVisibility::AutoHideAndResizeViewport)) { bool isHScrollBarEnabled = false; if (checkScrollBarVisibility) { EBUS_EVENT_ID_RESULT(isHScrollBarEnabled, m_hScrollBarEntity, UiElementBus, IsEnabled); } if (isHScrollBarEnabled) { // Get height of horizontal scrollbar AZ::Vector2 hScrollBarSize; EBUS_EVENT_ID_RESULT(hScrollBarSize, m_hScrollBarEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); if (IsHorizontalScrollBarOnBottom()) { offsets.m_top = 0.0f; offsets.m_bottom = -hScrollBarSize.GetY(); } else { offsets.m_top = hScrollBarSize.GetY(); offsets.m_bottom = 0.0f; } } else { offsets.m_top = 0.0f; offsets.m_bottom = 0.0f; } } if (m_vScrollBarEntity.IsValid() && m_isVerticalScrollingEnabled && (m_vScrollBarVisibility == ScrollBarVisibility::AutoHideAndResizeViewport)) { bool isVScrollBarEnabled = false; if (checkScrollBarVisibility) { EBUS_EVENT_ID_RESULT(isVScrollBarEnabled, m_vScrollBarEntity, UiElementBus, IsEnabled); } if (isVScrollBarEnabled) { // Get width of vertical scrollbar AZ::Vector2 vScrollBarSize; EBUS_EVENT_ID_RESULT(vScrollBarSize, m_vScrollBarEntity, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate); if (IsVerticalScrollBarOnRight()) { offsets.m_left = 0.0f; offsets.m_right = -vScrollBarSize.GetX(); } else { offsets.m_left = vScrollBarSize.GetX(); offsets.m_right = 0.0f; } } else { offsets.m_left = 0.0f; offsets.m_right = 0.0f; } } EBUS_EVENT_ID(contentParentEntity->GetId(), UiTransform2dBus, SetOffsets, offsets); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiScrollBoxComponent::ContentOrParentSizeChanged() { // Initialize content parent offsets if they are being controlled by scrollbar visibility behavior. // Offsets are initialized as if scrollbars are not visible UpdateContentParentOffsets(false); // Set whether scrollbars are visible based on scrollbar visibility behavior, content size and the size of its parent UpdateScrollBarVisiblity(); // Set scrollbar anchors and offsets based on scrollbar visibility behavior and whether the other scrollbar is visible UpdateScrollBarAnchorsAndOffsets(); // Set content parent offsets based on scrollbar visibility behavior and whether scrollbars are visible UpdateContentParentOffsets(true); // Notify listeners of ratio change between content size and the size of its parent AZ::Vector2 parentToContentRatio; bool result = GetScrollableParentToContentRatio(parentToContentRatio); if (result) { EBUS_EVENT_ID(GetEntityId(), UiScrollableToScrollerNotificationBus, OnScrollableParentToContentRatioChanged, parentToContentRatio); } if (DoSnap()) { // Reset drag info if (m_isDragging) { m_pressedScrollOffset = m_scrollOffset; m_pressedPoint = m_lastDragPoint; } NotifyScrollersOnValueChanged(); DoChangedActions(); } else { NotifyScrollersOnValueChanged(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiScrollBoxComponent::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { // conversion from version 1 to 2: // - Need to convert AZStd::string sprites to AzFramework::SimpleAssetReference if (classElement.GetVersion() < 2) { if (!LyShine::ConvertSubElementFromAzStringToAssetRef(context, classElement, "SelectedSprite")) { return false; } if (!LyShine::ConvertSubElementFromAzStringToAssetRef(context, classElement, "DisabledSprite")) { return false; } } // Conversion from version 2 to 3: if (classElement.GetVersion() < 3) { // find the base class (AZ::Component) // NOTE: in very old versions there may not be a base class because the base class was not serialized int componentBaseClassIndex = classElement.FindElement(AZ_CRC("BaseClass1", 0xd4925735)); // If there was a base class, make a copy and remove it AZ::SerializeContext::DataElementNode componentBaseClassNode; if (componentBaseClassIndex != -1) { // make a local copy of the component base class node componentBaseClassNode = classElement.GetSubElement(componentBaseClassIndex); // remove the component base class from the button classElement.RemoveElement(componentBaseClassIndex); } // Add a new base class (UiInteractableComponent) int interactableBaseClassIndex = classElement.AddElement(context, "BaseClass1"); AZ::SerializeContext::DataElementNode& interactableBaseClassNode = classElement.GetSubElement(interactableBaseClassIndex); // if there was previously a base class... if (componentBaseClassIndex != -1) { // copy the component base class into the new interactable base class // Since AZ::Component is now the base class of UiInteractableComponent interactableBaseClassNode.AddElement(componentBaseClassNode); } // Move the selected/hover state to the base class if (!UiSerialize::MoveToInteractableStateActions(context, classElement, "HoverStateActions", "SelectedColor", "SelectedAlpha", "SelectedSprite")) { return false; } // Move the disabled state to the base class if (!UiSerialize::MoveToInteractableStateActions(context, classElement, "DisabledStateActions", "DisabledColor", "DisabledAlpha", "DisabledSprite")) { return false; } } // Conversion from version 3 to 4: // - Need to convert Vec2 to AZ::Vector2 if (classElement.GetVersion() < 4) { if (!LyShine::ConvertSubElementFromVec2ToVector2(context, classElement, "ScrollOffset")) { return false; } if (!LyShine::ConvertSubElementFromVec2ToVector2(context, classElement, "SnapGrid")) { return false; } } return true; }