Add core popup overlay primitive

This commit is contained in:
2026-04-06 22:32:40 +08:00
parent 9568cf0a16
commit 620717f8b4
16 changed files with 1261 additions and 4 deletions

View 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