Files
XCEngine/new_editor/app/Features/Inspector/InspectorPanel.cpp

994 lines
34 KiB
C++

#include "InspectorPanel.h"
#include "Composition/EditorContext.h"
#include "State/EditorColorPickerToolState.h"
#include <XCEditor/Collections/UIEditorScrollView.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h"
#include "Scene/EditorSceneRuntime.h"
#include "State/EditorCommandFocusService.h"
#include <algorithm>
#include <cmath>
namespace XCEngine::UI::Editor::App {
namespace {
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIDrawList;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIPointerButton;
using ::XCEngine::UI::UIRect;
using ::XCEngine::UI::UIInputEventType;
constexpr float kPanelPadding = 10.0f;
constexpr float kTitleHeight = 18.0f;
constexpr float kSubtitleHeight = 16.0f;
constexpr float kHeaderGap = 10.0f;
constexpr float kTitleFontSize = 13.0f;
constexpr float kSubtitleFontSize = 11.0f;
constexpr float kAddComponentButtonHeight = 24.0f;
constexpr float kAddComponentButtonTopGap = 10.0f;
constexpr float kAddComponentButtonFontSize = 12.0f;
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);
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;
}
float ResolvePropertyGridContentBottom(
const Widgets::UIEditorPropertyGridLayout& gridLayout,
const UIRect& contentBounds) {
float contentBottom = contentBounds.y;
if (!gridLayout.sectionHeaderRects.empty()) {
const UIRect& lastSectionRect = gridLayout.sectionHeaderRects.back();
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);
}
return contentBottom;
}
float ResolveAddComponentButtonTop(
const Widgets::UIEditorPropertyGridLayout& gridLayout,
const UIRect& contentBounds) {
return ResolvePropertyGridContentBottom(gridLayout, contentBounds) +
kAddComponentButtonTopGap;
}
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;
}
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;
}
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;
}
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;
}
float ResolveTextTop(float rectY, float rectHeight, float fontSize) {
const float lineHeight = fontSize * 1.6f;
return rectY + std::floor((rectHeight - lineHeight) * 0.5f);
}
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;
}
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;
}
} // namespace
void InspectorPanel::SetCommandFocusService(
EditorCommandFocusService* commandFocusService) {
m_commandFocusService = commandFocusService;
}
void InspectorPanel::ResetPanelState() {
m_visible = false;
m_bounds = {};
m_sceneRuntime = nullptr;
m_subject = {};
m_subjectKey.clear();
m_presentation = {};
m_gridFrame = {};
m_knownSectionIds.clear();
m_lastSceneSelectionStamp = 0u;
m_lastProjectSelectionStamp = 0u;
m_lastSceneInspectorRevision = 0u;
ResetInteractionState();
}
void InspectorPanel::ResetInteractionState() {
m_fieldSelection.ClearSelection();
m_sectionExpansion.Clear();
m_propertyEditModel = {};
m_scrollInteractionState = {};
m_scrollFrame = {};
m_scrollVerticalOffset = 0.0f;
m_interactionState = {};
m_gridFrame = {};
m_lastAppliedColorPickerRevision = 0u;
ResetAddComponentButtonState();
}
bool InspectorPanel::HasActivePointerCapture() const {
return HasActiveUIEditorScrollViewPointerCapture(m_scrollInteractionState);
}
void InspectorPanel::SyncExpansionState(bool subjectChanged) {
if (subjectChanged) {
m_sectionExpansion.Clear();
m_knownSectionIds.clear();
}
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);
}
}
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();
m_interactionState.editableFieldSession = {};
}
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";
}
}
float InspectorPanel::ResolveHeaderHeight() const {
if (!m_presentation.showHeader) {
return 0.0f;
}
return kTitleHeight + kSubtitleHeight + kHeaderGap;
}
bool InspectorPanel::ShouldShowAddComponentButton() const {
return m_subject.kind == InspectorSubjectKind::SceneObject;
}
UIRect InspectorPanel::BuildScrollViewportBounds() const {
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();
const float height =
(std::max)(m_bounds.y + m_bounds.height - bottomPadding - y, 0.0f);
return UIRect(x, y, width, height);
}
UIRect InspectorPanel::BuildAddComponentButtonRect(
const Widgets::UIEditorPropertyGridLayout& gridLayout,
const UIRect& contentBounds) const {
if (!ShouldShowAddComponentButton()) {
return {};
}
return UIRect(
contentBounds.x,
ResolveAddComponentButtonTop(gridLayout, contentBounds),
contentBounds.width,
kAddComponentButtonHeight);
}
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());
contentBottom = ResolvePropertyGridContentBottom(layout, contentBounds);
}
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);
}
}
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;
}
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);
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;
}
const UIRect buttonRect =
BuildAddComponentButtonRect(m_gridFrame.layout, m_scrollFrame.layout.contentRect);
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;
}
}
}
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);
}
void InspectorPanel::Update(
EditorContext& context,
const UIEditorHostedPanelDispatchEntry& dispatchEntry,
const std::vector<UIInputEvent>& inputEvents) {
if (!dispatchEntry.mounted) {
ResetPanelState();
return;
}
m_visible = true;
m_bounds = dispatchEntry.bounds;
m_sceneRuntime = &context.GetSceneRuntime();
m_subject = BuildInspectorSubject(context.GetSession(), context.GetSceneRuntime());
const std::string nextSubjectKey = BuildSubjectKey();
const bool subjectChanged = m_subjectKey != nextSubjectKey;
m_subjectKey = nextSubjectKey;
if (subjectChanged) {
ResetInteractionState();
}
RefreshPresentation(context, subjectChanged);
ApplyColorPickerToolValue(context);
const std::vector<UIInputEvent> filteredEvents =
BuildUIEditorPanelInputEvents(
m_bounds,
inputEvents,
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = dispatchEntry.allowInteraction,
.allowPointerWhileCaptured = HasActivePointerCapture(),
.allowKeyboardInput = dispatchEntry.focused,
.allowFocusEvents =
dispatchEntry.focused ||
HasActivePointerCapture() ||
dispatchEntry.focusGained ||
dispatchEntry.focusLost,
.includePointerLeave =
dispatchEntry.allowInteraction ||
dispatchEntry.focused ||
HasActivePointerCapture()
},
dispatchEntry.focusGained,
dispatchEntry.focusLost);
TryClaimHostedPanelCommandFocus(
m_commandFocusService,
EditorActionRoute::Inspector,
filteredEvents,
m_bounds,
dispatchEntry.allowInteraction);
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(
m_interactionState,
m_fieldSelection,
m_sectionExpansion,
m_propertyEditModel,
m_scrollFrame.layout.contentRect,
m_presentation.sections,
gridEvents,
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
m_gridFrame.result = interactionFrame.result;
if (interactionFrame.result.pickerRequested &&
!interactionFrame.result.requestedFieldId.empty()) {
RequestColorPicker(context, interactionFrame.result.requestedFieldId);
}
if (interactionFrame.result.fieldValueChanged &&
!interactionFrame.result.changedFieldId.empty()) {
if (ApplyChangedField(interactionFrame.result.changedFieldId)) {
RefreshPresentation(context, false);
} else {
ForceResyncPresentation(context);
}
}
}
RebuildScrollableLayout();
UpdateAddComponentButton(context, filteredEvents);
}
void InspectorPanel::Append(UIDrawList& drawList) const {
if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) {
return;
}
drawList.AddFilledRect(m_bounds, kSurfaceColor);
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);
}
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);
if (!m_presentation.sections.empty()) {
Widgets::AppendUIEditorPropertyGridBackground(
drawList,
m_gridFrame.layout,
m_presentation.sections,
m_fieldSelection,
m_propertyEditModel,
m_interactionState.propertyGridState,
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(),
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
Widgets::AppendUIEditorPropertyGridForeground(
drawList,
m_gridFrame.layout,
m_presentation.sections,
m_fieldSelection,
m_interactionState.propertyGridState,
m_propertyEditModel,
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(),
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
}
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);
}
}
drawList.PopClipRect();
}
UIEditorHostCommandEvaluationResult InspectorPanel::EvaluateEditCommand(
std::string_view commandId) const {
const InspectorPresentationComponentBinding* binding =
FindSelectedComponentBinding();
if (binding == nullptr) {
return BuildEvaluationResult(
false,
"Select an inspector component field first.");
}
if (commandId == "edit.delete") {
if (!binding->removable || m_sceneRuntime == nullptr ||
!m_sceneRuntime->CanRemoveSelectedComponent(binding->componentId)) {
return BuildEvaluationResult(
false,
"'" + binding->displayName + "' cannot be removed.");
}
return BuildEvaluationResult(
true,
"Remove inspector component '" + binding->displayName + "'.");
}
return BuildEvaluationResult(
false,
"Inspector does not expose this edit command.");
}
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.");
}
ResetInteractionState();
return BuildDispatchResult(
true,
"Removed inspector component '" + binding->displayName + "'.");
}
return BuildDispatchResult(
false,
"Inspector does not expose this edit command.");
}
} // namespace XCEngine::UI::Editor::App