Stabilize XCEditor shell foundation widgets
This commit is contained in:
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