#include "ProductProjectPanel.h" #include "Icons/ProductBuiltInIcons.h" #include "Panels/ProductTreeViewStyle.h" #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; constexpr std::string_view kProjectPanelId = "project"; constexpr std::size_t kInvalidLayoutIndex = static_cast(-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(text.size()) * fontSize * 0.56f; } 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; } ::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); } 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_browserModel.Initialize(repoRoot); SyncCurrentFolderSelection(); } void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) { m_icons = icons; m_browserModel.SetFolderIcon(ResolveFolderIcon(m_icons)); 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::GetFrameEvents() const { return m_frameEvents; } const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry( std::string_view itemId) const { return m_browserModel.FindFolderEntry(itemId); } const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry( std::string_view itemId) const { return m_browserModel.FindAssetEntry(itemId); } 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 auto& assetEntries = m_browserModel.GetAssetEntries(); const std::vector breadcrumbSegments = m_browserModel.BuildBreadcrumbSegments(); 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; for (std::size_t index = 0u; index < breadcrumbSegments.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; } const ProductProjectPanel::BrowserModel::BreadcrumbSegment& segment = breadcrumbSegments[index]; const float labelWidth = MeasureTextWidth(m_textMeasurer, segment.label, kHeaderFontSize); const float itemWidth = labelWidth + kBreadcrumbItemPaddingX * 2.0f; const float availableWidth = headerRight - nextItemX; if (availableWidth <= 0.0f) { break; } layout.breadcrumbItems.push_back({ segment.label, segment.targetFolderId, UIRect( nextItemX, breadcrumbY, ClampNonNegative((std::min)(itemWidth, availableWidth)), breadcrumbRowHeight), false, !segment.current, segment.current }); 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(assetEntries.size()); for (std::size_t index = 0; index < 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::SyncCurrentFolderSelection() { const std::string& currentFolderId = m_browserModel.GetCurrentFolderId(); if (currentFolderId.empty()) { m_folderSelection.ClearSelection(); return; } const std::vector ancestorFolderIds = m_browserModel.CollectCurrentFolderAncestorIds(); for (const std::string& ancestorFolderId : ancestorFolderIds) { m_folderExpansion.Expand(ancestorFolderId); } m_folderSelection.SetSelection(currentFolderId); } bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) { if (!m_browserModel.NavigateToFolder(itemId)) { return false; } SyncCurrentFolderSelection(); m_assetSelection.ClearSelection(); m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_browserModel.GetCurrentFolderId())); 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 = folder->label; 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::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& 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_browserModel.GetTreeItems().empty()) { m_browserModel.Refresh(); SyncCurrentFolderSelection(); } 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 Widgets::UIEditorTreeViewMetrics treeMetrics = BuildProductTreeViewMetrics(); const std::vector treeEvents = FilterTreeInputEvents(filteredEvents, m_splitterDragging); m_treeFrame = UpdateUIEditorTreeViewInteraction( m_treeInteractionState, m_folderSelection, m_folderExpansion, m_layout.treeRect, m_browserModel.GetTreeItems(), treeEvents, treeMetrics); if (m_treeFrame.result.selectionChanged && !m_treeFrame.result.selectedItemId.empty() && m_treeFrame.result.selectedItemId != m_browserModel.GetCurrentFolderId()) { 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); const auto& assetEntries = m_browserModel.GetAssetEntries(); m_hoveredAssetItemId = hoveredAssetIndex < assetEntries.size() ? 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 auto& assetEntries = m_browserModel.GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { if (m_assetSelection.HasSelection()) { m_assetSelection.ClearSelection(); EmitSelectionClearedEvent(EventSource::Background); } break; } const AssetEntry& assetEntry = 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(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 auto& assetEntries = m_browserModel.GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { EmitEvent( EventKind::ContextMenuRequested, EventSource::Background, static_cast(nullptr)); break; } const AssetEntry& assetEntry = 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; } const auto& assetEntries = m_browserModel.GetAssetEntries(); 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_browserModel.GetTreeItems(), m_folderSelection, m_treeInteractionState.treeViewState, treePalette, treeMetrics); AppendUIEditorTreeViewForeground( drawList, m_treeFrame.layout, m_browserModel.GetTreeItems(), 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 >= assetEntries.size()) { continue; } const AssetEntry& assetEntry = 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 (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