#include #include #include #include #include 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(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& 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& 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 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& 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& 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