Stabilize XCEditor shell foundation widgets
This commit is contained in:
194
new_editor/src/Core/UIEditorMenuSession.cpp
Normal file
194
new_editor/src/Core/UIEditorMenuSession.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
#include <XCEditor/Core/UIEditorMenuSession.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
bool UIEditorMenuSession::IsPopupOpen(std::string_view popupId) const {
|
||||
return m_popupOverlayModel.FindPopup(popupId) != nullptr;
|
||||
}
|
||||
|
||||
const UIEditorMenuPopupState* UIEditorMenuSession::FindPopupState(
|
||||
std::string_view popupId) const {
|
||||
for (const UIEditorMenuPopupState& state : m_popupStates) {
|
||||
if (state.popupId == popupId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UIEditorMenuSession::Reset() {
|
||||
m_openRootMenuId.clear();
|
||||
m_popupOverlayModel = {};
|
||||
m_popupStates.clear();
|
||||
m_openSubmenuItemIds.clear();
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenMenuBarRoot(
|
||||
std::string_view menuId,
|
||||
Widgets::UIPopupOverlayEntry entry) {
|
||||
if (menuId.empty() || entry.popupId.empty()) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
const Widgets::UIPopupOverlayEntry* rootPopup = m_popupOverlayModel.GetRootPopup();
|
||||
if (rootPopup != nullptr &&
|
||||
m_openRootMenuId == menuId &&
|
||||
rootPopup->popupId == entry.popupId) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
entry.parentPopupId.clear();
|
||||
const Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.OpenPopup(std::move(entry));
|
||||
if (mutation.changed) {
|
||||
m_popupStates.clear();
|
||||
|
||||
UIEditorMenuPopupState rootState = {};
|
||||
rootState.popupId = mutation.openedPopupId;
|
||||
rootState.menuId = std::string(menuId);
|
||||
m_popupStates.push_back(std::move(rootState));
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverMenuBarRoot(
|
||||
std::string_view menuId,
|
||||
Widgets::UIPopupOverlayEntry entry) {
|
||||
if (!HasOpenMenu() || IsMenuOpen(menuId)) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
return OpenMenuBarRoot(menuId, std::move(entry));
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverSubmenu(
|
||||
std::string_view itemId,
|
||||
Widgets::UIPopupOverlayEntry entry) {
|
||||
if (!HasOpenMenu() ||
|
||||
itemId.empty() ||
|
||||
entry.popupId.empty() ||
|
||||
entry.parentPopupId.empty() ||
|
||||
m_popupOverlayModel.FindPopup(entry.parentPopupId) == nullptr) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
const Widgets::UIPopupOverlayEntry* existingPopup =
|
||||
m_popupOverlayModel.FindPopup(entry.popupId);
|
||||
if (existingPopup != nullptr &&
|
||||
existingPopup->parentPopupId == entry.parentPopupId) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
const Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.OpenPopup(std::move(entry));
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
|
||||
UIEditorMenuPopupState popupState = {};
|
||||
popupState.popupId = mutation.openedPopupId;
|
||||
popupState.menuId = m_openRootMenuId;
|
||||
popupState.itemId = std::string(itemId);
|
||||
m_popupStates.push_back(std::move(popupState));
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::CloseAll(
|
||||
Widgets::UIPopupDismissReason dismissReason) {
|
||||
const Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.CloseAll(dismissReason);
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromEscape() {
|
||||
const Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.DismissFromEscape();
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromPointerDown(
|
||||
const UIInputPath& hitPath) {
|
||||
const Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.DismissFromPointerDown(hitPath);
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::DismissFromFocusLoss(
|
||||
const UIInputPath& focusedPath) {
|
||||
const Widgets::UIPopupOverlayMutationResult mutation =
|
||||
m_popupOverlayModel.DismissFromFocusLoss(focusedPath);
|
||||
if (mutation.changed) {
|
||||
RemoveClosedPopupStates(mutation.closedPopupIds);
|
||||
RebuildDerivedState();
|
||||
}
|
||||
|
||||
return BuildResult(mutation);
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::BuildResult(
|
||||
const Widgets::UIPopupOverlayMutationResult& mutation) const {
|
||||
UIEditorMenuSessionMutationResult result = {};
|
||||
result.changed = mutation.changed;
|
||||
result.openRootMenuId = m_openRootMenuId;
|
||||
result.openedPopupId = mutation.openedPopupId;
|
||||
result.closedPopupIds = mutation.closedPopupIds;
|
||||
result.dismissReason = mutation.dismissReason;
|
||||
return result;
|
||||
}
|
||||
|
||||
void UIEditorMenuSession::RemoveClosedPopupStates(
|
||||
const std::vector<std::string>& closedPopupIds) {
|
||||
if (closedPopupIds.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::erase_if(
|
||||
m_popupStates,
|
||||
[&closedPopupIds](const UIEditorMenuPopupState& state) {
|
||||
return std::find(
|
||||
closedPopupIds.begin(),
|
||||
closedPopupIds.end(),
|
||||
state.popupId) != closedPopupIds.end();
|
||||
});
|
||||
}
|
||||
|
||||
void UIEditorMenuSession::RebuildDerivedState() {
|
||||
m_openSubmenuItemIds.clear();
|
||||
|
||||
if (m_popupStates.empty() || !m_popupOverlayModel.HasOpenPopups()) {
|
||||
m_openRootMenuId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
m_openRootMenuId = m_popupStates.front().menuId;
|
||||
for (std::size_t index = 1u; index < m_popupStates.size(); ++index) {
|
||||
if (!m_popupStates[index].itemId.empty()) {
|
||||
m_openSubmenuItemIds.push_back(m_popupStates[index].itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
341
new_editor/src/Widgets/UIEditorPanelFrame.cpp
Normal file
341
new_editor/src/Widgets/UIEditorPanelFrame.cpp
Normal file
@@ -0,0 +1,341 @@
|
||||
#include <XCEditor/Widgets/UIEditorPanelFrame.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
float ResolveActionButtonExtent(
|
||||
const UIEditorPanelFrameMetrics& metrics,
|
||||
float headerHeight) {
|
||||
const float inset = (std::max)(metrics.actionInsetX, 0.0f);
|
||||
return (std::min)(
|
||||
(std::max)(metrics.actionButtonExtent, 0.0f),
|
||||
(std::max)(headerHeight - inset * 2.0f, 0.0f));
|
||||
}
|
||||
|
||||
UIColor ResolveActionFillColor(
|
||||
bool selected,
|
||||
bool hovered,
|
||||
const UIEditorPanelFramePalette& palette) {
|
||||
if (selected) {
|
||||
return palette.actionButtonSelectedColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.actionButtonHoveredColor;
|
||||
}
|
||||
|
||||
return palette.actionButtonColor;
|
||||
}
|
||||
|
||||
void AppendActionButton(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
std::string_view glyph,
|
||||
bool selected,
|
||||
bool hovered,
|
||||
const UIEditorPanelFramePalette& palette) {
|
||||
if (rect.width <= 0.0f || rect.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawList.AddFilledRect(
|
||||
rect,
|
||||
ResolveActionFillColor(selected, hovered, palette),
|
||||
5.0f);
|
||||
drawList.AddRectOutline(
|
||||
rect,
|
||||
palette.actionButtonBorderColor,
|
||||
1.0f,
|
||||
5.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 5.0f, rect.y + 2.0f),
|
||||
std::string(glyph),
|
||||
palette.actionGlyphColor,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsUIEditorPanelFramePointInside(
|
||||
const UIRect& rect,
|
||||
const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool IsUIEditorPanelFramePinButtonVisible(const UIEditorPanelFrameState& state) {
|
||||
return state.pinnable;
|
||||
}
|
||||
|
||||
bool IsUIEditorPanelFrameCloseButtonVisible(const UIEditorPanelFrameState& state) {
|
||||
return state.closable;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameLayout BuildUIEditorPanelFrameLayout(
|
||||
const UIRect& frameRect,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
UIEditorPanelFrameLayout layout = {};
|
||||
layout.frameRect = UIRect(
|
||||
frameRect.x,
|
||||
frameRect.y,
|
||||
ClampNonNegative(frameRect.width),
|
||||
ClampNonNegative(frameRect.height));
|
||||
|
||||
const float headerHeight =
|
||||
(std::min)(ClampNonNegative(metrics.headerHeight), layout.frameRect.height);
|
||||
const float footerHeight =
|
||||
state.showFooter
|
||||
? (std::min)(
|
||||
ClampNonNegative(metrics.footerHeight),
|
||||
(std::max)(layout.frameRect.height - headerHeight, 0.0f))
|
||||
: 0.0f;
|
||||
|
||||
layout.hasFooter = footerHeight > 0.0f;
|
||||
layout.showPinButton = IsUIEditorPanelFramePinButtonVisible(state);
|
||||
layout.showCloseButton = IsUIEditorPanelFrameCloseButtonVisible(state);
|
||||
|
||||
layout.headerRect = UIRect(
|
||||
layout.frameRect.x,
|
||||
layout.frameRect.y,
|
||||
layout.frameRect.width,
|
||||
headerHeight);
|
||||
|
||||
if (layout.hasFooter) {
|
||||
layout.footerRect = UIRect(
|
||||
layout.frameRect.x,
|
||||
layout.frameRect.y + layout.frameRect.height - footerHeight,
|
||||
layout.frameRect.width,
|
||||
footerHeight);
|
||||
}
|
||||
|
||||
const float bodyBandTop = layout.headerRect.y + layout.headerRect.height;
|
||||
const float bodyBandBottom = layout.hasFooter
|
||||
? layout.footerRect.y
|
||||
: layout.frameRect.y + layout.frameRect.height;
|
||||
const float contentPadding = ClampNonNegative(metrics.contentPadding);
|
||||
|
||||
layout.bodyRect = UIRect(
|
||||
layout.frameRect.x + contentPadding,
|
||||
bodyBandTop + contentPadding,
|
||||
(std::max)(layout.frameRect.width - contentPadding * 2.0f, 0.0f),
|
||||
(std::max)(bodyBandBottom - bodyBandTop - contentPadding * 2.0f, 0.0f));
|
||||
|
||||
if (layout.headerRect.height <= 0.0f) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const float buttonExtent = ResolveActionButtonExtent(metrics, layout.headerRect.height);
|
||||
if (buttonExtent <= 0.0f) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const float buttonY = layout.headerRect.y + (layout.headerRect.height - buttonExtent) * 0.5f;
|
||||
float buttonRight = layout.headerRect.x + layout.headerRect.width - ClampNonNegative(metrics.actionInsetX);
|
||||
|
||||
if (layout.showCloseButton) {
|
||||
layout.closeButtonRect = UIRect(
|
||||
buttonRight - buttonExtent,
|
||||
buttonY,
|
||||
buttonExtent,
|
||||
buttonExtent);
|
||||
buttonRight = layout.closeButtonRect.x - ClampNonNegative(metrics.actionGap);
|
||||
}
|
||||
|
||||
if (layout.showPinButton) {
|
||||
layout.pinButtonRect = UIRect(
|
||||
buttonRight - buttonExtent,
|
||||
buttonY,
|
||||
buttonExtent,
|
||||
buttonExtent);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIColor ResolveUIEditorPanelFrameBorderColor(
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFramePalette& palette) {
|
||||
if (state.focused) {
|
||||
return palette.focusedBorderColor;
|
||||
}
|
||||
|
||||
if (state.active) {
|
||||
return palette.activeBorderColor;
|
||||
}
|
||||
|
||||
if (state.hovered) {
|
||||
return palette.hoveredBorderColor;
|
||||
}
|
||||
|
||||
return palette.borderColor;
|
||||
}
|
||||
|
||||
float ResolveUIEditorPanelFrameBorderThickness(
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
if (state.focused) {
|
||||
return metrics.focusedBorderThickness;
|
||||
}
|
||||
|
||||
if (state.active) {
|
||||
return metrics.activeBorderThickness;
|
||||
}
|
||||
|
||||
if (state.hovered) {
|
||||
return metrics.hoveredBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameAction HitTestUIEditorPanelFrameAction(
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIPoint& point) {
|
||||
if (layout.showPinButton &&
|
||||
IsUIEditorPanelFramePinButtonVisible(state) &&
|
||||
IsUIEditorPanelFramePointInside(layout.pinButtonRect, point)) {
|
||||
return UIEditorPanelFrameAction::Pin;
|
||||
}
|
||||
|
||||
if (layout.showCloseButton &&
|
||||
IsUIEditorPanelFrameCloseButtonVisible(state) &&
|
||||
IsUIEditorPanelFramePointInside(layout.closeButtonRect, point)) {
|
||||
return UIEditorPanelFrameAction::Close;
|
||||
}
|
||||
|
||||
return UIEditorPanelFrameAction::None;
|
||||
}
|
||||
|
||||
UIEditorPanelFrameHitTarget HitTestUIEditorPanelFrame(
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIPoint& point) {
|
||||
if (!IsUIEditorPanelFramePointInside(layout.frameRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::None;
|
||||
}
|
||||
|
||||
switch (HitTestUIEditorPanelFrameAction(layout, state, point)) {
|
||||
case UIEditorPanelFrameAction::Pin:
|
||||
return UIEditorPanelFrameHitTarget::PinButton;
|
||||
case UIEditorPanelFrameAction::Close:
|
||||
return UIEditorPanelFrameHitTarget::CloseButton;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsUIEditorPanelFramePointInside(layout.headerRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::Header;
|
||||
}
|
||||
|
||||
if (layout.hasFooter && IsUIEditorPanelFramePointInside(layout.footerRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::Footer;
|
||||
}
|
||||
|
||||
if (IsUIEditorPanelFramePointInside(layout.bodyRect, point)) {
|
||||
return UIEditorPanelFrameHitTarget::Body;
|
||||
}
|
||||
|
||||
return UIEditorPanelFrameHitTarget::Body;
|
||||
}
|
||||
|
||||
void AppendUIEditorPanelFrameBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFramePalette& palette,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.frameRect, palette.surfaceColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.frameRect,
|
||||
ResolveUIEditorPanelFrameBorderColor(state, palette),
|
||||
ResolveUIEditorPanelFrameBorderThickness(state, metrics),
|
||||
metrics.cornerRounding);
|
||||
drawList.AddFilledRect(layout.headerRect, palette.headerColor, metrics.cornerRounding);
|
||||
if (layout.hasFooter) {
|
||||
drawList.AddFilledRect(layout.footerRect, palette.footerColor, metrics.cornerRounding);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorPanelFrameForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorPanelFrameLayout& layout,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameText& text,
|
||||
const UIEditorPanelFramePalette& palette,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
if (!text.title.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.frameRect.x + metrics.titleInsetX, layout.headerRect.y + metrics.titleInsetY),
|
||||
std::string(text.title),
|
||||
palette.textPrimary,
|
||||
16.0f);
|
||||
}
|
||||
|
||||
if (!text.subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.frameRect.x + metrics.titleInsetX, layout.headerRect.y + metrics.subtitleInsetY),
|
||||
std::string(text.subtitle),
|
||||
palette.textSecondary,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
if (layout.hasFooter && !text.footer.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(layout.footerRect.x + metrics.footerInsetX, layout.footerRect.y + metrics.footerInsetY),
|
||||
std::string(text.footer),
|
||||
palette.textMuted,
|
||||
11.0f);
|
||||
}
|
||||
|
||||
if (layout.showPinButton) {
|
||||
AppendActionButton(
|
||||
drawList,
|
||||
layout.pinButtonRect,
|
||||
"P",
|
||||
state.pinned,
|
||||
state.pinHovered,
|
||||
palette);
|
||||
}
|
||||
|
||||
if (layout.showCloseButton) {
|
||||
AppendActionButton(
|
||||
drawList,
|
||||
layout.closeButtonRect,
|
||||
"X",
|
||||
false,
|
||||
state.closeHovered,
|
||||
palette);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorPanelFrame(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& frameRect,
|
||||
const UIEditorPanelFrameState& state,
|
||||
const UIEditorPanelFrameText& text,
|
||||
const UIEditorPanelFramePalette& palette,
|
||||
const UIEditorPanelFrameMetrics& metrics) {
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(frameRect, state, metrics);
|
||||
AppendUIEditorPanelFrameBackground(drawList, layout, state, palette, metrics);
|
||||
AppendUIEditorPanelFrameForeground(drawList, layout, state, text, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
396
new_editor/src/Widgets/UIEditorTabStrip.cpp
Normal file
396
new_editor/src/Widgets/UIEditorTabStrip.cpp
Normal file
@@ -0,0 +1,396 @@
|
||||
#include <XCEditor/Widgets/UIEditorTabStrip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Layout::ArrangeUITabStrip;
|
||||
using ::XCEngine::UI::Layout::MeasureUITabStripHeaderWidth;
|
||||
|
||||
constexpr float kTabRounding = 7.0f;
|
||||
constexpr float kStripRounding = 8.0f;
|
||||
constexpr float kHeaderFontSize = 13.0f;
|
||||
constexpr float kCloseFontSize = 11.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
bool IsPointInsideRect(
|
||||
const UIRect& rect,
|
||||
const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
std::size_t ResolveSelectedIndex(
|
||||
std::size_t itemCount,
|
||||
std::size_t selectedIndex) {
|
||||
if (itemCount == 0u) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (selectedIndex == UIEditorTabStripInvalidIndex || selectedIndex >= itemCount) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
float ResolveEstimatedLabelWidth(
|
||||
const UIEditorTabStripItem& item,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
if (item.desiredHeaderLabelWidth > 0.0f) {
|
||||
return item.desiredHeaderLabelWidth;
|
||||
}
|
||||
|
||||
return static_cast<float>(item.title.size()) * ClampNonNegative(metrics.estimatedGlyphWidth);
|
||||
}
|
||||
|
||||
float ResolveTabTextTop(
|
||||
const UIRect& rect,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kHeaderFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
float ResolveCloseTextTop(const UIRect& rect) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kCloseFontSize) * 0.5f) - 0.5f;
|
||||
}
|
||||
|
||||
UIColor ResolveStripBorderColor(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
return state.focused ? palette.focusedBorderColor : palette.stripBorderColor;
|
||||
}
|
||||
|
||||
float ResolveStripBorderThickness(
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
return state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIColor ResolveTabFillColor(
|
||||
bool selected,
|
||||
bool hovered,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
if (selected) {
|
||||
return palette.tabSelectedColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.tabHoveredColor;
|
||||
}
|
||||
|
||||
return palette.tabColor;
|
||||
}
|
||||
|
||||
UIColor ResolveTabBorderColor(
|
||||
bool selected,
|
||||
bool hovered,
|
||||
bool focused,
|
||||
const UIEditorTabStripPalette& palette) {
|
||||
if (selected) {
|
||||
return focused ? palette.focusedBorderColor : palette.tabSelectedBorderColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.tabHoveredBorderColor;
|
||||
}
|
||||
|
||||
return palette.tabBorderColor;
|
||||
}
|
||||
|
||||
float ResolveTabBorderThickness(
|
||||
bool selected,
|
||||
bool focused,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
if (selected) {
|
||||
return focused ? metrics.focusedBorderThickness : metrics.selectedBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
UIRect BuildCloseButtonRect(
|
||||
const UIRect& headerRect,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const float insetY = ClampNonNegative(metrics.closeInsetY);
|
||||
const float extent = (std::min)(
|
||||
ClampNonNegative(metrics.closeButtonExtent),
|
||||
(std::max)(headerRect.height - insetY * 2.0f, 0.0f));
|
||||
if (extent <= 0.0f) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return UIRect(
|
||||
headerRect.x + headerRect.width - ClampNonNegative(metrics.closeInsetRight) - extent,
|
||||
headerRect.y + insetY + (std::max)(0.0f, headerRect.height - insetY * 2.0f - extent) * 0.5f,
|
||||
extent,
|
||||
extent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
const UIEditorTabStripItem& item,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
|
||||
const float horizontalPadding = ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
const float extraLeftInset = (std::max)(ClampNonNegative(metrics.labelInsetX) - horizontalPadding, 0.0f);
|
||||
const float extraRightInset = (std::max)(ClampNonNegative(metrics.closeInsetRight) - horizontalPadding, 0.0f);
|
||||
const float closeBudget = item.closable
|
||||
? ClampNonNegative(metrics.closeButtonExtent) +
|
||||
ClampNonNegative(metrics.closeButtonGap) +
|
||||
extraRightInset
|
||||
: 0.0f;
|
||||
return labelWidth + extraLeftInset + closeBudget;
|
||||
}
|
||||
|
||||
std::size_t ResolveUIEditorTabStripSelectedIndex(
|
||||
const std::vector<UIEditorTabStripItem>& items,
|
||||
std::string_view selectedTabId,
|
||||
std::size_t fallbackIndex) {
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
if (items[index].tabId == selectedTabId) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
if (items.empty()) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
return ResolveSelectedIndex(items.size(), fallbackIndex);
|
||||
}
|
||||
|
||||
std::size_t ResolveUIEditorTabStripSelectedIndexAfterClose(
|
||||
std::size_t selectedIndex,
|
||||
std::size_t closedIndex,
|
||||
std::size_t itemCountBeforeClose) {
|
||||
if (itemCountBeforeClose == 0u || closedIndex >= itemCountBeforeClose) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
const std::size_t itemCountAfterClose = itemCountBeforeClose - 1u;
|
||||
if (itemCountAfterClose == 0u) {
|
||||
return UIEditorTabStripInvalidIndex;
|
||||
}
|
||||
|
||||
if (selectedIndex == UIEditorTabStripInvalidIndex || selectedIndex >= itemCountBeforeClose) {
|
||||
return (std::min)(closedIndex, itemCountAfterClose - 1u);
|
||||
}
|
||||
|
||||
if (closedIndex < selectedIndex) {
|
||||
return selectedIndex - 1u;
|
||||
}
|
||||
|
||||
if (closedIndex > selectedIndex) {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
return (std::min)(selectedIndex, itemCountAfterClose - 1u);
|
||||
}
|
||||
|
||||
UIEditorTabStripLayout BuildUIEditorTabStripLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorTabStripItem>& items,
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
UIEditorTabStripLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.selectedIndex = ResolveSelectedIndex(items.size(), state.selectedIndex);
|
||||
|
||||
std::vector<float> desiredHeaderWidths = {};
|
||||
desiredHeaderWidths.reserve(items.size());
|
||||
for (const UIEditorTabStripItem& item : items) {
|
||||
desiredHeaderWidths.push_back(
|
||||
MeasureUITabStripHeaderWidth(
|
||||
ResolveUIEditorTabStripDesiredHeaderLabelWidth(item, metrics),
|
||||
metrics.layoutMetrics));
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::Layout::UITabStripLayoutResult arranged =
|
||||
ArrangeUITabStrip(layout.bounds, desiredHeaderWidths, metrics.layoutMetrics);
|
||||
layout.headerRect = arranged.headerRect;
|
||||
layout.contentRect = arranged.contentRect;
|
||||
layout.tabHeaderRects = arranged.tabHeaderRects;
|
||||
layout.closeButtonRects.resize(items.size());
|
||||
layout.showCloseButtons.resize(items.size(), false);
|
||||
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
if (!items[index].closable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
layout.closeButtonRects[index] = BuildCloseButtonRect(layout.tabHeaderRects[index], metrics);
|
||||
layout.showCloseButtons[index] =
|
||||
layout.closeButtonRects[index].width > 0.0f && layout.closeButtonRects[index].height > 0.0f;
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorTabStripHitTarget HitTestUIEditorTabStrip(
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const UIEditorTabStripState&,
|
||||
const UIPoint& point) {
|
||||
UIEditorTabStripHitTarget target = {};
|
||||
|
||||
if (!IsPointInsideRect(layout.bounds, point)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.closeButtonRects.size(); ++index) {
|
||||
if (layout.showCloseButtons[index] &&
|
||||
IsPointInsideRect(layout.closeButtonRects[index], point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::CloseButton;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||
if (IsPointInsideRect(layout.tabHeaderRects[index], point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::Tab;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsPointInsideRect(layout.headerRect, point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::HeaderBackground;
|
||||
return target;
|
||||
}
|
||||
|
||||
if (IsPointInsideRect(layout.contentRect, point)) {
|
||||
target.kind = UIEditorTabStripHitTargetKind::Content;
|
||||
return target;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
void AppendUIEditorTabStripBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.stripBackgroundColor, kStripRounding);
|
||||
if (layout.contentRect.height > 0.0f) {
|
||||
drawList.AddFilledRect(layout.contentRect, palette.contentBackgroundColor, kStripRounding);
|
||||
}
|
||||
if (layout.headerRect.height > 0.0f) {
|
||||
drawList.AddFilledRect(layout.headerRect, palette.headerBackgroundColor, kStripRounding);
|
||||
}
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
ResolveStripBorderColor(state, palette),
|
||||
ResolveStripBorderThickness(state, metrics),
|
||||
kStripRounding);
|
||||
|
||||
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
|
||||
const bool selected = layout.selectedIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
||||
drawList.AddFilledRect(
|
||||
layout.tabHeaderRects[index],
|
||||
ResolveTabFillColor(selected, hovered, palette),
|
||||
kTabRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.tabHeaderRects[index],
|
||||
ResolveTabBorderColor(selected, hovered, state.focused, palette),
|
||||
ResolveTabBorderThickness(selected, state.focused, metrics),
|
||||
kTabRounding);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorTabStripForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorTabStripLayout& layout,
|
||||
const std::vector<UIEditorTabStripItem>& items,
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const float leftInset = (std::max)(
|
||||
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
||||
ClampNonNegative(metrics.labelInsetX));
|
||||
|
||||
for (std::size_t index = 0; index < items.size() && index < layout.tabHeaderRects.size(); ++index) {
|
||||
const UIRect& tabRect = layout.tabHeaderRects[index];
|
||||
const bool selected = layout.selectedIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index || state.closeHoveredIndex == index;
|
||||
const float textLeft = tabRect.x + leftInset;
|
||||
float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
||||
if (layout.showCloseButtons[index]) {
|
||||
textRight = layout.closeButtonRects[index].x - ClampNonNegative(metrics.closeButtonGap);
|
||||
}
|
||||
|
||||
if (textRight > textLeft) {
|
||||
const UIRect clipRect(
|
||||
textLeft,
|
||||
tabRect.y,
|
||||
textRight - textLeft,
|
||||
tabRect.height);
|
||||
drawList.PushClipRect(clipRect, true);
|
||||
drawList.AddText(
|
||||
UIPoint(textLeft, ResolveTabTextTop(tabRect, metrics)),
|
||||
items[index].title,
|
||||
selected || hovered ? palette.textPrimary : palette.textSecondary,
|
||||
kHeaderFontSize);
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
|
||||
if (!layout.showCloseButtons[index]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool closeHovered = state.closeHoveredIndex == index;
|
||||
const UIRect& closeRect = layout.closeButtonRects[index];
|
||||
drawList.AddFilledRect(
|
||||
closeRect,
|
||||
closeHovered ? palette.closeButtonHoveredColor : palette.closeButtonColor,
|
||||
4.0f);
|
||||
drawList.AddRectOutline(
|
||||
closeRect,
|
||||
palette.closeButtonBorderColor,
|
||||
1.0f,
|
||||
4.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
closeRect.x + (std::max)(0.0f, (closeRect.width - 7.0f) * 0.5f),
|
||||
ResolveCloseTextTop(closeRect)),
|
||||
"X",
|
||||
palette.closeGlyphColor,
|
||||
kCloseFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorTabStrip(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorTabStripItem>& items,
|
||||
const UIEditorTabStripState& state,
|
||||
const UIEditorTabStripPalette& palette,
|
||||
const UIEditorTabStripMetrics& metrics) {
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(bounds, items, state, metrics);
|
||||
AppendUIEditorTabStripBackground(drawList, layout, state, palette, metrics);
|
||||
AppendUIEditorTabStripForeground(drawList, layout, items, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
Reference in New Issue
Block a user