#include #include namespace XCEngine::UI::Editor::Widgets { namespace { float ClampNonNegative(float value) { return (std::max)(value, 0.0f); } float ResolveSectionHeaderHeight( const UIEditorPropertyGridSection& section, const UIEditorPropertyGridMetrics& metrics) { return section.desiredHeaderHeight > 0.0f ? section.desiredHeaderHeight : metrics.sectionHeaderHeight; } float ResolveFieldRowHeight( const UIEditorPropertyGridField& field, const UIEditorPropertyGridMetrics& metrics) { return field.desiredHeight > 0.0f ? field.desiredHeight : metrics.fieldRowHeight; } ::XCEngine::UI::UIPoint ResolveDisclosureGlyphPosition( const ::XCEngine::UI::UIRect& rect, float textInsetY) { return ::XCEngine::UI::UIPoint(rect.x + 2.0f, rect.y + textInsetY - 1.0f); } const std::string& ResolveDisplayedFieldValue( const UIEditorPropertyGridField& field, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel) { if (propertyEditModel.HasActiveEdit() && propertyEditModel.GetActiveFieldId() == field.fieldId) { return propertyEditModel.GetStagedValue(); } return field.valueText; } } // namespace bool IsUIEditorPropertyGridPointInside( const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } std::size_t FindUIEditorPropertyGridSectionIndex( const std::vector& sections, std::string_view sectionId) { for (std::size_t sectionIndex = 0u; sectionIndex < sections.size(); ++sectionIndex) { if (sections[sectionIndex].sectionId == sectionId) { return sectionIndex; } } return UIEditorPropertyGridInvalidIndex; } UIEditorPropertyGridFieldLocation FindUIEditorPropertyGridFieldLocation( const std::vector& sections, std::string_view fieldId) { for (std::size_t sectionIndex = 0u; sectionIndex < sections.size(); ++sectionIndex) { const auto& fields = sections[sectionIndex].fields; for (std::size_t fieldIndex = 0u; fieldIndex < fields.size(); ++fieldIndex) { if (fields[fieldIndex].fieldId == fieldId) { return { sectionIndex, fieldIndex }; } } } return {}; } std::size_t FindUIEditorPropertyGridVisibleFieldIndex( const UIEditorPropertyGridLayout& layout, std::string_view fieldId, const std::vector& sections) { for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleFieldIndices.size(); ++visibleIndex) { const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleIndex]; const std::size_t fieldIndex = layout.visibleFieldIndices[visibleIndex]; if (sectionIndex >= sections.size() || fieldIndex >= sections[sectionIndex].fields.size()) { continue; } if (sections[sectionIndex].fields[fieldIndex].fieldId == fieldId) { return visibleIndex; } } return UIEditorPropertyGridInvalidIndex; } UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( const ::XCEngine::UI::UIRect& bounds, const std::vector& sections, const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, const UIEditorPropertyGridMetrics& metrics) { UIEditorPropertyGridLayout layout = {}; layout.bounds = ::XCEngine::UI::UIRect( bounds.x, bounds.y, ClampNonNegative(bounds.width), ClampNonNegative(bounds.height)); const float inset = ClampNonNegative(metrics.contentInset); const float contentX = layout.bounds.x + inset; const float contentY = layout.bounds.y + inset; const float contentWidth = (std::max)(0.0f, layout.bounds.width - inset * 2.0f); float cursorY = contentY; for (std::size_t sectionIndex = 0u; sectionIndex < sections.size(); ++sectionIndex) { const UIEditorPropertyGridSection& section = sections[sectionIndex]; const float headerHeight = ResolveSectionHeaderHeight(section, metrics); const bool expanded = expansionModel.IsExpanded(section.sectionId); const ::XCEngine::UI::UIRect headerRect( contentX, cursorY, contentWidth, headerHeight); const ::XCEngine::UI::UIRect disclosureRect( headerRect.x + metrics.horizontalPadding, headerRect.y + (headerRect.height - metrics.disclosureExtent) * 0.5f, metrics.disclosureExtent, metrics.disclosureExtent); const float titleX = disclosureRect.x + disclosureRect.width + metrics.disclosureLabelGap; const ::XCEngine::UI::UIRect titleRect( titleX, headerRect.y, (std::max)(0.0f, headerRect.x + headerRect.width - titleX - metrics.horizontalPadding), headerRect.height); layout.sectionIndices.push_back(sectionIndex); layout.sectionHeaderRects.push_back(headerRect); layout.sectionDisclosureRects.push_back(disclosureRect); layout.sectionTitleRects.push_back(titleRect); layout.sectionExpanded.push_back(expanded); cursorY += headerHeight; if (expanded) { for (std::size_t fieldIndex = 0u; fieldIndex < section.fields.size(); ++fieldIndex) { const UIEditorPropertyGridField& field = section.fields[fieldIndex]; const float rowHeight = ResolveFieldRowHeight(field, metrics); const ::XCEngine::UI::UIRect rowRect( contentX, cursorY, contentWidth, rowHeight); const float controlX = (std::min)( rowRect.x + rowRect.width - metrics.horizontalPadding, rowRect.x + metrics.controlColumnStart); const float labelWidth = (std::max)( 0.0f, controlX - rowRect.x - metrics.horizontalPadding - metrics.labelControlGap); const ::XCEngine::UI::UIRect labelRect( rowRect.x + metrics.horizontalPadding, rowRect.y, labelWidth, rowRect.height); const ::XCEngine::UI::UIRect valueRect( controlX, rowRect.y + metrics.valueBoxInsetY, (std::max)( 0.0f, rowRect.x + rowRect.width - controlX - metrics.horizontalPadding), (std::max)(0.0f, rowRect.height - metrics.valueBoxInsetY * 2.0f)); layout.visibleFieldSectionIndices.push_back(sectionIndex); layout.visibleFieldIndices.push_back(fieldIndex); layout.fieldRowRects.push_back(rowRect); layout.fieldLabelRects.push_back(labelRect); layout.fieldValueRects.push_back(valueRect); layout.fieldReadOnly.push_back(field.readOnly); cursorY += rowHeight + metrics.rowGap; } if (!section.fields.empty()) { cursorY -= metrics.rowGap; } } cursorY += metrics.sectionGap; } return layout; } UIEditorPropertyGridHitTarget HitTestUIEditorPropertyGrid( const UIEditorPropertyGridLayout& layout, const ::XCEngine::UI::UIPoint& point) { for (std::size_t sectionVisibleIndex = 0u; sectionVisibleIndex < layout.sectionHeaderRects.size(); ++sectionVisibleIndex) { if (!IsUIEditorPropertyGridPointInside(layout.sectionHeaderRects[sectionVisibleIndex], point)) { continue; } UIEditorPropertyGridHitTarget target = {}; target.kind = UIEditorPropertyGridHitTargetKind::SectionHeader; target.sectionIndex = layout.sectionIndices[sectionVisibleIndex]; return target; } for (std::size_t visibleFieldIndex = 0u; visibleFieldIndex < layout.fieldRowRects.size(); ++visibleFieldIndex) { if (!IsUIEditorPropertyGridPointInside(layout.fieldRowRects[visibleFieldIndex], point)) { continue; } UIEditorPropertyGridHitTarget target = {}; target.visibleFieldIndex = visibleFieldIndex; target.sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; target.fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; target.kind = IsUIEditorPropertyGridPointInside(layout.fieldValueRects[visibleFieldIndex], point) ? UIEditorPropertyGridHitTargetKind::ValueBox : UIEditorPropertyGridHitTargetKind::FieldRow; return target; } return {}; } void AppendUIEditorPropertyGridBackground( ::XCEngine::UI::UIDrawList& drawList, const UIEditorPropertyGridLayout& layout, const std::vector& sections, const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridState& state, const UIEditorPropertyGridPalette& palette, const UIEditorPropertyGridMetrics& metrics) { drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding); drawList.AddRectOutline( layout.bounds, state.focused ? palette.focusedBorderColor : palette.borderColor, state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, metrics.cornerRounding); for (std::size_t sectionVisibleIndex = 0u; sectionVisibleIndex < layout.sectionHeaderRects.size(); ++sectionVisibleIndex) { const UIEditorPropertyGridSection& section = sections[layout.sectionIndices[sectionVisibleIndex]]; const bool hovered = state.hoveredFieldId.empty() && state.hoveredSectionId == section.sectionId; drawList.AddFilledRect( layout.sectionHeaderRects[sectionVisibleIndex], hovered ? palette.sectionHeaderHoverColor : palette.sectionHeaderColor, metrics.cornerRounding); } for (std::size_t visibleFieldIndex = 0u; visibleFieldIndex < layout.fieldRowRects.size(); ++visibleFieldIndex) { const UIEditorPropertyGridSection& section = sections[layout.visibleFieldSectionIndices[visibleFieldIndex]]; const UIEditorPropertyGridField& field = section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; const bool selected = selectionModel.IsSelected(field.fieldId); const bool hovered = state.hoveredFieldId == field.fieldId; const bool editing = propertyEditModel.HasActiveEdit() && propertyEditModel.GetActiveFieldId() == field.fieldId; if (selected || hovered) { drawList.AddFilledRect( layout.fieldRowRects[visibleFieldIndex], selected ? (state.focused ? palette.fieldSelectedFocusedColor : palette.fieldSelectedColor) : palette.fieldHoverColor, metrics.cornerRounding); } const ::XCEngine::UI::UIColor valueBoxColor = field.readOnly ? palette.valueBoxReadOnlyColor : (editing ? palette.valueBoxEditingColor : (hovered ? palette.valueBoxHoverColor : palette.valueBoxColor)); drawList.AddFilledRect( layout.fieldValueRects[visibleFieldIndex], valueBoxColor, metrics.valueBoxRounding); drawList.AddRectOutline( layout.fieldValueRects[visibleFieldIndex], editing ? palette.valueBoxEditingBorderColor : palette.valueBoxBorderColor, editing ? metrics.editOutlineThickness : metrics.borderThickness, metrics.valueBoxRounding); } } void AppendUIEditorPropertyGridForeground( ::XCEngine::UI::UIDrawList& drawList, const UIEditorPropertyGridLayout& layout, const std::vector& sections, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridPalette& palette, const UIEditorPropertyGridMetrics& metrics) { drawList.PushClipRect(layout.bounds); for (std::size_t sectionVisibleIndex = 0u; sectionVisibleIndex < layout.sectionHeaderRects.size(); ++sectionVisibleIndex) { const UIEditorPropertyGridSection& section = sections[layout.sectionIndices[sectionVisibleIndex]]; drawList.AddText( ResolveDisclosureGlyphPosition( layout.sectionDisclosureRects[sectionVisibleIndex], metrics.sectionTextInsetY), layout.sectionExpanded[sectionVisibleIndex] ? "v" : ">", palette.disclosureColor, 12.0f); drawList.AddText( ::XCEngine::UI::UIPoint( layout.sectionTitleRects[sectionVisibleIndex].x, layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY), section.title, palette.sectionTextColor, 12.0f); } for (std::size_t visibleFieldIndex = 0u; visibleFieldIndex < layout.fieldRowRects.size(); ++visibleFieldIndex) { const UIEditorPropertyGridSection& section = sections[layout.visibleFieldSectionIndices[visibleFieldIndex]]; const UIEditorPropertyGridField& field = section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; const bool editing = propertyEditModel.HasActiveEdit() && propertyEditModel.GetActiveFieldId() == field.fieldId; const std::string& displayedValue = ResolveDisplayedFieldValue(field, propertyEditModel); drawList.PushClipRect(layout.fieldLabelRects[visibleFieldIndex]); drawList.AddText( ::XCEngine::UI::UIPoint( layout.fieldLabelRects[visibleFieldIndex].x, layout.fieldLabelRects[visibleFieldIndex].y + metrics.labelTextInsetY), field.label, palette.labelTextColor, 12.0f); drawList.PopClipRect(); drawList.PushClipRect(layout.fieldValueRects[visibleFieldIndex]); drawList.AddText( ::XCEngine::UI::UIPoint( layout.fieldValueRects[visibleFieldIndex].x + metrics.valueBoxInsetX, layout.fieldValueRects[visibleFieldIndex].y + metrics.valueTextInsetY), displayedValue, field.readOnly ? palette.readOnlyValueTextColor : palette.valueTextColor, 12.0f); if (editing) { drawList.AddText( ::XCEngine::UI::UIPoint( layout.fieldValueRects[visibleFieldIndex].x + (std::max)(0.0f, layout.fieldValueRects[visibleFieldIndex].width - 34.0f), layout.fieldValueRects[visibleFieldIndex].y + metrics.valueTextInsetY), "EDIT", palette.editTagColor, 11.0f); } drawList.PopClipRect(); } drawList.PopClipRect(); } void AppendUIEditorPropertyGrid( ::XCEngine::UI::UIDrawList& drawList, const ::XCEngine::UI::UIRect& bounds, const std::vector& sections, const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridState& state, const UIEditorPropertyGridPalette& palette, const UIEditorPropertyGridMetrics& metrics) { const UIEditorPropertyGridLayout layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); AppendUIEditorPropertyGridBackground( drawList, layout, sections, selectionModel, propertyEditModel, state, palette, metrics); AppendUIEditorPropertyGridForeground( drawList, layout, sections, propertyEditModel, palette, metrics); } } // namespace XCEngine::UI::Editor::Widgets