diff --git a/new_editor/app/Features/Inspector/InspectorPanel.cpp b/new_editor/app/Features/Inspector/InspectorPanel.cpp index 344ca637..e6adec66 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.cpp +++ b/new_editor/app/Features/Inspector/InspectorPanel.cpp @@ -100,18 +100,29 @@ Widgets::UIEditorPropertyGridLayout TranslatePropertyGridLayoutForScroll( return translated; } +float ResolvePropertyGridContentBottom( + const Widgets::UIEditorPropertyGridLayout& gridLayout, + const UIRect& contentBounds) { + float contentBottom = contentBounds.y; + if (!gridLayout.sectionHeaderRects.empty()) { + const UIRect& lastSectionRect = gridLayout.sectionHeaderRects.back(); + contentBottom = + (std::max)(contentBottom, lastSectionRect.y + lastSectionRect.height); + } + if (!gridLayout.fieldRowRects.empty()) { + const UIRect& lastFieldRect = gridLayout.fieldRowRects.back(); + contentBottom = + (std::max)(contentBottom, lastFieldRect.y + lastFieldRect.height); + } + + return contentBottom; +} + float ResolveAddComponentButtonTop( const Widgets::UIEditorPropertyGridLayout& gridLayout, const UIRect& contentBounds) { - if (!gridLayout.fieldRowRects.empty()) { - const UIRect& lastFieldRect = gridLayout.fieldRowRects.back(); - return lastFieldRect.y + lastFieldRect.height + kAddComponentButtonTopGap; - } - if (!gridLayout.sectionHeaderRects.empty()) { - const UIRect& lastSectionRect = gridLayout.sectionHeaderRects.back(); - return lastSectionRect.y + lastSectionRect.height + kAddComponentButtonTopGap; - } - return contentBounds.y; + return ResolvePropertyGridContentBottom(gridLayout, contentBounds) + + kAddComponentButtonTopGap; } Widgets::UIEditorScrollViewPalette BuildInspectorScrollPalette() { @@ -342,13 +353,7 @@ float InspectorPanel::MeasureScrollableContentHeight( m_presentation.sections, m_sectionExpansion, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); - if (!layout.fieldRowRects.empty()) { - const UIRect& lastFieldRect = layout.fieldRowRects.back(); - contentBottom = lastFieldRect.y + lastFieldRect.height; - } else if (!layout.sectionHeaderRects.empty()) { - const UIRect& lastSectionRect = layout.sectionHeaderRects.back(); - contentBottom = lastSectionRect.y + lastSectionRect.height; - } + contentBottom = ResolvePropertyGridContentBottom(layout, contentBounds); } if (ShouldShowAddComponentButton()) { diff --git a/new_editor/app/Features/Project/ProjectPanel.cpp b/new_editor/app/Features/Project/ProjectPanel.cpp index 0276f00e..70798990 100644 --- a/new_editor/app/Features/Project/ProjectPanel.cpp +++ b/new_editor/app/Features/Project/ProjectPanel.cpp @@ -1,5 +1,6 @@ #include "ProjectPanel.h" #include "Rendering/Assets/BuiltInIcons.h" +#include #include #include #include @@ -84,6 +85,8 @@ void AppendTilePreview( const UIRect& previewRect, bool directory, const UITextureHandle* texture); +Widgets::UIEditorScrollViewPalette BuildProjectBrowserScrollPalette(); +int ResolveAssetGridColumnCount(float gridWidth); } // namespace XCEngine::UI::Editor::App @@ -214,6 +217,23 @@ void AppendTilePreview( drawList.AddRectOutline(sheetRect, kTilePreviewOutlineColor, 1.0f, 1.0f); } +Widgets::UIEditorScrollViewPalette BuildProjectBrowserScrollPalette() { + Widgets::UIEditorScrollViewPalette palette = + ResolveUIEditorScrollViewPalette(); + palette.surfaceColor.a = 0.0f; + palette.borderColor.a = 0.0f; + palette.focusedBorderColor.a = 0.0f; + return palette; +} + +int ResolveAssetGridColumnCount(float gridWidth) { + const float effectiveTileWidth = kGridTileWidth + kGridTileGapX; + int columnCount = effectiveTileWidth > 0.0f + ? static_cast((gridWidth + kGridTileGapX) / effectiveTileWidth) + : 1; + return (std::max)(columnCount, 1); +} + } // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { @@ -358,6 +378,9 @@ void ProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) { void ProjectPanel::ResetInteractionState() { m_assetDragState = {}; m_treeDragState = {}; + m_browserScrollInteractionState = {}; + m_browserScrollFrame = {}; + m_browserVerticalOffset = 0.0f; m_treeInteractionState = {}; m_treeFrame = {}; m_contextMenu = {}; @@ -383,6 +406,7 @@ ProjectPanel::CursorKind ProjectPanel::GetCursorKind() const { bool ProjectPanel::HasActivePointerCapture() const { return m_splitterDragging || + HasActiveUIEditorScrollViewPointerCapture(m_browserScrollInteractionState) || GridDrag::HasActivePointerCapture(m_assetDragState) || TreeDrag::HasActivePointerCapture(m_treeDragState) || HasActiveUIEditorTreeViewPointerCapture(m_treeInteractionState); @@ -668,7 +692,7 @@ void ProjectPanel::SyncAssetSelectionFromRuntime() { Widgets::UIEditorTreeViewMetrics ProjectPanel::RebuildPanelLayout(const UIRect& bounds) { const Widgets::UIEditorTreeViewMetrics treeMetrics = ResolveUIEditorTreeViewMetrics(); - m_layout = BuildLayout(bounds); + m_layout = BuildLayout(bounds, {}, 0.0f); m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( m_layout.treeRect, GetWindowTreeItems(), @@ -678,6 +702,74 @@ Widgets::UIEditorTreeViewMetrics ProjectPanel::RebuildPanelLayout(const UIRect& return treeMetrics; } +float ProjectPanel::MeasureBrowserContentHeight(const UIRect& browserContentRect) const { + if (!HasValidBounds(browserContentRect)) { + return 0.0f; + } + + const float gridWidth = + ClampNonNegative(browserContentRect.width - kGridInsetX * 2.0f); + if (gridWidth <= 0.0f) { + return 0.0f; + } + + const std::size_t assetCount = GetBrowserModel().GetAssetEntries().size(); + const int columnCount = ResolveAssetGridColumnCount(gridWidth); + const std::size_t rowCount = + assetCount == 0u + ? 0u + : (assetCount + static_cast(columnCount) - 1u) / + static_cast(columnCount); + + float contentHeight = kGridInsetY * 2.0f; + if (rowCount > 0u) { + contentHeight += + static_cast(rowCount) * kGridTileHeight + + static_cast(rowCount - 1u) * kGridTileGapY; + } else { + contentHeight += 18.0f; + } + + return contentHeight; +} + +void ProjectPanel::RebuildBrowserScrollLayout() { + m_browserScrollFrame = {}; + + if (!HasValidBounds(m_layout.browserBodyRect)) { + m_browserVerticalOffset = 0.0f; + return; + } + + const Widgets::UIEditorScrollViewMetrics& scrollMetrics = + ResolveUIEditorScrollViewMetrics(); + + float contentHeight = MeasureBrowserContentHeight(m_layout.browserBodyRect); + m_browserVerticalOffset = Widgets::ClampUIEditorScrollViewOffset( + m_layout.browserBodyRect, + contentHeight, + m_browserVerticalOffset, + scrollMetrics); + m_browserScrollFrame.layout = Widgets::BuildUIEditorScrollViewLayout( + m_layout.browserBodyRect, + contentHeight, + m_browserVerticalOffset, + scrollMetrics); + + contentHeight = MeasureBrowserContentHeight(m_browserScrollFrame.layout.contentRect); + m_browserVerticalOffset = Widgets::ClampUIEditorScrollViewOffset( + m_layout.browserBodyRect, + contentHeight, + m_browserVerticalOffset, + scrollMetrics); + m_browserScrollFrame.layout = Widgets::BuildUIEditorScrollViewLayout( + m_layout.browserBodyRect, + contentHeight, + m_browserVerticalOffset, + scrollMetrics); + m_browserScrollFrame.result.verticalOffset = m_browserVerticalOffset; +} + bool ProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) { if (!ResolveProjectRuntime()->NavigateToFolder(itemId)) { return false; @@ -704,6 +796,11 @@ bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source) if (navigated && HasValidBounds(m_layout.bounds)) { SyncSelectionsFromRuntime(); RebuildPanelLayout(m_layout.bounds); + RebuildBrowserScrollLayout(); + m_layout = BuildLayout( + m_layout.bounds, + m_browserScrollFrame.layout.contentRect, + m_browserVerticalOffset); m_hoveredAssetItemId.clear(); EmitEvent( EventKind::FolderNavigated, @@ -1198,6 +1295,11 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); if (HasValidBounds(m_layout.bounds)) { RebuildPanelLayout(m_layout.bounds); + RebuildBrowserScrollLayout(); + m_layout = BuildLayout( + m_layout.bounds, + m_browserScrollFrame.layout.contentRect, + m_browserVerticalOffset); } } @@ -1230,6 +1332,11 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); if (HasValidBounds(m_layout.bounds)) { RebuildPanelLayout(m_layout.bounds); + RebuildBrowserScrollLayout(); + m_layout = BuildLayout( + m_layout.bounds, + m_browserScrollFrame.layout.contentRect, + m_browserVerticalOffset); } } @@ -1485,6 +1592,33 @@ void ProjectPanel::Update( m_navigationWidth = ClampNavigationWidth(m_navigationWidth, dispatchEntry.bounds.width); const Widgets::UIEditorTreeViewMetrics treeMetrics = RebuildPanelLayout(dispatchEntry.bounds); + const auto refreshBrowserLayout = [&]() { + RebuildBrowserScrollLayout(); + m_layout = BuildLayout( + dispatchEntry.bounds, + m_browserScrollFrame.layout.contentRect, + m_browserVerticalOffset); + }; + const auto updateBrowserScrollInteraction = [&]() { + RebuildBrowserScrollLayout(); + if (HasValidBounds(m_layout.browserBodyRect)) { + m_browserScrollFrame = UpdateUIEditorScrollViewInteraction( + m_browserScrollInteractionState, + m_browserVerticalOffset, + m_layout.browserBodyRect, + m_browserScrollFrame.layout.contentHeight, + filteredEvents, + ResolveUIEditorScrollViewMetrics()); + } else { + m_browserScrollFrame = {}; + m_browserVerticalOffset = 0.0f; + } + + m_layout = BuildLayout( + dispatchEntry.bounds, + m_browserScrollFrame.layout.contentRect, + m_browserVerticalOffset); + }; if (m_contextMenu.open) { RebuildContextMenu(); } @@ -1497,6 +1631,7 @@ void ProjectPanel::Update( } if (m_renameState.active || !m_pendingRenameItemId.empty()) { + updateBrowserScrollInteraction(); TryStartQueuedRenameSession(); UpdateRenameSession(filteredEvents); return; @@ -1522,6 +1657,7 @@ void ProjectPanel::Update( CloseContextMenu(); NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree); RebuildPanelLayout(dispatchEntry.bounds); + refreshBrowserLayout(); } if (m_treeFrame.result.renameRequested && !m_treeFrame.result.renameItemId.empty()) { @@ -1607,8 +1743,11 @@ void ProjectPanel::Update( EmitSelectionClearedEvent(EventSource::Tree); } RebuildPanelLayout(dispatchEntry.bounds); + refreshBrowserLayout(); } + updateBrowserScrollInteraction(); + struct ProjectAssetDragCallbacks { ::XCEngine::UI::Widgets::UISelectionModel& assetSelection; ::XCEngine::UI::Widgets::UIExpansionModel& folderExpansion; @@ -1723,6 +1862,7 @@ void ProjectPanel::Update( } RebuildPanelLayout(dispatchEntry.bounds); + refreshBrowserLayout(); } const bool suppressPanelPointerEvents = @@ -1770,7 +1910,8 @@ void ProjectPanel::Update( ClampNavigationWidth( event.position.x - dispatchEntry.bounds.x, dispatchEntry.bounds.width); - m_layout = BuildLayout(dispatchEntry.bounds); + RebuildPanelLayout(dispatchEntry.bounds); + refreshBrowserLayout(); } m_splitterHovered = @@ -1892,6 +2033,7 @@ void ProjectPanel::Update( if (item.clickable) { NavigateToFolder(item.targetFolderId, EventSource::Breadcrumb); RebuildPanelLayout(dispatchEntry.bounds); + refreshBrowserLayout(); } } m_pressedBreadcrumbIndex = kInvalidLayoutIndex; @@ -1904,7 +2046,10 @@ void ProjectPanel::Update( } } -ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { +ProjectPanel::Layout ProjectPanel::BuildLayout( + const UIRect& bounds, + const UIRect& browserContentRect, + float browserVerticalOffset) const { Layout layout = {}; const auto& assetEntries = GetBrowserModel().GetAssetEntries(); const std::vector breadcrumbSegments = @@ -1949,11 +2094,15 @@ ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { layout.browserHeaderRect.y + layout.browserHeaderRect.height, layout.rightPaneRect.width, ClampNonNegative(layout.rightPaneRect.height - layout.browserHeaderRect.height)); + const UIRect effectiveBrowserContentRect = + HasValidBounds(browserContentRect) + ? browserContentRect + : layout.browserBodyRect; 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)); + effectiveBrowserContentRect.x + kGridInsetX, + effectiveBrowserContentRect.y + kGridInsetY, + ClampNonNegative(effectiveBrowserContentRect.width - kGridInsetX * 2.0f), + ClampNonNegative(effectiveBrowserContentRect.height - kGridInsetY * 2.0f)); const float breadcrumbRowHeight = kHeaderFontSize + kBreadcrumbItemPaddingY * 2.0f; const float breadcrumbY = @@ -2004,20 +2153,16 @@ ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { 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; - } + const int columnCount = ResolveAssetGridColumnCount(layout.gridRect.width); 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); + const float tileY = + layout.gridRect.y - browserVerticalOffset + + static_cast(row) * (kGridTileHeight + kGridTileGapY); AssetTileLayout tile = {}; tile.itemIndex = index; @@ -2183,6 +2328,16 @@ void ProjectPanel::Append(UIDrawList& drawList) const { } drawList.PopClipRect(); + if (HasValidBounds(m_browserScrollFrame.layout.bounds)) { + Widgets::AppendUIEditorScrollViewBackground( + drawList, + m_browserScrollFrame.layout, + m_browserScrollInteractionState.scrollViewState, + BuildProjectBrowserScrollPalette(), + ResolveUIEditorScrollViewMetrics()); + } + + drawList.PushClipRect(m_browserScrollFrame.layout.contentRect); for (const AssetTileLayout& tile : m_layout.assetTiles) { if (tile.itemIndex >= assetEntries.size()) { continue; @@ -2274,6 +2429,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const { kTextMuted, kHeaderFontSize); } + drawList.PopClipRect(); AppendContextMenu(drawList); } diff --git a/new_editor/app/Features/Project/ProjectPanel.h b/new_editor/app/Features/Project/ProjectPanel.h index 7fc553da..2613f5aa 100644 --- a/new_editor/app/Features/Project/ProjectPanel.h +++ b/new_editor/app/Features/Project/ProjectPanel.h @@ -6,6 +6,7 @@ #include "Commands/EditorEditCommandRoute.h" #include #include +#include #include #include #include @@ -176,7 +177,10 @@ private: std::optional ResolveEditCommandTarget( std::string_view explicitItemId = {}, bool forceCurrentFolder = false) const; - Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const; + Layout BuildLayout( + const ::XCEngine::UI::UIRect& bounds, + const ::XCEngine::UI::UIRect& browserContentRect, + float browserVerticalOffset) const; std::size_t HitTestBreadcrumbItem(const ::XCEngine::UI::UIPoint& point) const; std::size_t HitTestAssetTile(const ::XCEngine::UI::UIPoint& point) const; std::string ResolveAssetDropTargetItemId( @@ -184,6 +188,9 @@ private: DropTargetSurface* surface = nullptr) const; void SyncCurrentFolderSelection(); bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None); + float MeasureBrowserContentHeight( + const ::XCEngine::UI::UIRect& browserContentRect) const; + void RebuildBrowserScrollLayout(); void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder); void EmitEvent(EventKind kind, EventSource source, const AssetEntry* asset); void EmitSelectionClearedEvent(EventSource source); @@ -247,6 +254,9 @@ private: ::XCEngine::UI::Widgets::UISelectionModel m_assetSelection = {}; Collections::GridDragDrop::State m_assetDragState = {}; Collections::TreeDragDrop::State m_treeDragState = {}; + UIEditorScrollViewInteractionState m_browserScrollInteractionState = {}; + UIEditorScrollViewInteractionFrame m_browserScrollFrame = {}; + float m_browserVerticalOffset = 0.0f; UIEditorTreeViewInteractionState m_treeInteractionState = {}; UIEditorTreeViewInteractionFrame m_treeFrame = {}; UIEditorInlineRenameSessionState m_renameState = {};