feat(xcui): add tab strip and workspace compose foundations

This commit is contained in:
2026-04-06 04:27:54 +08:00
parent 3540dbc94d
commit b14a4fb7bb
27 changed files with 2075 additions and 41 deletions

View File

@@ -0,0 +1,133 @@
#pragma once
#include <XCEngine/UI/Layout/LayoutTypes.h>
#include <algorithm>
#include <cstddef>
#include <vector>
namespace XCEngine {
namespace UI {
namespace Layout {
struct UITabStripMetrics {
float headerHeight = 34.0f;
float tabMinWidth = 72.0f;
float tabHorizontalPadding = 12.0f;
float tabGap = 1.0f;
};
struct UITabStripMeasureItem {
float desiredHeaderLabelWidth = 0.0f;
UISize desiredContentSize = {};
UISize minimumContentSize = {};
};
struct UITabStripMeasureResult {
UISize desiredSize = {};
UISize minimumSize = {};
};
struct UITabStripLayoutResult {
UIRect headerRect = {};
UIRect contentRect = {};
std::vector<UIRect> tabHeaderRects = {};
};
inline float MeasureUITabStripHeaderWidth(
float labelWidth,
const UITabStripMetrics& metrics) {
return (std::max)(
metrics.tabMinWidth,
(std::max)(0.0f, labelWidth) + (std::max)(0.0f, metrics.tabHorizontalPadding) * 2.0f);
}
inline UITabStripMeasureResult MeasureUITabStrip(
const std::vector<UITabStripMeasureItem>& items,
const UITabStripMetrics& metrics) {
UITabStripMeasureResult result = {};
const float gap = (std::max)(0.0f, metrics.tabGap);
float desiredHeaderWidth = 0.0f;
float minimumHeaderWidth = 0.0f;
float desiredContentWidth = 0.0f;
float desiredContentHeight = 0.0f;
float minimumContentWidth = 0.0f;
float minimumContentHeight = 0.0f;
for (std::size_t index = 0; index < items.size(); ++index) {
const UITabStripMeasureItem& item = items[index];
desiredHeaderWidth += MeasureUITabStripHeaderWidth(
item.desiredHeaderLabelWidth,
metrics);
minimumHeaderWidth += metrics.tabMinWidth;
if (index > 0u) {
desiredHeaderWidth += gap;
minimumHeaderWidth += gap;
}
desiredContentWidth = (std::max)(desiredContentWidth, item.desiredContentSize.width);
desiredContentHeight = (std::max)(desiredContentHeight, item.desiredContentSize.height);
minimumContentWidth = (std::max)(minimumContentWidth, item.minimumContentSize.width);
minimumContentHeight = (std::max)(minimumContentHeight, item.minimumContentSize.height);
}
const float headerHeight = items.empty() ? 0.0f : (std::max)(0.0f, metrics.headerHeight);
result.desiredSize = UISize(
(std::max)(desiredHeaderWidth, desiredContentWidth),
headerHeight + desiredContentHeight);
result.minimumSize = UISize(
(std::max)(minimumHeaderWidth, minimumContentWidth),
headerHeight + minimumContentHeight);
return result;
}
inline UITabStripLayoutResult ArrangeUITabStrip(
const UIRect& bounds,
const std::vector<float>& desiredHeaderWidths,
const UITabStripMetrics& metrics) {
UITabStripLayoutResult result = {};
const float headerHeight = desiredHeaderWidths.empty()
? 0.0f
: (std::min)((std::max)(0.0f, metrics.headerHeight), bounds.height);
result.headerRect = UIRect(bounds.x, bounds.y, bounds.width, headerHeight);
result.contentRect = UIRect(
bounds.x,
bounds.y + headerHeight,
bounds.width,
(std::max)(0.0f, bounds.height - headerHeight));
result.tabHeaderRects.resize(desiredHeaderWidths.size());
if (desiredHeaderWidths.empty()) {
return result;
}
const float gap = (std::max)(0.0f, metrics.tabGap);
const float totalGapWidth = gap * static_cast<float>(desiredHeaderWidths.size() - 1u);
const float availableTabsWidth = (std::max)(0.0f, bounds.width - totalGapWidth);
float totalDesiredWidth = 0.0f;
for (float width : desiredHeaderWidths) {
totalDesiredWidth += (std::max)(0.0f, width);
}
const float scale = totalDesiredWidth > 0.0f && totalDesiredWidth > availableTabsWidth
? availableTabsWidth / totalDesiredWidth
: 1.0f;
float cursorX = bounds.x;
for (std::size_t index = 0; index < desiredHeaderWidths.size(); ++index) {
const float width = totalDesiredWidth > 0.0f
? (std::max)(0.0f, desiredHeaderWidths[index]) * scale
: availableTabsWidth / static_cast<float>(desiredHeaderWidths.size());
result.tabHeaderRects[index] = UIRect(cursorX, bounds.y, width, headerHeight);
cursorX += width + gap;
}
return result;
}
} // namespace Layout
} // namespace UI
} // namespace XCEngine

