Split camera frame render-graph stage recording helpers
This commit is contained in:
575
new_editor/app/Features/Hierarchy/HierarchyPanel.cpp
Normal file
575
new_editor/app/Features/Hierarchy/HierarchyPanel.cpp
Normal file
@@ -0,0 +1,575 @@
|
||||
#include "ProductHierarchyPanel.h"
|
||||
|
||||
#include "Icons/ProductBuiltInIcons.h"
|
||||
#include "Panels/ProductTreeViewStyle.h"
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using Widgets::AppendUIEditorTreeViewBackground;
|
||||
using Widgets::AppendUIEditorTreeViewForeground;
|
||||
using Widgets::DoesUIEditorTreeViewItemHaveChildren;
|
||||
using Widgets::HitTestUIEditorTreeView;
|
||||
using Widgets::IsUIEditorTreeViewPointInside;
|
||||
using Widgets::UIEditorTreeViewHitTarget;
|
||||
using Widgets::UIEditorTreeViewHitTargetKind;
|
||||
using Widgets::UIEditorTreeViewInvalidIndex;
|
||||
|
||||
constexpr std::string_view kHierarchyPanelId = "hierarchy";
|
||||
constexpr float kDragThreshold = 4.0f;
|
||||
constexpr UIColor kDragPreviewColor(0.92f, 0.92f, 0.92f, 0.42f);
|
||||
|
||||
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 ProductBuiltInIcons* icons) {
|
||||
return icons != nullptr
|
||||
? icons->Resolve(ProductBuiltInIconKind::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 = nullptr) {
|
||||
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
|
||||
|
||||
void ProductHierarchyPanel::Initialize() {
|
||||
m_model = ProductHierarchyModel::BuildDefault();
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
|
||||
m_icons = icons;
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::ResetInteractionState() {
|
||||
m_treeInteractionState = {};
|
||||
m_treeFrame = {};
|
||||
m_dragState = {};
|
||||
ResetTransientState();
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostPanelState* ProductHierarchyPanel::FindMountedHierarchyPanel(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame) const {
|
||||
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
|
||||
if (panelState.panelId == kHierarchyPanelId && panelState.mounted) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::ResetTransientState() {
|
||||
m_frameEvents.clear();
|
||||
m_dragState.requestPointerCapture = false;
|
||||
m_dragState.requestPointerRelease = false;
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::RebuildItems() {
|
||||
const auto icon = ResolveGameObjectIcon(m_icons);
|
||||
const std::string previousSelection =
|
||||
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
|
||||
|
||||
m_treeItems = m_model.BuildTreeItems(icon);
|
||||
m_expansion.Expand("player");
|
||||
m_expansion.Expand("environment");
|
||||
m_expansion.Expand("props");
|
||||
|
||||
if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) {
|
||||
m_selection.SetSelection(previousSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_treeItems.empty()) {
|
||||
m_selection.SetSelection(m_treeItems.front().itemId);
|
||||
}
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::EmitSelectionEvent() {
|
||||
if (!m_selection.HasSelection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ProductHierarchyNode* node = m_model.FindNode(m_selection.GetSelectedId());
|
||||
if (node == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
Event event = {};
|
||||
event.kind = EventKind::SelectionChanged;
|
||||
event.itemId = node->nodeId;
|
||||
event.label = node->label;
|
||||
m_frameEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::EmitReparentEvent(
|
||||
EventKind kind,
|
||||
std::string itemId,
|
||||
std::string targetItemId) {
|
||||
Event event = {};
|
||||
event.kind = kind;
|
||||
event.itemId = std::move(itemId);
|
||||
event.targetItemId = std::move(targetItemId);
|
||||
if (const ProductHierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
|
||||
event.label = node->label;
|
||||
}
|
||||
m_frameEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
std::vector<UIInputEvent> ProductHierarchyPanel::BuildInteractionInputEvents(
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const UIRect& bounds,
|
||||
bool allowInteraction,
|
||||
bool panelActive) const {
|
||||
const std::vector<UIInputEvent> rawEvents = FilterHierarchyInputEvents(
|
||||
bounds,
|
||||
inputEvents,
|
||||
allowInteraction,
|
||||
panelActive,
|
||||
HasActivePointerCapture());
|
||||
|
||||
struct DragPreviewState {
|
||||
std::string armedItemId = {};
|
||||
UIPoint pressPosition = {};
|
||||
bool armed = false;
|
||||
bool dragging = false;
|
||||
};
|
||||
|
||||
const Widgets::UIEditorTreeViewLayout layout =
|
||||
m_treeFrame.layout.bounds.width > 0.0f ? m_treeFrame.layout : Widgets::BuildUIEditorTreeViewLayout(
|
||||
bounds,
|
||||
m_treeItems,
|
||||
m_expansion,
|
||||
BuildProductTreeViewMetrics());
|
||||
DragPreviewState preview = {};
|
||||
preview.armed = m_dragState.armed;
|
||||
preview.armedItemId = m_dragState.armedItemId;
|
||||
preview.pressPosition = m_dragState.pressPosition;
|
||||
preview.dragging = m_dragState.dragging;
|
||||
|
||||
std::vector<UIInputEvent> filteredEvents = {};
|
||||
filteredEvents.reserve(rawEvents.size());
|
||||
for (const UIInputEvent& event : rawEvents) {
|
||||
bool suppress = false;
|
||||
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
UIEditorTreeViewHitTarget hitTarget = {};
|
||||
const Widgets::UIEditorTreeViewItem* hitItem =
|
||||
ResolveHitItem(layout, m_treeItems, event.position, &hitTarget);
|
||||
if (hitItem != nullptr &&
|
||||
hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
|
||||
preview.armed = true;
|
||||
preview.armedItemId = hitItem->itemId;
|
||||
preview.pressPosition = event.position;
|
||||
} else {
|
||||
preview.armed = false;
|
||||
preview.armedItemId.clear();
|
||||
}
|
||||
}
|
||||
if (preview.dragging) {
|
||||
suppress = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
if (preview.dragging) {
|
||||
suppress = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (preview.armed &&
|
||||
ComputeSquaredDistance(event.position, preview.pressPosition) >=
|
||||
kDragThreshold * kDragThreshold) {
|
||||
preview.dragging = true;
|
||||
suppress = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
if (preview.dragging) {
|
||||
suppress = true;
|
||||
preview.dragging = false;
|
||||
}
|
||||
preview.armed = false;
|
||||
preview.armedItemId.clear();
|
||||
} else if (preview.dragging) {
|
||||
suppress = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerLeave:
|
||||
if (preview.dragging) {
|
||||
suppress = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::FocusLost:
|
||||
preview.armed = false;
|
||||
preview.dragging = false;
|
||||
preview.armedItemId.clear();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!suppress) {
|
||||
filteredEvents.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::ProcessDragAndFrameEvents(
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const UIRect& bounds,
|
||||
bool allowInteraction,
|
||||
bool panelActive) {
|
||||
const std::vector<UIInputEvent> filteredEvents = FilterHierarchyInputEvents(
|
||||
bounds,
|
||||
inputEvents,
|
||||
allowInteraction,
|
||||
panelActive,
|
||||
HasActivePointerCapture());
|
||||
|
||||
if (m_treeFrame.result.selectionChanged) {
|
||||
EmitSelectionEvent();
|
||||
}
|
||||
if (m_treeFrame.result.renameRequested &&
|
||||
!m_treeFrame.result.renameItemId.empty()) {
|
||||
Event event = {};
|
||||
event.kind = EventKind::RenameRequested;
|
||||
event.itemId = m_treeFrame.result.renameItemId;
|
||||
if (const ProductHierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
|
||||
event.label = node->label;
|
||||
}
|
||||
m_frameEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
for (const UIInputEvent& event : filteredEvents) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
UIEditorTreeViewHitTarget hitTarget = {};
|
||||
const Widgets::UIEditorTreeViewItem* hitItem =
|
||||
ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget);
|
||||
if (hitItem != nullptr &&
|
||||
hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
|
||||
m_dragState.armed = true;
|
||||
m_dragState.armedItemId = hitItem->itemId;
|
||||
m_dragState.pressPosition = event.position;
|
||||
} else {
|
||||
m_dragState.armed = false;
|
||||
m_dragState.armedItemId.clear();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
if (m_dragState.armed && !m_dragState.dragging &&
|
||||
ComputeSquaredDistance(event.position, m_dragState.pressPosition) >=
|
||||
kDragThreshold * kDragThreshold) {
|
||||
m_dragState.dragging = !m_dragState.armedItemId.empty();
|
||||
m_dragState.draggedItemId = m_dragState.armedItemId;
|
||||
m_dragState.dropTargetItemId.clear();
|
||||
m_dragState.dropToRoot = false;
|
||||
m_dragState.validDropTarget = false;
|
||||
if (m_dragState.dragging) {
|
||||
m_dragState.requestPointerCapture = true;
|
||||
if (!m_selection.IsSelected(m_dragState.draggedItemId)) {
|
||||
m_selection.SetSelection(m_dragState.draggedItemId);
|
||||
EmitSelectionEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_dragState.dragging) {
|
||||
UIEditorTreeViewHitTarget hitTarget = {};
|
||||
const Widgets::UIEditorTreeViewItem* hitItem =
|
||||
ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget);
|
||||
|
||||
m_dragState.dropTargetItemId.clear();
|
||||
m_dragState.dropToRoot = false;
|
||||
m_dragState.validDropTarget = false;
|
||||
|
||||
if (hitItem != nullptr &&
|
||||
(hitTarget.kind == UIEditorTreeViewHitTargetKind::Row ||
|
||||
hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) {
|
||||
m_dragState.dropTargetItemId = hitItem->itemId;
|
||||
m_dragState.validDropTarget =
|
||||
m_model.CanReparent(
|
||||
m_dragState.draggedItemId,
|
||||
m_dragState.dropTargetItemId);
|
||||
} else if (ContainsPoint(bounds, event.position)) {
|
||||
m_dragState.dropToRoot = true;
|
||||
m_dragState.validDropTarget =
|
||||
m_model.GetParentId(m_dragState.draggedItemId).has_value();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
if (event.pointerButton != UIPointerButton::Left) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_dragState.dragging) {
|
||||
if (m_dragState.validDropTarget) {
|
||||
const std::string draggedItemId = m_dragState.draggedItemId;
|
||||
const std::string dropTargetItemId = m_dragState.dropTargetItemId;
|
||||
const bool changed =
|
||||
m_dragState.dropToRoot
|
||||
? m_model.MoveToRoot(draggedItemId)
|
||||
: m_model.Reparent(draggedItemId, dropTargetItemId);
|
||||
if (changed) {
|
||||
RebuildItems();
|
||||
EmitReparentEvent(
|
||||
m_dragState.dropToRoot ? EventKind::MovedToRoot : EventKind::Reparented,
|
||||
draggedItemId,
|
||||
dropTargetItemId);
|
||||
}
|
||||
}
|
||||
|
||||
m_dragState.armed = false;
|
||||
m_dragState.dragging = false;
|
||||
m_dragState.armedItemId.clear();
|
||||
m_dragState.draggedItemId.clear();
|
||||
m_dragState.dropTargetItemId.clear();
|
||||
m_dragState.dropToRoot = false;
|
||||
m_dragState.validDropTarget = false;
|
||||
m_dragState.requestPointerRelease = true;
|
||||
} else {
|
||||
m_dragState.armed = false;
|
||||
m_dragState.armedItemId.clear();
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::FocusLost:
|
||||
if (m_dragState.dragging) {
|
||||
m_dragState.requestPointerRelease = true;
|
||||
}
|
||||
m_dragState = {};
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::Update(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
bool allowInteraction,
|
||||
bool panelActive) {
|
||||
ResetTransientState();
|
||||
|
||||
const UIEditorPanelContentHostPanelState* panelState =
|
||||
FindMountedHierarchyPanel(contentHostFrame);
|
||||
if (panelState == nullptr) {
|
||||
m_visible = false;
|
||||
m_treeFrame = {};
|
||||
m_dragState = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_treeItems.empty()) {
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
m_visible = true;
|
||||
const std::vector<UIInputEvent> interactionEvents =
|
||||
BuildInteractionInputEvents(
|
||||
inputEvents,
|
||||
panelState->bounds,
|
||||
allowInteraction,
|
||||
panelActive);
|
||||
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
||||
m_treeInteractionState,
|
||||
m_selection,
|
||||
m_expansion,
|
||||
panelState->bounds,
|
||||
m_treeItems,
|
||||
interactionEvents,
|
||||
BuildProductTreeViewMetrics());
|
||||
ProcessDragAndFrameEvents(
|
||||
inputEvents,
|
||||
panelState->bounds,
|
||||
allowInteraction,
|
||||
panelActive);
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
if (!m_visible || m_treeFrame.layout.bounds.width <= 0.0f || m_treeFrame.layout.bounds.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorTreeViewPalette palette = BuildProductTreeViewPalette();
|
||||
const Widgets::UIEditorTreeViewMetrics metrics = BuildProductTreeViewMetrics();
|
||||
AppendUIEditorTreeViewBackground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_selection,
|
||||
m_treeInteractionState.treeViewState,
|
||||
palette,
|
||||
metrics);
|
||||
AppendUIEditorTreeViewForeground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
palette,
|
||||
metrics);
|
||||
|
||||
if (!m_dragState.dragging || !m_dragState.validDropTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_dragState.dropToRoot) {
|
||||
drawList.AddRectOutline(
|
||||
m_treeFrame.layout.bounds,
|
||||
kDragPreviewColor,
|
||||
1.0f,
|
||||
0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t visibleIndex = FindVisibleIndexForItemId(
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_dragState.dropTargetItemId);
|
||||
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
|
||||
visibleIndex >= m_treeFrame.layout.rowRects.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawList.AddRectOutline(
|
||||
m_treeFrame.layout.rowRects[visibleIndex],
|
||||
kDragPreviewColor,
|
||||
1.0f,
|
||||
0.0f);
|
||||
}
|
||||
|
||||
bool ProductHierarchyPanel::WantsHostPointerCapture() const {
|
||||
return m_dragState.requestPointerCapture;
|
||||
}
|
||||
|
||||
bool ProductHierarchyPanel::WantsHostPointerRelease() const {
|
||||
return m_dragState.requestPointerRelease;
|
||||
}
|
||||
|
||||
bool ProductHierarchyPanel::HasActivePointerCapture() const {
|
||||
return m_dragState.dragging;
|
||||
}
|
||||
|
||||
const std::vector<ProductHierarchyPanel::Event>& ProductHierarchyPanel::GetFrameEvents() const {
|
||||
return m_frameEvents;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
Reference in New Issue
Block a user