refactor(new_editor): unify panel input and rename primitives

This commit is contained in:
2026-04-19 03:23:16 +08:00
parent c59cd83c38
commit 78bcd2e9ca
17 changed files with 652 additions and 338 deletions

View File

@@ -67,6 +67,7 @@ set(XCUI_EDITOR_COLLECTION_SOURCES
src/Collections/UIEditorListViewInteraction.cpp
src/Collections/UIEditorScrollView.cpp
src/Collections/UIEditorScrollViewInteraction.cpp
src/Collections/UIEditorTreePanelBehavior.cpp
src/Collections/UIEditorTabStrip.cpp
src/Collections/UIEditorTabStripInteraction.cpp
src/Collections/UIEditorTreeView.cpp

View File

@@ -2,6 +2,7 @@
#include "Scene/EditorSceneRuntime.h"
#include <XCEditor/Collections/UIEditorTreePanelBehavior.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
@@ -321,28 +322,15 @@ void HierarchyPanel::SyncTreeFocusState(
UIRect HierarchyPanel::BuildRenameBounds(
std::string_view itemId,
const Widgets::UIEditorTreeViewLayout& layout) const {
if (itemId.empty()) {
return {};
}
const std::size_t visibleIndex =
FindVisibleIndexForItemId(layout, m_treeItems, itemId);
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
visibleIndex >= layout.rowRects.size() ||
visibleIndex >= layout.labelRects.size()) {
return {};
}
const Widgets::UIEditorTextFieldMetrics hostedMetrics =
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics());
const UIRect& rowRect = layout.rowRects[visibleIndex];
const UIRect& labelRect = layout.labelRects[visibleIndex];
const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX);
const float right = rowRect.x + rowRect.width - 8.0f;
const float width = (std::max)(120.0f, right - x);
return UIRect(x, rowRect.y, width, rowRect.height);
return BuildUIEditorTreePanelInlineRenameBounds(
layout,
m_treeItems,
itemId,
hostedMetrics);
}
bool HierarchyPanel::WantsHostPointerCapture() const {
@@ -464,12 +452,14 @@ void HierarchyPanel::ProcessDragAndFrameEvents(
const UIRect& bounds,
bool allowInteraction,
bool panelActive) {
const std::vector<UIInputEvent> filteredEvents = FilterHierarchyInputEvents(
const std::vector<UIInputEvent> filteredEvents = FilterUIEditorTreePanelInputEvents(
bounds,
inputEvents,
allowInteraction,
panelActive,
HasActivePointerCapture());
UIEditorTreePanelInputFilterOptions{
.allowInteraction = allowInteraction,
.panelActive = panelActive,
.captureActive = HasActivePointerCapture()
});
if (m_treeFrame.result.selectionChanged) {
SyncSceneRuntimeSelectionFromTree();
@@ -565,12 +555,14 @@ void HierarchyPanel::Update(
}
m_visible = true;
const std::vector<UIInputEvent> filteredEvents = FilterHierarchyInputEvents(
const std::vector<UIInputEvent> filteredEvents = FilterUIEditorTreePanelInputEvents(
panelState->bounds,
inputEvents,
allowInteraction,
panelActive,
HasActivePointerCapture());
UIEditorTreePanelInputFilterOptions{
.allowInteraction = allowInteraction,
.panelActive = panelActive,
.captureActive = HasActivePointerCapture()
});
SyncTreeFocusState(filteredEvents);
const Widgets::UIEditorTreeViewMetrics treeMetrics =
@@ -591,11 +583,12 @@ void HierarchyPanel::Update(
}
const std::vector<UIInputEvent> interactionEvents =
TreeDrag::BuildInteractionInputEvents(
BuildUIEditorTreePanelInteractionInputEvents(
m_dragState,
layout,
m_treeItems,
filteredEvents,
false,
kDragThreshold);
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_treeInteractionState,
@@ -655,18 +648,10 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics()));
Widgets::AppendUIEditorTextFieldBackground(
AppendUIEditorInlineRenameSession(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
textFieldPalette,
textFieldMetrics);
Widgets::AppendUIEditorTextFieldForeground(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
m_renameFrame,
m_renameState,
textFieldPalette,
textFieldMetrics);
}
@@ -684,11 +669,11 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
return;
}
const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId(
const std::size_t visibleIndex = FindUIEditorTreePanelVisibleItemIndex(
m_treeFrame.layout,
m_treeItems,
m_dragState.dropTargetItemId);
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
if (visibleIndex == Widgets::UIEditorTreeViewInvalidIndex ||
visibleIndex >= m_treeFrame.layout.rowRects.size()) {
return;
}

View File

@@ -1,106 +1,11 @@
#include "HierarchyPanelInternal.h"
#include <cmath>
namespace XCEngine::UI::Editor::App::HierarchyPanelInternal {
using ::XCEngine::UI::UIInputEventType;
using Widgets::HitTestUIEditorTreeView;
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 ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs) {
const float dx = lhs.x - rhs.x;
const float dy = lhs.y - rhs.y;
return dx * dx + dy * dy;
}
::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(const BuiltInIcons* icons) {
return icons != nullptr
? icons->Resolve(BuiltInIconKind::GameObject)
: ::XCEngine::UI::UITextureHandle {};
}
std::vector<UIInputEvent> FilterHierarchyInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive,
bool captureActive) {
if (!allowInteraction && !captureActive) {
return {};
}
std::vector<UIInputEvent> 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;
}
const Widgets::UIEditorTreeViewItem* ResolveHitItem(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
const UIPoint& point,
UIEditorTreeViewHitTarget* hitTargetOutput) {
const UIEditorTreeViewHitTarget hitTarget = HitTestUIEditorTreeView(layout, point);
if (hitTargetOutput != nullptr) {
*hitTargetOutput = hitTarget;
}
if (hitTarget.itemIndex >= items.size()) {
return nullptr;
}
return &items[hitTarget.itemIndex];
}
std::size_t FindVisibleIndexForItemId(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
std::string_view itemId) {
for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size(); ++visibleIndex) {
const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex];
if (itemIndex < items.size() && items[itemIndex].itemId == itemId) {
return visibleIndex;
}
}
return UIEditorTreeViewInvalidIndex;
}
} // namespace XCEngine::UI::Editor::App::HierarchyPanelInternal

