2026-04-15 08:24:06 +08:00
|
|
|
#include "InspectorPanel.h"
|
2026-04-12 11:12:27 +08:00
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
#include "Composition/EditorContext.h"
|
|
|
|
|
#include "State/EditorColorPickerToolState.h"
|
2026-04-22 22:53:01 +08:00
|
|
|
#include <XCEditor/Collections/UIEditorScrollView.h>
|
2026-04-19 00:03:25 +08:00
|
|
|
#include <XCEditor/Fields/UIEditorFieldStyle.h>
|
2026-04-19 03:23:16 +08:00
|
|
|
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
|
2026-04-12 11:12:27 +08:00
|
|
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
|
|
|
|
|
#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h"
|
|
|
|
|
#include "Scene/EditorSceneRuntime.h"
|
2026-04-19 04:36:52 +08:00
|
|
|
#include "State/EditorCommandFocusService.h"
|
2026-04-19 00:03:25 +08:00
|
|
|
|
2026-04-12 11:12:27 +08:00
|
|
|
#include <algorithm>
|
2026-04-19 00:03:25 +08:00
|
|
|
#include <cmath>
|
2026-04-12 11:12:27 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine::UI::Editor::App {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
using ::XCEngine::UI::UIColor;
|
|
|
|
|
using ::XCEngine::UI::UIDrawList;
|
2026-04-19 00:03:25 +08:00
|
|
|
using ::XCEngine::UI::UIInputEvent;
|
2026-04-12 11:12:27 +08:00
|
|
|
using ::XCEngine::UI::UIPoint;
|
2026-04-22 22:07:02 +08:00
|
|
|
using ::XCEngine::UI::UIPointerButton;
|
2026-04-12 11:12:27 +08:00
|
|
|
using ::XCEngine::UI::UIRect;
|
2026-04-22 22:07:02 +08:00
|
|
|
using ::XCEngine::UI::UIInputEventType;
|
2026-04-12 11:12:27 +08:00
|
|
|
|
|
|
|
|
constexpr float kPanelPadding = 10.0f;
|
2026-04-19 00:03:25 +08:00
|
|
|
constexpr float kTitleHeight = 18.0f;
|
|
|
|
|
constexpr float kSubtitleHeight = 16.0f;
|
|
|
|
|
constexpr float kHeaderGap = 10.0f;
|
2026-04-12 11:12:27 +08:00
|
|
|
constexpr float kTitleFontSize = 13.0f;
|
|
|
|
|
constexpr float kSubtitleFontSize = 11.0f;
|
2026-04-22 22:07:02 +08:00
|
|
|
constexpr float kAddComponentButtonHeight = 24.0f;
|
|
|
|
|
constexpr float kAddComponentButtonTopGap = 10.0f;
|
|
|
|
|
constexpr float kAddComponentButtonFontSize = 12.0f;
|
2026-04-14 14:41:45 +08:00
|
|
|
constexpr UIColor kTitleColor(0.930f, 0.930f, 0.930f, 1.0f);
|
2026-04-15 08:24:06 +08:00
|
|
|
constexpr UIColor kSubtitleColor(0.660f, 0.660f, 0.660f, 1.0f);
|
2026-04-19 00:03:25 +08:00
|
|
|
constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f);
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
void OffsetRectY(UIRect& rect, float deltaY) {
|
|
|
|
|
rect.y += deltaY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ShouldOffsetPointerEventPosition(UIInputEventType type) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case UIInputEventType::PointerMove:
|
|
|
|
|
case UIInputEventType::PointerEnter:
|
|
|
|
|
case UIInputEventType::PointerLeave:
|
|
|
|
|
case UIInputEventType::PointerButtonDown:
|
|
|
|
|
case UIInputEventType::PointerButtonUp:
|
|
|
|
|
case UIInputEventType::PointerWheel:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<UIInputEvent> BuildScrolledPropertyGridInputEvents(
|
|
|
|
|
const std::vector<UIInputEvent>& inputEvents,
|
|
|
|
|
float verticalOffset) {
|
|
|
|
|
std::vector<UIInputEvent> adjustedEvents = inputEvents;
|
|
|
|
|
for (UIInputEvent& event : adjustedEvents) {
|
|
|
|
|
if (ShouldOffsetPointerEventPosition(event.type)) {
|
|
|
|
|
event.position.y += verticalOffset;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return adjustedEvents;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widgets::UIEditorPropertyGridLayout TranslatePropertyGridLayoutForScroll(
|
|
|
|
|
const Widgets::UIEditorPropertyGridLayout& layout,
|
|
|
|
|
float verticalOffset) {
|
|
|
|
|
Widgets::UIEditorPropertyGridLayout translated = layout;
|
|
|
|
|
if (verticalOffset == 0.0f) {
|
|
|
|
|
return translated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float deltaY = -verticalOffset;
|
|
|
|
|
for (UIRect& rect : translated.sectionHeaderRects) {
|
|
|
|
|
OffsetRectY(rect, deltaY);
|
|
|
|
|
}
|
|
|
|
|
for (UIRect& rect : translated.sectionDisclosureRects) {
|
|
|
|
|
OffsetRectY(rect, deltaY);
|
|
|
|
|
}
|
|
|
|
|
for (UIRect& rect : translated.sectionTitleRects) {
|
|
|
|
|
OffsetRectY(rect, deltaY);
|
|
|
|
|
}
|
|
|
|
|
for (UIRect& rect : translated.fieldRowRects) {
|
|
|
|
|
OffsetRectY(rect, deltaY);
|
|
|
|
|
}
|
|
|
|
|
for (UIRect& rect : translated.fieldLabelRects) {
|
|
|
|
|
OffsetRectY(rect, deltaY);
|
|
|
|
|
}
|
|
|
|
|
for (UIRect& rect : translated.fieldValueRects) {
|
|
|
|
|
OffsetRectY(rect, deltaY);
|
|
|
|
|
}
|
|
|
|
|
return translated;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 23:30:17 +08:00
|
|
|
float ResolvePropertyGridContentBottom(
|
2026-04-22 22:53:01 +08:00
|
|
|
const Widgets::UIEditorPropertyGridLayout& gridLayout,
|
|
|
|
|
const UIRect& contentBounds) {
|
2026-04-22 23:30:17 +08:00
|
|
|
float contentBottom = contentBounds.y;
|
2026-04-22 22:53:01 +08:00
|
|
|
if (!gridLayout.sectionHeaderRects.empty()) {
|
|
|
|
|
const UIRect& lastSectionRect = gridLayout.sectionHeaderRects.back();
|
2026-04-22 23:30:17 +08:00
|
|
|
contentBottom =
|
|
|
|
|
(std::max)(contentBottom, lastSectionRect.y + lastSectionRect.height);
|
|
|
|
|
}
|
|
|
|
|
if (!gridLayout.fieldRowRects.empty()) {
|
|
|
|
|
const UIRect& lastFieldRect = gridLayout.fieldRowRects.back();
|
|
|
|
|
contentBottom =
|
|
|
|
|
(std::max)(contentBottom, lastFieldRect.y + lastFieldRect.height);
|
2026-04-22 22:53:01 +08:00
|
|
|
}
|
2026-04-22 23:30:17 +08:00
|
|
|
|
|
|
|
|
return contentBottom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ResolveAddComponentButtonTop(
|
|
|
|
|
const Widgets::UIEditorPropertyGridLayout& gridLayout,
|
|
|
|
|
const UIRect& contentBounds) {
|
|
|
|
|
return ResolvePropertyGridContentBottom(gridLayout, contentBounds) +
|
|
|
|
|
kAddComponentButtonTopGap;
|
2026-04-22 22:53:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widgets::UIEditorScrollViewPalette BuildInspectorScrollPalette() {
|
|
|
|
|
Widgets::UIEditorScrollViewPalette palette =
|
|
|
|
|
::XCEngine::UI::Editor::ResolveUIEditorScrollViewPalette();
|
|
|
|
|
palette.surfaceColor.a = 0.0f;
|
|
|
|
|
palette.borderColor.a = 0.0f;
|
|
|
|
|
palette.focusedBorderColor.a = 0.0f;
|
|
|
|
|
return palette;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
float ResolveInspectorHorizontalPadding(
|
|
|
|
|
const InspectorPresentationModel& presentation) {
|
|
|
|
|
return presentation.showHeader ? kPanelPadding : 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ResolveInspectorTopPadding(
|
|
|
|
|
const InspectorPresentationModel& presentation) {
|
|
|
|
|
return presentation.showHeader ? kPanelPadding : 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ResolveInspectorBottomPadding(
|
|
|
|
|
const InspectorPresentationModel& presentation) {
|
|
|
|
|
return presentation.showHeader ? kPanelPadding : 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 04:36:52 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
bool AreColorsEqual(const UIColor& lhs, const UIColor& rhs) {
|
|
|
|
|
return lhs.r == rhs.r &&
|
|
|
|
|
lhs.g == rhs.g &&
|
|
|
|
|
lhs.b == rhs.b &&
|
|
|
|
|
lhs.a == rhs.a;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 11:12:27 +08:00
|
|
|
float ResolveTextTop(float rectY, float rectHeight, float fontSize) {
|
|
|
|
|
const float lineHeight = fontSize * 1.6f;
|
|
|
|
|
return rectY + std::floor((rectHeight - lineHeight) * 0.5f);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:07:02 +08:00
|
|
|
float ResolveCenteredTextX(
|
|
|
|
|
const UIRect& rect,
|
|
|
|
|
std::string_view text,
|
|
|
|
|
float fontSize) {
|
|
|
|
|
const float estimatedTextWidth =
|
|
|
|
|
static_cast<float>(text.size()) * fontSize * 0.56f;
|
|
|
|
|
return rect.x + std::floor((std::max)(rect.width - estimatedTextWidth, 0.0f) * 0.5f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIColor ResolveAddComponentButtonFillColor(bool hovered, bool pressed) {
|
|
|
|
|
const Widgets::UIEditorPropertyGridPalette& palette =
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette();
|
|
|
|
|
if (pressed) {
|
|
|
|
|
return palette.valueBoxEditingColor;
|
|
|
|
|
}
|
|
|
|
|
if (hovered) {
|
|
|
|
|
return palette.valueBoxHoverColor;
|
|
|
|
|
}
|
|
|
|
|
return palette.valueBoxColor;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
UIEditorHostCommandEvaluationResult BuildEvaluationResult(
|
|
|
|
|
bool executable,
|
|
|
|
|
std::string message) {
|
|
|
|
|
UIEditorHostCommandEvaluationResult result = {};
|
|
|
|
|
result.executable = executable;
|
|
|
|
|
result.message = std::move(message);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorHostCommandDispatchResult BuildDispatchResult(
|
|
|
|
|
bool commandExecuted,
|
|
|
|
|
std::string message) {
|
|
|
|
|
UIEditorHostCommandDispatchResult result = {};
|
|
|
|
|
result.commandExecuted = commandExecuted;
|
|
|
|
|
result.message = std::move(message);
|
|
|
|
|
return result;
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-04-19 04:36:52 +08:00
|
|
|
void InspectorPanel::SetCommandFocusService(
|
|
|
|
|
EditorCommandFocusService* commandFocusService) {
|
|
|
|
|
m_commandFocusService = commandFocusService;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
void InspectorPanel::ResetPanelState() {
|
|
|
|
|
m_visible = false;
|
|
|
|
|
m_bounds = {};
|
2026-04-21 00:57:14 +08:00
|
|
|
m_sceneRuntime = nullptr;
|
2026-04-19 00:03:25 +08:00
|
|
|
m_subject = {};
|
|
|
|
|
m_subjectKey.clear();
|
|
|
|
|
m_presentation = {};
|
|
|
|
|
m_gridFrame = {};
|
|
|
|
|
m_knownSectionIds.clear();
|
2026-04-21 00:57:14 +08:00
|
|
|
m_lastSceneSelectionStamp = 0u;
|
|
|
|
|
m_lastProjectSelectionStamp = 0u;
|
|
|
|
|
m_lastSceneInspectorRevision = 0u;
|
2026-04-19 00:03:25 +08:00
|
|
|
ResetInteractionState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::ResetInteractionState() {
|
|
|
|
|
m_fieldSelection.ClearSelection();
|
|
|
|
|
m_sectionExpansion.Clear();
|
|
|
|
|
m_propertyEditModel = {};
|
2026-04-22 22:53:01 +08:00
|
|
|
m_scrollInteractionState = {};
|
|
|
|
|
m_scrollFrame = {};
|
|
|
|
|
m_scrollVerticalOffset = 0.0f;
|
2026-04-19 00:03:25 +08:00
|
|
|
m_interactionState = {};
|
|
|
|
|
m_gridFrame = {};
|
2026-04-21 00:57:14 +08:00
|
|
|
m_lastAppliedColorPickerRevision = 0u;
|
2026-04-22 22:07:02 +08:00
|
|
|
ResetAddComponentButtonState();
|
2026-04-19 00:03:25 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
bool InspectorPanel::HasActivePointerCapture() const {
|
|
|
|
|
return HasActiveUIEditorScrollViewPointerCapture(m_scrollInteractionState);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
void InspectorPanel::SyncExpansionState(bool subjectChanged) {
|
|
|
|
|
if (subjectChanged) {
|
|
|
|
|
m_sectionExpansion.Clear();
|
|
|
|
|
m_knownSectionIds.clear();
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
std::unordered_set<std::string> currentSectionIds = {};
|
|
|
|
|
for (const Widgets::UIEditorPropertyGridSection& section :
|
|
|
|
|
m_presentation.sections) {
|
|
|
|
|
currentSectionIds.insert(section.sectionId);
|
|
|
|
|
if (m_knownSectionIds.find(section.sectionId) == m_knownSectionIds.end()) {
|
|
|
|
|
m_sectionExpansion.Expand(section.sectionId);
|
2026-04-18 00:45:14 +08:00
|
|
|
}
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
2026-04-18 00:45:14 +08:00
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
m_knownSectionIds = std::move(currentSectionIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::SyncSelectionState() {
|
|
|
|
|
if (m_fieldSelection.HasSelection() &&
|
|
|
|
|
!Widgets::FindUIEditorPropertyGridFieldLocation(
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_fieldSelection.GetSelectedId())
|
|
|
|
|
.IsValid()) {
|
|
|
|
|
m_fieldSelection.ClearSelection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_propertyEditModel.HasActiveEdit() &&
|
|
|
|
|
!Widgets::FindUIEditorPropertyGridFieldLocation(
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_propertyEditModel.GetActiveFieldId())
|
|
|
|
|
.IsValid()) {
|
|
|
|
|
m_propertyEditModel.CancelEdit();
|
2026-04-21 00:57:14 +08:00
|
|
|
m_interactionState.editableFieldSession = {};
|
2026-04-19 00:03:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!m_interactionState.propertyGridState.popupFieldId.empty() &&
|
|
|
|
|
!Widgets::FindUIEditorPropertyGridFieldLocation(
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_interactionState.propertyGridState.popupFieldId)
|
|
|
|
|
.IsValid()) {
|
|
|
|
|
m_interactionState.propertyGridState.popupFieldId.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string InspectorPanel::BuildSubjectKey() const {
|
|
|
|
|
switch (m_subject.kind) {
|
|
|
|
|
case InspectorSubjectKind::ProjectAsset:
|
|
|
|
|
return std::string("project:") + m_subject.projectAsset.selection.itemId;
|
|
|
|
|
case InspectorSubjectKind::SceneObject:
|
|
|
|
|
return std::string("scene:") + m_subject.sceneObject.itemId;
|
|
|
|
|
case InspectorSubjectKind::None:
|
|
|
|
|
default:
|
|
|
|
|
return "none";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
float InspectorPanel::ResolveHeaderHeight() const {
|
|
|
|
|
if (!m_presentation.showHeader) {
|
|
|
|
|
return 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return kTitleHeight + kSubtitleHeight + kHeaderGap;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:07:02 +08:00
|
|
|
bool InspectorPanel::ShouldShowAddComponentButton() const {
|
|
|
|
|
return m_subject.kind == InspectorSubjectKind::SceneObject;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
UIRect InspectorPanel::BuildScrollViewportBounds() const {
|
2026-04-21 00:57:14 +08:00
|
|
|
const float horizontalPadding =
|
|
|
|
|
ResolveInspectorHorizontalPadding(m_presentation);
|
|
|
|
|
const float topPadding = ResolveInspectorTopPadding(m_presentation);
|
|
|
|
|
const float bottomPadding = ResolveInspectorBottomPadding(m_presentation);
|
|
|
|
|
const float x = m_bounds.x + horizontalPadding;
|
|
|
|
|
const float width =
|
|
|
|
|
(std::max)(m_bounds.width - horizontalPadding * 2.0f, 0.0f);
|
|
|
|
|
const float y = m_bounds.y + topPadding + ResolveHeaderHeight();
|
2026-04-19 00:03:25 +08:00
|
|
|
const float height =
|
2026-04-21 00:57:14 +08:00
|
|
|
(std::max)(m_bounds.y + m_bounds.height - bottomPadding - y, 0.0f);
|
2026-04-19 00:03:25 +08:00
|
|
|
return UIRect(x, y, width, height);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
UIRect InspectorPanel::BuildAddComponentButtonRect(
|
|
|
|
|
const Widgets::UIEditorPropertyGridLayout& gridLayout,
|
|
|
|
|
const UIRect& contentBounds) const {
|
2026-04-22 22:07:02 +08:00
|
|
|
if (!ShouldShowAddComponentButton()) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return UIRect(
|
2026-04-22 22:53:01 +08:00
|
|
|
contentBounds.x,
|
|
|
|
|
ResolveAddComponentButtonTop(gridLayout, contentBounds),
|
|
|
|
|
contentBounds.width,
|
2026-04-22 22:07:02 +08:00
|
|
|
kAddComponentButtonHeight);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
float InspectorPanel::MeasureScrollableContentHeight(
|
|
|
|
|
const UIRect& contentBounds) const {
|
|
|
|
|
float contentBottom = contentBounds.y;
|
|
|
|
|
Widgets::UIEditorPropertyGridLayout layout = {};
|
|
|
|
|
if (!m_presentation.sections.empty()) {
|
|
|
|
|
layout = Widgets::BuildUIEditorPropertyGridLayout(
|
|
|
|
|
contentBounds,
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_sectionExpansion,
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
2026-04-22 23:30:17 +08:00
|
|
|
contentBottom = ResolvePropertyGridContentBottom(layout, contentBounds);
|
2026-04-22 22:53:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ShouldShowAddComponentButton()) {
|
|
|
|
|
const UIRect buttonRect = BuildAddComponentButtonRect(layout, contentBounds);
|
|
|
|
|
contentBottom = (std::max)(contentBottom, buttonRect.y + buttonRect.height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (std::max)(contentBottom - contentBounds.y, 0.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::RebuildScrollableLayout() {
|
|
|
|
|
m_scrollFrame = {};
|
|
|
|
|
m_gridFrame.layout = {};
|
|
|
|
|
|
|
|
|
|
const UIRect viewportBounds = BuildScrollViewportBounds();
|
|
|
|
|
if (viewportBounds.width <= 0.0f || viewportBounds.height <= 0.0f) {
|
|
|
|
|
m_scrollVerticalOffset = 0.0f;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Widgets::UIEditorScrollViewMetrics& scrollMetrics =
|
|
|
|
|
::XCEngine::UI::Editor::ResolveUIEditorScrollViewMetrics();
|
|
|
|
|
const float contentHeight = MeasureScrollableContentHeight(viewportBounds);
|
|
|
|
|
m_scrollVerticalOffset = Widgets::ClampUIEditorScrollViewOffset(
|
|
|
|
|
viewportBounds,
|
|
|
|
|
contentHeight,
|
|
|
|
|
m_scrollVerticalOffset,
|
|
|
|
|
scrollMetrics);
|
|
|
|
|
m_scrollFrame.layout = Widgets::BuildUIEditorScrollViewLayout(
|
|
|
|
|
viewportBounds,
|
|
|
|
|
contentHeight,
|
|
|
|
|
m_scrollVerticalOffset,
|
|
|
|
|
scrollMetrics);
|
|
|
|
|
m_scrollFrame.result.verticalOffset = m_scrollVerticalOffset;
|
|
|
|
|
|
|
|
|
|
if (!m_presentation.sections.empty()) {
|
|
|
|
|
const Widgets::UIEditorPropertyGridLayout unscrolledLayout =
|
|
|
|
|
Widgets::BuildUIEditorPropertyGridLayout(
|
|
|
|
|
m_scrollFrame.layout.contentRect,
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_sectionExpansion,
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
|
|
|
|
m_gridFrame.layout =
|
|
|
|
|
TranslatePropertyGridLayoutForScroll(unscrolledLayout, m_scrollVerticalOffset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
const InspectorPresentationComponentBinding* InspectorPanel::FindSelectedComponentBinding() const {
|
|
|
|
|
if (!m_fieldSelection.HasSelection()) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& fieldId = m_fieldSelection.GetSelectedId();
|
|
|
|
|
for (const InspectorPresentationComponentBinding& binding :
|
|
|
|
|
m_presentation.componentBindings) {
|
|
|
|
|
for (const std::string& ownedFieldId : binding.fieldIds) {
|
|
|
|
|
if (ownedFieldId == fieldId) {
|
|
|
|
|
return &binding;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
const Widgets::UIEditorPropertyGridField* InspectorPanel::FindField(
|
|
|
|
|
std::string_view fieldId) const {
|
|
|
|
|
const auto location =
|
|
|
|
|
Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId);
|
|
|
|
|
if (!location.IsValid() ||
|
|
|
|
|
location.sectionIndex >= m_presentation.sections.size() ||
|
|
|
|
|
location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &m_presentation.sections[location.sectionIndex].fields[location.fieldIndex];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widgets::UIEditorPropertyGridField* InspectorPanel::FindMutableField(
|
|
|
|
|
std::string_view fieldId) {
|
|
|
|
|
const auto location =
|
|
|
|
|
Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId);
|
|
|
|
|
if (!location.IsValid() ||
|
|
|
|
|
location.sectionIndex >= m_presentation.sections.size() ||
|
|
|
|
|
location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &m_presentation.sections[location.sectionIndex].fields[location.fieldIndex];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::CapturePresentationStamps(const EditorContext& context) {
|
|
|
|
|
m_lastSceneSelectionStamp = context.GetSceneRuntime().GetSelectionStamp();
|
|
|
|
|
m_lastProjectSelectionStamp = context.GetProjectRuntime().GetSelectionStamp();
|
|
|
|
|
m_lastSceneInspectorRevision = context.GetSceneRuntime().GetInspectorRevision();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::RebuildPresentation(
|
|
|
|
|
EditorContext& context,
|
|
|
|
|
bool subjectChanged) {
|
|
|
|
|
m_presentation = BuildInspectorPresentationModel(
|
|
|
|
|
m_subject,
|
|
|
|
|
context.GetSceneRuntime(),
|
|
|
|
|
InspectorComponentEditorRegistry::Get());
|
|
|
|
|
CapturePresentationStamps(context);
|
|
|
|
|
SyncExpansionState(subjectChanged);
|
|
|
|
|
SyncSelectionState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::ForceResyncPresentation(EditorContext& context) {
|
|
|
|
|
if (m_subject.kind != InspectorSubjectKind::SceneObject) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string structureSignature = BuildInspectorStructureSignature(
|
|
|
|
|
m_subject,
|
|
|
|
|
context.GetSceneRuntime(),
|
|
|
|
|
InspectorComponentEditorRegistry::Get());
|
|
|
|
|
if (structureSignature != m_presentation.structureSignature) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InspectorPresentationSyncResult syncResult =
|
|
|
|
|
SyncInspectorPresentationModelValues(
|
|
|
|
|
m_presentation,
|
|
|
|
|
m_subject,
|
|
|
|
|
context.GetSceneRuntime(),
|
|
|
|
|
InspectorComponentEditorRegistry::Get());
|
|
|
|
|
if (!syncResult.success) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_presentation.structureSignature = structureSignature;
|
|
|
|
|
CapturePresentationStamps(context);
|
|
|
|
|
SyncSelectionState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::RefreshPresentation(
|
|
|
|
|
EditorContext& context,
|
|
|
|
|
bool subjectChanged) {
|
|
|
|
|
if (subjectChanged) {
|
|
|
|
|
RebuildPresentation(context, true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (m_subject.kind) {
|
|
|
|
|
case InspectorSubjectKind::ProjectAsset:
|
|
|
|
|
if (m_lastProjectSelectionStamp !=
|
|
|
|
|
context.GetProjectRuntime().GetSelectionStamp()) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::SceneObject: {
|
|
|
|
|
const EditorSceneRuntime& sceneRuntime = context.GetSceneRuntime();
|
|
|
|
|
if (m_lastSceneSelectionStamp != sceneRuntime.GetSelectionStamp()) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_lastSceneInspectorRevision == sceneRuntime.GetInspectorRevision()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string structureSignature = BuildInspectorStructureSignature(
|
|
|
|
|
m_subject,
|
|
|
|
|
sceneRuntime,
|
|
|
|
|
InspectorComponentEditorRegistry::Get());
|
|
|
|
|
if (structureSignature != m_presentation.structureSignature) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InspectorPresentationSyncResult syncResult =
|
|
|
|
|
SyncInspectorPresentationModelValues(
|
|
|
|
|
m_presentation,
|
|
|
|
|
m_subject,
|
|
|
|
|
sceneRuntime,
|
|
|
|
|
InspectorComponentEditorRegistry::Get());
|
|
|
|
|
if (!syncResult.success) {
|
|
|
|
|
RebuildPresentation(context, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_presentation.structureSignature = structureSignature;
|
|
|
|
|
CapturePresentationStamps(context);
|
|
|
|
|
SyncSelectionState();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::None:
|
|
|
|
|
default:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool InspectorPanel::ApplyColorPickerToolValue(EditorContext& context) {
|
|
|
|
|
EditorColorPickerToolState& toolState = context.GetColorPickerToolState();
|
|
|
|
|
if (m_sceneRuntime == nullptr ||
|
|
|
|
|
!toolState.active ||
|
|
|
|
|
toolState.revision == m_lastAppliedColorPickerRevision ||
|
|
|
|
|
!IsEditorColorPickerToolTarget(
|
|
|
|
|
toolState,
|
|
|
|
|
m_subjectKey,
|
|
|
|
|
toolState.inspectorTarget.fieldId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widgets::UIEditorPropertyGridField* field =
|
|
|
|
|
FindMutableField(toolState.inspectorTarget.fieldId);
|
|
|
|
|
if (field == nullptr || field->kind != Widgets::UIEditorPropertyGridFieldKind::Color) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (AreColorsEqual(field->colorValue.value, toolState.color)) {
|
|
|
|
|
m_lastAppliedColorPickerRevision = toolState.revision;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
field->colorValue.value = toolState.color;
|
|
|
|
|
const bool applied = ApplyChangedField(field->fieldId);
|
|
|
|
|
m_lastAppliedColorPickerRevision = toolState.revision;
|
|
|
|
|
if (!applied) {
|
|
|
|
|
ForceResyncPresentation(context);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshPresentation(context, false);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::RequestColorPicker(
|
|
|
|
|
EditorContext& context,
|
|
|
|
|
std::string_view fieldId) {
|
|
|
|
|
const Widgets::UIEditorPropertyGridField* field = FindField(fieldId);
|
|
|
|
|
if (field == nullptr ||
|
|
|
|
|
field->kind != Widgets::UIEditorPropertyGridFieldKind::Color ||
|
|
|
|
|
field->readOnly) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OpenEditorColorPickerToolForInspectorField(
|
|
|
|
|
context.GetColorPickerToolState(),
|
|
|
|
|
m_subjectKey,
|
|
|
|
|
field->fieldId,
|
|
|
|
|
field->colorValue.value,
|
|
|
|
|
field->colorValue.showAlpha);
|
2026-04-22 22:07:02 +08:00
|
|
|
context.RequestOpenUtilityWindow(EditorUtilityWindowKind::ColorPicker);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::ResetAddComponentButtonState() {
|
|
|
|
|
m_addComponentButtonHovered = false;
|
|
|
|
|
m_addComponentButtonPressed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InspectorPanel::UpdateAddComponentButton(
|
|
|
|
|
EditorContext& context,
|
|
|
|
|
const std::vector<UIInputEvent>& inputEvents) {
|
|
|
|
|
if (!ShouldShowAddComponentButton()) {
|
|
|
|
|
ResetAddComponentButtonState();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
const UIRect buttonRect =
|
|
|
|
|
BuildAddComponentButtonRect(m_gridFrame.layout, m_scrollFrame.layout.contentRect);
|
2026-04-22 22:07:02 +08:00
|
|
|
if (buttonRect.width <= 0.0f || buttonRect.height <= 0.0f) {
|
|
|
|
|
ResetAddComponentButtonState();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const UIInputEvent& event : inputEvents) {
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case UIInputEventType::PointerMove:
|
|
|
|
|
m_addComponentButtonHovered = ContainsPoint(buttonRect, event.position);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::PointerLeave:
|
|
|
|
|
case UIInputEventType::FocusLost:
|
|
|
|
|
ResetAddComponentButtonState();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::PointerButtonDown:
|
|
|
|
|
if (event.pointerButton != UIPointerButton::Left) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_addComponentButtonHovered = ContainsPoint(buttonRect, event.position);
|
|
|
|
|
m_addComponentButtonPressed = m_addComponentButtonHovered;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::PointerButtonUp:
|
|
|
|
|
if (event.pointerButton != UIPointerButton::Left) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_addComponentButtonPressed &&
|
|
|
|
|
ContainsPoint(buttonRect, event.position)) {
|
|
|
|
|
context.RequestOpenUtilityWindow(EditorUtilityWindowKind::AddComponent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_addComponentButtonHovered = ContainsPoint(buttonRect, event.position);
|
|
|
|
|
m_addComponentButtonPressed = false;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-21 00:57:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
bool InspectorPanel::ApplyChangedField(std::string_view fieldId) {
|
|
|
|
|
if (m_sceneRuntime == nullptr ||
|
|
|
|
|
m_subject.kind != InspectorSubjectKind::SceneObject) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto location =
|
|
|
|
|
Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId);
|
|
|
|
|
if (!location.IsValid() ||
|
|
|
|
|
location.sectionIndex >= m_presentation.sections.size() ||
|
|
|
|
|
location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Widgets::UIEditorPropertyGridField& field =
|
|
|
|
|
m_presentation.sections[location.sectionIndex].fields[location.fieldIndex];
|
|
|
|
|
const InspectorPresentationComponentBinding* binding = nullptr;
|
|
|
|
|
for (const InspectorPresentationComponentBinding& candidate :
|
|
|
|
|
m_presentation.componentBindings) {
|
|
|
|
|
if (std::find(
|
|
|
|
|
candidate.fieldIds.begin(),
|
|
|
|
|
candidate.fieldIds.end(),
|
|
|
|
|
field.fieldId) != candidate.fieldIds.end()) {
|
|
|
|
|
binding = &candidate;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (binding == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const IInspectorComponentEditor* editor =
|
|
|
|
|
InspectorComponentEditorRegistry::Get().FindEditor(binding->typeName);
|
|
|
|
|
if (editor == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InspectorComponentEditorContext context = {};
|
|
|
|
|
context.gameObject = m_subject.sceneObject.gameObject;
|
|
|
|
|
context.componentId = binding->componentId;
|
|
|
|
|
context.typeName = binding->typeName;
|
|
|
|
|
context.displayName = binding->displayName;
|
|
|
|
|
context.removable = binding->removable;
|
|
|
|
|
for (const EditorSceneComponentDescriptor& descriptor :
|
|
|
|
|
m_sceneRuntime->GetSelectedComponents()) {
|
|
|
|
|
if (descriptor.componentId == binding->componentId) {
|
|
|
|
|
context.component = descriptor.component;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return editor->ApplyFieldValue(*m_sceneRuntime, context, field);
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
void InspectorPanel::Update(
|
2026-04-21 00:57:14 +08:00
|
|
|
EditorContext& context,
|
2026-04-22 16:24:59 +08:00
|
|
|
const UIEditorHostedPanelDispatchEntry& dispatchEntry,
|
|
|
|
|
const std::vector<UIInputEvent>& inputEvents) {
|
|
|
|
|
if (!dispatchEntry.mounted) {
|
2026-04-19 00:03:25 +08:00
|
|
|
ResetPanelState();
|
2026-04-12 11:12:27 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_visible = true;
|
2026-04-22 16:24:59 +08:00
|
|
|
m_bounds = dispatchEntry.bounds;
|
2026-04-21 00:57:14 +08:00
|
|
|
m_sceneRuntime = &context.GetSceneRuntime();
|
|
|
|
|
m_subject = BuildInspectorSubject(context.GetSession(), context.GetSceneRuntime());
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
const std::string nextSubjectKey = BuildSubjectKey();
|
|
|
|
|
const bool subjectChanged = m_subjectKey != nextSubjectKey;
|
|
|
|
|
m_subjectKey = nextSubjectKey;
|
|
|
|
|
if (subjectChanged) {
|
|
|
|
|
ResetInteractionState();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
RefreshPresentation(context, subjectChanged);
|
|
|
|
|
ApplyColorPickerToolValue(context);
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
const std::vector<UIInputEvent> filteredEvents =
|
2026-04-19 15:52:28 +08:00
|
|
|
BuildUIEditorPanelInputEvents(
|
2026-04-19 00:03:25 +08:00
|
|
|
m_bounds,
|
|
|
|
|
inputEvents,
|
2026-04-19 03:23:16 +08:00
|
|
|
UIEditorPanelInputFilterOptions{
|
2026-04-22 16:24:59 +08:00
|
|
|
.allowPointerInBounds = dispatchEntry.allowInteraction,
|
2026-04-22 22:53:01 +08:00
|
|
|
.allowPointerWhileCaptured = HasActivePointerCapture(),
|
2026-04-22 16:24:59 +08:00
|
|
|
.allowKeyboardInput = dispatchEntry.focused,
|
2026-04-19 15:52:28 +08:00
|
|
|
.allowFocusEvents =
|
2026-04-22 16:24:59 +08:00
|
|
|
dispatchEntry.focused ||
|
2026-04-22 22:53:01 +08:00
|
|
|
HasActivePointerCapture() ||
|
2026-04-22 16:24:59 +08:00
|
|
|
dispatchEntry.focusGained ||
|
|
|
|
|
dispatchEntry.focusLost,
|
2026-04-19 15:52:28 +08:00
|
|
|
.includePointerLeave =
|
2026-04-22 22:53:01 +08:00
|
|
|
dispatchEntry.allowInteraction ||
|
|
|
|
|
dispatchEntry.focused ||
|
|
|
|
|
HasActivePointerCapture()
|
2026-04-19 15:52:28 +08:00
|
|
|
},
|
2026-04-22 16:24:59 +08:00
|
|
|
dispatchEntry.focusGained,
|
|
|
|
|
dispatchEntry.focusLost);
|
2026-04-22 18:42:46 +08:00
|
|
|
TryClaimHostedPanelCommandFocus(
|
|
|
|
|
m_commandFocusService,
|
|
|
|
|
EditorActionRoute::Inspector,
|
|
|
|
|
filteredEvents,
|
|
|
|
|
m_bounds,
|
|
|
|
|
dispatchEntry.allowInteraction);
|
2026-04-22 22:53:01 +08:00
|
|
|
|
|
|
|
|
RebuildScrollableLayout();
|
|
|
|
|
const UIRect scrollViewportBounds = BuildScrollViewportBounds();
|
|
|
|
|
if (scrollViewportBounds.width > 0.0f && scrollViewportBounds.height > 0.0f) {
|
|
|
|
|
m_scrollInteractionState.scrollViewState.focused =
|
|
|
|
|
m_interactionState.propertyGridState.focused;
|
|
|
|
|
m_scrollFrame = UpdateUIEditorScrollViewInteraction(
|
|
|
|
|
m_scrollInteractionState,
|
|
|
|
|
m_scrollVerticalOffset,
|
|
|
|
|
scrollViewportBounds,
|
|
|
|
|
MeasureScrollableContentHeight(scrollViewportBounds),
|
|
|
|
|
filteredEvents,
|
|
|
|
|
::XCEngine::UI::Editor::ResolveUIEditorScrollViewMetrics());
|
|
|
|
|
if (m_scrollFrame.result.focusChanged) {
|
|
|
|
|
m_interactionState.propertyGridState.focused =
|
|
|
|
|
m_scrollInteractionState.scrollViewState.focused;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
m_scrollFrame = {};
|
|
|
|
|
m_scrollVerticalOffset = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_gridFrame.result = {};
|
|
|
|
|
if (!m_presentation.sections.empty() &&
|
|
|
|
|
m_scrollFrame.layout.contentRect.width > 0.0f &&
|
|
|
|
|
m_scrollFrame.layout.contentRect.height > 0.0f) {
|
|
|
|
|
const std::vector<UIInputEvent> gridEvents =
|
|
|
|
|
BuildScrolledPropertyGridInputEvents(filteredEvents, m_scrollVerticalOffset);
|
|
|
|
|
const UIEditorPropertyGridInteractionFrame interactionFrame =
|
|
|
|
|
UpdateUIEditorPropertyGridInteraction(
|
2026-04-22 22:07:02 +08:00
|
|
|
m_interactionState,
|
|
|
|
|
m_fieldSelection,
|
|
|
|
|
m_sectionExpansion,
|
|
|
|
|
m_propertyEditModel,
|
2026-04-22 22:53:01 +08:00
|
|
|
m_scrollFrame.layout.contentRect,
|
2026-04-22 22:07:02 +08:00
|
|
|
m_presentation.sections,
|
2026-04-22 22:53:01 +08:00
|
|
|
gridEvents,
|
2026-04-22 22:07:02 +08:00
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
2026-04-22 22:53:01 +08:00
|
|
|
m_gridFrame.result = interactionFrame.result;
|
|
|
|
|
if (interactionFrame.result.pickerRequested &&
|
|
|
|
|
!interactionFrame.result.requestedFieldId.empty()) {
|
|
|
|
|
RequestColorPicker(context, interactionFrame.result.requestedFieldId);
|
2026-04-22 22:07:02 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
if (interactionFrame.result.fieldValueChanged &&
|
|
|
|
|
!interactionFrame.result.changedFieldId.empty()) {
|
|
|
|
|
if (ApplyChangedField(interactionFrame.result.changedFieldId)) {
|
2026-04-22 22:07:02 +08:00
|
|
|
RefreshPresentation(context, false);
|
|
|
|
|
} else {
|
|
|
|
|
ForceResyncPresentation(context);
|
|
|
|
|
}
|
2026-04-21 00:57:14 +08:00
|
|
|
}
|
2026-04-19 00:03:25 +08:00
|
|
|
}
|
2026-04-22 22:53:01 +08:00
|
|
|
|
|
|
|
|
RebuildScrollableLayout();
|
2026-04-22 22:07:02 +08:00
|
|
|
UpdateAddComponentButton(context, filteredEvents);
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
void InspectorPanel::Append(UIDrawList& drawList) const {
|
2026-04-12 11:12:27 +08:00
|
|
|
if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawList.AddFilledRect(m_bounds, kSurfaceColor);
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
const float horizontalPadding =
|
|
|
|
|
ResolveInspectorHorizontalPadding(m_presentation);
|
|
|
|
|
const float topPadding = ResolveInspectorTopPadding(m_presentation);
|
|
|
|
|
const float contentX = m_bounds.x + horizontalPadding;
|
|
|
|
|
const float contentWidth =
|
|
|
|
|
(std::max)(m_bounds.width - horizontalPadding * 2.0f, 0.0f);
|
|
|
|
|
float nextY = m_bounds.y + topPadding;
|
|
|
|
|
|
|
|
|
|
if (m_presentation.showHeader) {
|
|
|
|
|
const UIRect titleRect(contentX, nextY, contentWidth, kTitleHeight);
|
|
|
|
|
drawList.AddText(
|
|
|
|
|
UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)),
|
|
|
|
|
m_presentation.title,
|
|
|
|
|
kTitleColor,
|
|
|
|
|
kTitleFontSize);
|
|
|
|
|
nextY += titleRect.height;
|
|
|
|
|
|
|
|
|
|
const UIRect subtitleRect(contentX, nextY, contentWidth, kSubtitleHeight);
|
|
|
|
|
drawList.AddText(
|
|
|
|
|
UIPoint(
|
|
|
|
|
subtitleRect.x,
|
|
|
|
|
ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)),
|
|
|
|
|
m_presentation.subtitle,
|
|
|
|
|
kSubtitleColor,
|
|
|
|
|
kSubtitleFontSize);
|
|
|
|
|
}
|
2026-04-12 11:12:27 +08:00
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
if (m_scrollFrame.layout.bounds.width <= 0.0f ||
|
|
|
|
|
m_scrollFrame.layout.bounds.height <= 0.0f) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widgets::AppendUIEditorScrollViewBackground(
|
|
|
|
|
drawList,
|
|
|
|
|
m_scrollFrame.layout,
|
|
|
|
|
m_scrollInteractionState.scrollViewState,
|
|
|
|
|
BuildInspectorScrollPalette(),
|
|
|
|
|
::XCEngine::UI::Editor::ResolveUIEditorScrollViewMetrics());
|
|
|
|
|
|
|
|
|
|
drawList.PushClipRect(m_scrollFrame.layout.contentRect);
|
2026-04-22 22:07:02 +08:00
|
|
|
if (!m_presentation.sections.empty()) {
|
2026-04-22 22:53:01 +08:00
|
|
|
Widgets::AppendUIEditorPropertyGridBackground(
|
2026-04-22 22:07:02 +08:00
|
|
|
drawList,
|
2026-04-22 22:53:01 +08:00
|
|
|
m_gridFrame.layout,
|
2026-04-22 22:07:02 +08:00
|
|
|
m_presentation.sections,
|
|
|
|
|
m_fieldSelection,
|
|
|
|
|
m_propertyEditModel,
|
|
|
|
|
m_interactionState.propertyGridState,
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(),
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
2026-04-22 22:53:01 +08:00
|
|
|
Widgets::AppendUIEditorPropertyGridForeground(
|
|
|
|
|
drawList,
|
|
|
|
|
m_gridFrame.layout,
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_fieldSelection,
|
|
|
|
|
m_interactionState.propertyGridState,
|
|
|
|
|
m_propertyEditModel,
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(),
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
2026-04-22 22:07:02 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 22:53:01 +08:00
|
|
|
if (ShouldShowAddComponentButton()) {
|
|
|
|
|
const UIRect buttonRect =
|
|
|
|
|
BuildAddComponentButtonRect(m_gridFrame.layout, m_scrollFrame.layout.contentRect);
|
|
|
|
|
if (buttonRect.width > 0.0f && buttonRect.height > 0.0f) {
|
|
|
|
|
const Widgets::UIEditorPropertyGridPalette& palette =
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette();
|
|
|
|
|
const Widgets::UIEditorPropertyGridMetrics& metrics =
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics();
|
|
|
|
|
drawList.AddFilledRect(
|
|
|
|
|
buttonRect,
|
|
|
|
|
ResolveAddComponentButtonFillColor(
|
|
|
|
|
m_addComponentButtonHovered,
|
|
|
|
|
m_addComponentButtonPressed),
|
|
|
|
|
metrics.valueBoxRounding);
|
|
|
|
|
drawList.AddRectOutline(
|
|
|
|
|
buttonRect,
|
|
|
|
|
palette.valueBoxBorderColor,
|
|
|
|
|
metrics.borderThickness,
|
|
|
|
|
metrics.valueBoxRounding);
|
|
|
|
|
drawList.AddText(
|
|
|
|
|
UIPoint(
|
|
|
|
|
ResolveCenteredTextX(buttonRect, "Add Component", kAddComponentButtonFontSize),
|
|
|
|
|
ResolveTextTop(buttonRect.y, buttonRect.height, kAddComponentButtonFontSize)),
|
|
|
|
|
"Add Component",
|
|
|
|
|
palette.valueTextColor,
|
|
|
|
|
kAddComponentButtonFontSize);
|
|
|
|
|
}
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
2026-04-22 22:53:01 +08:00
|
|
|
drawList.PopClipRect();
|
2026-04-19 00:03:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorHostCommandEvaluationResult InspectorPanel::EvaluateEditCommand(
|
|
|
|
|
std::string_view commandId) const {
|
|
|
|
|
const InspectorPresentationComponentBinding* binding =
|
|
|
|
|
FindSelectedComponentBinding();
|
|
|
|
|
if (binding == nullptr) {
|
|
|
|
|
return BuildEvaluationResult(
|
|
|
|
|
false,
|
|
|
|
|
"Select an inspector component field first.");
|
|
|
|
|
}
|
2026-04-12 11:12:27 +08:00
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
if (commandId == "edit.delete") {
|
|
|
|
|
if (!binding->removable || m_sceneRuntime == nullptr ||
|
|
|
|
|
!m_sceneRuntime->CanRemoveSelectedComponent(binding->componentId)) {
|
|
|
|
|
return BuildEvaluationResult(
|
|
|
|
|
false,
|
|
|
|
|
"'" + binding->displayName + "' cannot be removed.");
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
return BuildEvaluationResult(
|
|
|
|
|
true,
|
|
|
|
|
"Remove inspector component '" + binding->displayName + "'.");
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
return BuildEvaluationResult(
|
|
|
|
|
false,
|
|
|
|
|
"Inspector does not expose this edit command.");
|
2026-04-12 11:12:27 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
UIEditorHostCommandDispatchResult InspectorPanel::DispatchEditCommand(
|
|
|
|
|
std::string_view commandId) {
|
|
|
|
|
const UIEditorHostCommandEvaluationResult evaluation =
|
|
|
|
|
EvaluateEditCommand(commandId);
|
|
|
|
|
if (!evaluation.executable) {
|
|
|
|
|
return BuildDispatchResult(false, evaluation.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InspectorPresentationComponentBinding* binding =
|
|
|
|
|
FindSelectedComponentBinding();
|
|
|
|
|
if (binding == nullptr || m_sceneRuntime == nullptr) {
|
|
|
|
|
return BuildDispatchResult(
|
|
|
|
|
false,
|
|
|
|
|
"Inspector component route is unavailable.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (commandId == "edit.delete") {
|
|
|
|
|
if (!m_sceneRuntime->RemoveSelectedComponent(binding->componentId)) {
|
|
|
|
|
return BuildDispatchResult(
|
|
|
|
|
false,
|
|
|
|
|
"Failed to remove inspector component.");
|
|
|
|
|
}
|
2026-04-15 08:24:06 +08:00
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
ResetInteractionState();
|
|
|
|
|
return BuildDispatchResult(
|
|
|
|
|
true,
|
|
|
|
|
"Removed inspector component '" + binding->displayName + "'.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return BuildDispatchResult(
|
|
|
|
|
false,
|
|
|
|
|
"Inspector does not expose this edit command.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace XCEngine::UI::Editor::App
|