/* * 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 "UiMarkupButtonComponent.h" #include <LyShine/Bus/UiCanvasBus.h> #include <LyShine/Bus/UiMarkupButtonBus.h> #include <AzCore/Math/Crc.h> #include <AzCore/Serialization/SerializeContext.h> #include <AzCore/Serialization/EditContext.h> #include <AzCore/RTTI/BehaviorContext.h> namespace { //! Given a UI element on a canvas, return the mouse position. AZ::Vector2 GetMousePosition(AZ::EntityId entityId) { AZ::EntityId canvasId; EBUS_EVENT_ID_RESULT(canvasId, entityId, UiElementBus, GetCanvasEntityId); AZ::Vector2 mousePos = AZ::Vector2::CreateZero(); EBUS_EVENT_ID_RESULT(mousePos, canvasId, UiCanvasBus, GetMousePosition); return mousePos; } //! Returns an index to the given list of clickable text rects that contains the given mouse position, otherwise returns negative. int FindClickableTextRectIndexFromCanvasSpacePoint(const AZ::Vector2& canvasSpacePosition, const UiClickableTextInterface::ClickableTextRects& clickableTextRects) { // Iterate through the clickable rects to find one that contains the point int clickableRectIndex = -1; const int numClickableRects = clickableTextRects.size(); for (int i = 0; i < numClickableRects; ++i) { const auto& clickableRect = clickableTextRects[i]; const UiTransformInterface::Rect& rect = clickableRect.rect; const bool containedX = canvasSpacePosition.GetX() >= rect.left && canvasSpacePosition.GetX() <= rect.right; if (containedX) { const bool containedY = canvasSpacePosition.GetY() >= rect.top && canvasSpacePosition.GetY() <= rect.bottom; if (containedY) { clickableRectIndex = i; break; } } } return clickableRectIndex; } //! Returns an index to the given list of clickable text rects that contains the given mouse position, otherwise returns negative. int FindClickableTextRectIndexFromViewportSpacePoint(AZ::EntityId entityId, const AZ::Vector2& mousePos, const UiClickableTextInterface::ClickableTextRects& clickableTextRects) { // first transform the mousePos from viewport space to "canvas space no-scale-rotate", which is the space that clickableTextRects // are stored in. AZ::Matrix4x4 transformFromViewport; EBUS_EVENT_ID(entityId, UiTransformBus, GetTransformFromViewport, transformFromViewport); AZ::Vector3 point3(mousePos.GetX(), mousePos.GetY(), 0.0f); point3 = transformFromViewport * point3; AZ::Vector2 canvasSpacePosition(point3.GetX(), point3.GetY()); return FindClickableTextRectIndexFromCanvasSpacePoint(canvasSpacePosition, clickableTextRects); } } //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiMarkupButtonNotificationBus Behavior context handler class class UiMarkupButtonNotificationBusBehaviorHandler : public UiMarkupButtonNotificationsBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(UiMarkupButtonNotificationBusBehaviorHandler, "{ACCF73DC-86DD-4D1C-85B3-1E016BAAA495}", AZ::SystemAllocator, OnHoverStart, OnHoverEnd, OnPressed, OnReleased, OnClick); void OnHoverStart(int id, const AZStd::string& action, const AZStd::string& data) override { Call(FN_OnHoverStart, id, action, data); } void OnHoverEnd(int id, const AZStd::string& action, const AZStd::string& data) override { Call(FN_OnHoverEnd, id, action, data); } void OnPressed(int id, const AZStd::string& action, const AZStd::string& data) override { Call(FN_OnPressed, id, action, data); } void OnReleased(int id, const AZStd::string& action, const AZStd::string& data) override { Call(FN_OnReleased, id, action, data); } void OnClick(int id, const AZStd::string& action, const AZStd::string& data) override { Call(FN_OnClick, id, action, data); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// UiMarkupButtonComponent::UiMarkupButtonComponent() { } //////////////////////////////////////////////////////////////////////////////////////////////////// UiMarkupButtonComponent::~UiMarkupButtonComponent() { } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Color UiMarkupButtonComponent::GetLinkColor() { return m_linkColor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::SetLinkColor(const AZ::Color& linkColor) { m_linkColor = linkColor; OnLinkColorChanged(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Color UiMarkupButtonComponent::GetLinkHoverColor() { return m_linkHoverColor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::SetLinkHoverColor(const AZ::Color& linkHoverColor) { m_linkHoverColor = linkHoverColor; OnLinkHoverColorChanged(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiMarkupButtonComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive) { const bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive); if (!handled) { return false; } const int clickableRectIndex = FindClickableTextRectIndexFromViewportSpacePoint(GetEntityId(), point, m_clickableTextRects); if (clickableRectIndex >= 0) { const int clickableId = m_clickableTextRects[clickableRectIndex].id; const AZStd::string& action = m_clickableTextRects[clickableRectIndex].action; const AZStd::string& data = m_clickableTextRects[clickableRectIndex].data; m_clickableRectPressedIndex = clickableRectIndex; EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnPressed, clickableId, action, data); } return handled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiMarkupButtonComponent::HandleReleased(AZ::Vector2 point) { const bool handled = UiInteractableComponent::HandleReleased(point); if (!handled) { m_clickableRectPressedIndex = -1; return false; } // This could be negative if the clickable text change since the pressed // event occurred (OnClickableTextChanged resets the pressed index value). if (m_clickableRectPressedIndex < 0) { EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnReleased, -1, AZStd::string(), AZStd::string()); } else { const int pressedClickableId = m_clickableTextRects[m_clickableRectPressedIndex].id; const AZStd::string& action = m_clickableTextRects[m_clickableRectPressedIndex].action; const AZStd::string& data = m_clickableTextRects[m_clickableRectPressedIndex].data; EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnReleased, pressedClickableId, action, data); bool onClickTriggered = false; const int releasedClickableRectIndex = FindClickableTextRectIndexFromViewportSpacePoint(GetEntityId(), point, m_clickableTextRects); if (releasedClickableRectIndex >= 0) { // If the release happens on the pressed link ID, trigger a click. const int releasedClickableId = m_clickableTextRects[releasedClickableRectIndex].id; if (releasedClickableId == pressedClickableId) { EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnClick, pressedClickableId, action, data); onClickTriggered = true; } } if (!onClickTriggered) { // Clear the hover state now in case this entity is no longer // being hovered. This can happen when the user releases the // mouse outside of the clickable text rect. HandleClickableHoverEnd(); } } m_clickableRectPressedIndex = -1; return handled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::Update(float deltaTime) { UiInteractableComponent::Update(deltaTime); UpdateHover(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::OnClickableTextChanged() { m_clickableTextRects.clear(); EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, GetClickableTextRects, m_clickableTextRects); // Reset all links back to their non-hover color int lastClickableId = -1; for (const auto& clickableText : m_clickableTextRects) { // Color is assigned by clickable ID and it's possible for // multiple clickable text rects to share the same ID, so guard // against unnecessary calls to set the color for IDs that have // previously been set. if (lastClickableId != clickableText.id) { EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableText.id, m_linkColor); lastClickableId = clickableText.id; } } // Because the clickable text has changed, our current hover and pressed // states may no longer apply. Update it again based on the new clickable // text rects and current mouse position. m_clickableRectHoverIndex = -1; m_clickableRectPressedIndex = -1; UpdateHover(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::Activate() { UiInteractableComponent::Activate(); UiMarkupButtonBus::Handler::BusConnect(GetEntityId()); UiClickableTextNotificationsBus::Handler::BusConnect(GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::Deactivate() { UiInteractableComponent::Deactivate(); UiMarkupButtonBus::Handler::BusDisconnect(GetEntityId()); UiClickableTextNotificationsBus::Handler::BusDisconnect(GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::UpdateHover() { // Don't update hover state when we're actively being pressed. If we ever // add a pressed color, we could update this logic so that the pressed // color updates when the mouse moves on/off the clickable text. if (m_isHandlingEvents && !m_isPressed) { AZ::EntityId canvasId; EBUS_EVENT_ID_RESULT(canvasId, GetEntityId(), UiElementBus, GetCanvasEntityId); AZ::EntityId hoverInteractable; EBUS_EVENT_ID_RESULT(hoverInteractable, canvasId, UiCanvasBus, GetHoverInteractable); // Similarly, the hover interactable won't updated while another // element is being pressed - we don't want to update hover state // of any clickable text (on any entity) while a press is happening. if (hoverInteractable == GetEntityId()) { const int rectIndex = FindClickableTextRectIndexFromViewportSpacePoint(GetEntityId(), GetMousePosition(GetEntityId()), m_clickableTextRects); int rectIndexClickableId = -1; if (rectIndex >= 0) { rectIndexClickableId = m_clickableTextRects[rectIndex].id; } int hoverClickableId = -1; if (m_clickableRectHoverIndex >= 0) { hoverClickableId = m_clickableTextRects[m_clickableRectHoverIndex].id; } const bool enteringHover = rectIndexClickableId >= 0 && hoverClickableId < 0; const bool leavingHover = rectIndexClickableId < 0 && hoverClickableId >= 0; const bool switchingHoverRect = rectIndexClickableId >= 0 && hoverClickableId >= 0 && rectIndexClickableId != hoverClickableId; if (enteringHover) { HandleClickableHoverStart(rectIndex); } else if (leavingHover) { HandleClickableHoverEnd(); } else if (switchingHoverRect) { HandleClickableHoverEnd(); HandleClickableHoverStart(rectIndex); } } else { // Not being pressed or hovered, so reset the hover index element // just in case it is set (this can occur if we never receive a // release event for the interactable). HandleClickableHoverEnd(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::HandleClickableHoverStart(int clickableRectIndex) { m_clickableRectHoverIndex = clickableRectIndex; const int clickableId = m_clickableTextRects[m_clickableRectHoverIndex].id; // Set the link color prior to notification being triggered in case listeners want to set // the color themselves. EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableId, m_linkHoverColor); const AZStd::string& action = m_clickableTextRects[m_clickableRectHoverIndex].action; const AZStd::string& data = m_clickableTextRects[m_clickableRectHoverIndex].data; EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnHoverStart, clickableId, action, data); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::HandleClickableHoverEnd() { if (m_clickableRectHoverIndex >= 0) { const int clickableId = m_clickableTextRects[m_clickableRectHoverIndex].id; // Set the link color prior to notification being triggered in case listeners want to set // the color themselves. EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableId, m_linkColor); const AZStd::string& action = m_clickableTextRects[m_clickableRectHoverIndex].action; const AZStd::string& data = m_clickableTextRects[m_clickableRectHoverIndex].data; m_clickableRectHoverIndex = -1; EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnHoverEnd, clickableId, action, data); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::OnLinkColorChanged() { // If a link is being hovered (e.g. if SetLinkColor called at runtime while a link is being hovered) // then we do not want to set the color of that link int hoverClickableId = -1; if (m_clickableRectHoverIndex >= 0) { hoverClickableId = m_clickableTextRects[m_clickableRectHoverIndex].id; } // Set all links to the new link color (unless they are currently being hovered) int lastClickableId = -1; for (const auto& clickableText : m_clickableTextRects) { // Color is assigned by clickable ID and it's possible for // multiple clickable text rects to share the same ID, so guard // against unnecessary calls to set the color for IDs that have // previously been set. // We also don't want to set the text to the link color if it is currently being hovered. if (lastClickableId != clickableText.id && clickableText.id != hoverClickableId) { EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableText.id, m_linkColor); lastClickableId = clickableText.id; } } } //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::OnLinkHoverColorChanged() { // If a link is being hovered (e.g. if SetLinkColor called at runtime while a link is being hovered) // then we want to set the color of that link to the new hover color int hoverClickableId = -1; if (m_clickableRectHoverIndex >= 0) { hoverClickableId = m_clickableTextRects[m_clickableRectHoverIndex].id; } // Set any hovered links to the new link hover color int lastClickableId = -1; for (const auto& clickableText : m_clickableTextRects) { // Color is assigned by clickable ID and it's possible for // multiple clickable text rects to share the same ID, so guard // against unnecessary calls to set the color for IDs that have // previously been set. if (lastClickableId != clickableText.id) { // If it is currently being hovered then set its color to the new link hover color if (clickableText.id == hoverClickableId) { EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableText.id, m_linkHoverColor); } lastClickableId = clickableText.id; } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiMarkupButtonComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); if (serializeContext) { serializeContext->Class<UiMarkupButtonComponent, UiInteractableComponent>() ->Version(1, &VersionConverter) ->Field("LinkColor", &UiMarkupButtonComponent::m_linkColor) ->Field("LinkHoverColor", &UiMarkupButtonComponent::m_linkHoverColor); AZ::EditContext* ec = serializeContext->GetEditContext(); if (ec) { auto editInfo = ec->Class<UiMarkupButtonComponent>("MarkupButton", "An interactable component for enabling clicks from markup text (mouse support only)."); editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "UI") // Need to request markup button component icons for LY ML ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiMarkupButton.png") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiMarkupButton.png") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0)) ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiMarkupButtonComponent::m_linkColor, "Link Color", "Link text color.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiMarkupButtonComponent::OnLinkColorChanged); editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiMarkupButtonComponent::m_linkHoverColor, "Link Hover Color", "Link text hover color.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiMarkupButtonComponent::OnLinkHoverColorChanged); } } AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context); if (behaviorContext) { behaviorContext->EBus<UiMarkupButtonBus>("UiMarkupButtonBus") ->Event("GetLinkColor", &UiMarkupButtonBus::Events::GetLinkColor) ->Event("SetLinkColor", &UiMarkupButtonBus::Events::SetLinkColor) ->Event("GetLinkHoverColor", &UiMarkupButtonBus::Events::GetLinkHoverColor) ->Event("SetLinkHoverColor", &UiMarkupButtonBus::Events::SetLinkHoverColor); behaviorContext->EBus<UiMarkupButtonNotificationsBus>("UiMarkupButtonNotificationsBus") ->Handler<UiMarkupButtonNotificationBusBehaviorHandler>(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiMarkupButtonComponent::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { return true; } #include "Tests/internal/test_UiMarkupButtonComponent.cpp"