Skip to content

Unreal Engine Details panel (C++ / Slate) — documentation only

The corpus's reference design for property handles and multi-object editing, and the one subject whose source could not be read.

IMPORTANT

Epistemic status: documentation, not source. EpicGames/UnrealEngine requires an authenticated account, so nothing here was verified against the implementation. Everything below comes from the archived Unreal Engine 5.1 API reference pages cited at the bottom; API-reference tables truncate long type names, so a few return types are marked INFERENCE. Where the other four deep-dives say "read at revision X", this one says "read from the 5.1 API reference as archived on 2023-02-04 / 2023-04-01".

Language / toolkitC++ / Slate
Licenseproprietary (source access gated)
ModulePropertyEditor (/Engine/Source/Editor/PropertyEditor/)
Read fromUE 5.1 API reference, archived
CategoryRetained node tree behind a handle abstraction
Metadata sourceruntime — UPROPERTY reflection + a meta= string map

Overview

What it solves

The editor's Details panel: an inspector over one or many selected UObjects, with per-class layout customization (IDetailCustomization), per-struct-type customization (IPropertyTypeCustomization), and a property abstraction that hides the transaction/notification protocol from customizers.

Design philosophy

The handle's own summary states both jobs it does:

A handle to a property which is used to read and write the value without needing to handle Pre/PostEditChange, transactions, package modification A handle also is used to identify the property in detail customization interfaces

IPropertyHandle, UE 5.1 API reference

Two things are bundled deliberately: identity ("which property is this?") and mediated access ("write it correctly"). No other subject in the corpus fuses them — Qt's QtProperty is identity with no protocol, Godot's (object, path) pair is identity that each caller must protocol itself, and bevy-inspector-egui has access with no identity at all.

Model & addressing

IPropertyHandle is the address, and it is a shared pointer (TSharedPtr<IPropertyHandle>), obtained from the layout builder or by navigation:

  • GetChildHandle(FName ChildName, bool bRecurse) and GetChildHandle(uint32 Index) — "Gets a child handle of this handle. Useful for accessing properties in structs."
  • GetParentHandle(), GetNumChildren(uint32& OutNumChildren)
  • GetKeyHandle() for map keys, AsArray() / AsMap() / AsSet() for container-typed handles
  • GeneratePathToProperty() — "Generates a path from the parent UObject class to this property"
  • IsValidHandle() — the explicit answer to "did my handle survive?"

The presence of both a navigable handle graph and a generated path string, plus an explicit validity predicate, is the strongest evidence in the corpus that a long-lived node address is worth its cost: customizations hold handles across panel rebuilds and ask whether they are still valid. FPropertyNode is the underlying node type (GetPropertyNode() is exposed on the handle), i.e. the panel does keep a retained tree behind the abstraction. (That FPropertyNode is a tree of nodes rather than something else is INFERENCE from the accessor's name and the handle's navigation surface.)

Metadata

UPROPERTY reflection plus a string-keyed metadata map, readable from the handle at both class and instance scope:

  • GetMetaData(FName Key), HasMetaData, and typed readers GetBoolMetaData, GetIntMetaData, GetFloatMetaData, GetClassMetaData
  • GetInstanceMetaData(FName Key) / GetInstanceMetaDataMap() — "Get metadata value for 'Key' for this property instance (as opposed to the class)"

The instance-vs-class split is notable: metadata is not purely a property of the type, so a customization can attach per-occurrence data to a handle. There is also a restriction layer — AddRestriction(TSharedRef<const FPropertyRestriction>), IsRestricted(const FString& Value, TArray<FText>& OutReasons), IsHidden, IsDisabled, GenerateRestrictionToolTip — i.e. per-value (not per-property) conditional availability, with reasons carried for display. No other subject in the corpus models "this value of this enum is unavailable here, and here is why".

Recursion

Descent is by handle: a struct-typed property yields child handles, and AddChildStructure(TSharedRef<FStructOnScope>) lets a customization graft an entirely synthetic struct into the tree. Containers are entered through AsArray()/AsMap()/AsSet(), which expose add/insert/delete/swap interfaces (IPropertyHandleArray etc.), and GetIndexInArray() reports an element's position.

Laziness and cycle handling could not be determined from the API reference: neither is visible in the public surface. Unreal's object graph certainly contains cycles (an actor's components reference their owner), so some guard must exist — but this survey has no evidence for what it is, and none is claimed here.

Editing & mutation

The handle is the mutation protocol:

  • SetValue(T, EPropertyValueSetFlags::Type) — flags carry the transient/committed distinction; EPropertyValueSetFlags::DefaultFlags is the default parameter (SetValue). The flag vocabulary (an InteractiveChange flag for drags, a NotTransactable flag) is widely used in customization code, but the enum's own page is not archived, so its enumerators are not verified here.
  • NotifyPreChange(), NotifyPostChange(EPropertyChangeType::Type), NotifyFinishedChangingProperties() — each documented as not needed when SetValue is used, "since it will be called automatically". The existence of NotifyFinishedChangingProperties as a separate call is the corpus's clearest statement that "still dragging" and "done dragging" are different events.
  • SetOnPropertyValueChanged, SetOnChildPropertyValueChanged, SetOnChildPropertyValuePreChange, SetOnPropertyResetToDefault — per-handle observers, including the child-changed direction that WinForms expresses as [NotifyParentProperty].
  • ResetToDefault(), CanResetToDefault(), DiffersFromDefault(), and an override hook ExecuteCustomResetToDefault(FResetToDefaultOverride) — "default" is a first-class concept, not a convention.
  • SetIgnoreValidation(bool) — "Sets whether or not data validation should occur for this property and all of its children", so validation is a subtree-scoped switch.

