#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 = 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; } float ResolveStripRounding(const UIEditorTabStripMetrics& metrics) { (void)metrics; return kStripRounding; } float ResolveTabRounding(const UIEditorTabStripMetrics& metrics) { (void)metrics; return kTabRounding; } 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 ResolveTabTextLeft( const UIRect& rect, const UIEditorTabStripItem& item, const UIEditorTabStripMetrics& metrics) { const float padding = (std::max)( ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding), ClampNonNegative(metrics.labelInsetX)); const float availableLeft = rect.x + padding; const float availableRight = rect.x + rect.width - padding; const float availableWidth = (std::max)(availableRight - availableLeft, 0.0f); const float labelWidth = ResolveEstimatedLabelWidth(item, metrics); return availableLeft + (std::max)(0.0f, (availableWidth - 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); } UIEditorTabStripInsertionPreview BuildInsertionPreview( const UIEditorTabStripLayout& layout, const UIEditorTabStripState& state, const UIEditorTabStripMetrics& metrics) { UIEditorTabStripInsertionPreview preview = {}; if (!state.reorder.dragging || state.reorder.previewInsertionIndex == UIEditorTabStripInvalidIndex || layout.tabHeaderRects.empty() || state.reorder.previewInsertionIndex > layout.tabHeaderRects.size() || layout.headerRect.height <= 0.0f) { return preview; } float indicatorX = layout.tabHeaderRects.front().x; if (state.reorder.previewInsertionIndex >= layout.tabHeaderRects.size()) { const UIRect& lastRect = layout.tabHeaderRects.back(); indicatorX = lastRect.x + lastRect.width; } else { indicatorX = layout.tabHeaderRects[state.reorder.previewInsertionIndex].x; } const float thickness = (std::max)(ClampNonNegative(metrics.reorderPreviewThickness), 1.0f); const float insetY = ClampNonNegative(metrics.reorderPreviewInsetY); const float indicatorHeight = (std::max)(layout.headerRect.height - insetY * 2.0f, thickness); preview.visible = true; preview.insertionIndex = state.reorder.previewInsertionIndex; preview.indicatorRect = UIRect( indicatorX - thickness * 0.5f, layout.headerRect.y + insetY, thickness, indicatorHeight); return preview; } } // 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& 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& 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); layout.insertionPreview = BuildInsertionPreview(layout, state, metrics); 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) { const float stripRounding = ResolveStripRounding(metrics); const float tabRounding = ResolveTabRounding(metrics); drawList.AddFilledRect(layout.bounds, palette.stripBackgroundColor, stripRounding); if (layout.contentRect.height > 0.0f) { drawList.AddFilledRect(layout.contentRect, palette.contentBackgroundColor, stripRounding); } if (layout.headerRect.height > 0.0f) { 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), tabRounding); drawList.AddRectOutline( layout.tabHeaderRects[index], ResolveTabBorderColor(selected, hovered, state.focused, palette), ResolveTabBorderThickness(selected, state.focused, metrics), tabRounding); if (selected) { AppendSelectedTabBottomBorderMask( drawList, layout.tabHeaderRects[index], ResolveTabBorderThickness(selected, state.focused, metrics), palette); } } if (layout.insertionPreview.visible) { drawList.AddFilledRect( layout.insertionPreview.indicatorRect, palette.reorderPreviewColor, 0.0f); } } void AppendUIEditorTabStripForeground( UIDrawList& drawList, const UIEditorTabStripLayout& layout, const std::vector& 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& 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