View File

@@ -5,42 +5,15 @@
#include "Rendering/Assets/BuiltInIcons.h"
#include "Composition/EditorPanelIds.h"
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <algorithm>
#include <string_view>
namespace XCEngine::UI::Editor::App::HierarchyPanelInternal {
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
using Widgets::UIEditorTreeViewHitTarget;
using Widgets::UIEditorTreeViewHitTargetKind;
using Widgets::UIEditorTreeViewInvalidIndex;
inline constexpr float kDragThreshold = 4.0f;
inline constexpr UIColor kDragPreviewColor(0.92f, 0.92f, 0.92f, 0.42f);
bool ContainsPoint(const UIRect& rect, const UIPoint& point);
float ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs);
::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(const BuiltInIcons* icons);
std::vector<UIInputEvent> FilterHierarchyInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive,
bool captureActive);
const Widgets::UIEditorTreeViewItem* ResolveHitItem(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
const UIPoint& point,
UIEditorTreeViewHitTarget* hitTargetOutput = nullptr);
std::size_t FindVisibleIndexForItemId(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
std::string_view itemId);
} // namespace XCEngine::UI::Editor::App::HierarchyPanelInternal

View File

@@ -2,6 +2,7 @@
#include "Composition/EditorPanelIds.h"
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
@@ -18,7 +19,6 @@ namespace {
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIDrawList;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
@@ -32,63 +32,11 @@ constexpr UIColor kTitleColor(0.930f, 0.930f, 0.930f, 1.0f);
constexpr UIColor kSubtitleColor(0.660f, 0.660f, 0.660f, 1.0f);
constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f);
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 ResolveTextTop(float rectY, float rectHeight, float fontSize) {
const float lineHeight = fontSize * 1.6f;
return rectY + std::floor((rectHeight - lineHeight) * 0.5f);
}
std::vector<UIInputEvent> FilterInspectorInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive) {
if (!allowInteraction && !panelActive) {
return {};
}
std::vector<UIInputEvent> 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 (allowInteraction && 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) {
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;
}
UIEditorHostCommandEvaluationResult BuildEvaluationResult(
bool executable,
std::string message) {
@@ -317,11 +265,16 @@ void InspectorPanel::Update(
}
const std::vector<UIInputEvent> filteredEvents =
FilterInspectorInputEvents(
FilterUIEditorPanelInputEvents(
m_bounds,
inputEvents,
allowInteraction,
panelActive);
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = allowInteraction,
.allowPointerWhileCaptured = false,
.allowKeyboardInput = panelActive,
.allowFocusEvents = panelActive,
.includePointerLeave = allowInteraction || panelActive
});
m_gridFrame = UpdateUIEditorPropertyGridInteraction(
m_interactionState,
m_fieldSelection,

View File

@@ -2,7 +2,9 @@
#include "Project/EditorProjectRuntime.h"
#include <XCEditor/Collections/UIEditorTreePanelBehavior.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include "Internal/StringEncoding.h"
@@ -358,22 +360,11 @@ UIRect ProjectPanel::BuildRenameBounds(
ResolveUIEditorTextFieldMetrics());
if (surface == RenameSurface::Tree) {
const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId(
return BuildUIEditorTreePanelInlineRenameBounds(
m_treeFrame.layout,
GetBrowserModel().GetTreeItems(),
itemId);
if (visibleIndex == Widgets::UIEditorTreeViewInvalidIndex ||
visibleIndex >= m_treeFrame.layout.rowRects.size() ||
visibleIndex >= m_treeFrame.layout.labelRects.size()) {
return {};
}
const UIRect& rowRect = m_treeFrame.layout.rowRects[visibleIndex];
const UIRect& labelRect = m_treeFrame.layout.labelRects[visibleIndex];
const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX);
const float right = rowRect.x + rowRect.width - 8.0f;
const float width = (std::max)(120.0f, right - x);
return UIRect(x, rowRect.y, width, rowRect.height);
itemId,
hostedMetrics);
}
if (surface == RenameSurface::Grid) {
@@ -962,14 +953,16 @@ std::vector<UIInputEvent> ProjectPanel::BuildTreeInteractionInputEvents(
bool allowInteraction,
bool panelActive) const {
const std::vector<UIInputEvent> rawEvents =
FilterProjectPanelInputEvents(
FilterUIEditorPanelInputEvents(
bounds,
inputEvents,
allowInteraction,
panelActive,
HasActivePointerCapture());
const std::vector<UIInputEvent> treeRawEvents =
FilterTreeInputEvents(rawEvents, m_splitterDragging || m_assetDragState.dragging);
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = allowInteraction,
.allowPointerWhileCaptured = HasActivePointerCapture(),
.allowKeyboardInput = panelActive,
.allowFocusEvents = panelActive || HasActivePointerCapture(),
.includePointerLeave = allowInteraction || HasActivePointerCapture()
});
const Widgets::UIEditorTreeViewLayout layout =
m_treeFrame.layout.bounds.width > 0.0f
@@ -979,11 +972,12 @@ std::vector<UIInputEvent> ProjectPanel::BuildTreeInteractionInputEvents(
GetBrowserModel().GetTreeItems(),
m_folderExpansion,
ResolveUIEditorTreeViewMetrics());
return TreeDrag::BuildInteractionInputEvents(
return BuildUIEditorTreePanelInteractionInputEvents(
m_treeDragState,
layout,
GetBrowserModel().GetTreeItems(),
treeRawEvents);
rawEvents,
m_splitterDragging || m_assetDragState.dragging);
}
UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateAssetCommand(
@@ -1360,12 +1354,16 @@ void ProjectPanel::Update(
m_visible = true;
SyncAssetSelectionFromRuntime();
const std::vector<UIInputEvent> filteredEvents =
FilterProjectPanelInputEvents(
FilterUIEditorPanelInputEvents(
panelState->bounds,
inputEvents,
allowInteraction,
panelActive,
HasActivePointerCapture());
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = allowInteraction,
.allowPointerWhileCaptured = HasActivePointerCapture(),
.allowKeyboardInput = panelActive,
.allowFocusEvents = panelActive || HasActivePointerCapture(),
.includePointerLeave = allowInteraction || HasActivePointerCapture()
});
m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width);
m_layout = BuildLayout(panelState->bounds);
@@ -1482,7 +1480,9 @@ void ProjectPanel::Update(
m_treeDragState,
m_treeFrame.layout,
GetBrowserModel().GetTreeItems(),
FilterTreeInputEvents(filteredEvents, m_splitterDragging || m_assetDragState.dragging),
FilterUIEditorTreePanelPointerInputEvents(
filteredEvents,
m_splitterDragging || m_assetDragState.dragging),
m_layout.treeRect,
treeDragCallbacks);
if (treeDragResult.dropCommitted) {
@@ -2041,7 +2041,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const {
1.0f,
0.0f);
} else {
const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId(
const std::size_t visibleIndex = FindUIEditorTreePanelVisibleItemIndex(
m_treeFrame.layout,
GetBrowserModel().GetTreeItems(),
m_treeDragState.dropTargetItemId);
@@ -2059,7 +2059,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const {
if (m_assetDragState.dragging &&
m_assetDragState.validDropTarget &&
m_assetDropTargetSurface == DropTargetSurface::Tree) {
const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId(
const std::size_t visibleIndex = FindUIEditorTreePanelVisibleItemIndex(
m_treeFrame.layout,
GetBrowserModel().GetTreeItems(),
m_assetDragState.dropTargetItemId);
@@ -2157,18 +2157,10 @@ void ProjectPanel::Append(UIDrawList& drawList) const {
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics()));
Widgets::AppendUIEditorTextFieldBackground(
AppendUIEditorInlineRenameSession(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
textFieldPalette,
textFieldMetrics);
Widgets::AppendUIEditorTextFieldForeground(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
m_renameFrame,
m_renameState,
textFieldPalette,
textFieldMetrics);
}

View File

@@ -39,77 +39,6 @@ float MeasureTextWidth(
return static_cast<float>(text.size()) * fontSize * 0.56f;
}
std::vector<UIInputEvent> FilterProjectPanelInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive,
bool captureActive) {
if (!allowInteraction && !captureActive) {
return {};
}
std::vector<UIInputEvent> 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<UIInputEvent> FilterTreeInputEvents(
const std::vector<UIInputEvent>& inputEvents,
bool suppressPointerInput) {
if (!suppressPointerInput) {
return inputEvents;
}
std::vector<UIInputEvent> 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 BuiltInIcons* icons) {
return icons != nullptr
? icons->Resolve(BuiltInIconKind::Folder)

View File

@@ -64,15 +64,6 @@ float MeasureTextWidth(
const UIEditorTextMeasurer* textMeasurer,
std::string_view text,
float fontSize);
std::vector<UIInputEvent> FilterProjectPanelInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive,
bool captureActive);
std::vector<UIInputEvent> FilterTreeInputEvents(
const std::vector<UIInputEvent>& inputEvents,
bool suppressPointerInput);
::XCEngine::UI::UITextureHandle ResolveFolderIcon(const BuiltInIcons* icons);
float ClampNavigationWidth(float value, float totalWidth);
void AppendTilePreview(

View File

@@ -45,6 +45,13 @@ Widgets::UIEditorTextFieldMetrics BuildUIEditorInlineRenameTextFieldMetrics(
const ::XCEngine::UI::UIRect& bounds,
const Widgets::UIEditorTextFieldMetrics& metrics = {});
void AppendUIEditorInlineRenameSession(
::XCEngine::UI::UIDrawList& drawList,
const UIEditorInlineRenameSessionFrame& frame,
const UIEditorInlineRenameSessionState& state,
const Widgets::UIEditorTextFieldPalette& palette = {},
const Widgets::UIEditorTextFieldMetrics& metrics = {});
UIEditorInlineRenameSessionFrame UpdateUIEditorInlineRenameSession(
UIEditorInlineRenameSessionState& state,
const UIEditorInlineRenameSessionRequest& request,

View File

@@ -0,0 +1,50 @@
#pragma once
#include <XCEditor/Collections/UIEditorTreeDragDrop.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <XCEngine/UI/Types.h>
#include <cstddef>
#include <string_view>
#include <vector>
namespace XCEngine::UI::Editor {
struct UIEditorTreePanelInputFilterOptions {
bool allowInteraction = false;
bool panelActive = false;
bool captureActive = false;
};
std::vector<::XCEngine::UI::UIInputEvent> FilterUIEditorTreePanelInputEvents(
const ::XCEngine::UI::UIRect& bounds,
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
const UIEditorTreePanelInputFilterOptions& options);
std::vector<::XCEngine::UI::UIInputEvent> FilterUIEditorTreePanelPointerInputEvents(
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
bool suppressPointerInput);
std::vector<::XCEngine::UI::UIInputEvent> BuildUIEditorTreePanelInteractionInputEvents(
const Collections::TreeDragDrop::State& dragState,
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
bool suppressPointerInput = false,
float dragThreshold = Collections::TreeDragDrop::kDefaultDragThreshold);
std::size_t FindUIEditorTreePanelVisibleItemIndex(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
std::string_view itemId);
::XCEngine::UI::UIRect BuildUIEditorTreePanelInlineRenameBounds(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
std::string_view itemId,
const Widgets::UIEditorTextFieldMetrics& hostedMetrics,
float minWidth = 120.0f,
float trailingPadding = 8.0f);
} // namespace XCEngine::UI::Editor

View File

@@ -0,0 +1,82 @@
#pragma once
#include <XCEngine/UI/Types.h>
#include <vector>
namespace XCEngine::UI::Editor {
struct UIEditorPanelInputFilterOptions {
bool allowPointerInBounds = false;
bool allowPointerWhileCaptured = false;
bool allowKeyboardInput = false;
bool allowFocusEvents = false;
bool includePointerLeave = false;
};
inline std::vector<::XCEngine::UI::UIInputEvent> FilterUIEditorPanelInputEvents(
const ::XCEngine::UI::UIRect& bounds,
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
const UIEditorPanelInputFilterOptions& options) {
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
if (!options.allowPointerInBounds &&
!options.allowPointerWhileCaptured &&
!options.allowKeyboardInput &&
!options.allowFocusEvents &&
!options.includePointerLeave) {
return {};
}
auto containsPoint = [&bounds](const ::XCEngine::UI::UIPoint& point) {
return point.x >= bounds.x &&
point.x <= bounds.x + bounds.width &&
point.y >= bounds.y &&
point.y <= bounds.y + bounds.height;
};
std::vector<UIInputEvent> 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 (options.allowPointerWhileCaptured ||
(options.allowPointerInBounds && containsPoint(event.position))) {
filteredEvents.push_back(event);
}
break;
case UIInputEventType::PointerLeave:
if (options.includePointerLeave) {
filteredEvents.push_back(event);
}
break;
case UIInputEventType::FocusGained:
case UIInputEventType::FocusLost:
if (options.allowFocusEvents) {
filteredEvents.push_back(event);
}
break;
case UIInputEventType::KeyDown:
case UIInputEventType::KeyUp:
case UIInputEventType::Character:
if (options.allowKeyboardInput) {
filteredEvents.push_back(event);
}
break;
default:
break;
}
}
return filteredEvents;
}
} // namespace XCEngine::UI::Editor

View File

@@ -17,6 +17,32 @@ Widgets::UIEditorTextFieldMetrics BuildUIEditorInlineRenameTextFieldMetrics(
return resolved;
}
void AppendUIEditorInlineRenameSession(
::XCEngine::UI::UIDrawList& drawList,
const UIEditorInlineRenameSessionFrame& frame,
const UIEditorInlineRenameSessionState& state,
const Widgets::UIEditorTextFieldPalette& palette,
const Widgets::UIEditorTextFieldMetrics& metrics) {
if (!state.active) {
return;
}
Widgets::AppendUIEditorTextFieldBackground(
drawList,
frame.layout,
state.textFieldSpec,
state.textFieldInteraction.textFieldState,
palette,
metrics);
Widgets::AppendUIEditorTextFieldForeground(
drawList,
frame.layout,
state.textFieldSpec,
state.textFieldInteraction.textFieldState,
palette,
metrics);
}
namespace {
void ResetSession(UIEditorInlineRenameSessionState& state) {

View File

@@ -0,0 +1,126 @@
#include <XCEditor/Collections/UIEditorTreePanelBehavior.h>
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
#include <XCEngine/UI/Types.h>
#include <algorithm>
namespace XCEngine::UI::Editor {
namespace {
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
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;
}
} // namespace
std::vector<UIInputEvent> FilterUIEditorTreePanelInputEvents(
const UIRect& bounds,
const std::vector<UIInputEvent>& inputEvents,
const UIEditorTreePanelInputFilterOptions& options) {
return FilterUIEditorPanelInputEvents(
bounds,
inputEvents,
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = options.allowInteraction,
.allowPointerWhileCaptured = options.captureActive,
.allowKeyboardInput = options.panelActive,
.allowFocusEvents = options.panelActive || options.captureActive,
.includePointerLeave = options.allowInteraction || options.captureActive
});
}
std::vector<UIInputEvent> FilterUIEditorTreePanelPointerInputEvents(
const std::vector<UIInputEvent>& inputEvents,
bool suppressPointerInput) {
if (!suppressPointerInput) {
return inputEvents;
}
std::vector<UIInputEvent> 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;
}
std::vector<UIInputEvent> BuildUIEditorTreePanelInteractionInputEvents(
const Collections::TreeDragDrop::State& dragState,
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
const std::vector<UIInputEvent>& inputEvents,
bool suppressPointerInput,
float dragThreshold) {
return Collections::TreeDragDrop::BuildInteractionInputEvents(
dragState,
layout,
items,
FilterUIEditorTreePanelPointerInputEvents(inputEvents, suppressPointerInput),
dragThreshold);
}
std::size_t FindUIEditorTreePanelVisibleItemIndex(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
std::string_view itemId) {
for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size();
++visibleIndex) {
const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex];
if (itemIndex < items.size() && items[itemIndex].itemId == itemId) {
return visibleIndex;
}
}
return Widgets::UIEditorTreeViewInvalidIndex;
}
UIRect BuildUIEditorTreePanelInlineRenameBounds(
const Widgets::UIEditorTreeViewLayout& layout,
const std::vector<Widgets::UIEditorTreeViewItem>& items,
std::string_view itemId,
const Widgets::UIEditorTextFieldMetrics& hostedMetrics,
float minWidth,
float trailingPadding) {
if (itemId.empty()) {
return {};
}
const std::size_t visibleIndex =
FindUIEditorTreePanelVisibleItemIndex(layout, items, itemId);
if (visibleIndex == Widgets::UIEditorTreeViewInvalidIndex ||
visibleIndex >= layout.rowRects.size() ||
visibleIndex >= layout.labelRects.size()) {
return {};
}
const UIRect& rowRect = layout.rowRects[visibleIndex];
const UIRect& labelRect = layout.labelRects[visibleIndex];
const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX);
const float right = rowRect.x + rowRect.width - trailingPadding;
const float width = (std::max)(minWidth, right - x);
return UIRect(x, rowRect.y, width, rowRect.height);
}
} // namespace XCEngine::UI::Editor

View File

@@ -9,6 +9,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_menu_popup.cpp
test_ui_editor_panel_content_host.cpp
test_ui_editor_panel_host_lifecycle.cpp
test_ui_editor_panel_input_filter.cpp
test_ui_editor_property_grid.cpp
test_ui_editor_property_grid_interaction.cpp
test_ui_editor_shell_compose.cpp
@@ -47,6 +48,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_status_bar.cpp
test_ui_editor_tab_strip.cpp
test_ui_editor_tab_strip_interaction.cpp
test_ui_editor_tree_panel_behavior.cpp
test_ui_editor_tree_view.cpp
test_ui_editor_tree_view_interaction.cpp
test_ui_editor_viewport_input_bridge.cpp

View File

@@ -7,10 +7,14 @@
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::AppendUIEditorInlineRenameSession;
using XCEngine::UI::Editor::UIEditorInlineRenameSessionRequest;
using XCEngine::UI::Editor::UIEditorInlineRenameSessionFrame;
using XCEngine::UI::Editor::UIEditorInlineRenameSessionState;
using XCEngine::UI::Editor::UpdateUIEditorInlineRenameSession;
@@ -122,3 +126,19 @@ TEST(UIEditorInlineRenameSessionTest, FocusLostCommitsEditedValue) {
EXPECT_EQ(frame.result.valueAfter, "CameraX");
EXPECT_FALSE(state.active);
}
TEST(UIEditorInlineRenameSessionTest, AppendUsesActiveSessionStateToEmitTextFieldCommands) {
UIEditorInlineRenameSessionState state = {};
UIDrawList drawList("InlineRename");
const UIEditorInlineRenameSessionFrame inactiveFrame = {};
AppendUIEditorInlineRenameSession(drawList, inactiveFrame, state);
EXPECT_TRUE(drawList.Empty());
const UIEditorInlineRenameSessionFrame activeFrame =
UpdateUIEditorInlineRenameSession(state, MakeRequest(true), {});
AppendUIEditorInlineRenameSession(drawList, activeFrame, state);
ASSERT_FALSE(drawList.Empty());
EXPECT_EQ(drawList.GetCommands().front().type, UIDrawCommandType::FilledRect);
}

View File

@@ -0,0 +1,95 @@
#include <gtest/gtest.h>
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
namespace {
using XCEngine::UI::Editor::FilterUIEditorPanelInputEvents;
using XCEngine::UI::Editor::UIEditorPanelInputFilterOptions;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIPointerButton;
using ::XCEngine::UI::UIRect;
UIInputEvent MakePointerMove(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerMove;
event.position = UIPoint(x, y);
return event;
}
UIInputEvent MakePointerButtonDown(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonDown;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
return event;
}
UIInputEvent MakeKeyDown() {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
return event;
}
UIInputEvent MakeFocusLost() {
UIInputEvent event = {};
event.type = UIInputEventType::FocusLost;
return event;
}
UIInputEvent MakePointerLeave() {
UIInputEvent event = {};
event.type = UIInputEventType::PointerLeave;
return event;
}
} // namespace
TEST(UIEditorPanelInputFilterTests, FiltersPointerByBoundsAndPreservesKeyboardAndFocusChannels) {
const std::vector<UIInputEvent> filtered =
FilterUIEditorPanelInputEvents(
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
{
MakePointerMove(20.0f, 20.0f),
MakePointerButtonDown(140.0f, 20.0f),
MakeKeyDown(),
MakeFocusLost(),
MakePointerLeave()
},
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = true,
.allowPointerWhileCaptured = false,
.allowKeyboardInput = true,
.allowFocusEvents = true,
.includePointerLeave = true
});
ASSERT_EQ(filtered.size(), 4u);
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerMove);
EXPECT_EQ(filtered[1].type, UIInputEventType::KeyDown);
EXPECT_EQ(filtered[2].type, UIInputEventType::FocusLost);
EXPECT_EQ(filtered[3].type, UIInputEventType::PointerLeave);
}
TEST(UIEditorPanelInputFilterTests, CapturedPointerEventsBypassBoundsWhenEnabled) {
const std::vector<UIInputEvent> filtered =
FilterUIEditorPanelInputEvents(
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
{
MakePointerMove(140.0f, 20.0f),
MakePointerButtonDown(180.0f, 32.0f)
},
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = false,
.allowPointerWhileCaptured = true,
.allowKeyboardInput = false,
.allowFocusEvents = false,
.includePointerLeave = false
});
ASSERT_EQ(filtered.size(), 2u);
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerMove);
EXPECT_EQ(filtered[1].type, UIInputEventType::PointerButtonDown);
}

