Files
XCEngine/new_editor/src/Collections/UIEditorTabStrip.cpp

379 lines
12 KiB
C++
Raw Normal View History

2026-04-10 00:41:28 +08:00
#include <XCEditor/Collections/UIEditorTabStrip.h>
#include <algorithm>
#include <cmath>
#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 = 0.0f;
constexpr float kStripRounding = 0.0f;
constexpr float kHeaderFontSize = 13.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;
}
2026-04-10 00:41:28 +08:00
float ResolveStripRounding(const UIEditorTabStripMetrics& metrics) {
(void)metrics;
return kStripRounding;
2026-04-10 00:41:28 +08:00
}
float ResolveTabRounding(const UIEditorTabStripMetrics& metrics) {
(void)metrics;
return kTabRounding;
2026-04-10 00:41:28 +08:00
}
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 ResolveTabTextLeft(
const UIRect& rect,
const UIEditorTabStripItem& item,
const UIEditorTabStripMetrics& metrics) {
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
return rect.x + std::floor((std::max)(0.0f, rect.width - labelWidth) * 0.5f);
}
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) {
(void)focused;
return palette.tabSelectedBorderColor;
}
if (hovered) {
return palette.tabHoveredBorderColor;
}
return palette.tabBorderColor;
}
float ResolveTabBorderThickness(bool selected, bool focused, const UIEditorTabStripMetrics& metrics) {
if (selected) {
(void)focused;
return metrics.selectedBorderThickness;
}
return metrics.baseBorderThickness;
}
void AppendHeaderContentSeparator(
UIDrawList& drawList,
const UIEditorTabStripLayout& layout,
const UIEditorTabStripPalette& palette,
const UIEditorTabStripMetrics& metrics) {
if (layout.headerRect.width <= 0.0f ||
layout.headerRect.height <= 0.0f ||
layout.contentRect.height <= 0.0f) {
return;
}
const float thickness = (std::max)(ClampNonNegative(metrics.baseBorderThickness), 1.0f);
const float separatorY = layout.contentRect.y;
const float separatorLeft = layout.headerRect.x;
const float separatorRight = layout.headerRect.x + layout.headerRect.width;
if (layout.selectedIndex == UIEditorTabStripInvalidIndex ||
layout.selectedIndex >= layout.tabHeaderRects.size()) {
drawList.AddFilledRect(
UIRect(
separatorLeft,
separatorY,
(std::max)(separatorRight - separatorLeft, 0.0f),
thickness),
palette.headerContentSeparatorColor);
return;
}
const UIRect& selectedRect = layout.tabHeaderRects[layout.selectedIndex];
const float gapLeft = (std::max)(separatorLeft, selectedRect.x + 1.0f);
const float gapRight = (std::min)(separatorRight, selectedRect.x + selectedRect.width - 1.0f);
if (gapLeft > separatorLeft) {
drawList.AddFilledRect(
UIRect(
separatorLeft,
separatorY,
(std::max)(gapLeft - separatorLeft, 0.0f),
thickness),
palette.headerContentSeparatorColor);
}
if (gapRight < separatorRight) {
drawList.AddFilledRect(
UIRect(
gapRight,
separatorY,
(std::max)(separatorRight - gapRight, 0.0f),
thickness),
palette.headerContentSeparatorColor);
}
}
void AppendSelectedTabBottomBorderMask(
UIDrawList& drawList,
const UIRect& rect,
float thickness,
const UIEditorTabStripPalette& palette) {
if (rect.width <= 0.0f || rect.height <= 0.0f || thickness <= 0.0f) {
return;
}
const float maskHeight = (std::max)(1.0f, thickness + 1.0f);
drawList.AddFilledRect(
UIRect(
rect.x + 1.0f,
rect.y + rect.height - maskHeight,
(std::max)(rect.width - 2.0f, 0.0f),
maskHeight + 1.0f),
palette.contentBackgroundColor,
0.0f);
}
} // 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);
(void)item;
return labelWidth + extraLeftInset;
}
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);
}
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;
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.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) {
2026-04-10 00:41:28 +08:00
const float stripRounding = ResolveStripRounding(metrics);
const float tabRounding = ResolveTabRounding(metrics);
drawList.AddFilledRect(layout.bounds, palette.stripBackgroundColor, stripRounding);
if (layout.contentRect.height > 0.0f) {
2026-04-10 00:41:28 +08:00
drawList.AddFilledRect(layout.contentRect, palette.contentBackgroundColor, stripRounding);
}
if (layout.headerRect.height > 0.0f) {
2026-04-10 00:41:28 +08:00
drawList.AddFilledRect(layout.headerRect, palette.headerBackgroundColor, stripRounding);
}
for (std::size_t index = 0; index < layout.tabHeaderRects.size(); ++index) {
const bool selected = layout.selectedIndex == index;
const bool hovered = state.hoveredIndex == index;
drawList.AddFilledRect(
layout.tabHeaderRects[index],
ResolveTabFillColor(selected, hovered, palette),
2026-04-10 00:41:28 +08:00
tabRounding);
drawList.AddRectOutline(
layout.tabHeaderRects[index],
ResolveTabBorderColor(selected, hovered, state.focused, palette),
ResolveTabBorderThickness(selected, state.focused, metrics),
2026-04-10 00:41:28 +08:00
tabRounding);
if (selected) {
AppendSelectedTabBottomBorderMask(
drawList,
layout.tabHeaderRects[index],
ResolveTabBorderThickness(selected, state.focused, metrics),
palette);
}
}
}
void AppendUIEditorTabStripForeground(
UIDrawList& drawList,
const UIEditorTabStripLayout& layout,
const std::vector<UIEditorTabStripItem>& items,
const UIEditorTabStripState& state,
const UIEditorTabStripPalette& palette,
const UIEditorTabStripMetrics& metrics) {
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
const float horizontalPadding = (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;
const float clipLeft = tabRect.x + horizontalPadding;
const float textLeft = ResolveTabTextLeft(tabRect, items[index], metrics);
const float textRight = tabRect.x + tabRect.width - horizontalPadding;
if (textRight > clipLeft) {
const UIRect clipRect(
clipLeft,
tabRect.y,
textRight - clipLeft,
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();
}
}
}
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