Transactions and package modification are stated to be the handle's responsibility, so undo is automatic for anything written through it.

Type coverage

  • CollectionsAsArray/AsMap/AsSet plus GetKeyHandle, i.e. containers are handled inside the tree with a dedicated interface per kind (unlike WinForms' modal collection editor).
  • Polymorphic valuesGeneratePossibleValues(TArray<TSharedPtr<FString>>& Out, TArray<FText>& OutToolTips, TArray<bool>& OutRestrictedItems) — "Generates a list of possible enum/class options for the property", and the restriction machinery greys entries out with reasons. This is the same shape as bevy-inspector-egui's constructable check, generalised to classes and driven by metadata rather than by constructability.
  • Optional / nullable — not determined.
  • Opaque typesAccessRawData(TArray<void*>&) and EnumerateRawData are the escape hatch: a customization can reach the bytes for anything the panel cannot present.

Multi-object editing

This is the axis Unreal designed for, and it is visible directly in the handle API:

  • GetNumPerObjectValues() — "Gets the number of objects that this handle is editing"
  • GetPerObjectValues(TArray<FString>& OutPerObjectValues) — "Gets a unique value for each object this handle is editing"
  • GetPerObjectValue(int32 ObjectIndex, FString& OutObjectValue), SetPerObjectValue, SetPerObjectValues
  • GetOuterObjects(TArray<UObject*>&), GetNumOuterObjects(), GetOuterBaseClass(), ReplaceOuterObjects

So a handle is inherently plural: it addresses "this property, across N objects", and the single-object case is N = 1. Every getter returns an FPropertyAcc…-prefixed result type rather than a bare value — the API tables truncate it, and the enumerators are not verified, but the shape (a result code beside the out-parameter) is what lets "the objects disagree" be a normal answer rather than an error. Compare the alternatives: WinForms reaches the same place with a merged descriptor and an allEqual out-parameter, while Godot returns the first object's value and shows nothing at all.

Customization

Two interfaces, at two scopes:

  • IDetailCustomization — "Interface for any class that lays out details for a specific class", with CustomizeDetails(IDetailLayoutBuilder&). Whole-class layout: reorder categories, hide properties, add rows.
  • IPropertyTypeCustomization — "Base class for property type customizations", with CustomizeHeader(TSharedRef<IPropertyHandle>, FDetailWidgetRow&, IPropertyTypeCustomizationUtils&) and CustomizeChildren(...), plus ShouldInlineKey(). Per-type, everywhere that type appears; the header/children split means a customization can collapse a struct into a single inline row and still expose its children.

The archived page lists 30+ engine-internal implementations of IPropertyTypeCustomization (FMathStructCustomization, FColorStructCustomization, FSlateBrushStructCustomization, FInstancedStructDetails, …), which is itself evidence: the built-in composite types are customizations rather than special cases in the panel.

Handles also carry the panel's own default widgets — CreatePropertyNameWidget, CreatePropertyValueWidget, CreateDefaultPropertyButtonWidgets, CreateDefaultPropertyCopyPasteActions — so a customization can replace part of a row and keep the rest.

Strengths (as documented)

  • The handle unifies identity, navigation, mediated writes, transactions and multi-object access behind one interface.
  • Multi-object editing is the default shape of the API, not an add-on.
  • Transient vs committed edits are explicit (SetValue flags plus NotifyFinishedChangingProperties).
  • Per-value restrictions with human-readable reasons.
  • Header/children customization split allows inlining without losing the subtree.

Weaknesses (as documented)

  • The API is very large: IPropertyHandle alone lists dozens of GetValue overloads, one per engine type — the price of a typed accessor over an untyped node.
  • Metadata is stringly-typed (GetMetaData(FName) plus typed parsers).
  • Everything is TSharedPtr/TSharedRef, so the node graph is reference-counted and lifetime is implicit.
  • Source access is gated, so none of the above can be checked against behaviour.

Key design decisions and trade-offs

DecisionRationale (documented)Trade-off
One handle type for identity + accessCustomizations never write the change protocolThe interface accretes: value access, metadata, restrictions, widgets, per-object values
Handles are inherently N-objectMulti-select is the editor's normal caseEvery read returns a result code; "one object" is a degenerate case everywhere
SetValue owns transactions and notificationUndo cannot be forgottenWriting outside the handle requires the manual NotifyPre/PostChange pair
Type customizations rather than built-in compositesFVector, FColor, FSlateBrush use the same seam as user typesThe seam must be powerful enough for the engine's own needs, hence its size
Restrictions as first-class, with reasonsA value can be unavailable for reasons the user can readAnother metadata channel to consult when rendering any picker

Sources

Archived Unreal Engine 5.1 API reference pages (the live pages return HTTP 403 to the link checker):