View File

@@ -0,0 +1,177 @@
#include <gtest/gtest.h>
#include <XCEditor/Collections/UIEditorTreePanelBehavior.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <algorithm>
namespace {
namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop;
namespace Widgets = XCEngine::UI::Editor::Widgets;
using XCEngine::UI::Editor::BuildUIEditorTreePanelInlineRenameBounds;
using XCEngine::UI::Editor::BuildUIEditorTreePanelInteractionInputEvents;
using XCEngine::UI::Editor::FilterUIEditorTreePanelInputEvents;
using XCEngine::UI::Editor::FilterUIEditorTreePanelPointerInputEvents;
using XCEngine::UI::Editor::FindUIEditorTreePanelVisibleItemIndex;
using XCEngine::UI::Editor::UIEditorTreePanelInputFilterOptions;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIPointerButton;
using ::XCEngine::UI::UIRect;
UIInputEvent MakePointerMove(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerMove;
event.position = UIPoint(x, y);
return event;
}
UIInputEvent MakePointerButtonDown(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonDown;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
return event;
}
UIInputEvent MakePointerButtonUp(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonUp;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
return event;
}
UIInputEvent MakeKeyDown() {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
return event;
}
UIInputEvent MakeFocusLost() {
UIInputEvent event = {};
event.type = UIInputEventType::FocusLost;
return event;
}
struct TreeFixture {
std::vector<Widgets::UIEditorTreeViewItem> items = {};
::XCEngine::UI::Widgets::UIExpansionModel expansion = {};
Widgets::UIEditorTreeViewLayout layout = {};
};
TreeFixture BuildTreeFixture() {
TreeFixture fixture = {};
fixture.items = {
Widgets::UIEditorTreeViewItem{ .itemId = "root", .label = "Root" },
Widgets::UIEditorTreeViewItem{ .itemId = "child", .label = "Child", .depth = 1u }
};
fixture.expansion.Expand("root");
fixture.layout = Widgets::BuildUIEditorTreeViewLayout(
UIRect(0.0f, 0.0f, 240.0f, 120.0f),
fixture.items,
fixture.expansion);
return fixture;
}
} // namespace
TEST(UIEditorTreePanelBehaviorTests, FilterPanelInputHonorsBoundsAndPanelActivity) {
const std::vector<UIInputEvent> filtered =
FilterUIEditorTreePanelInputEvents(
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
{
MakePointerMove(40.0f, 40.0f),
MakePointerMove(140.0f, 40.0f),
MakeKeyDown(),
MakeFocusLost()
},
UIEditorTreePanelInputFilterOptions{
.allowInteraction = true,
.panelActive = true,
.captureActive = false
});
ASSERT_EQ(filtered.size(), 3u);
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerMove);
EXPECT_EQ(filtered[1].type, UIInputEventType::KeyDown);
EXPECT_EQ(filtered[2].type, UIInputEventType::FocusLost);
}
TEST(UIEditorTreePanelBehaviorTests, FilterPointerInputSuppressesPointerOnlyEvents) {
const std::vector<UIInputEvent> filtered =
FilterUIEditorTreePanelPointerInputEvents(
{
MakePointerMove(10.0f, 10.0f),
MakePointerButtonDown(10.0f, 10.0f),
MakeKeyDown(),
MakeFocusLost()
},
true);
ASSERT_EQ(filtered.size(), 2u);
EXPECT_EQ(filtered[0].type, UIInputEventType::KeyDown);
EXPECT_EQ(filtered[1].type, UIInputEventType::FocusLost);
}
TEST(UIEditorTreePanelBehaviorTests, BuildInteractionInputEventsSuppressesDragGesturePreview) {
const TreeFixture fixture = BuildTreeFixture();
TreeDrag::State dragState = {};
ASSERT_GE(fixture.layout.rowRects.size(), 2u);
const UIRect sourceRow = fixture.layout.rowRects[1];
const UIRect targetRow = fixture.layout.rowRects[0];
const std::vector<UIInputEvent> filtered =
BuildUIEditorTreePanelInteractionInputEvents(
dragState,
fixture.layout,
fixture.items,
{
MakePointerButtonDown(
sourceRow.x + 12.0f,
sourceRow.y + sourceRow.height * 0.5f),
MakePointerMove(
sourceRow.x + 24.0f,
sourceRow.y + sourceRow.height * 0.5f),
MakePointerMove(
targetRow.x + 12.0f,
targetRow.y + targetRow.height * 0.5f),
MakePointerButtonUp(
targetRow.x + 12.0f,
targetRow.y + targetRow.height * 0.5f),
MakeFocusLost()
});
ASSERT_EQ(filtered.size(), 2u);
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerButtonDown);
EXPECT_EQ(filtered[1].type, UIInputEventType::FocusLost);
}
TEST(UIEditorTreePanelBehaviorTests, VisibleIndexAndRenameBoundsFollowCurrentLayout) {
const TreeFixture fixture = BuildTreeFixture();
const Widgets::UIEditorTextFieldMetrics hostedMetrics = {
.valueTextInsetX = 8.0f
};
const std::size_t visibleIndex =
FindUIEditorTreePanelVisibleItemIndex(fixture.layout, fixture.items, "child");
ASSERT_EQ(visibleIndex, 1u);
const UIRect bounds =
BuildUIEditorTreePanelInlineRenameBounds(
fixture.layout,
fixture.items,
"child",
hostedMetrics);
const UIRect& rowRect = fixture.layout.rowRects[visibleIndex];
const UIRect& labelRect = fixture.layout.labelRects[visibleIndex];
EXPECT_FLOAT_EQ(bounds.x, (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX));
EXPECT_FLOAT_EQ(bounds.y, rowRect.y);
EXPECT_FLOAT_EQ(bounds.height, rowRect.height);
EXPECT_FLOAT_EQ(bounds.width, (std::max)(120.0f, rowRect.x + rowRect.width - 8.0f - bounds.x));
}