Files
XCEngine/new_editor/app/Panels/ProductProjectPanel.cpp

1102 lines
38 KiB
C++

#include "ProductProjectPanel.h"
#include "Icons/ProductBuiltInIcons.h"
#include "Panels/ProductTreeViewStyle.h"
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <string_view>
#include <system_error>
#include <utility>
#include <windows.h>
namespace XCEngine::UI::Editor::App {
namespace {
using ::XCEngine::UI::Editor::UIEditorTextMeasureRequest;
using ::XCEngine::UI::Editor::UIEditorTextMeasurer;
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIDrawList;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
using Widgets::AppendUIEditorTreeViewBackground;
using Widgets::AppendUIEditorTreeViewForeground;
constexpr std::string_view kProjectPanelId = "project";
constexpr std::string_view kAssetsRootId = "Assets";
constexpr std::size_t kInvalidLayoutIndex = static_cast<std::size_t>(-1);
constexpr float kBrowserHeaderHeight = 24.0f;
constexpr float kNavigationMinWidth = 180.0f;
constexpr float kBrowserMinWidth = 260.0f;
constexpr float kHeaderHorizontalPadding = 10.0f;
constexpr float kHeaderBottomBorderThickness = 1.0f;
constexpr float kBreadcrumbItemPaddingX = 4.0f;
constexpr float kBreadcrumbItemPaddingY = 1.0f;
constexpr float kBreadcrumbSpacing = 3.0f;
constexpr float kTreeTopPadding = 0.0f;
constexpr float kGridInsetX = 16.0f;
constexpr float kGridInsetY = 12.0f;
constexpr float kGridTileWidth = 92.0f;
constexpr float kGridTileHeight = 92.0f;
constexpr float kGridTileGapX = 12.0f;
constexpr float kGridTileGapY = 12.0f;
constexpr float kGridPreviewWidth = 68.0f;
constexpr float kGridPreviewHeight = 54.0f;
constexpr float kHeaderFontSize = 12.0f;
constexpr float kTileLabelFontSize = 11.0f;
constexpr UIColor kSurfaceColor(0.200f, 0.200f, 0.200f, 1.0f);
constexpr UIColor kPaneColor(0.205f, 0.205f, 0.205f, 1.0f);
constexpr UIColor kHeaderColor(0.215f, 0.215f, 0.215f, 1.0f);
constexpr UIColor kTextPrimary(0.830f, 0.830f, 0.830f, 1.0f);
constexpr UIColor kTextStrong(0.910f, 0.910f, 0.910f, 1.0f);
constexpr UIColor kTextMuted(0.560f, 0.560f, 0.560f, 1.0f);
constexpr UIColor kTileHoverColor(0.245f, 0.245f, 0.245f, 1.0f);
constexpr UIColor kTileSelectedColor(0.300f, 0.300f, 0.300f, 1.0f);
constexpr UIColor kTilePreviewFillColor(0.700f, 0.700f, 0.700f, 1.0f);
constexpr UIColor kTilePreviewShadeColor(0.610f, 0.610f, 0.610f, 1.0f);
constexpr UIColor kTilePreviewOutlineColor(0.860f, 0.860f, 0.860f, 0.35f);
bool ContainsPoint(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 ClampNonNegative(float value) {
return (std::max)(value, 0.0f);
}
float ResolveTextTop(float rectY, float rectHeight, float fontSize) {
const float lineHeight = fontSize * 1.6f;
return rectY + std::floor((rectHeight - lineHeight) * 0.5f);
}
float MeasureTextWidth(
const UIEditorTextMeasurer* textMeasurer,
std::string_view text,
float fontSize) {
if (text.empty()) {
return 0.0f;
}
if (textMeasurer != nullptr) {
const float measuredWidth =
textMeasurer->MeasureTextWidth(UIEditorTextMeasureRequest{ text, fontSize });
if (measuredWidth > 0.0f) {
return measuredWidth;
}
}
return static_cast<float>(text.size()) * fontSize * 0.56f;
}
std::string ToLowerCopy(std::string value) {
std::transform(
value.begin(),
value.end(),
value.begin(),
[](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return value;
}
std::string WideToUtf8(std::wstring_view value) {
if (value.empty()) {
return {};
}
const int requiredSize = WideCharToMultiByte(
CP_UTF8,
0,
value.data(),
static_cast<int>(value.size()),
nullptr,
0,
nullptr,
nullptr);
if (requiredSize <= 0) {
return {};
}
std::string result(static_cast<std::size_t>(requiredSize), '\0');
WideCharToMultiByte(
CP_UTF8,
0,
value.data(),
static_cast<int>(value.size()),
result.data(),
requiredSize,
nullptr,
nullptr);
return result;
}
std::string PathToUtf8String(const std::filesystem::path& path) {
return WideToUtf8(path.native());
}
std::string NormalizePathSeparators(std::string value) {
std::replace(value.begin(), value.end(), '\\', '/');
return value;
}
std::string BuildRelativeItemId(
const std::filesystem::path& path,
const std::filesystem::path& assetsRoot) {
const std::filesystem::path relative =
std::filesystem::relative(path, assetsRoot.parent_path());
const std::string normalized =
NormalizePathSeparators(PathToUtf8String(relative.lexically_normal()));
return normalized.empty() ? std::string(kAssetsRootId) : normalized;
}
std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) {
if (directory) {
return PathToUtf8String(path.filename());
}
const std::string filename = PathToUtf8String(path.filename());
const std::size_t extensionOffset = filename.find_last_of('.');
if (extensionOffset == std::string::npos || extensionOffset == 0u) {
return filename;
}
return filename.substr(0u, extensionOffset);
}
bool IsMetaFile(const std::filesystem::path& path) {
return ToLowerCopy(path.extension().string()) == ".meta";
}
bool HasChildDirectories(const std::filesystem::path& folderPath) {
std::error_code errorCode = {};
const std::filesystem::directory_iterator end = {};
for (std::filesystem::directory_iterator iterator(folderPath, errorCode);
!errorCode && iterator != end;
iterator.increment(errorCode)) {
if (iterator->is_directory(errorCode)) {
return true;
}
}
return false;
}
std::vector<std::filesystem::path> CollectSortedChildDirectories(
const std::filesystem::path& folderPath) {
std::vector<std::filesystem::path> paths = {};
std::error_code errorCode = {};
const std::filesystem::directory_iterator end = {};
for (std::filesystem::directory_iterator iterator(folderPath, errorCode);
!errorCode && iterator != end;
iterator.increment(errorCode)) {
if (iterator->is_directory(errorCode)) {
paths.push_back(iterator->path());
}
}
std::sort(
paths.begin(),
paths.end(),
[](const std::filesystem::path& lhs, const std::filesystem::path& rhs) {
return ToLowerCopy(PathToUtf8String(lhs.filename())) <
ToLowerCopy(PathToUtf8String(rhs.filename()));
});
return paths;
}
std::vector<UIInputEvent> FilterProjectPanelInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive,
bool captureActive) {
if (!allowInteraction && !captureActive) {
return {};
}
std::vector<UIInputEvent> filteredEvents = {};
filteredEvents.reserve(inputEvents.size());
for (const UIInputEvent& event : inputEvents) {
switch (event.type) {
case UIInputEventType::PointerMove:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
case UIInputEventType::PointerWheel:
if (captureActive || ContainsPoint(bounds, event.position)) {
filteredEvents.push_back(event);
}
break;
case UIInputEventType::PointerLeave:
filteredEvents.push_back(event);
break;
case UIInputEventType::FocusGained:
case UIInputEventType::FocusLost:
if (panelActive || captureActive) {
filteredEvents.push_back(event);
}
break;
case UIInputEventType::KeyDown:
case UIInputEventType::KeyUp:
case UIInputEventType::Character:
if (panelActive) {
filteredEvents.push_back(event);
}
break;
default:
break;
}
}
return filteredEvents;
}
std::vector<UIInputEvent> FilterTreeInputEvents(
const std::vector<UIInputEvent>& inputEvents,
bool suppressPointerInput) {
if (!suppressPointerInput) {
return inputEvents;
}
std::vector<UIInputEvent> filteredEvents = {};
filteredEvents.reserve(inputEvents.size());
for (const UIInputEvent& event : inputEvents) {
switch (event.type) {
case UIInputEventType::PointerMove:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
case UIInputEventType::PointerWheel:
case UIInputEventType::PointerEnter:
break;
default:
filteredEvents.push_back(event);
break;
}
}
return filteredEvents;
}
::XCEngine::UI::UITextureHandle ResolveFolderIcon(const ProductBuiltInIcons* icons) {
return icons != nullptr
? icons->Resolve(ProductBuiltInIconKind::Folder)
: ::XCEngine::UI::UITextureHandle {};
}
float ClampNavigationWidth(float value, float totalWidth) {
const float maxWidth =
(std::max)(
kNavigationMinWidth,
totalWidth - kBrowserMinWidth - ResolveUIEditorDockHostMetrics().splitterMetrics.thickness);
return std::clamp(value, kNavigationMinWidth, maxWidth);
}
std::vector<std::string> BuildBreadcrumbSegments(std::string_view currentFolderId) {
std::vector<std::string> segments = {};
if (currentFolderId.empty()) {
segments.push_back(std::string(kAssetsRootId));
return segments;
}
std::size_t segmentStart = 0u;
while (segmentStart < currentFolderId.size()) {
const std::size_t separator = currentFolderId.find('/', segmentStart);
const std::size_t segmentLength =
separator == std::string_view::npos
? currentFolderId.size() - segmentStart
: separator - segmentStart;
if (segmentLength > 0u) {
segments.emplace_back(currentFolderId.substr(segmentStart, segmentLength));
}
if (separator == std::string_view::npos) {
break;
}
segmentStart = separator + 1u;
}
if (segments.empty()) {
segments.push_back(std::string(kAssetsRootId));
}
return segments;
}
void AppendTilePreview(
UIDrawList& drawList,
const UIRect& previewRect,
bool directory) {
if (directory) {
const UIRect tabRect(
previewRect.x + 8.0f,
previewRect.y + 7.0f,
22.0f,
7.0f);
const UIRect bodyRect(
previewRect.x + 4.0f,
previewRect.y + 13.0f,
previewRect.width - 8.0f,
previewRect.height - 18.0f);
drawList.AddFilledRect(tabRect, kTilePreviewShadeColor, 1.0f);
drawList.AddFilledRect(bodyRect, kTilePreviewFillColor, 1.0f);
drawList.AddRectOutline(bodyRect, kTilePreviewOutlineColor, 1.0f, 1.0f);
return;
}
const UIRect sheetRect(
previewRect.x + 10.0f,
previewRect.y + 4.0f,
previewRect.width - 20.0f,
previewRect.height - 8.0f);
const UIRect accentRect(
sheetRect.x,
sheetRect.y,
sheetRect.width,
7.0f);
drawList.AddFilledRect(sheetRect, kTilePreviewFillColor, 1.0f);
drawList.AddFilledRect(accentRect, kTilePreviewShadeColor, 1.0f);
drawList.AddRectOutline(sheetRect, kTilePreviewOutlineColor, 1.0f, 1.0f);
}
} // namespace
void ProductProjectPanel::Initialize(const std::filesystem::path& repoRoot) {
m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal();
RefreshFolderTree();
SyncCurrentFolderSelection();
RefreshAssetList();
}
void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
m_icons = icons;
if (!m_assetsRootPath.empty()) {
RefreshFolderTree();
SyncCurrentFolderSelection();
}
}
void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) {
m_textMeasurer = textMeasurer;
}
ProductProjectPanel::CursorKind ProductProjectPanel::GetCursorKind() const {
return (m_splitterHovered || m_splitterDragging) ? CursorKind::ResizeEW : CursorKind::Arrow;
}
bool ProductProjectPanel::WantsHostPointerCapture() const {
return m_requestPointerCapture;
}
bool ProductProjectPanel::WantsHostPointerRelease() const {
return m_requestPointerRelease;
}
bool ProductProjectPanel::HasActivePointerCapture() const {
return m_splitterDragging;
}
const std::vector<ProductProjectPanel::Event>& ProductProjectPanel::GetFrameEvents() const {
return m_frameEvents;
}
const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry(
std::string_view itemId) const {
for (const FolderEntry& entry : m_folderEntries) {
if (entry.itemId == itemId) {
return &entry;
}
}
return nullptr;
}
const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry(
std::string_view itemId) const {
for (const AssetEntry& entry : m_assetEntries) {
if (entry.itemId == itemId) {
return &entry;
}
}
return nullptr;
}
const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel(
const UIEditorPanelContentHostFrame& contentHostFrame) const {
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
if (panelState.panelId == kProjectPanelId && panelState.mounted) {
return &panelState;
}
}
return nullptr;
}
ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bounds) const {
Layout layout = {};
const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness;
layout.bounds = UIRect(
bounds.x,
bounds.y,
ClampNonNegative(bounds.width),
ClampNonNegative(bounds.height));
const float leftWidth = ClampNavigationWidth(m_navigationWidth, layout.bounds.width);
layout.leftPaneRect = UIRect(
layout.bounds.x,
layout.bounds.y,
leftWidth,
layout.bounds.height);
layout.dividerRect = UIRect(
layout.leftPaneRect.x + layout.leftPaneRect.width,
layout.bounds.y,
dividerThickness,
layout.bounds.height);
layout.rightPaneRect = UIRect(
layout.dividerRect.x + layout.dividerRect.width,
layout.bounds.y,
ClampNonNegative(layout.bounds.width - layout.leftPaneRect.width - layout.dividerRect.width),
layout.bounds.height);
layout.treeRect = UIRect(
layout.leftPaneRect.x,
layout.leftPaneRect.y + kTreeTopPadding,
layout.leftPaneRect.width,
ClampNonNegative(layout.leftPaneRect.height - kTreeTopPadding));
layout.browserHeaderRect = UIRect(
layout.rightPaneRect.x,
layout.rightPaneRect.y,
layout.rightPaneRect.width,
(std::min)(kBrowserHeaderHeight, layout.rightPaneRect.height));
layout.browserBodyRect = UIRect(
layout.rightPaneRect.x,
layout.browserHeaderRect.y + layout.browserHeaderRect.height,
layout.rightPaneRect.width,
ClampNonNegative(layout.rightPaneRect.height - layout.browserHeaderRect.height));
layout.gridRect = UIRect(
layout.browserBodyRect.x + kGridInsetX,
layout.browserBodyRect.y + kGridInsetY,
ClampNonNegative(layout.browserBodyRect.width - kGridInsetX * 2.0f),
ClampNonNegative(layout.browserBodyRect.height - kGridInsetY * 2.0f));
const float breadcrumbRowHeight = kHeaderFontSize + kBreadcrumbItemPaddingY * 2.0f;
const float breadcrumbY =
layout.browserHeaderRect.y + std::floor((layout.browserHeaderRect.height - breadcrumbRowHeight) * 0.5f);
const float headerRight =
layout.browserHeaderRect.x + layout.browserHeaderRect.width - kHeaderHorizontalPadding;
float nextItemX = layout.browserHeaderRect.x + kHeaderHorizontalPadding;
std::string cumulativeFolderId = {};
const std::vector<std::string> segments = BuildBreadcrumbSegments(m_currentFolderId);
for (std::size_t index = 0u; index < segments.size(); ++index) {
if (index > 0u) {
const float separatorWidth =
MeasureTextWidth(m_textMeasurer, ">", kHeaderFontSize);
if (nextItemX < headerRight && separatorWidth > 0.0f) {
layout.breadcrumbItems.push_back({
">",
{},
UIRect(
nextItemX,
breadcrumbY,
ClampNonNegative((std::min)(separatorWidth, headerRight - nextItemX)),
breadcrumbRowHeight),
true,
false,
false
});
}
nextItemX += separatorWidth + kBreadcrumbSpacing;
}
if (index == 0u) {
cumulativeFolderId = segments[index];
} else {
cumulativeFolderId += "/";
cumulativeFolderId += segments[index];
}
const float labelWidth =
MeasureTextWidth(m_textMeasurer, segments[index], kHeaderFontSize);
const float itemWidth = labelWidth + kBreadcrumbItemPaddingX * 2.0f;
const float availableWidth = headerRight - nextItemX;
if (availableWidth <= 0.0f) {
break;
}
layout.breadcrumbItems.push_back({
segments[index],
cumulativeFolderId,
UIRect(
nextItemX,
breadcrumbY,
ClampNonNegative((std::min)(itemWidth, availableWidth)),
breadcrumbRowHeight),
false,
index + 1u != segments.size(),
index + 1u == segments.size()
});
nextItemX += itemWidth + kBreadcrumbSpacing;
}
const float effectiveTileWidth = kGridTileWidth + kGridTileGapX;
int columnCount = effectiveTileWidth > 0.0f
? static_cast<int>((layout.gridRect.width + kGridTileGapX) / effectiveTileWidth)
: 1;
if (columnCount < 1) {
columnCount = 1;
}
layout.assetTiles.reserve(m_assetEntries.size());
for (std::size_t index = 0; index < m_assetEntries.size(); ++index) {
const int column = static_cast<int>(index % static_cast<std::size_t>(columnCount));
const int row = static_cast<int>(index / static_cast<std::size_t>(columnCount));
const float tileX = layout.gridRect.x + static_cast<float>(column) * (kGridTileWidth + kGridTileGapX);
const float tileY = layout.gridRect.y + static_cast<float>(row) * (kGridTileHeight + kGridTileGapY);
AssetTileLayout tile = {};
tile.itemIndex = index;
tile.tileRect = UIRect(tileX, tileY, kGridTileWidth, kGridTileHeight);
tile.previewRect = UIRect(
tile.tileRect.x + (tile.tileRect.width - kGridPreviewWidth) * 0.5f,
tile.tileRect.y + 6.0f,
kGridPreviewWidth,
kGridPreviewHeight);
tile.labelRect = UIRect(
tile.tileRect.x + 4.0f,
tile.previewRect.y + tile.previewRect.height + 8.0f,
tile.tileRect.width - 8.0f,
18.0f);
layout.assetTiles.push_back(tile);
}
return layout;
}
std::size_t ProductProjectPanel::HitTestBreadcrumbItem(const UIPoint& point) const {
for (std::size_t index = 0u; index < m_layout.breadcrumbItems.size(); ++index) {
const BreadcrumbItemLayout& item = m_layout.breadcrumbItems[index];
if (!item.separator && ContainsPoint(item.rect, point)) {
return index;
}
}
return kInvalidLayoutIndex;
}
std::size_t ProductProjectPanel::HitTestAssetTile(const UIPoint& point) const {
for (const AssetTileLayout& tile : m_layout.assetTiles) {
if (ContainsPoint(tile.tileRect, point)) {
return tile.itemIndex;
}
}
return kInvalidLayoutIndex;
}
void ProductProjectPanel::RefreshFolderTree() {
m_folderEntries.clear();
m_treeItems.clear();
if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) {
return;
}
const auto appendFolderRecursive =
[&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void {
const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath);
FolderEntry folderEntry = {};
folderEntry.itemId = itemId;
folderEntry.absolutePath = folderPath;
m_folderEntries.push_back(std::move(folderEntry));
Widgets::UIEditorTreeViewItem item = {};
item.itemId = itemId;
item.label = PathToUtf8String(folderPath.filename());
item.depth = depth;
item.forceLeaf = !HasChildDirectories(folderPath);
item.leadingIcon = ResolveFolderIcon(m_icons);
m_treeItems.push_back(std::move(item));
const std::vector<std::filesystem::path> childFolders =
CollectSortedChildDirectories(folderPath);
for (const std::filesystem::path& childPath : childFolders) {
self(self, childPath, depth + 1u);
}
};
appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u);
}
void ProductProjectPanel::EnsureValidCurrentFolder() {
if (m_currentFolderId.empty()) {
m_currentFolderId = std::string(kAssetsRootId);
}
if (FindFolderEntry(m_currentFolderId) == nullptr && !m_treeItems.empty()) {
m_currentFolderId = m_treeItems.front().itemId;
}
}
void ProductProjectPanel::ExpandFolderAncestors(std::string_view itemId) {
const FolderEntry* folderEntry = FindFolderEntry(itemId);
if (folderEntry == nullptr) {
return;
}
std::filesystem::path path = folderEntry->absolutePath;
while (true) {
m_folderExpansion.Expand(BuildRelativeItemId(path, m_assetsRootPath));
if (path == m_assetsRootPath) {
break;
}
path = path.parent_path();
}
}
void ProductProjectPanel::SyncCurrentFolderSelection() {
EnsureValidCurrentFolder();
ExpandFolderAncestors(m_currentFolderId);
m_folderSelection.SetSelection(m_currentFolderId);
}
bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) {
if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) {
return false;
}
m_currentFolderId = std::string(itemId);
SyncCurrentFolderSelection();
m_assetSelection.ClearSelection();
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId.clear();
RefreshAssetList();
EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_currentFolderId));
return true;
}
void ProductProjectPanel::EmitEvent(
EventKind kind,
EventSource source,
const FolderEntry* folder) {
if (kind == EventKind::None || folder == nullptr) {
return;
}
Event event = {};
event.kind = kind;
event.source = source;
event.itemId = folder->itemId;
event.absolutePath = folder->absolutePath;
event.displayName = PathToUtf8String(folder->absolutePath.filename());
event.directory = true;
m_frameEvents.push_back(std::move(event));
}
void ProductProjectPanel::EmitEvent(
EventKind kind,
EventSource source,
const AssetEntry* asset) {
if (kind == EventKind::None) {
return;
}
Event event = {};
event.kind = kind;
event.source = source;
if (asset != nullptr) {
event.itemId = asset->itemId;
event.absolutePath = asset->absolutePath;
event.displayName = asset->displayName;
event.directory = asset->directory;
}
m_frameEvents.push_back(std::move(event));
}
void ProductProjectPanel::EmitSelectionClearedEvent(EventSource source) {
Event event = {};
event.kind = EventKind::AssetSelectionCleared;
event.source = source;
m_frameEvents.push_back(std::move(event));
}
void ProductProjectPanel::RefreshAssetList() {
EnsureValidCurrentFolder();
m_assetEntries.clear();
const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId);
if (currentFolder == nullptr) {
return;
}
std::vector<std::filesystem::directory_entry> entries = {};
std::error_code errorCode = {};
const std::filesystem::directory_iterator end = {};
for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode);
!errorCode && iterator != end;
iterator.increment(errorCode)) {
if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) {
continue;
}
if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) {
continue;
}
entries.push_back(*iterator);
}
std::sort(
entries.begin(),
entries.end(),
[](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) {
const bool lhsDirectory = lhs.is_directory();
const bool rhsDirectory = rhs.is_directory();
if (lhsDirectory != rhsDirectory) {
return lhsDirectory && !rhsDirectory;
}
return ToLowerCopy(PathToUtf8String(lhs.path().filename())) <
ToLowerCopy(PathToUtf8String(rhs.path().filename()));
});
for (const std::filesystem::directory_entry& entry : entries) {
AssetEntry assetEntry = {};
assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath);
assetEntry.absolutePath = entry.path();
assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory());
assetEntry.directory = entry.is_directory();
m_assetEntries.push_back(std::move(assetEntry));
}
}
void ProductProjectPanel::ResetTransientFrames() {
m_treeFrame = {};
m_frameEvents.clear();
m_layout = {};
m_hoveredAssetItemId.clear();
m_hoveredBreadcrumbIndex = kInvalidLayoutIndex;
m_pressedBreadcrumbIndex = kInvalidLayoutIndex;
m_splitterHovered = false;
m_splitterDragging = false;
}
void ProductProjectPanel::Update(
const UIEditorPanelContentHostFrame& contentHostFrame,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive) {
m_requestPointerCapture = false;
m_requestPointerRelease = false;
m_frameEvents.clear();
const UIEditorPanelContentHostPanelState* panelState =
FindMountedProjectPanel(contentHostFrame);
if (panelState == nullptr) {
if (m_splitterDragging) {
m_requestPointerRelease = true;
}
m_visible = false;
ResetTransientFrames();
return;
}
if (m_treeItems.empty()) {
RefreshFolderTree();
SyncCurrentFolderSelection();
RefreshAssetList();
}
m_visible = true;
const std::vector<UIInputEvent> filteredEvents =
FilterProjectPanelInputEvents(
panelState->bounds,
inputEvents,
allowInteraction,
panelActive,
m_splitterDragging);
m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width);
m_layout = BuildLayout(panelState->bounds);
const Widgets::UIEditorTreeViewMetrics treeMetrics = BuildProductTreeViewMetrics();
const std::vector<UIInputEvent> treeEvents =
FilterTreeInputEvents(filteredEvents, m_splitterDragging);
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_treeInteractionState,
m_folderSelection,
m_folderExpansion,
m_layout.treeRect,
m_treeItems,
treeEvents,
treeMetrics);
if (m_treeFrame.result.selectionChanged &&
!m_treeFrame.result.selectedItemId.empty() &&
m_treeFrame.result.selectedItemId != m_currentFolderId) {
NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree);
m_layout = BuildLayout(panelState->bounds);
}
for (const UIInputEvent& event : filteredEvents) {
switch (event.type) {
case UIInputEventType::FocusLost:
m_hoveredAssetItemId.clear();
m_hoveredBreadcrumbIndex = kInvalidLayoutIndex;
m_pressedBreadcrumbIndex = kInvalidLayoutIndex;
m_splitterHovered = false;
if (m_splitterDragging) {
m_splitterDragging = false;
m_requestPointerRelease = true;
}
break;
case UIInputEventType::PointerMove: {
if (m_splitterDragging) {
m_navigationWidth =
ClampNavigationWidth(event.position.x - panelState->bounds.x, panelState->bounds.width);
m_layout = BuildLayout(panelState->bounds);
}
m_splitterHovered =
m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position);
m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position);
const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position);
m_hoveredAssetItemId =
hoveredAssetIndex < m_assetEntries.size()
? m_assetEntries[hoveredAssetIndex].itemId
: std::string();
break;
}
case UIInputEventType::PointerLeave:
if (!m_splitterDragging) {
m_splitterHovered = false;
}
m_hoveredBreadcrumbIndex = kInvalidLayoutIndex;
m_hoveredAssetItemId.clear();
break;
case UIInputEventType::PointerButtonDown:
if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Left) {
if (ContainsPoint(m_layout.dividerRect, event.position)) {
m_splitterDragging = true;
m_splitterHovered = true;
m_pressedBreadcrumbIndex = kInvalidLayoutIndex;
m_requestPointerCapture = true;
break;
}
m_pressedBreadcrumbIndex = HitTestBreadcrumbItem(event.position);
if (!ContainsPoint(m_layout.gridRect, event.position)) {
break;
}
const std::size_t hitIndex = HitTestAssetTile(event.position);
if (hitIndex >= m_assetEntries.size()) {
if (m_assetSelection.HasSelection()) {
m_assetSelection.ClearSelection();
EmitSelectionClearedEvent(EventSource::Background);
}
break;
}
const AssetEntry& assetEntry = m_assetEntries[hitIndex];
const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId);
const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId);
if (selectionChanged) {
EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, &assetEntry);
}
const std::uint64_t nowMs = GetTickCount64();
const std::uint64_t doubleClickThresholdMs =
static_cast<std::uint64_t>(GetDoubleClickTime());
const bool doubleClicked =
alreadySelected &&
m_lastPrimaryClickedAssetId == assetEntry.itemId &&
nowMs >= m_lastPrimaryClickTimeMs &&
nowMs - m_lastPrimaryClickTimeMs <= doubleClickThresholdMs;
m_lastPrimaryClickedAssetId = assetEntry.itemId;
m_lastPrimaryClickTimeMs = nowMs;
if (!doubleClicked) {
break;
}
if (assetEntry.directory) {
NavigateToFolder(assetEntry.itemId, EventSource::GridDoubleClick);
m_layout = BuildLayout(panelState->bounds);
m_hoveredAssetItemId.clear();
} else {
EmitEvent(EventKind::AssetOpened, EventSource::GridDoubleClick, &assetEntry);
}
break;
}
if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right &&
ContainsPoint(m_layout.gridRect, event.position)) {
const std::size_t hitIndex = HitTestAssetTile(event.position);
if (hitIndex >= m_assetEntries.size()) {
EmitEvent(EventKind::ContextMenuRequested, EventSource::Background, static_cast<const AssetEntry*>(nullptr));
break;
}
const AssetEntry& assetEntry = m_assetEntries[hitIndex];
if (!m_assetSelection.IsSelected(assetEntry.itemId)) {
m_assetSelection.SetSelection(assetEntry.itemId);
EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry);
}
EmitEvent(EventKind::ContextMenuRequested, EventSource::GridSecondary, &assetEntry);
}
break;
case UIInputEventType::PointerButtonUp:
if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) {
break;
}
if (m_splitterDragging) {
m_splitterDragging = false;
m_splitterHovered = ContainsPoint(m_layout.dividerRect, event.position);
m_requestPointerRelease = true;
break;
}
{
const std::size_t releasedBreadcrumbIndex =
HitTestBreadcrumbItem(event.position);
if (m_pressedBreadcrumbIndex != kInvalidLayoutIndex &&
m_pressedBreadcrumbIndex == releasedBreadcrumbIndex &&
releasedBreadcrumbIndex < m_layout.breadcrumbItems.size()) {
const BreadcrumbItemLayout& item =
m_layout.breadcrumbItems[releasedBreadcrumbIndex];
if (item.clickable) {
NavigateToFolder(item.targetFolderId, EventSource::Breadcrumb);
m_layout = BuildLayout(panelState->bounds);
}
}
m_pressedBreadcrumbIndex = kInvalidLayoutIndex;
}
break;
default:
break;
}
}
}
void ProductProjectPanel::Append(UIDrawList& drawList) const {
if (!m_visible || m_layout.bounds.width <= 0.0f || m_layout.bounds.height <= 0.0f) {
return;
}
drawList.AddFilledRect(m_layout.bounds, kSurfaceColor);
drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor);
drawList.AddFilledRect(m_layout.rightPaneRect, kPaneColor);
drawList.AddFilledRect(
m_layout.dividerRect,
ResolveUIEditorDockHostPalette().splitterColor);
drawList.AddFilledRect(m_layout.browserHeaderRect, kHeaderColor);
drawList.AddFilledRect(
UIRect(
m_layout.browserHeaderRect.x,
m_layout.browserHeaderRect.y + m_layout.browserHeaderRect.height - kHeaderBottomBorderThickness,
m_layout.browserHeaderRect.width,
kHeaderBottomBorderThickness),
ResolveUIEditorDockHostPalette().splitterColor);
const Widgets::UIEditorTreeViewPalette treePalette = BuildProductTreeViewPalette();
const Widgets::UIEditorTreeViewMetrics treeMetrics = BuildProductTreeViewMetrics();
AppendUIEditorTreeViewBackground(
drawList,
m_treeFrame.layout,
m_treeItems,
m_folderSelection,
m_treeInteractionState.treeViewState,
treePalette,
treeMetrics);
AppendUIEditorTreeViewForeground(
drawList,
m_treeFrame.layout,
m_treeItems,
treePalette,
treeMetrics);
drawList.PushClipRect(m_layout.browserHeaderRect);
for (std::size_t index = 0u; index < m_layout.breadcrumbItems.size(); ++index) {
const BreadcrumbItemLayout& item = m_layout.breadcrumbItems[index];
const UIColor textColor =
item.separator
? kTextMuted
: (index == m_hoveredBreadcrumbIndex && item.clickable
? kTextStrong
: (item.current ? kTextPrimary : kTextMuted));
const float textWidth = MeasureTextWidth(m_textMeasurer, item.label, kHeaderFontSize);
const float textX = item.separator
? item.rect.x
: item.rect.x + (item.rect.width - textWidth) * 0.5f;
drawList.AddText(
UIPoint(textX, ResolveTextTop(item.rect.y, item.rect.height, kHeaderFontSize)),
item.label,
textColor,
kHeaderFontSize);
}
drawList.PopClipRect();
for (const AssetTileLayout& tile : m_layout.assetTiles) {
if (tile.itemIndex >= m_assetEntries.size()) {
continue;
}
const AssetEntry& assetEntry = m_assetEntries[tile.itemIndex];
const bool selected = m_assetSelection.IsSelected(assetEntry.itemId);
const bool hovered = m_hoveredAssetItemId == assetEntry.itemId;
if (selected || hovered) {
drawList.AddFilledRect(
tile.tileRect,
selected ? kTileSelectedColor : kTileHoverColor);
}
AppendTilePreview(drawList, tile.previewRect, assetEntry.directory);
drawList.PushClipRect(tile.labelRect);
const float textWidth =
MeasureTextWidth(m_textMeasurer, assetEntry.displayName, kTileLabelFontSize);
drawList.AddText(
UIPoint(
tile.labelRect.x + (tile.labelRect.width - textWidth) * 0.5f,
ResolveTextTop(tile.labelRect.y, tile.labelRect.height, kTileLabelFontSize)),
assetEntry.displayName,
kTextPrimary,
kTileLabelFontSize);
drawList.PopClipRect();
}
if (m_assetEntries.empty()) {
const UIRect messageRect(
m_layout.gridRect.x,
m_layout.gridRect.y,
m_layout.gridRect.width,
18.0f);
drawList.AddText(
UIPoint(messageRect.x, ResolveTextTop(messageRect.y, messageRect.height, kHeaderFontSize)),
"Current folder is empty.",
kTextMuted,
kHeaderFontSize);
}
}
} // namespace XCEngine::UI::Editor::App