Refine editor tree alignment and project panel events

This commit is contained in:
2026-04-11 22:31:14 +08:00
parent 8848cfd958
commit 0ff02150c0
6 changed files with 219 additions and 35 deletions

View File

@@ -328,6 +328,65 @@ std::string DescribeInputEventType(const UIInputEvent& event) {
} }
} }
std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& event) {
std::ostringstream stream = {};
switch (event.kind) {
case App::ProductProjectPanel::EventKind::AssetSelected:
stream << "AssetSelected";
break;
case App::ProductProjectPanel::EventKind::AssetSelectionCleared:
stream << "AssetSelectionCleared";
break;
case App::ProductProjectPanel::EventKind::FolderNavigated:
stream << "FolderNavigated";
break;
case App::ProductProjectPanel::EventKind::AssetOpened:
stream << "AssetOpened";
break;
case App::ProductProjectPanel::EventKind::ContextMenuRequested:
stream << "ContextMenuRequested";
break;
case App::ProductProjectPanel::EventKind::None:
default:
stream << "None";
break;
}
stream << " source=";
switch (event.source) {
case App::ProductProjectPanel::EventSource::Tree:
stream << "Tree";
break;
case App::ProductProjectPanel::EventSource::Breadcrumb:
stream << "Breadcrumb";
break;
case App::ProductProjectPanel::EventSource::GridPrimary:
stream << "GridPrimary";
break;
case App::ProductProjectPanel::EventSource::GridDoubleClick:
stream << "GridDoubleClick";
break;
case App::ProductProjectPanel::EventSource::GridSecondary:
stream << "GridSecondary";
break;
case App::ProductProjectPanel::EventSource::Background:
stream << "Background";
break;
case App::ProductProjectPanel::EventSource::None:
default:
stream << "None";
break;
}
if (!event.itemId.empty()) {
stream << " item=" << event.itemId;
}
if (!event.displayName.empty()) {
stream << " label=" << event.displayName;
}
return stream.str();
}
std::vector<UIInputEvent> FilterShellInputEventsForHostedContentCapture( std::vector<UIInputEvent> FilterShellInputEventsForHostedContentCapture(
const std::vector<UIInputEvent>& inputEvents) { const std::vector<UIInputEvent>& inputEvents) {
std::vector<UIInputEvent> filteredEvents = {}; std::vector<UIInputEvent> filteredEvents = {};
@@ -568,6 +627,11 @@ void Application::RenderFrame() {
hostedContentEvents, hostedContentEvents,
!m_shellFrame.result.workspaceInputSuppressed, !m_shellFrame.result.workspaceInputSuppressed,
m_workspaceController.GetWorkspace().activePanelId == "project"); m_workspaceController.GetWorkspace().activePanelId == "project");
for (const App::ProductProjectPanel::Event& event : m_projectPanel.GetFrameEvents()) {
LogRuntimeTrace("project", DescribeProjectPanelEvent(event));
m_lastStatus = "Project";
m_lastMessage = DescribeProjectPanelEvent(event);
}
ApplyHostedContentCaptureRequests(); ApplyHostedContentCaptureRequests();
ApplyCurrentCursor(); ApplyCurrentCursor();
const UIEditorShellComposeModel shellComposeModel = const UIEditorShellComposeModel shellComposeModel =

View File

@@ -405,6 +405,10 @@ bool ProductProjectPanel::HasActivePointerCapture() const {
return m_splitterDragging; return m_splitterDragging;
} }
const std::vector<ProductProjectPanel::Event>& ProductProjectPanel::GetFrameEvents() const {
return m_frameEvents;
}
const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry( const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry(
std::string_view itemId) const { std::string_view itemId) const {
for (const FolderEntry& entry : m_folderEntries) { for (const FolderEntry& entry : m_folderEntries) {
@@ -416,6 +420,17 @@ const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry(
return nullptr; 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 UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel(
const UIEditorPanelContentHostFrame& contentHostFrame) const { const UIEditorPanelContentHostFrame& contentHostFrame) const {
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
@@ -656,9 +671,9 @@ void ProductProjectPanel::SyncCurrentFolderSelection() {
m_folderSelection.SetSelection(m_currentFolderId); m_folderSelection.SetSelection(m_currentFolderId);
} }
void ProductProjectPanel::NavigateToFolder(std::string_view itemId) { bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) {
if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) {
return; return false;
} }
m_currentFolderId = std::string(itemId); m_currentFolderId = std::string(itemId);
@@ -667,6 +682,53 @@ void ProductProjectPanel::NavigateToFolder(std::string_view itemId) {
m_hoveredAssetItemId.clear(); m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId.clear(); m_lastPrimaryClickedAssetId.clear();
RefreshAssetList(); 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() { void ProductProjectPanel::RefreshAssetList() {
@@ -720,6 +782,7 @@ void ProductProjectPanel::RefreshAssetList() {
void ProductProjectPanel::ResetTransientFrames() { void ProductProjectPanel::ResetTransientFrames() {
m_treeFrame = {}; m_treeFrame = {};
m_frameEvents.clear();
m_layout = {}; m_layout = {};
m_hoveredAssetItemId.clear(); m_hoveredAssetItemId.clear();
m_hoveredBreadcrumbIndex = kInvalidLayoutIndex; m_hoveredBreadcrumbIndex = kInvalidLayoutIndex;
@@ -735,6 +798,7 @@ void ProductProjectPanel::Update(
bool panelActive) { bool panelActive) {
m_requestPointerCapture = false; m_requestPointerCapture = false;
m_requestPointerRelease = false; m_requestPointerRelease = false;
m_frameEvents.clear();
const UIEditorPanelContentHostPanelState* panelState = const UIEditorPanelContentHostPanelState* panelState =
FindMountedProjectPanel(contentHostFrame); FindMountedProjectPanel(contentHostFrame);
@@ -779,7 +843,7 @@ void ProductProjectPanel::Update(
if (m_treeFrame.result.selectionChanged && if (m_treeFrame.result.selectionChanged &&
!m_treeFrame.result.selectedItemId.empty() && !m_treeFrame.result.selectedItemId.empty() &&
m_treeFrame.result.selectedItemId != m_currentFolderId) { m_treeFrame.result.selectedItemId != m_currentFolderId) {
NavigateToFolder(m_treeFrame.result.selectedItemId); NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree);
m_layout = BuildLayout(panelState->bounds); m_layout = BuildLayout(panelState->bounds);
} }
@@ -823,39 +887,35 @@ void ProductProjectPanel::Update(
break; break;
case UIInputEventType::PointerButtonDown: case UIInputEventType::PointerButtonDown:
if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) { 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;
}
if (ContainsPoint(m_layout.dividerRect, event.position)) { m_pressedBreadcrumbIndex = HitTestBreadcrumbItem(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;
}
if (!ContainsPoint(m_layout.gridRect, event.position)) {
break;
}
{
const std::size_t hitIndex = HitTestAssetTile(event.position); const std::size_t hitIndex = HitTestAssetTile(event.position);
if (hitIndex >= m_assetEntries.size()) { if (hitIndex >= m_assetEntries.size()) {
m_assetSelection.ClearSelection(); if (m_assetSelection.HasSelection()) {
m_assetSelection.ClearSelection();
EmitSelectionClearedEvent(EventSource::Background);
}
break; break;
} }
const AssetEntry& assetEntry = m_assetEntries[hitIndex]; const AssetEntry& assetEntry = m_assetEntries[hitIndex];
const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId); const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId);
m_assetSelection.SetSelection(assetEntry.itemId); const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId);
if (selectionChanged) {
if (!assetEntry.directory) { EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, &assetEntry);
m_lastPrimaryClickedAssetId = assetEntry.itemId;
m_lastPrimaryClickTimeMs = GetTickCount64();
break;
} }
const std::uint64_t nowMs = GetTickCount64(); const std::uint64_t nowMs = GetTickCount64();
@@ -873,9 +933,30 @@ void ProductProjectPanel::Update(
break; break;
} }
NavigateToFolder(assetEntry.itemId); if (assetEntry.directory) {
m_layout = BuildLayout(panelState->bounds); NavigateToFolder(assetEntry.itemId, EventSource::GridDoubleClick);
m_hoveredAssetItemId.clear(); 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; break;
@@ -900,7 +981,7 @@ void ProductProjectPanel::Update(
const BreadcrumbItemLayout& item = const BreadcrumbItemLayout& item =
m_layout.breadcrumbItems[releasedBreadcrumbIndex]; m_layout.breadcrumbItems[releasedBreadcrumbIndex];
if (item.clickable) { if (item.clickable) {
NavigateToFolder(item.targetFolderId); NavigateToFolder(item.targetFolderId, EventSource::Breadcrumb);
m_layout = BuildLayout(panelState->bounds); m_layout = BuildLayout(panelState->bounds);
} }
} }

View File

@@ -25,6 +25,34 @@ public:
ResizeEW ResizeEW
}; };
enum class EventKind : std::uint8_t {
None = 0,
AssetSelected,
AssetSelectionCleared,
FolderNavigated,
AssetOpened,
ContextMenuRequested
};
enum class EventSource : std::uint8_t {
None = 0,
Tree,
Breadcrumb,
GridPrimary,
GridDoubleClick,
GridSecondary,
Background
};
struct Event {
EventKind kind = EventKind::None;
EventSource source = EventSource::None;
std::string itemId = {};
std::filesystem::path absolutePath = {};
std::string displayName = {};
bool directory = false;
};
void Initialize(const std::filesystem::path& repoRoot); void Initialize(const std::filesystem::path& repoRoot);
void SetBuiltInIcons(const ProductBuiltInIcons* icons); void SetBuiltInIcons(const ProductBuiltInIcons* icons);
void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer);
@@ -39,6 +67,7 @@ public:
bool WantsHostPointerCapture() const; bool WantsHostPointerCapture() const;
bool WantsHostPointerRelease() const; bool WantsHostPointerRelease() const;
bool HasActivePointerCapture() const; bool HasActivePointerCapture() const;
const std::vector<Event>& GetFrameEvents() const;
private: private:
struct FolderEntry { struct FolderEntry {
@@ -83,6 +112,7 @@ private:
}; };
const FolderEntry* FindFolderEntry(std::string_view itemId) const; const FolderEntry* FindFolderEntry(std::string_view itemId) const;
const AssetEntry* FindAssetEntry(std::string_view itemId) const;
const UIEditorPanelContentHostPanelState* FindMountedProjectPanel( const UIEditorPanelContentHostPanelState* FindMountedProjectPanel(
const UIEditorPanelContentHostFrame& contentHostFrame) const; const UIEditorPanelContentHostFrame& contentHostFrame) const;
Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const; Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const;
@@ -93,7 +123,10 @@ private:
void EnsureValidCurrentFolder(); void EnsureValidCurrentFolder();
void ExpandFolderAncestors(std::string_view itemId); void ExpandFolderAncestors(std::string_view itemId);
void SyncCurrentFolderSelection(); void SyncCurrentFolderSelection();
void NavigateToFolder(std::string_view itemId); bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None);
void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder);
void EmitEvent(EventKind kind, EventSource source, const AssetEntry* asset);
void EmitSelectionClearedEvent(EventSource source);
void ResetTransientFrames(); void ResetTransientFrames();
std::filesystem::path m_assetsRootPath = {}; std::filesystem::path m_assetsRootPath = {};
@@ -107,6 +140,7 @@ private:
::XCEngine::UI::Widgets::UISelectionModel m_assetSelection = {}; ::XCEngine::UI::Widgets::UISelectionModel m_assetSelection = {};
UIEditorTreeViewInteractionState m_treeInteractionState = {}; UIEditorTreeViewInteractionState m_treeInteractionState = {};
UIEditorTreeViewInteractionFrame m_treeFrame = {}; UIEditorTreeViewInteractionFrame m_treeFrame = {};
std::vector<Event> m_frameEvents = {};
Layout m_layout = {}; Layout m_layout = {};
std::string m_currentFolderId = {}; std::string m_currentFolderId = {};
std::string m_hoveredAssetItemId = {}; std::string m_hoveredAssetItemId = {};

View File

@@ -20,6 +20,7 @@ inline Widgets::UIEditorTreeViewMetrics BuildProductTreeViewMetrics() {
metrics.disclosureLabelGap = 2.0f; metrics.disclosureLabelGap = 2.0f;
metrics.iconExtent = 18.0f; metrics.iconExtent = 18.0f;
metrics.iconLabelGap = 2.0f; metrics.iconLabelGap = 2.0f;
metrics.iconInsetY = -1.0f;
metrics.labelInsetY = 0.0f; metrics.labelInsetY = 0.0f;
metrics.cornerRounding = 0.0f; metrics.cornerRounding = 0.0f;
metrics.borderThickness = 0.0f; metrics.borderThickness = 0.0f;

View File

@@ -43,6 +43,7 @@ struct UIEditorTreeViewMetrics {
float disclosureLabelGap = 6.0f; float disclosureLabelGap = 6.0f;
float iconExtent = 18.0f; float iconExtent = 18.0f;
float iconLabelGap = 2.0f; float iconLabelGap = 2.0f;
float iconInsetY = 0.0f;
float labelInsetY = 6.0f; float labelInsetY = 6.0f;
float cornerRounding = 6.0f; float cornerRounding = 6.0f;
float borderThickness = 1.0f; float borderThickness = 1.0f;

View File

@@ -27,9 +27,12 @@ float ResolveTreeViewRowHeight(
return item.desiredHeight > 0.0f ? item.desiredHeight : metrics.rowHeight; return item.desiredHeight > 0.0f ? item.desiredHeight : metrics.rowHeight;
} }
float ResolveTextTop(const ::XCEngine::UI::UIRect& rect, float fontSize) { float ResolveTextTop(
const ::XCEngine::UI::UIRect& rect,
float fontSize,
float insetY) {
const float lineHeight = fontSize * 1.6f; const float lineHeight = fontSize * 1.6f;
return rect.y + std::floor((rect.height - lineHeight) * 0.5f); return rect.y + std::floor((rect.height - lineHeight) * 0.5f) + insetY;
} }
void AppendDisclosureArrow( void AppendDisclosureArrow(
@@ -217,7 +220,7 @@ UIEditorTreeViewLayout BuildUIEditorTreeViewLayout(
const float contentStartX = disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap; const float contentStartX = disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap;
const ::XCEngine::UI::UIRect iconRect( const ::XCEngine::UI::UIRect iconRect(
hasLeadingIcon ? contentStartX : 0.0f, hasLeadingIcon ? contentStartX : 0.0f,
rowRect.y + (rowRect.height - iconExtent) * 0.5f, rowRect.y + (rowRect.height - iconExtent) * 0.5f + metrics.iconInsetY,
hasLeadingIcon ? iconExtent : 0.0f, hasLeadingIcon ? iconExtent : 0.0f,
hasLeadingIcon ? iconExtent : 0.0f); hasLeadingIcon ? iconExtent : 0.0f);
const float labelStartX = const float labelStartX =
@@ -322,7 +325,7 @@ void AppendUIEditorTreeViewForeground(
drawList.AddText( drawList.AddText(
::XCEngine::UI::UIPoint( ::XCEngine::UI::UIPoint(
layout.labelRects[visibleOffset].x, layout.labelRects[visibleOffset].x,
ResolveTextTop(layout.labelRects[visibleOffset], kTreeFontSize)), ResolveTextTop(layout.labelRects[visibleOffset], kTreeFontSize, metrics.labelInsetY)),
item.label, item.label,
palette.textColor, palette.textColor,
kTreeFontSize); kTreeFontSize);