#include "ProductProjectPanel.h" #include #include #include #include #include #include #include #include #include #include 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; using Widgets::UIEditorTreeViewMetrics; using Widgets::UIEditorTreeViewPalette; constexpr std::string_view kProjectPanelId = "project"; constexpr std::string_view kAssetsRootId = "Assets"; constexpr std::size_t kInvalidLayoutIndex = static_cast(-1); constexpr float kBrowserHeaderHeight = 28.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(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(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(value.size()), nullptr, 0, nullptr, nullptr); if (requiredSize <= 0) { return {}; } std::string result(static_cast(requiredSize), '\0'); WideCharToMultiByte( CP_UTF8, 0, value.data(), static_cast(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 CollectSortedChildDirectories( const std::filesystem::path& folderPath) { std::vector 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 FilterProjectPanelInputEvents( const UIRect& bounds, const std::vector& inputEvents, bool allowInteraction, bool panelActive, bool captureActive) { if (!allowInteraction && !captureActive) { return {}; } std::vector 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 FilterTreeInputEvents( const std::vector& inputEvents, bool suppressPointerInput) { if (!suppressPointerInput) { return inputEvents; } std::vector 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; } UIEditorTreeViewMetrics BuildTreeMetrics() { UIEditorTreeViewMetrics metrics = {}; metrics.rowHeight = 20.0f; metrics.rowGap = 0.0f; metrics.horizontalPadding = 6.0f; metrics.indentWidth = 14.0f; metrics.disclosureExtent = 10.0f; metrics.disclosureLabelGap = 4.0f; metrics.labelInsetY = 0.0f; metrics.cornerRounding = 0.0f; metrics.borderThickness = 0.0f; metrics.focusedBorderThickness = 0.0f; return metrics; } UIEditorTreeViewPalette BuildTreePalette() { UIEditorTreeViewPalette palette = {}; palette.surfaceColor = kPaneColor; palette.borderColor = kPaneColor; palette.focusedBorderColor = kPaneColor; palette.rowHoverColor = kTileHoverColor; palette.rowSelectedColor = kTileSelectedColor; palette.rowSelectedFocusedColor = kTileSelectedColor; palette.disclosureColor = kTextMuted; palette.textColor = kTextPrimary; return palette; } 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 BuildBreadcrumbSegments(std::string_view currentFolderId) { std::vector 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::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 ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry( std::string_view itemId) const { for (const FolderEntry& entry : m_folderEntries) { 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 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((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(index % static_cast(columnCount)); const int row = static_cast(index / static_cast(columnCount)); const float tileX = layout.gridRect.x + static_cast(column) * (kGridTileWidth + kGridTileGapX); const float tileY = layout.gridRect.y + static_cast(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); m_treeItems.push_back(std::move(item)); const std::vector 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); } void ProductProjectPanel::NavigateToFolder(std::string_view itemId) { if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { return; } m_currentFolderId = std::string(itemId); SyncCurrentFolderSelection(); m_assetSelection.ClearSelection(); m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); RefreshAssetList(); } void ProductProjectPanel::RefreshAssetList() { EnsureValidCurrentFolder(); m_assetEntries.clear(); const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); if (currentFolder == nullptr) { return; } std::vector 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_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& inputEvents, bool allowInteraction, bool panelActive) { m_requestPointerCapture = false; m_requestPointerRelease = false; 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 filteredEvents = FilterProjectPanelInputEvents( panelState->bounds, inputEvents, allowInteraction, panelActive, m_splitterDragging); m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width); m_layout = BuildLayout(panelState->bounds); const UIEditorTreeViewMetrics treeMetrics = BuildTreeMetrics(); const std::vector 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); 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) { break; } 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()) { m_assetSelection.ClearSelection(); break; } const AssetEntry& assetEntry = m_assetEntries[hitIndex]; const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId); m_assetSelection.SetSelection(assetEntry.itemId); if (!assetEntry.directory) { m_lastPrimaryClickedAssetId = assetEntry.itemId; m_lastPrimaryClickTimeMs = GetTickCount64(); break; } const std::uint64_t nowMs = GetTickCount64(); const std::uint64_t doubleClickThresholdMs = static_cast(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; } NavigateToFolder(assetEntry.itemId); m_layout = BuildLayout(panelState->bounds); m_hoveredAssetItemId.clear(); } 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); 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 UIEditorTreeViewPalette treePalette = BuildTreePalette(); const UIEditorTreeViewMetrics treeMetrics = BuildTreeMetrics(); 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