Add editor tree view widget contract
This commit is contained in:
@@ -24,6 +24,7 @@ add_library(XCUIEditorLib STATIC
|
||||
src/Core/UIEditorShellCompose.cpp
|
||||
src/Core/UIEditorShellInteraction.cpp
|
||||
src/Core/UIEditorShortcutManager.cpp
|
||||
src/Core/UIEditorTreeViewInteraction.cpp
|
||||
src/Core/UIEditorViewportInputBridge.cpp
|
||||
src/Core/UIEditorViewportShell.cpp
|
||||
src/Core/UIEditorWorkspaceCompose.cpp
|
||||
@@ -39,6 +40,7 @@ add_library(XCUIEditorLib STATIC
|
||||
src/Widgets/UIEditorPanelFrame.cpp
|
||||
src/Widgets/UIEditorStatusBar.cpp
|
||||
src/Widgets/UIEditorTabStrip.cpp
|
||||
src/Widgets/UIEditorTreeView.cpp
|
||||
src/Widgets/UIEditorViewportSlot.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorTreeView.h>
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorTreeViewInteractionState {
|
||||
Widgets::UIEditorTreeViewState treeViewState = {};
|
||||
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||
bool hasPointerPosition = false;
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewInteractionResult {
|
||||
bool consumed = false;
|
||||
bool selectionChanged = false;
|
||||
bool expansionChanged = false;
|
||||
bool secondaryClicked = false;
|
||||
Widgets::UIEditorTreeViewHitTarget hitTarget = {};
|
||||
std::string selectedItemId = {};
|
||||
std::string toggledItemId = {};
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewInteractionFrame {
|
||||
Widgets::UIEditorTreeViewLayout layout = {};
|
||||
UIEditorTreeViewInteractionResult result = {};
|
||||
};
|
||||
|
||||
UIEditorTreeViewInteractionFrame UpdateUIEditorTreeViewInteraction(
|
||||
UIEditorTreeViewInteractionState& state,
|
||||
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||||
::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorTreeViewMetrics& metrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
145
new_editor/include/XCEditor/Widgets/UIEditorTreeView.h
Normal file
145
new_editor/include/XCEditor/Widgets/UIEditorTreeView.h
Normal file
@@ -0,0 +1,145 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorTreeViewInvalidIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
enum class UIEditorTreeViewHitTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
Row,
|
||||
Disclosure
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewItem {
|
||||
std::string itemId = {};
|
||||
std::string label = {};
|
||||
std::uint32_t depth = 0u;
|
||||
bool forceLeaf = false;
|
||||
float desiredHeight = 0.0f;
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewState {
|
||||
std::string hoveredItemId = {};
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewMetrics {
|
||||
float rowHeight = 28.0f;
|
||||
float rowGap = 2.0f;
|
||||
float horizontalPadding = 8.0f;
|
||||
float indentWidth = 18.0f;
|
||||
float disclosureExtent = 12.0f;
|
||||
float disclosureLabelGap = 6.0f;
|
||||
float labelInsetY = 6.0f;
|
||||
float cornerRounding = 6.0f;
|
||||
float borderThickness = 1.0f;
|
||||
float focusedBorderThickness = 2.0f;
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewPalette {
|
||||
::XCEngine::UI::UIColor surfaceColor =
|
||||
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||
::XCEngine::UI::UIColor rowHoverColor =
|
||||
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
::XCEngine::UI::UIColor rowSelectedColor =
|
||||
::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
::XCEngine::UI::UIColor rowSelectedFocusedColor =
|
||||
::XCEngine::UI::UIColor(0.40f, 0.40f, 0.40f, 1.0f);
|
||||
::XCEngine::UI::UIColor disclosureColor =
|
||||
::XCEngine::UI::UIColor(0.74f, 0.74f, 0.74f, 1.0f);
|
||||
::XCEngine::UI::UIColor textColor =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewLayout {
|
||||
::XCEngine::UI::UIRect bounds = {};
|
||||
std::vector<std::size_t> visibleItemIndices = {};
|
||||
std::vector<::XCEngine::UI::UIRect> rowRects = {};
|
||||
std::vector<::XCEngine::UI::UIRect> disclosureRects = {};
|
||||
std::vector<::XCEngine::UI::UIRect> labelRects = {};
|
||||
std::vector<bool> itemHasChildren = {};
|
||||
std::vector<bool> itemExpanded = {};
|
||||
};
|
||||
|
||||
struct UIEditorTreeViewHitTarget {
|
||||
UIEditorTreeViewHitTargetKind kind = UIEditorTreeViewHitTargetKind::None;
|
||||
std::size_t visibleIndex = UIEditorTreeViewInvalidIndex;
|
||||
std::size_t itemIndex = UIEditorTreeViewInvalidIndex;
|
||||
};
|
||||
|
||||
bool IsUIEditorTreeViewPointInside(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
const ::XCEngine::UI::UIPoint& point);
|
||||
|
||||
bool DoesUIEditorTreeViewItemHaveChildren(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
std::size_t itemIndex);
|
||||
|
||||
std::size_t FindUIEditorTreeViewItemIndex(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
std::string_view itemId);
|
||||
|
||||
std::size_t FindUIEditorTreeViewParentItemIndex(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
std::size_t itemIndex);
|
||||
|
||||
std::vector<std::size_t> CollectUIEditorTreeViewVisibleItemIndices(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel);
|
||||
|
||||
std::size_t FindUIEditorTreeViewFirstVisibleChildItemIndex(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
std::size_t itemIndex);
|
||||
|
||||
UIEditorTreeViewLayout BuildUIEditorTreeViewLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
const UIEditorTreeViewMetrics& metrics = {});
|
||||
|
||||
UIEditorTreeViewHitTarget HitTestUIEditorTreeView(
|
||||
const UIEditorTreeViewLayout& layout,
|
||||
const ::XCEngine::UI::UIPoint& point);
|
||||
|
||||
void AppendUIEditorTreeViewBackground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorTreeViewLayout& layout,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||||
const UIEditorTreeViewState& state,
|
||||
const UIEditorTreeViewPalette& palette = {},
|
||||
const UIEditorTreeViewMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorTreeViewForeground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorTreeViewLayout& layout,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const UIEditorTreeViewPalette& palette = {},
|
||||
const UIEditorTreeViewMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorTreeView(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
const UIEditorTreeViewState& state,
|
||||
const UIEditorTreeViewPalette& palette = {},
|
||||
const UIEditorTreeViewMetrics& metrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
192
new_editor/src/Core/UIEditorTreeViewInteraction.cpp
Normal file
192
new_editor/src/Core/UIEditorTreeViewInteraction.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
#include <XCEditor/Core/UIEditorTreeViewInteraction.h>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using Widgets::BuildUIEditorTreeViewLayout;
|
||||
using Widgets::DoesUIEditorTreeViewItemHaveChildren;
|
||||
using Widgets::HitTestUIEditorTreeView;
|
||||
using Widgets::IsUIEditorTreeViewPointInside;
|
||||
using Widgets::UIEditorTreeViewHitTarget;
|
||||
using Widgets::UIEditorTreeViewHitTargetKind;
|
||||
|
||||
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SyncHoverTarget(
|
||||
UIEditorTreeViewInteractionState& state,
|
||||
const Widgets::UIEditorTreeViewLayout& layout,
|
||||
const std::vector<Widgets::UIEditorTreeViewItem>& items) {
|
||||
state.treeViewState.hoveredItemId.clear();
|
||||
if (!state.hasPointerPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorTreeViewHitTarget hitTarget =
|
||||
HitTestUIEditorTreeView(layout, state.pointerPosition);
|
||||
if (hitTarget.itemIndex < items.size()) {
|
||||
state.treeViewState.hoveredItemId = items[hitTarget.itemIndex].itemId;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorTreeViewInteractionFrame UpdateUIEditorTreeViewInteraction(
|
||||
UIEditorTreeViewInteractionState& state,
|
||||
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||||
::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorTreeViewMetrics& metrics) {
|
||||
Widgets::UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(bounds, items, expansionModel, metrics);
|
||||
SyncHoverTarget(state, layout, items);
|
||||
|
||||
UIEditorTreeViewInteractionResult interactionResult = {};
|
||||
for (const UIInputEvent& event : inputEvents) {
|
||||
if (ShouldUsePointerPosition(event)) {
|
||||
state.pointerPosition = event.position;
|
||||
state.hasPointerPosition = true;
|
||||
} else if (event.type == UIInputEventType::PointerLeave) {
|
||||
state.hasPointerPosition = false;
|
||||
}
|
||||
|
||||
UIEditorTreeViewInteractionResult eventResult = {};
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusGained:
|
||||
state.treeViewState.focused = true;
|
||||
break;
|
||||
|
||||
case UIInputEventType::FocusLost:
|
||||
state.treeViewState.focused = false;
|
||||
state.hasPointerPosition = false;
|
||||
state.treeViewState.hoveredItemId.clear();
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerLeave:
|
||||
state.treeViewState.hoveredItemId.clear();
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonDown: {
|
||||
const UIEditorTreeViewHitTarget hitTarget =
|
||||
state.hasPointerPosition
|
||||
? HitTestUIEditorTreeView(layout, state.pointerPosition)
|
||||
: UIEditorTreeViewHitTarget {};
|
||||
eventResult.hitTarget = hitTarget;
|
||||
if ((event.pointerButton == UIPointerButton::Left ||
|
||||
event.pointerButton == UIPointerButton::Right) &&
|
||||
hitTarget.kind != UIEditorTreeViewHitTargetKind::None) {
|
||||
state.treeViewState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
} else if (event.pointerButton == UIPointerButton::Left &&
|
||||
(!state.hasPointerPosition ||
|
||||
!IsUIEditorTreeViewPointInside(layout.bounds, state.pointerPosition))) {
|
||||
state.treeViewState.focused = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UIInputEventType::PointerButtonUp: {
|
||||
const UIEditorTreeViewHitTarget hitTarget =
|
||||
state.hasPointerPosition
|
||||
? HitTestUIEditorTreeView(layout, state.pointerPosition)
|
||||
: UIEditorTreeViewHitTarget {};
|
||||
eventResult.hitTarget = hitTarget;
|
||||
|
||||
const bool insideTree =
|
||||
state.hasPointerPosition &&
|
||||
IsUIEditorTreeViewPointInside(layout.bounds, state.pointerPosition);
|
||||
|
||||
if (hitTarget.itemIndex >= items.size()) {
|
||||
if (event.pointerButton == UIPointerButton::Left && insideTree) {
|
||||
eventResult.consumed = true;
|
||||
state.treeViewState.focused = true;
|
||||
} else if (event.pointerButton == UIPointerButton::Left) {
|
||||
state.treeViewState.focused = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorTreeViewItem& item = items[hitTarget.itemIndex];
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
if (hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure &&
|
||||
DoesUIEditorTreeViewItemHaveChildren(items, hitTarget.itemIndex)) {
|
||||
eventResult.expansionChanged =
|
||||
expansionModel.ToggleExpanded(item.itemId);
|
||||
eventResult.toggledItemId = item.itemId;
|
||||
eventResult.consumed = true;
|
||||
state.treeViewState.focused = true;
|
||||
} else if (hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
|
||||
eventResult.selectionChanged =
|
||||
selectionModel.SetSelection(item.itemId);
|
||||
eventResult.selectedItemId = item.itemId;
|
||||
eventResult.consumed = true;
|
||||
state.treeViewState.focused = true;
|
||||
}
|
||||
} else if (event.pointerButton == UIPointerButton::Right &&
|
||||
(hitTarget.kind == UIEditorTreeViewHitTargetKind::Row ||
|
||||
hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) {
|
||||
eventResult.selectionChanged =
|
||||
selectionModel.SetSelection(item.itemId);
|
||||
eventResult.selectedItemId = item.itemId;
|
||||
eventResult.secondaryClicked = true;
|
||||
eventResult.consumed = true;
|
||||
state.treeViewState.focused = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
layout = BuildUIEditorTreeViewLayout(bounds, items, expansionModel, metrics);
|
||||
SyncHoverTarget(state, layout, items);
|
||||
if (eventResult.hitTarget.kind == UIEditorTreeViewHitTargetKind::None &&
|
||||
state.hasPointerPosition) {
|
||||
eventResult.hitTarget = HitTestUIEditorTreeView(layout, state.pointerPosition);
|
||||
}
|
||||
|
||||
if (eventResult.consumed ||
|
||||
eventResult.selectionChanged ||
|
||||
eventResult.expansionChanged ||
|
||||
eventResult.secondaryClicked ||
|
||||
eventResult.hitTarget.kind != UIEditorTreeViewHitTargetKind::None ||
|
||||
!eventResult.selectedItemId.empty() ||
|
||||
!eventResult.toggledItemId.empty()) {
|
||||
interactionResult = std::move(eventResult);
|
||||
}
|
||||
}
|
||||
|
||||
layout = BuildUIEditorTreeViewLayout(bounds, items, expansionModel, metrics);
|
||||
SyncHoverTarget(state, layout, items);
|
||||
if (interactionResult.hitTarget.kind == UIEditorTreeViewHitTargetKind::None &&
|
||||
state.hasPointerPosition) {
|
||||
interactionResult.hitTarget = HitTestUIEditorTreeView(layout, state.pointerPosition);
|
||||
}
|
||||
|
||||
return {
|
||||
std::move(layout),
|
||||
std::move(interactionResult)
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
314
new_editor/src/Widgets/UIEditorTreeView.cpp
Normal file
314
new_editor/src/Widgets/UIEditorTreeView.cpp
Normal file
@@ -0,0 +1,314 @@
|
||||
#include <XCEditor/Widgets/UIEditorTreeView.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
std::vector<std::size_t> BuildItemOffsets(std::size_t count) {
|
||||
std::vector<std::size_t> offsets(count);
|
||||
std::iota(offsets.begin(), offsets.end(), 0u);
|
||||
return offsets;
|
||||
}
|
||||
|
||||
float ResolveTreeViewRowHeight(
|
||||
const UIEditorTreeViewItem& item,
|
||||
const UIEditorTreeViewMetrics& metrics) {
|
||||
return item.desiredHeight > 0.0f ? item.desiredHeight : metrics.rowHeight;
|
||||
}
|
||||
|
||||
::XCEngine::UI::UIPoint ResolveDisclosureGlyphPosition(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
float insetY) {
|
||||
return ::XCEngine::UI::UIPoint(rect.x + 2.0f, rect.y + insetY - 1.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsUIEditorTreeViewPointInside(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
const ::XCEngine::UI::UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool DoesUIEditorTreeViewItemHaveChildren(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
std::size_t itemIndex) {
|
||||
if (itemIndex >= items.size() || items[itemIndex].forceLeaf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
|
||||
return ::XCEngine::UI::Widgets::UIFlatHierarchyHasChildren(
|
||||
itemOffsets,
|
||||
itemIndex,
|
||||
[&](std::size_t offset) {
|
||||
return items[offset].depth;
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t FindUIEditorTreeViewItemIndex(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
std::string_view itemId) {
|
||||
for (std::size_t itemIndex = 0u; itemIndex < items.size(); ++itemIndex) {
|
||||
if (items[itemIndex].itemId == itemId) {
|
||||
return itemIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return UIEditorTreeViewInvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t FindUIEditorTreeViewParentItemIndex(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
std::size_t itemIndex) {
|
||||
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
|
||||
const std::size_t parentOffset = ::XCEngine::UI::Widgets::UIFlatHierarchyFindParentOffset(
|
||||
itemOffsets,
|
||||
itemIndex,
|
||||
[&](std::size_t offset) {
|
||||
return items[offset].depth;
|
||||
});
|
||||
return parentOffset != ::XCEngine::UI::Widgets::kInvalidUIFlatHierarchyItemOffset
|
||||
? parentOffset
|
||||
: UIEditorTreeViewInvalidIndex;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> CollectUIEditorTreeViewVisibleItemIndices(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel) {
|
||||
std::vector<std::size_t> visibleItemIndices = {};
|
||||
if (items.empty()) {
|
||||
return visibleItemIndices;
|
||||
}
|
||||
|
||||
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
|
||||
for (std::size_t itemOffset = 0u; itemOffset < items.size(); ++itemOffset) {
|
||||
const bool visible = ::XCEngine::UI::Widgets::UIFlatHierarchyIsVisible(
|
||||
itemOffsets,
|
||||
itemOffset,
|
||||
[&](std::size_t offset) {
|
||||
return items[offset].depth;
|
||||
},
|
||||
[&](std::size_t offset) {
|
||||
return expansionModel.IsExpanded(items[offset].itemId);
|
||||
});
|
||||
if (visible) {
|
||||
visibleItemIndices.push_back(itemOffset);
|
||||
}
|
||||
}
|
||||
|
||||
return visibleItemIndices;
|
||||
}
|
||||
|
||||
std::size_t FindUIEditorTreeViewFirstVisibleChildItemIndex(
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
std::size_t itemIndex) {
|
||||
if (itemIndex >= items.size()) {
|
||||
return UIEditorTreeViewInvalidIndex;
|
||||
}
|
||||
|
||||
const std::vector<std::size_t> itemOffsets = BuildItemOffsets(items.size());
|
||||
const std::size_t childOffset =
|
||||
::XCEngine::UI::Widgets::UIFlatHierarchyFindFirstVisibleChildOffset(
|
||||
itemOffsets,
|
||||
itemIndex,
|
||||
[&](std::size_t offset) {
|
||||
return items[offset].depth;
|
||||
},
|
||||
[&](std::size_t offset) {
|
||||
return ::XCEngine::UI::Widgets::UIFlatHierarchyIsVisible(
|
||||
itemOffsets,
|
||||
offset,
|
||||
[&](std::size_t visibleOffset) {
|
||||
return items[visibleOffset].depth;
|
||||
},
|
||||
[&](std::size_t visibleOffset) {
|
||||
return expansionModel.IsExpanded(items[visibleOffset].itemId);
|
||||
});
|
||||
});
|
||||
return childOffset != ::XCEngine::UI::Widgets::kInvalidUIFlatHierarchyItemOffset
|
||||
? childOffset
|
||||
: UIEditorTreeViewInvalidIndex;
|
||||
}
|
||||
|
||||
UIEditorTreeViewLayout BuildUIEditorTreeViewLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
const UIEditorTreeViewMetrics& metrics) {
|
||||
UIEditorTreeViewLayout layout = {};
|
||||
layout.bounds = ::XCEngine::UI::UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.visibleItemIndices = CollectUIEditorTreeViewVisibleItemIndices(items, expansionModel);
|
||||
layout.rowRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.disclosureRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.labelRects.reserve(layout.visibleItemIndices.size());
|
||||
layout.itemHasChildren.reserve(layout.visibleItemIndices.size());
|
||||
layout.itemExpanded.reserve(layout.visibleItemIndices.size());
|
||||
|
||||
float rowY = layout.bounds.y;
|
||||
for (std::size_t visibleOffset = 0u;
|
||||
visibleOffset < layout.visibleItemIndices.size();
|
||||
++visibleOffset) {
|
||||
const std::size_t itemIndex = layout.visibleItemIndices[visibleOffset];
|
||||
const UIEditorTreeViewItem& item = items[itemIndex];
|
||||
const float rowHeight = ResolveTreeViewRowHeight(item, metrics);
|
||||
const bool hasChildren = DoesUIEditorTreeViewItemHaveChildren(items, itemIndex);
|
||||
const bool expanded = hasChildren && expansionModel.IsExpanded(item.itemId);
|
||||
|
||||
const ::XCEngine::UI::UIRect rowRect(
|
||||
layout.bounds.x,
|
||||
rowY,
|
||||
layout.bounds.width,
|
||||
rowHeight);
|
||||
const float contentX =
|
||||
rowRect.x + metrics.horizontalPadding + static_cast<float>(item.depth) * metrics.indentWidth;
|
||||
const ::XCEngine::UI::UIRect disclosureRect(
|
||||
contentX,
|
||||
rowRect.y + (rowRect.height - metrics.disclosureExtent) * 0.5f,
|
||||
metrics.disclosureExtent,
|
||||
metrics.disclosureExtent);
|
||||
const ::XCEngine::UI::UIRect labelRect(
|
||||
disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap,
|
||||
rowRect.y,
|
||||
(rowRect.x + rowRect.width) -
|
||||
(disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap) -
|
||||
metrics.horizontalPadding,
|
||||
rowRect.height);
|
||||
|
||||
layout.rowRects.push_back(rowRect);
|
||||
layout.disclosureRects.push_back(disclosureRect);
|
||||
layout.labelRects.push_back(labelRect);
|
||||
layout.itemHasChildren.push_back(hasChildren);
|
||||
layout.itemExpanded.push_back(expanded);
|
||||
|
||||
rowY += rowHeight + metrics.rowGap;
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorTreeViewHitTarget HitTestUIEditorTreeView(
|
||||
const UIEditorTreeViewLayout& layout,
|
||||
const ::XCEngine::UI::UIPoint& point) {
|
||||
for (std::size_t visibleOffset = 0u; visibleOffset < layout.rowRects.size(); ++visibleOffset) {
|
||||
if (!IsUIEditorTreeViewPointInside(layout.rowRects[visibleOffset], point)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorTreeViewHitTarget target = {};
|
||||
target.visibleIndex = visibleOffset;
|
||||
target.itemIndex = layout.visibleItemIndices[visibleOffset];
|
||||
target.kind =
|
||||
layout.itemHasChildren[visibleOffset] &&
|
||||
IsUIEditorTreeViewPointInside(layout.disclosureRects[visibleOffset], point)
|
||||
? UIEditorTreeViewHitTargetKind::Disclosure
|
||||
: UIEditorTreeViewHitTargetKind::Row;
|
||||
return target;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void AppendUIEditorTreeViewBackground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorTreeViewLayout& layout,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||||
const UIEditorTreeViewState& state,
|
||||
const UIEditorTreeViewPalette& palette,
|
||||
const UIEditorTreeViewMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
for (std::size_t visibleOffset = 0u; visibleOffset < layout.rowRects.size(); ++visibleOffset) {
|
||||
const UIEditorTreeViewItem& item = items[layout.visibleItemIndices[visibleOffset]];
|
||||
const bool selected = selectionModel.IsSelected(item.itemId);
|
||||
const bool hovered = state.hoveredItemId == item.itemId;
|
||||
if (!selected && !hovered) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ::XCEngine::UI::UIColor rowColor =
|
||||
selected
|
||||
? (state.focused ? palette.rowSelectedFocusedColor : palette.rowSelectedColor)
|
||||
: palette.rowHoverColor;
|
||||
drawList.AddFilledRect(layout.rowRects[visibleOffset], rowColor, metrics.cornerRounding);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorTreeViewForeground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorTreeViewLayout& layout,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const UIEditorTreeViewPalette& palette,
|
||||
const UIEditorTreeViewMetrics& metrics) {
|
||||
drawList.PushClipRect(layout.bounds);
|
||||
for (std::size_t visibleOffset = 0u; visibleOffset < layout.rowRects.size(); ++visibleOffset) {
|
||||
const UIEditorTreeViewItem& item = items[layout.visibleItemIndices[visibleOffset]];
|
||||
if (layout.itemHasChildren[visibleOffset]) {
|
||||
drawList.AddText(
|
||||
ResolveDisclosureGlyphPosition(
|
||||
layout.disclosureRects[visibleOffset],
|
||||
metrics.labelInsetY),
|
||||
layout.itemExpanded[visibleOffset] ? "v" : ">",
|
||||
palette.disclosureColor,
|
||||
12.0f);
|
||||
}
|
||||
|
||||
drawList.PushClipRect(layout.labelRects[visibleOffset]);
|
||||
drawList.AddText(
|
||||
::XCEngine::UI::UIPoint(
|
||||
layout.labelRects[visibleOffset].x,
|
||||
layout.labelRects[visibleOffset].y + metrics.labelInsetY),
|
||||
item.label,
|
||||
palette.textColor,
|
||||
12.0f);
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
|
||||
void AppendUIEditorTreeView(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorTreeViewItem>& items,
|
||||
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||||
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||||
const UIEditorTreeViewState& state,
|
||||
const UIEditorTreeViewPalette& palette,
|
||||
const UIEditorTreeViewMetrics& metrics) {
|
||||
const UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(bounds, items, expansionModel, metrics);
|
||||
AppendUIEditorTreeViewBackground(
|
||||
drawList,
|
||||
layout,
|
||||
items,
|
||||
selectionModel,
|
||||
state,
|
||||
palette,
|
||||
metrics);
|
||||
AppendUIEditorTreeViewForeground(drawList, layout, items, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
Reference in New Issue
Block a user