Stabilize XCEditor shell foundation widgets

This commit is contained in:
2026-04-07 01:42:02 +08:00
parent 998df9013a
commit 3b2a05a098
20 changed files with 4118 additions and 23 deletions

View 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

View 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

View 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