View File

@@ -4,6 +4,7 @@
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
@@ -91,6 +92,7 @@ private:
UIInputDispatcher m_inputDispatcher;
std::unordered_map<std::string, float> m_verticalScrollOffsets = {};
std::unordered_map<std::string, float> m_splitterRatios = {};
std::unordered_map<std::string, std::size_t> m_tabStripSelectedIndices = {};
PointerState m_pointerState = {};
SplitterDragRuntimeState m_splitterDragState = {};
InputDebugSnapshot m_inputDebugSnapshot = {};

View File

@@ -0,0 +1,110 @@
#pragma once
#include <algorithm>
#include <cstddef>
namespace XCEngine {
namespace UI {
namespace Widgets {
class UITabStripModel {
public:
static constexpr std::size_t InvalidIndex = static_cast<std::size_t>(-1);
std::size_t GetItemCount() const {
return m_itemCount;
}
bool SetItemCount(std::size_t itemCount) {
m_itemCount = itemCount;
return ClampSelection();
}
bool HasSelection() const {
return m_itemCount > 0u && m_selectedIndex != InvalidIndex;
}
std::size_t GetSelectedIndex() const {
return m_selectedIndex;
}
bool SetSelectedIndex(std::size_t index) {
if (m_itemCount == 0u) {
index = InvalidIndex;
} else if (index >= m_itemCount) {
return false;
}
if (m_selectedIndex == index) {
return false;
}
m_selectedIndex = index;
return true;
}
bool SelectFirst() {
return SetSelectedIndex(m_itemCount > 0u ? 0u : InvalidIndex);
}
bool SelectLast() {
return SetSelectedIndex(m_itemCount > 0u ? (m_itemCount - 1u) : InvalidIndex);
}
bool SelectNext() {
if (m_itemCount == 0u) {
return false;
}
if (m_selectedIndex == InvalidIndex) {
m_selectedIndex = 0u;
return true;
}
return SetSelectedIndex((std::min)(m_selectedIndex + 1u, m_itemCount - 1u));
}
bool SelectPrevious() {
if (m_itemCount == 0u) {
return false;
}
if (m_selectedIndex == InvalidIndex) {
m_selectedIndex = 0u;
return true;
}
return SetSelectedIndex(m_selectedIndex > 0u ? m_selectedIndex - 1u : 0u);
}
private:
bool ClampSelection() {
if (m_itemCount == 0u) {
if (m_selectedIndex == InvalidIndex) {
return false;
}
m_selectedIndex = InvalidIndex;
return true;
}
if (m_selectedIndex == InvalidIndex) {
m_selectedIndex = 0u;
return true;
}
if (m_selectedIndex < m_itemCount) {
return false;
}
m_selectedIndex = m_itemCount - 1u;
return true;
}
std::size_t m_itemCount = 0u;
std::size_t m_selectedIndex = InvalidIndex;
};
} // namespace Widgets
} // namespace UI
} // namespace XCEngine