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"
|
2026-04-19 02:48:41 +08:00
|
|
|
#include "Composition/EditorPanelIds.h"
|
2026-04-21 00:57:14 +08:00
|
|
|
#include "State/EditorColorPickerToolState.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;
|
|
|
|
|
using ::XCEngine::UI::UIRect;
|
|
|
|
|
|
|
|
|
|
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-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-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-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-15 08:24:06 +08:00
|
|
|
const UIEditorPanelContentHostPanelState* InspectorPanel::FindMountedInspectorPanel(
|
2026-04-12 11:12:27 +08:00
|
|
|
const UIEditorPanelContentHostFrame& contentHostFrame) const {
|
|
|
|
|
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
|
|
|
|
|
if (panelState.panelId == kInspectorPanelId && panelState.mounted) {
|
|
|
|
|
return &panelState;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
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 = {};
|
|
|
|
|
m_interactionState = {};
|
|
|
|
|
m_gridFrame = {};
|
2026-04-21 00:57:14 +08:00
|
|
|
m_lastAppliedColorPickerRevision = 0u;
|
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-19 00:03:25 +08:00
|
|
|
UIRect InspectorPanel::BuildGridBounds() 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-19 04:36:52 +08:00
|
|
|
void InspectorPanel::ClaimCommandFocus(
|
|
|
|
|
const std::vector<UIInputEvent>& inputEvents,
|
|
|
|
|
bool allowInteraction) {
|
|
|
|
|
if (m_commandFocusService == nullptr) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const UIInputEvent& event : inputEvents) {
|
|
|
|
|
if (event.type == UIInputEventType::FocusGained) {
|
|
|
|
|
m_commandFocusService->ClaimFocus(EditorActionRoute::Inspector);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!allowInteraction ||
|
|
|
|
|
event.type != UIInputEventType::PointerButtonDown ||
|
|
|
|
|
!ContainsPoint(m_bounds, event.position)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_commandFocusService->ClaimFocus(EditorActionRoute::Inspector);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-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-19 00:03:25 +08:00
|
|
|
const UIEditorPanelContentHostFrame& contentHostFrame,
|
|
|
|
|
const std::vector<UIInputEvent>& inputEvents,
|
2026-04-19 15:52:28 +08:00
|
|
|
const PanelInputContext& inputContext) {
|
2026-04-12 11:12:27 +08:00
|
|
|
const UIEditorPanelContentHostPanelState* panelState =
|
|
|
|
|
FindMountedInspectorPanel(contentHostFrame);
|
|
|
|
|
if (panelState == nullptr) {
|
2026-04-19 00:03:25 +08:00
|
|
|
ResetPanelState();
|
2026-04-12 11:12:27 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_visible = true;
|
|
|
|
|
m_bounds = panelState->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
|
|
|
|
|
|
|
|
if (m_presentation.sections.empty()) {
|
|
|
|
|
m_gridFrame = {};
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-19 15:52:28 +08:00
|
|
|
.allowPointerInBounds = inputContext.allowInteraction,
|
2026-04-19 03:23:16 +08:00
|
|
|
.allowPointerWhileCaptured = false,
|
2026-04-19 15:52:28 +08:00
|
|
|
.allowKeyboardInput = inputContext.hasInputFocus,
|
|
|
|
|
.allowFocusEvents =
|
|
|
|
|
inputContext.hasInputFocus ||
|
|
|
|
|
inputContext.focusGained ||
|
|
|
|
|
inputContext.focusLost,
|
|
|
|
|
.includePointerLeave =
|
|
|
|
|
inputContext.allowInteraction || inputContext.hasInputFocus
|
|
|
|
|
},
|
|
|
|
|
inputContext.focusGained,
|
|
|
|
|
inputContext.focusLost);
|
|
|
|
|
ClaimCommandFocus(filteredEvents, inputContext.allowInteraction);
|
2026-04-19 00:03:25 +08:00
|
|
|
m_gridFrame = UpdateUIEditorPropertyGridInteraction(
|
|
|
|
|
m_interactionState,
|
|
|
|
|
m_fieldSelection,
|
|
|
|
|
m_sectionExpansion,
|
|
|
|
|
m_propertyEditModel,
|
|
|
|
|
BuildGridBounds(),
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
filteredEvents,
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
2026-04-21 00:57:14 +08:00
|
|
|
if (m_gridFrame.result.pickerRequested &&
|
|
|
|
|
!m_gridFrame.result.requestedFieldId.empty()) {
|
|
|
|
|
RequestColorPicker(context, m_gridFrame.result.requestedFieldId);
|
|
|
|
|
}
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
if (m_gridFrame.result.fieldValueChanged &&
|
2026-04-21 00:57:14 +08:00
|
|
|
!m_gridFrame.result.changedFieldId.empty()) {
|
|
|
|
|
if (ApplyChangedField(m_gridFrame.result.changedFieldId)) {
|
|
|
|
|
RefreshPresentation(context, false);
|
|
|
|
|
} else {
|
|
|
|
|
ForceResyncPresentation(context);
|
|
|
|
|
}
|
2026-04-19 00:03:25 +08:00
|
|
|
}
|
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-19 00:03:25 +08:00
|
|
|
if (m_presentation.sections.empty()) {
|
2026-04-12 11:12:27 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
Widgets::AppendUIEditorPropertyGrid(
|
|
|
|
|
drawList,
|
|
|
|
|
BuildGridBounds(),
|
|
|
|
|
m_presentation.sections,
|
|
|
|
|
m_fieldSelection,
|
|
|
|
|
m_sectionExpansion,
|
|
|
|
|
m_propertyEditModel,
|
|
|
|
|
m_interactionState.propertyGridState,
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(),
|
|
|
|
|
::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|