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

@@ -42,6 +42,14 @@ void AutoScreenshotController::CaptureIfRequested(
}
std::error_code errorCode = {};
std::filesystem::create_directories(m_captureRoot, errorCode);
if (errorCode) {
m_lastCaptureError = "Failed to create screenshot directory: " + m_captureRoot.string();
m_lastCaptureSummary = "AutoShot failed";
m_capturePending = false;
return;
}
std::filesystem::create_directories(m_historyRoot, errorCode);
if (errorCode) {
m_lastCaptureError = "Failed to create screenshot directory: " + m_historyRoot.string();
@@ -52,14 +60,26 @@ void AutoScreenshotController::CaptureIfRequested(
std::string captureError = {};
const std::filesystem::path historyPath = BuildHistoryCapturePath(m_pendingReason);
if (!renderer.CaptureToPng(drawData, width, height, m_latestCapturePath, captureError) ||
!renderer.CaptureToPng(drawData, width, height, historyPath, captureError)) {
if (!renderer.CaptureToPng(drawData, width, height, historyPath, captureError)) {
m_lastCaptureError = std::move(captureError);
m_lastCaptureSummary = "AutoShot failed";
m_capturePending = false;
return;
}
errorCode.clear();
std::filesystem::copy_file(
historyPath,
m_latestCapturePath,
std::filesystem::copy_options::overwrite_existing,
errorCode);
if (errorCode) {
m_lastCaptureError = "Failed to update latest screenshot: " + m_latestCapturePath.string();
m_lastCaptureSummary = "AutoShot failed";
m_capturePending = false;
return;
}
++m_captureCount;
m_lastCaptureError.clear();
m_lastCaptureSummary =

View File

@@ -0,0 +1,298 @@
#include <XCNewEditor/Editor/UIEditorWorkspaceModel.h>
#include <cmath>
#include <unordered_set>
#include <utility>
namespace XCEngine::NewEditor {
namespace {
UIEditorWorkspaceValidationResult MakeValidationError(
UIEditorWorkspaceValidationCode code,
std::string message) {
UIEditorWorkspaceValidationResult result = {};
result.code = code;
result.message = std::move(message);
return result;
}
bool IsValidSplitRatio(float value) {
return std::isfinite(value) && value > 0.0f && value < 1.0f;
}
const UIEditorWorkspacePanelState* FindPanelRecursive(
const UIEditorWorkspaceNode& node,
std::string_view panelId) {
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
return node.panel.panelId == panelId ? &node.panel : nullptr;
}
for (const UIEditorWorkspaceNode& child : node.children) {
if (const UIEditorWorkspacePanelState* found = FindPanelRecursive(child, panelId)) {
return found;
}
}
return nullptr;
}
bool TryActivateRecursive(
UIEditorWorkspaceNode& node,
std::string_view panelId) {
switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel:
return node.panel.panelId == panelId;
case UIEditorWorkspaceNodeKind::TabStack:
for (std::size_t index = 0; index < node.children.size(); ++index) {
UIEditorWorkspaceNode& child = node.children[index];
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
child.panel.panelId == panelId) {
node.selectedTabIndex = index;
return true;
}
}
return false;
case UIEditorWorkspaceNodeKind::Split:
for (UIEditorWorkspaceNode& child : node.children) {
if (TryActivateRecursive(child, panelId)) {
return true;
}
}
return false;
}
return false;
}
void CollectVisiblePanelsRecursive(
const UIEditorWorkspaceNode& node,
std::string_view activePanelId,
std::vector<UIEditorWorkspaceVisiblePanel>& outPanels) {
switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel: {
UIEditorWorkspaceVisiblePanel panel = {};
panel.panelId = node.panel.panelId;
panel.title = node.panel.title;
panel.active = node.panel.panelId == activePanelId;
panel.placeholder = node.panel.placeholder;
outPanels.push_back(std::move(panel));
return;
}
case UIEditorWorkspaceNodeKind::TabStack:
if (node.selectedTabIndex < node.children.size()) {
CollectVisiblePanelsRecursive(
node.children[node.selectedTabIndex],
activePanelId,
outPanels);
}
return;
case UIEditorWorkspaceNodeKind::Split:
for (const UIEditorWorkspaceNode& child : node.children) {
CollectVisiblePanelsRecursive(child, activePanelId, outPanels);
}
return;
}
}
UIEditorWorkspaceValidationResult ValidateNodeRecursive(
const UIEditorWorkspaceNode& node,
std::unordered_set<std::string>& panelIds) {
if (node.nodeId.empty()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::EmptyNodeId,
"Workspace node id must not be empty.");
}
switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel:
if (!node.children.empty()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::NonPanelTabChild,
"Panel node '" + node.nodeId + "' must not contain child nodes.");
}
if (node.panel.panelId.empty()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::EmptyPanelId,
"Panel node '" + node.nodeId + "' must define a panelId.");
}
if (node.panel.title.empty()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::EmptyPanelTitle,
"Panel node '" + node.nodeId + "' must define a title.");
}
if (!panelIds.insert(node.panel.panelId).second) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::DuplicatePanelId,
"Panel id '" + node.panel.panelId + "' is duplicated in the workspace tree.");
}
return {};
case UIEditorWorkspaceNodeKind::TabStack:
if (node.children.empty()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::EmptyTabStack,
"Tab stack '" + node.nodeId + "' must contain at least one panel.");
}
if (node.selectedTabIndex >= node.children.size()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidSelectedTabIndex,
"Tab stack '" + node.nodeId + "' selectedTabIndex is out of range.");
}
for (const UIEditorWorkspaceNode& child : node.children) {
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::NonPanelTabChild,
"Tab stack '" + node.nodeId + "' may only contain panel leaf nodes.");
}
if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds);
!result.IsValid()) {
return result;
}
}
return {};
case UIEditorWorkspaceNodeKind::Split:
if (node.children.size() != 2u) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidSplitChildCount,
"Split node '" + node.nodeId + "' must contain exactly two child nodes.");
}
if (!IsValidSplitRatio(node.splitRatio)) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidSplitRatio,
"Split node '" + node.nodeId + "' must define a ratio in the open interval (0, 1).");
}
for (const UIEditorWorkspaceNode& child : node.children) {
if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds);
!result.IsValid()) {
return result;
}
}
return {};
}
return {};
}
} // namespace
UIEditorWorkspaceNode BuildUIEditorWorkspacePanel(
std::string nodeId,
std::string panelId,
std::string title,
bool placeholder) {
UIEditorWorkspaceNode node = {};
node.kind = UIEditorWorkspaceNodeKind::Panel;
node.nodeId = std::move(nodeId);
node.panel.panelId = std::move(panelId);
node.panel.title = std::move(title);
node.panel.placeholder = placeholder;
return node;
}
UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack(
std::string nodeId,
std::vector<UIEditorWorkspaceNode> panels,
std::size_t selectedTabIndex) {
UIEditorWorkspaceNode node = {};
node.kind = UIEditorWorkspaceNodeKind::TabStack;
node.nodeId = std::move(nodeId);
node.selectedTabIndex = selectedTabIndex;
node.children = std::move(panels);
return node;
}
UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit(
std::string nodeId,
UIEditorWorkspaceSplitAxis axis,
float splitRatio,
UIEditorWorkspaceNode primary,
UIEditorWorkspaceNode secondary) {
UIEditorWorkspaceNode node = {};
node.kind = UIEditorWorkspaceNodeKind::Split;
node.nodeId = std::move(nodeId);
node.splitAxis = axis;
node.splitRatio = splitRatio;
node.children.push_back(std::move(primary));
node.children.push_back(std::move(secondary));
return node;
}
UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace(
const UIEditorWorkspaceModel& workspace) {
std::unordered_set<std::string> panelIds = {};
UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(workspace.root, panelIds);
if (!result.IsValid()) {
return result;
}
if (!workspace.activePanelId.empty()) {
const UIEditorWorkspacePanelState* activePanel = FindUIEditorWorkspaceActivePanel(workspace);
if (activePanel == nullptr) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidActivePanelId,
"Active panel id '" + workspace.activePanelId + "' is missing or hidden by the current tab selection.");
}
}
return {};
}
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
const UIEditorWorkspaceModel& workspace) {
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {};
CollectVisiblePanelsRecursive(workspace.root, workspace.activePanelId, visiblePanels);
return visiblePanels;
}
bool ContainsUIEditorWorkspacePanel(
const UIEditorWorkspaceModel& workspace,
std::string_view panelId) {
return FindPanelRecursive(workspace.root, panelId) != nullptr;
}
const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel(
const UIEditorWorkspaceModel& workspace) {
if (workspace.activePanelId.empty()) {
return nullptr;
}
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
CollectUIEditorWorkspaceVisiblePanels(workspace);
for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) {
if (panel.panelId == workspace.activePanelId) {
return FindPanelRecursive(workspace.root, workspace.activePanelId);
}
}
return nullptr;
}
bool TryActivateUIEditorWorkspacePanel(
UIEditorWorkspaceModel& workspace,
std::string_view panelId) {
if (!TryActivateRecursive(workspace.root, panelId)) {
return false;
}
workspace.activePanelId = std::string(panelId);
return true;
}
} // namespace XCEngine::NewEditor