fix(new_editor): stabilize inspector add-component layout and project browser scrolling

This commit is contained in:
2026-04-22 23:30:17 +08:00
parent b0d3141eee
commit c10367a42e
3 changed files with 203 additions and 32 deletions

View File

@@ -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()) {

View File

@@ -1,5 +1,6 @@
#include "ProjectPanel.h"
#include "Rendering/Assets/BuiltInIcons.h"
#include <XCEditor/Collections/UIEditorScrollView.h>
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <algorithm>
@@ -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<int>((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<std::size_t>(columnCount) - 1u) /
static_cast<std::size_t>(columnCount);
float contentHeight = kGridInsetY * 2.0f;
if (rowCount > 0u) {
contentHeight +=
static_cast<float>(rowCount) * kGridTileHeight +
static_cast<float>(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<ProjectBrowserModel::BreadcrumbSegment> 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<int>((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<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);
const float tileY =
layout.gridRect.y - browserVerticalOffset +
static_cast<float>(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);
}

View File

@@ -6,6 +6,7 @@
#include "Commands/EditorEditCommandRoute.h"
#include <XCEditor/Collections/UIEditorGridDragDrop.h>
#include <XCEditor/Collections/UIEditorInlineRenameSession.h>
#include <XCEditor/Collections/UIEditorScrollViewInteraction.h>
#include <XCEditor/Collections/UIEditorTreeDragDrop.h>
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
@@ -176,7 +177,10 @@ private:
std::optional<EditCommandTarget> 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 = {};