/* * 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 "UiDraggableComponent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "UiNavigationHelpers.h" //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiDraggableNotificationBus Behavior context handler class class UiDraggableNotificationBusBehaviorHandler : public UiDraggableNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(UiDraggableNotificationBusBehaviorHandler, "{7EEA2A71-AB29-4F1D-AC76-4BE7237AB99B}", AZ::SystemAllocator, OnDragStart, OnDrag, OnDragEnd); void OnDragStart(AZ::Vector2 position) override { Call(FN_OnDragStart, position); } void OnDrag(AZ::Vector2 position) override { Call(FN_OnDrag, position); } void OnDragEnd(AZ::Vector2 position) override { Call(FN_OnDragEnd, position); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// UiDraggableComponent::UiDraggableComponent() { // Must be called in the same order as the states defined in UiDraggableInterface m_stateActionManager.AddState(&m_dragNormalStateActions); m_stateActionManager.AddState(&m_dragValidStateActions); m_stateActionManager.AddState(&m_dragInvalidStateActions); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiDraggableComponent::~UiDraggableComponent() { // delete all the state actions now rather than letting the base class do it automatically // because the m_stateActionManager has pointers to members in this derived class. m_stateActionManager.ClearStates(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiDraggableComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive) { bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive); if (handled) { // NOTE: Drag start does not happen until the mouse actually starts moving so HandlePressed does // not do much. Reset these member variables just in case they did not get reset in end drag m_isDragging = false; m_dragState = DragState::Normal; m_hoverDropTarget.SetInvalid(); } return handled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiDraggableComponent::HandleReleased(AZ::Vector2 point) { EndDragOperation(point, false); if (m_isPressed && m_isHandlingEvents) { UiInteractableComponent::TriggerReleasedAction(); } m_isPressed = false; m_pressedPoint = AZ::Vector2(0.0f, 0.0f); return m_isHandlingEvents; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiDraggableComponent::HandleEnterPressed(bool& shouldStayActive) { bool handled = UiInteractableComponent::HandleEnterPressed(shouldStayActive); if (handled) { AZ::Vector2 point(0.0f, 0.0f); EBUS_EVENT_ID_RESULT(point, GetEntityId(), UiTransformBus, GetViewportSpacePivot); // if we are not yet in the dragging state do some tests to see if we should be if (!m_isDragging) { // the draggable will stay active after released so that arrow keys can be used to place it // over a drop target shouldStayActive = true; m_isActive = true; // the drag was valid for this draggable, we are now dragging m_isDragging = true; m_dragState = DragState::Normal; EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, point); m_hoverDropTarget.SetInvalid(); // find closest drop target to the draggable's center AZ::EntityId closestDropTarget = FindClosestNavigableDropTarget(); if (closestDropTarget.IsValid()) { EBUS_EVENT_ID_RESULT(point, closestDropTarget, UiTransformBus, GetViewportPosition); } DoDrag(point, true); } } return handled; } ///////////////////////////////////////////////////////////////// bool UiDraggableComponent::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 (command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down || command == UiNavigationHelpers::Command::Left || command == UiNavigationHelpers::Command::Right) { AZ::EntityId closestDropTarget = FindClosestNavigableDropTarget(); AZ::EntityId newElement; if (m_hoverDropTarget.IsValid()) { LyShine::EntityArray navigableElements; FindNavigableDropTargetElements(m_hoverDropTarget, navigableElements); auto isValidDropTarget = [](AZ::EntityId entityId) { bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, entityId, UiElementBus, IsEnabled); if (isEnabled && UiDropTargetBus::FindFirstHandler(entityId)) { return true; } return false; }; newElement = UiNavigationHelpers::GetNextElement(m_hoverDropTarget, command, navigableElements, closestDropTarget, isValidDropTarget); } else { // find closest drop target to the draggable's center newElement = closestDropTarget; } if (newElement.IsValid()) { AZ::Vector2 point(0.0f, 0.0f); EBUS_EVENT_ID_RESULT(point, newElement, UiTransformBus, GetViewportSpacePivot); DoDrag(point, true); } result = true; } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::InputPositionUpdate(AZ::Vector2 point) { if (m_isPressed) { // if we are not yet in the dragging state do some tests to see if we should be if (!m_isDragging) { bool handOffDone = false; bool dragDetected = CheckForDragOrHandOffToParent(GetEntityId(), m_pressedPoint, point, 0.0f, handOffDone); if (dragDetected) { if (handOffDone) { // the drag was handed off to a parent, this draggable is no longer active m_isPressed = false; } else { // the drag was valid for this draggable, we are now dragging m_isDragging = true; m_dragState = DragState::Normal; EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, point); m_hoverDropTarget.SetInvalid(); } } } // if we are now in the dragging state do the drag update and handle start/end of drop hover if (m_isDragging) { DoDrag(point, false); } } } ///////////////////////////////////////////////////////////////// bool UiDraggableComponent::DoesSupportDragHandOff(AZ::Vector2 startPoint) { // this component does support hand-off, so long as the start point is in its bounds // i.e. if there is a child interactable element such as a button or checkbox and the user // drags it, then the drag can get handed off to the parent draggable element bool isPointInRect = false; EBUS_EVENT_ID_RESULT(isPointInRect, GetEntityId(), UiTransformBus, IsPointInRect, startPoint); return isPointInRect; } ///////////////////////////////////////////////////////////////// bool UiDraggableComponent::OfferDragHandOff(AZ::EntityId currentActiveInteractable, AZ::Vector2 startPoint, AZ::Vector2 currentPoint, float dragThreshold) { // A child interactable element is offering to hand-off a drag interaction to this element bool result = false; bool handedOffToParent = false; bool dragDetected = CheckForDragOrHandOffToParent(currentActiveInteractable, startPoint, currentPoint, dragThreshold, handedOffToParent); if (dragDetected) { if (!handedOffToParent) { // a drag was detected and it was not handed off to a parent, so this draggable is now taking the handoff m_isPressed = true; m_pressedPoint = startPoint; // tell the canvas that this is now the active interactable EBUS_EVENT_ID(currentActiveInteractable, UiInteractableActiveNotificationBus, ActiveChanged, GetEntityId(), false); // start the drag m_isDragging = true; m_dragState = DragState::Normal; EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, currentPoint); m_hoverDropTarget.SetInvalid(); // Send the OnDrag and any OnDropHoverStart immediately so that it doesn't require another frame to // update. DoDrag(currentPoint, false); } } return dragDetected; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::LostActiveStatus() { // this is called when keyboard or console operation is being used and Enter was used to end the // operation. UiInteractableComponent::LostActiveStatus(); AZ::Vector2 viewportPoint; EBUS_EVENT_ID_RESULT(viewportPoint, GetEntityId(), UiTransformBus, GetViewportSpacePivot); EndDragOperation(viewportPoint, true); m_isActive = false; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiDraggableInterface::DragState UiDraggableComponent::GetDragState() { return m_dragState; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::SetDragState(DragState dragState) { m_dragState = dragState; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::RedoDrag(AZ::Vector2 point) { DoDrag(point, true); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::SetAsProxy(AZ::EntityId originalDraggableId, AZ::Vector2 point) { // find the originalDraggable by Id AZ::Entity* originalDraggable = nullptr; EBUS_EVENT_RESULT(originalDraggable, AZ::ComponentApplicationBus, FindEntity, originalDraggableId); if (!originalDraggable) { AZ_Warning("UI", false, "SetAsProxy: Cannot find original draggable"); return; } // Find the UiDraggableComponent on the originalDraggable UiDraggableComponent* originalComponent = originalDraggable->FindComponent(); if (!originalComponent) { AZ_Warning("UI", false, "SetAsProxy: Cannot find draggable component"); return; } // Set the isProxyFor member variable, this indicates that this is a proxy m_isProxyFor = originalDraggableId; // put this draggable into the drag state and copy some of the state from the original m_isPressed = true; m_pressedPoint = originalComponent->m_pressedPoint; m_isActive = originalComponent->m_isActive; // tell the proxy draggable's canvas that this is now the active interactable AZ::EntityId canvasEntityId; EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId); EBUS_EVENT_ID(canvasEntityId, UiCanvasBus, ForceActiveInteractable, GetEntityId(), m_isActive, m_pressedPoint); // start the drag on the proxy m_isDragging = true; m_dragState = DragState::Normal; EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, point); m_hoverDropTarget.SetInvalid(); // Send the OnDrag and any OnDropHoverStart immediately so that it doesn't require another frame to // update. DoDrag(point, false); // Turn of these flags on the original, this stops it responding to HandleReleased, InputPositionUpdate, etc // If the original is on a different canvas to the proxy then the original withh still get these functions called. // They just won't do anything. originalComponent->m_isDragging = false; originalComponent->m_isPressed = false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::ProxyDragEnd(AZ::Vector2 point) { AZ::Entity* originalDraggable = nullptr; EBUS_EVENT_RESULT(originalDraggable, AZ::ComponentApplicationBus, FindEntity, m_isProxyFor); if (!originalDraggable) { AZ_Warning("UI", false, "ProxyDragEnd: Cannot find original draggable"); return; } UiDraggableComponent* originalComponent = originalDraggable->FindComponent(); if (!originalComponent) { AZ_Warning("UI", false, "ProxyDragEnd: Cannot find draggable component on original"); return; } // we don't want the proxy to get in the way of the search for a drop target under the original // draggable so disable interaction on it m_isHandlingEvents = false; originalComponent->m_isPressed = true; originalComponent->m_isDragging = true; originalComponent->HandleReleased(point); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiDraggableComponent::IsProxy() { return (m_isProxyFor.IsValid()) ? true : false; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiDraggableComponent::GetOriginalFromProxy() { return m_isProxyFor; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiDraggableComponent::GetCanDropOnAnyCanvas() { return m_canDropOnAnyCanvas; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::SetCanDropOnAnyCanvas(bool anyCanvas) { m_canDropOnAnyCanvas = anyCanvas; } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::Activate() { UiInteractableComponent::Activate(); UiDraggableBus::Handler::BusConnect(m_entity->GetId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::Deactivate() { UiInteractableComponent::Deactivate(); UiDraggableBus::Handler::BusDisconnect(); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiInteractableStatesInterface::State UiDraggableComponent::ComputeInteractableState() { UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal; if (!m_isHandlingEvents) { state = UiInteractableStatesInterface::StateDisabled; } else if (m_isDragging) { switch (m_dragState) { case DragState::Normal: state = StateDragNormal; break; case DragState::Valid: state = StateDragValid; break; case DragState::Invalid: state = StateDragInvalid; break; } } else if (m_isPressed || m_isActive) { // To support keyboard/console we stay in pressed state when active state = UiInteractableStatesInterface::StatePressed; } else if (m_isHover) { state = UiInteractableStatesInterface::StateHover; } return state; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::OnDragNormalStateActionsChanged() { m_stateActionManager.InitInteractableEntityForStateActions(m_dragNormalStateActions); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::OnDragValidStateActionsChanged() { m_stateActionManager.InitInteractableEntityForStateActions(m_dragValidStateActions); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::OnDragInvalidStateActionsChanged() { m_stateActionManager.InitInteractableEntityForStateActions(m_dragInvalidStateActions); } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { serializeContext->Class() ->Version(1) ->Field("DragNormalStateActions", &UiDraggableComponent::m_dragNormalStateActions) ->Field("DragValidStateActions", &UiDraggableComponent::m_dragValidStateActions) ->Field("DragInvalidStateActions", &UiDraggableComponent::m_dragInvalidStateActions); AZ::EditContext* ec = serializeContext->GetEditContext(); if (ec) { auto editInfo = ec->Class("Draggable", "An interactable component for drag and drop behavior"); editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "UI") ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiDraggable.png") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiDraggable.png") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0)) ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Drag States") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(0, &UiDraggableComponent::m_dragNormalStateActions, "Normal", "The normal drag state actions") ->Attribute(AZ::Edit::Attributes::AddNotify, &UiDraggableComponent::OnDragNormalStateActionsChanged); editInfo->DataElement(0, &UiDraggableComponent::m_dragValidStateActions, "Valid", "The valid drag state actions") ->Attribute(AZ::Edit::Attributes::AddNotify, &UiDraggableComponent::OnDragValidStateActionsChanged); editInfo->DataElement(0, &UiDraggableComponent::m_dragInvalidStateActions, "Invalid", "The invalid drag state actions") ->Attribute(AZ::Edit::Attributes::AddNotify, &UiDraggableComponent::OnDragInvalidStateActionsChanged); } } AZ::BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { behaviorContext->Enum<(int)UiDraggableInterface::DragState::Normal>("eUiDragState_Normal") ->Enum<(int)UiDraggableInterface::DragState::Valid>("eUiDragState_Valid") ->Enum<(int)UiDraggableInterface::DragState::Invalid>("eUiDragState_Invalid"); behaviorContext->EBus("UiDraggableBus") ->Event("GetDragState", &UiDraggableBus::Events::GetDragState) ->Event("SetDragState", &UiDraggableBus::Events::SetDragState) ->Event("RedoDrag", &UiDraggableBus::Events::RedoDrag) ->Event("SetAsProxy", &UiDraggableBus::Events::SetAsProxy) ->Event("ProxyDragEnd", &UiDraggableBus::Events::ProxyDragEnd) ->Event("IsProxy", &UiDraggableBus::Events::IsProxy) ->Event("GetOriginalFromProxy", &UiDraggableBus::Events::GetOriginalFromProxy) ->Event("GetCanDropOnAnyCanvas", &UiDraggableBus::Events::GetCanDropOnAnyCanvas) ->Event("SetCanDropOnAnyCanvas", &UiDraggableBus::Events::SetCanDropOnAnyCanvas); behaviorContext->EBus("UiDraggableNotificationBus") ->Handler(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiDraggableComponent::GetDropTargetUnderDraggable(AZ::Vector2 point, bool ignoreInteractables) { AZ::EntityId result; AZ::EntityId canvasEntity; EBUS_EVENT_ID_RESULT(canvasEntity, GetEntityId(), UiElementBus, GetCanvasEntityId); // We will ignore this element and all its children in the search AZ::EntityId ignoreElement = GetEntityId(); // Look for a drop target under the mouse position // recursively check the children of the canvas (in reverse order since children are in front of parent) if (m_canDropOnAnyCanvas) { result = FindDropTargetOrInteractableOnAllCanvases(point, ignoreElement, ignoreInteractables); } else { result = FindDropTargetOrInteractableOnCanvas(canvasEntity, point, ignoreElement, ignoreInteractables); } // The result could be an interactable that is not a drop target since an interactable in front of a drop target // can block dropping on it (unless it is the child of the drop target) if (!UiDropTargetBus::FindFirstHandler(result)) { result.SetInvalid(); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiDraggableComponent::CheckForDragOrHandOffToParent(AZ::EntityId currentActiveInteractable, AZ::Vector2 startPoint, AZ::Vector2 currentPoint, float childDragThreshold, bool& handOffDone) { // Currently a draggable never hands off the drag to a parent since a drag in any direction is valid. // Potentially this could change if we allowed, for example, a scroll box containing draggables where // dragging up and down scrolled the scroll box and dragging left and right initiated drag and drop. // In that case we would need a property to say in which direction a draggable can be dragged. bool result = false; // Possibly this should be a user defined property since it defines how much movement constitutes a drag start const float normalDragThreshold = 3.0f; float dragThreshold = normalDragThreshold; if (childDragThreshold > 0.0f) { dragThreshold = childDragThreshold; } float dragThresholdSq = dragThreshold * dragThreshold; // calculate how much we have dragged AZ::Vector2 dragVector = currentPoint - startPoint; float dragDistanceSq = dragVector.GetLengthSq(); if (dragDistanceSq > dragThresholdSq) { // we dragged above the threshold value result = true; } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::DoDrag(AZ::Vector2 viewportPoint, bool ignoreInteractables) { // In the case where a proxy has been created in the OnDragStart handler we would no longer // be in the dragging state, in that case do nothing here if (!m_isDragging) { return; } // Send the OnDrag notification EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDrag, viewportPoint); AZ::EntityId dropEntity = GetDropTargetUnderDraggable(viewportPoint, ignoreInteractables); // if we have a drop hover entity and we are no longer hovering over it if (m_hoverDropTarget.IsValid() && m_hoverDropTarget != dropEntity) { // end the drop hover EBUS_EVENT_ID(m_hoverDropTarget, UiDropTargetBus, HandleDropHoverEnd, GetEntityId()); m_hoverDropTarget.SetInvalid(); } // if we do not have a drop hover entity and we are hovering over a drop target if (!m_hoverDropTarget.IsValid() && dropEntity.IsValid()) { // start a drop hover EBUS_EVENT_ID(dropEntity, UiDropTargetBus, HandleDropHoverStart, GetEntityId()); m_hoverDropTarget = dropEntity; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::EndDragOperation(AZ::Vector2 viewportPoint, bool ignoreInteractables) { if (m_isDragging) { // If we were hovering over a drop target then forget it, we will recompute what we are over now if (m_hoverDropTarget.IsValid()) { EBUS_EVENT_ID(m_hoverDropTarget, UiDropTargetBus, HandleDropHoverEnd, GetEntityId()); m_hoverDropTarget.SetInvalid(); } // Search for a drop target before calling OnDragEnd in case OnDragEnd moves the drop target that we are over AZ::EntityId dropEntity = GetDropTargetUnderDraggable(viewportPoint, ignoreInteractables); // send a drag end notification EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragEnd, viewportPoint); // If there was a drop target under the cursor then send it a message to handle this draggable being dropped on it if (dropEntity.IsValid()) { EBUS_EVENT_ID(dropEntity, UiDropTargetBus, HandleDrop, GetEntityId()); } m_isDragging = false; m_dragState = DragState::Normal; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiDraggableComponent::FindNavigableDropTargetElements(AZ::EntityId ignoreElement, LyShine::EntityArray& result) { AZ::EntityId canvasEntity; EBUS_EVENT_ID_RESULT(canvasEntity, GetEntityId(), UiElementBus, GetCanvasEntityId); LyShine::EntityArray elements; EBUS_EVENT_ID_RESULT(elements, canvasEntity, UiCanvasBus, GetChildElements); std::list elementList(elements.begin(), elements.end()); while (!elementList.empty()) { auto entity = elementList.front(); elementList.pop_front(); if (ignoreElement.IsValid() && entity->GetId() == ignoreElement) { continue; // this is the element to ignore, ignore its children also } // Check if the element is enabled bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, entity->GetId(), UiElementBus, IsEnabled); if (!isEnabled) { continue; } bool isDropTarget = false; if (UiDropTargetBus::FindFirstHandler(entity->GetId())) { isDropTarget = true; } UiNavigationInterface::NavigationMode navigationMode = UiNavigationInterface::NavigationMode::None; EBUS_EVENT_ID_RESULT(navigationMode, entity->GetId(), UiNavigationBus, GetNavigationMode); bool isNavigable = (navigationMode != UiNavigationInterface::NavigationMode::None); if (isDropTarget && isNavigable) { result.push_back(entity); } else { LyShine::EntityArray childElements; EBUS_EVENT_ID_RESULT(childElements, entity->GetId(), UiElementBus, GetChildElements); elementList.insert(elementList.end(), childElements.begin(), childElements.end()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiDraggableComponent::FindClosestNavigableDropTarget() { UiTransformInterface::RectPoints srcPoints; EBUS_EVENT_ID(GetEntityId(), UiTransformBus, GetViewportSpacePoints, srcPoints); AZ::Vector2 srcCenter = srcPoints.GetCenter(); LyShine::EntityArray dropTargets; FindNavigableDropTargetElements(AZ::EntityId(), dropTargets); float shortestDist = FLT_MAX; AZ::EntityId closestElement; for (auto dropTarget : dropTargets) { UiTransformInterface::RectPoints destPoints; EBUS_EVENT_ID(dropTarget->GetId(), UiTransformBus, GetViewportSpacePoints, destPoints); AZ::Vector2 destCenter = destPoints.GetCenter(); float dist = (destCenter - srcCenter).GetLengthSq(); if (dist < shortestDist) { shortestDist = dist; closestElement = dropTarget->GetId(); } } return closestElement; } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE STATIC FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiDraggableComponent::FindDropTargetOrInteractableOnAllCanvases( AZ::Vector2 point, AZ::EntityId ignoreElement, bool ignoreInteractables) { AZ::EntityId result; UiCanvasManagerInterface::CanvasEntityList canvases; EBUS_EVENT_RESULT(canvases, UiCanvasManagerBus, GetLoadedCanvases); // reverse iterate over the loaded canvases so that the front most canvas gets first chance to // handle the event bool areAnyInWorldInputCanvasesLoaded = false; for (auto iter = canvases.rbegin(); iter != canvases.rend() && !result.IsValid(); ++iter) { AZ::EntityId canvasEntityId = *iter; result = FindDropTargetOrInteractableOnCanvas(canvasEntityId, point, ignoreElement, ignoreInteractables); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiDraggableComponent::FindDropTargetOrInteractableOnCanvas(AZ::EntityId canvasEntityId, AZ::Vector2 point, AZ::EntityId ignoreElement, bool ignoreInteractables) { AZ::EntityId result; // recursively check the children of the canvas (in reverse order since children are in front of parent) int numChildren = 0; EBUS_EVENT_ID_RESULT(numChildren, canvasEntityId, UiCanvasBus, GetNumChildElements); for (int i = numChildren - 1; !result.IsValid() && i >= 0; i--) { AZ::EntityId child; EBUS_EVENT_ID_RESULT(child, canvasEntityId, UiCanvasBus, GetChildElementEntityId, i); if (child != ignoreElement) { result = FindDropTargetOrInteractableUnderCursor(child, point, ignoreElement, ignoreInteractables); } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiDraggableComponent::FindDropTargetOrInteractableUnderCursor(AZ::EntityId element, AZ::Vector2 point, AZ::EntityId ignoreElement, bool ignoreInteractables) { AZ::EntityId result; bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, element, UiElementBus, IsEnabled); if (!isEnabled) { // Nothing to do return result; } // first check the children (in reverse order since children are in front of parent) { // if this element is masking children at this point then don't check the children bool isMasked = false; EBUS_EVENT_ID_RESULT(isMasked, element, UiInteractionMaskBus, IsPointMasked, point); if (!isMasked) { int numChildren = 0; EBUS_EVENT_ID_RESULT(numChildren, element, UiElementBus, GetNumChildElements); for (int i = numChildren - 1; !result.IsValid() && i >= 0; i--) { AZ::EntityId child; EBUS_EVENT_ID_RESULT(child, element, UiElementBus, GetChildEntityId, i); if (child != ignoreElement) { result = FindDropTargetOrInteractableUnderCursor(child, point, ignoreElement, ignoreInteractables); } } } } // if no match then check this element if (!result.IsValid()) { // if the point is in this element's rect bool isInRect = false; EBUS_EVENT_ID_RESULT(isInRect, element, UiTransformBus, IsPointInRect, point); if (isInRect) { // If this element has a drop target component if (UiDropTargetBus::FindFirstHandler(element)) { // this is the drop target under the cursor result = element; } // else if this element has an interactable component else if (!ignoreInteractables && UiInteractableBus::FindFirstHandler(element)) { // check if this interactable component is in a state where it can handle an event at the given point bool canHandle = false; EBUS_EVENT_ID_RESULT(canHandle, element, UiInteractableBus, CanHandleEvent, point); if (canHandle) { // in this case the interaction is blocked unless this interactable has a parent that is // a drop target AZ::EntityId parent; EBUS_EVENT_ID_RESULT(parent, element, UiElementBus, GetParentEntityId); while (parent.IsValid()) { bool isInParentRect = false; EBUS_EVENT_ID_RESULT(isInParentRect, parent, UiTransformBus, IsPointInRect, point); if (isInParentRect && UiDropTargetBus::FindFirstHandler(parent)) { // We found a parent drop target and the cursor is in its rect, // this is considered the drop target under the cursor result = parent; break; } EBUS_EVENT_ID_RESULT(parent, parent, UiElementBus, GetParentEntityId); } if (!result.IsValid()) { // no parent drop target was found, return this blocking interactable result = element; } } } } } return result; }