Add core popup overlay primitive
This commit is contained in:
303
engine/src/UI/Widgets/UIPopupOverlayModel.cpp
Normal file
303
engine/src/UI/Widgets/UIPopupOverlayModel.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(0.0f, value);
|
||||
}
|
||||
|
||||
float GetRectRight(const UIRect& rect) {
|
||||
return rect.x + ClampNonNegative(rect.width);
|
||||
}
|
||||
|
||||
float GetRectBottom(const UIRect& rect) {
|
||||
return rect.y + ClampNonNegative(rect.height);
|
||||
}
|
||||
|
||||
bool IsPathPrefix(const UIInputPath& prefix, const UIInputPath& path) {
|
||||
if (prefix.Empty() || prefix.Size() > path.Size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetUIInputPathCommonPrefixLength(prefix, path) == prefix.Size();
|
||||
}
|
||||
|
||||
UIPopupPlacement FlipPlacement(UIPopupPlacement placement) {
|
||||
switch (placement) {
|
||||
case UIPopupPlacement::BottomStart:
|
||||
return UIPopupPlacement::TopStart;
|
||||
case UIPopupPlacement::BottomEnd:
|
||||
return UIPopupPlacement::TopEnd;
|
||||
case UIPopupPlacement::TopStart:
|
||||
return UIPopupPlacement::BottomStart;
|
||||
case UIPopupPlacement::TopEnd:
|
||||
return UIPopupPlacement::BottomEnd;
|
||||
case UIPopupPlacement::RightStart:
|
||||
return UIPopupPlacement::LeftStart;
|
||||
case UIPopupPlacement::RightEnd:
|
||||
return UIPopupPlacement::LeftEnd;
|
||||
case UIPopupPlacement::LeftStart:
|
||||
return UIPopupPlacement::RightStart;
|
||||
case UIPopupPlacement::LeftEnd:
|
||||
return UIPopupPlacement::RightEnd;
|
||||
default:
|
||||
return placement;
|
||||
}
|
||||
}
|
||||
|
||||
bool CanPlace(const UIRect& anchorRect, const UISize& popupSize, const UIRect& viewportRect, UIPopupPlacement placement) {
|
||||
const float popupWidth = ClampNonNegative(popupSize.width);
|
||||
const float popupHeight = ClampNonNegative(popupSize.height);
|
||||
const float viewportRight = GetRectRight(viewportRect);
|
||||
const float viewportBottom = GetRectBottom(viewportRect);
|
||||
|
||||
switch (placement) {
|
||||
case UIPopupPlacement::BottomStart:
|
||||
case UIPopupPlacement::BottomEnd:
|
||||
return GetRectBottom(anchorRect) + popupHeight <= viewportBottom;
|
||||
case UIPopupPlacement::TopStart:
|
||||
case UIPopupPlacement::TopEnd:
|
||||
return anchorRect.y - popupHeight >= viewportRect.y;
|
||||
case UIPopupPlacement::RightStart:
|
||||
case UIPopupPlacement::RightEnd:
|
||||
return GetRectRight(anchorRect) + popupWidth <= viewportRight;
|
||||
case UIPopupPlacement::LeftStart:
|
||||
case UIPopupPlacement::LeftEnd:
|
||||
return anchorRect.x - popupWidth >= viewportRect.x;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
UIPoint ResolvePlacementOrigin(
|
||||
const UIRect& anchorRect,
|
||||
const UISize& popupSize,
|
||||
UIPopupPlacement placement) {
|
||||
const float popupWidth = ClampNonNegative(popupSize.width);
|
||||
const float popupHeight = ClampNonNegative(popupSize.height);
|
||||
|
||||
switch (placement) {
|
||||
case UIPopupPlacement::BottomStart:
|
||||
return UIPoint(anchorRect.x, GetRectBottom(anchorRect));
|
||||
case UIPopupPlacement::BottomEnd:
|
||||
return UIPoint(GetRectRight(anchorRect) - popupWidth, GetRectBottom(anchorRect));
|
||||
case UIPopupPlacement::TopStart:
|
||||
return UIPoint(anchorRect.x, anchorRect.y - popupHeight);
|
||||
case UIPopupPlacement::TopEnd:
|
||||
return UIPoint(GetRectRight(anchorRect) - popupWidth, anchorRect.y - popupHeight);
|
||||
case UIPopupPlacement::RightStart:
|
||||
return UIPoint(GetRectRight(anchorRect), anchorRect.y);
|
||||
case UIPopupPlacement::RightEnd:
|
||||
return UIPoint(GetRectRight(anchorRect), GetRectBottom(anchorRect) - popupHeight);
|
||||
case UIPopupPlacement::LeftStart:
|
||||
return UIPoint(anchorRect.x - popupWidth, anchorRect.y);
|
||||
case UIPopupPlacement::LeftEnd:
|
||||
return UIPoint(anchorRect.x - popupWidth, GetRectBottom(anchorRect) - popupHeight);
|
||||
default:
|
||||
return UIPoint(anchorRect.x, GetRectBottom(anchorRect));
|
||||
}
|
||||
}
|
||||
|
||||
float ClampRectOrigin(float origin, float viewportStart, float viewportExtent, float rectExtent, bool& clamped) {
|
||||
const float safeViewportExtent = ClampNonNegative(viewportExtent);
|
||||
const float safeRectExtent = ClampNonNegative(rectExtent);
|
||||
const float minOrigin = viewportStart;
|
||||
const float maxOrigin = (std::max)(minOrigin, viewportStart + safeViewportExtent - safeRectExtent);
|
||||
const float clampedOrigin = (std::clamp)(origin, minOrigin, maxOrigin);
|
||||
clamped = clampedOrigin != origin;
|
||||
return clampedOrigin;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIPopupPlacementResult ResolvePopupPlacementRect(
|
||||
const UIRect& anchorRect,
|
||||
const UISize& popupSize,
|
||||
const UIRect& viewportRect,
|
||||
UIPopupPlacement placement) {
|
||||
UIPopupPlacementResult result = {};
|
||||
result.effectivePlacement = placement;
|
||||
|
||||
const UIPopupPlacement flippedPlacement = FlipPlacement(placement);
|
||||
if (!CanPlace(anchorRect, popupSize, viewportRect, placement) &&
|
||||
CanPlace(anchorRect, popupSize, viewportRect, flippedPlacement)) {
|
||||
result.effectivePlacement = flippedPlacement;
|
||||
}
|
||||
|
||||
const UIPoint origin = ResolvePlacementOrigin(anchorRect, popupSize, result.effectivePlacement);
|
||||
result.rect.width = ClampNonNegative(popupSize.width);
|
||||
result.rect.height = ClampNonNegative(popupSize.height);
|
||||
result.rect.x = ClampRectOrigin(origin.x, viewportRect.x, viewportRect.width, result.rect.width, result.clampedX);
|
||||
result.rect.y = ClampRectOrigin(origin.y, viewportRect.y, viewportRect.height, result.rect.height, result.clampedY);
|
||||
return result;
|
||||
}
|
||||
|
||||
const UIPopupOverlayEntry* UIPopupOverlayModel::GetRootPopup() const {
|
||||
return m_popupChain.empty() ? nullptr : &m_popupChain.front();
|
||||
}
|
||||
|
||||
const UIPopupOverlayEntry* UIPopupOverlayModel::GetTopmostPopup() const {
|
||||
return m_popupChain.empty() ? nullptr : &m_popupChain.back();
|
||||
}
|
||||
|
||||
const UIPopupOverlayEntry* UIPopupOverlayModel::FindPopup(std::string_view popupId) const {
|
||||
const std::size_t index = FindPopupIndex(popupId);
|
||||
return index == InvalidIndex ? nullptr : &m_popupChain[index];
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::OpenPopup(UIPopupOverlayEntry entry) {
|
||||
UIPopupOverlayMutationResult result = {};
|
||||
if (entry.popupId.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (entry.parentPopupId.empty()) {
|
||||
result = CloseAll(UIPopupDismissReason::Programmatic);
|
||||
m_popupChain.clear();
|
||||
m_popupChain.push_back(std::move(entry));
|
||||
result.changed = true;
|
||||
result.openedPopupId = m_popupChain.back().popupId;
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::size_t parentIndex = FindPopupIndex(entry.parentPopupId);
|
||||
if (parentIndex == InvalidIndex || entry.popupId == entry.parentPopupId) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index <= parentIndex; ++index) {
|
||||
if (m_popupChain[index].popupId == entry.popupId) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentIndex + 1u < m_popupChain.size()) {
|
||||
result = CloseFromIndex(parentIndex + 1u, UIPopupDismissReason::Programmatic);
|
||||
}
|
||||
|
||||
m_popupChain.push_back(std::move(entry));
|
||||
result.changed = true;
|
||||
result.openedPopupId = m_popupChain.back().popupId;
|
||||
return result;
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::ClosePopup(
|
||||
std::string_view popupId,
|
||||
UIPopupDismissReason dismissReason) {
|
||||
return CloseFromIndex(FindPopupIndex(popupId), dismissReason);
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::CloseAll(
|
||||
UIPopupDismissReason dismissReason) {
|
||||
return CloseFromIndex(0u, dismissReason);
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::DismissFromEscape() {
|
||||
for (std::size_t index = m_popupChain.size(); index > 0u; --index) {
|
||||
if (m_popupChain[index - 1u].dismissOnEscape) {
|
||||
return CloseFromIndex(index - 1u, UIPopupDismissReason::EscapeKey);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::DismissFromPointerDown(const UIInputPath& hitPath) {
|
||||
const std::size_t containingIndex = FindDeepestContainingPopupIndex(hitPath);
|
||||
if (containingIndex == InvalidIndex) {
|
||||
for (std::size_t index = 0; index < m_popupChain.size(); ++index) {
|
||||
if (m_popupChain[index].dismissOnPointerOutside) {
|
||||
return CloseFromIndex(index, UIPopupDismissReason::PointerOutside);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::size_t index = containingIndex + 1u; index < m_popupChain.size(); ++index) {
|
||||
if (m_popupChain[index].dismissOnPointerOutside) {
|
||||
return CloseFromIndex(index, UIPopupDismissReason::PointerOutside);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::DismissFromFocusLoss(const UIInputPath& focusedPath) {
|
||||
const std::size_t containingIndex = FindDeepestContainingPopupIndex(focusedPath);
|
||||
if (containingIndex == InvalidIndex) {
|
||||
for (std::size_t index = 0; index < m_popupChain.size(); ++index) {
|
||||
if (m_popupChain[index].dismissOnFocusLoss) {
|
||||
return CloseFromIndex(index, UIPopupDismissReason::FocusLoss);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::size_t index = containingIndex + 1u; index < m_popupChain.size(); ++index) {
|
||||
if (m_popupChain[index].dismissOnFocusLoss) {
|
||||
return CloseFromIndex(index, UIPopupDismissReason::FocusLoss);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::size_t UIPopupOverlayModel::FindPopupIndex(std::string_view popupId) const {
|
||||
if (popupId.empty()) {
|
||||
return InvalidIndex;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < m_popupChain.size(); ++index) {
|
||||
if (m_popupChain[index].popupId == popupId) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return InvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t UIPopupOverlayModel::FindDeepestContainingPopupIndex(const UIInputPath& path) const {
|
||||
std::size_t containingIndex = InvalidIndex;
|
||||
for (std::size_t index = 0; index < m_popupChain.size(); ++index) {
|
||||
if (PopupContainsPath(m_popupChain[index], path)) {
|
||||
containingIndex = index;
|
||||
}
|
||||
}
|
||||
return containingIndex;
|
||||
}
|
||||
|
||||
UIPopupOverlayMutationResult UIPopupOverlayModel::CloseFromIndex(
|
||||
std::size_t index,
|
||||
UIPopupDismissReason dismissReason) {
|
||||
UIPopupOverlayMutationResult result = {};
|
||||
if (index >= m_popupChain.size()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.dismissReason = dismissReason;
|
||||
result.changed = true;
|
||||
result.closedPopupIds.reserve(m_popupChain.size() - index);
|
||||
for (std::size_t popupIndex = index; popupIndex < m_popupChain.size(); ++popupIndex) {
|
||||
result.closedPopupIds.push_back(m_popupChain[popupIndex].popupId);
|
||||
}
|
||||
|
||||
m_popupChain.resize(index);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool UIPopupOverlayModel::PopupContainsPath(
|
||||
const UIPopupOverlayEntry& entry,
|
||||
const UIInputPath& path) {
|
||||
return IsPathPrefix(entry.anchorPath, path) || IsPathPrefix(entry.surfacePath, path);
|
||||
}
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user