Inventory UI

A complete, drop-in inventory UI built on CrimsonInventory + CrimsonUI. Lives under Source/CrimsonCore/Public/UserInterface/Inventory/. All view-model bindings are wired in C++ only via CRIMSON_BIND_FIELD — Blueprint authors do not (and cannot) wire MVVM in the UMG binding graph; they implement visual events only.

Never wire MVVM in the UMG binding graph
Use BP only for the OnXVisual event reactions. The base classes route every UPROPERTY(FieldNotify, ...) through CRIMSON_BIND_FIELD; a stray UMG binding will fire alongside the C++ one and the order is undefined.

What ships

SurfaceFilesRole
HUD overlay (always-on)HUD/CrimsonInventoryHUDLayer.{h,cpp}UCommonUserWidget plugged into a HUD slot extension point. Hosts a UCrimsonListView of capacity bars and one of currency totals.
Toast spawner (always-on)HUD/CrimsonInventoryToastSpawner.{h,cpp} + CrimsonInventoryToastSettings.hULocalPlayerSubsystem + project settings. Listens to inventory message events and pushes one UCrimsonInventoryToastViewModel per event into a HUD slot via UCrimsonUIExtensionSubsystem::RegisterExtensionAsData. Retires the extension on a per-toast timer.
Inventory screen (push-on-open)Screens/CrimsonInventoryScreen.{h,cpp}UCrimsonActivatableWidget. The player's own inventory pane. Pushed onto UI.Layer.Menu via UCrimsonUIExtensions::PushContentToLayer_ForPlayer.
Container screen (push-on-open)Screens/CrimsonInventoryContainerScreen.{h,cpp}UCrimsonActivatableWidget. Two-pane variant — player inventory on one side, container (chest, stash, vendor) on the other.
Tooltips (per-slot, per-item-def)Widgets/CrimsonItemTooltipWidget.{h,cpp}Abstract UCommonUserWidget base. Item defs declare which tooltip subclass to use via the UCrimsonInventoryItemFragment_Tooltip fragment. The slot widget resolves the class and calls SetToolTip(Widget).
Entry / capacity / currency / toast widgetsSlots/CrimsonInventorySlotWidget.{h,cpp}, Slots/CrimsonInventoryGridSlotWidget.{h,cpp}, Widgets/CrimsonCapacityBarWidget.{h,cpp}, Widgets/CrimsonCurrencyEntryWidget.{h,cpp}, Widgets/CrimsonInventoryToastWidget.{h,cpp}List-entry widgets bound to per-row VMs. All implement IUserObjectListEntry; the list view (or extension point, for toasts) routes the data object into NativeOnListItemObjectSet, which calls CRIMSON_BIND_FIELD on each FieldNotify property.
Drag-drop + factoryBases/CrimsonInventoryDragDropOperation.{h,cpp}, Factories/CrimsonInventorySlotFactory.{h,cpp}Drag op carries source inventory + entry id + footprint + rotation; ApplyToTarget dispatches Routing->MoveItem or Routing->TransferItem. The factory maps an entry VM (footprint > 1×1) to the grid slot widget class; everything else uses the flat slot widget class.
View-modelsViewModels/CrimsonInventoryViewModel.{h,cpp}, ViewModels/CrimsonInventoryEntryViewModel.{h,cpp}, ViewModels/CrimsonCapacityBarViewModel.h, ViewModels/CrimsonCurrencyEntryViewModel.{h,cpp}, ViewModels/CrimsonInventoryToastViewModel.hScreen VM rebuilds entry/currency/capacity child VMs from FastArray + tag-stack containers on StackChanged / InventoryUpdated / ConstraintChanged / CurrencyChanged messages.
MVVM binding helpersBases/CrimsonViewModelBindingHelpers.hFCrimsonFieldBindingTracker + CRIMSON_BIND_FIELD macro — one-liner subscribe/track for any FieldNotify property; UnsubscribeAll on widget teardown.

Why it's structured this way

1. C++-only MVVM bindings
Every UPROPERTY(FieldNotify, ...) on a VM is subscribed by INotifyFieldValueChanged::AddFieldValueChangedDelegate from the widget's NativeOnInitialized / NativeOnListItemObjectSet. Subscriptions go through one FCrimsonFieldBindingTracker per widget instance and are released in NativeDestruct / NativeOnEntryReleased. BP MVVM graphs are silent on data flow (no compile-time check, no log when a binding silently breaks); the macro path makes every binding a grep-able call site.
2. Server-authoritative routing, optimistic-free client
Slot widgets, drag-drop, action handlers, and entry VMs all call into UCrimsonInventoryRoutingComponent on the owning PlayerController. The routing component owns every Server_* RPC. The UI never mutates the inventory directly.
3. HUD overlays go in slots; menus push to layers
Always-on overlays (UCrimsonInventoryHUDLayer, toasts) are plain UCommonUserWidgets registered into HUD slot tags via UGFA_AddWidget's Widgets array. The host UCrimsonHUDLayout (already on UI.Layer.Game) places UCrimsonUIExtensionPointWidgets with matching tags. Activatable menu screens (inventory + container) push themselves onto UI.Layer.Menu via UCrimsonUIExtensions::PushContentToLayer_ForPlayer. Nested activatable widgets would push input config twice — both screens are flat by design.
4. Per-event toast push, no persistent tray
Each toast is a single UCrimsonInventoryToastViewModel registered as data on a slot tag. The slot's UCrimsonUIExtensionPointWidget instantiates one widget per registration; the spawner unregisters after DurationSeconds, and the slot retires the widget. A persistent tray with its own list view would duplicate work the extension point already does and gate the toast lifetime on a frame loop instead of the message event.
5. Per-item-def tooltip class
Items declare their tooltip widget via UCrimsonInventoryItemFragment_Tooltip::TooltipWidgetClass. The slot resolves the class on bind (fragment override → DefaultTooltipWidgetClass), creates the widget, calls BindToEntry(VM), and hands it to UE via SetToolTip(Widget) — UE auto-shows on hover and tears it down on slot recycle. Weapon, consumable, and crafting-material tooltips diverge enough that one widget class with conditional sections is messier than dedicated subclasses.
6. Data lives in CrimsonInventory; widgets live in CrimsonCore
The tooltip fragment uses TSoftClassPtr<UUserWidget> (engine-only type) so the inventory plugin has no dependency on any UI plugin. CrimsonCore is the only place allowed to bridge CrimsonInput, CrimsonUI, and CrimsonInventory.

How it works at runtime

Read flow — replication and message subsystem fanning out to widgets:

text
┌─────────────────────────────────────────────────┐
server only: │ UCrimsonInventoryManagerComponent │ on PlayerState
│ FastArray entries + tag-stack constraint caps │
└────────────────────────┬────────────────────────┘
│ replicates
┌─────────────────────────────────────────────────┐
│ UCrimsonMessageSubsystem (per world) │
│ StackChanged · InventoryUpdated · │
│ ConstraintChanged · CurrencyChanged · │
│ ConstraintFailureMessage · ItemDropped │
└──────────────────┬──────────────────────────────┘
┌──────────────────────┼──────────────────────────────┐
▼ ▼ ▼
UCrimsonInventoryHUDLayer UCrimsonInventoryScreen UCrimsonInventoryToastSpawner
(slotted in HUD) (pushed to UI.Layer.Menu) (per-LocalPlayer subsystem)
│ │ │
▼ ▼ ▼
UCrimsonInventoryViewModel · UCrimsonInventoryViewModel · UCrimsonInventoryToastViewModel
rebuilds child VMs rebuilds child VMs (one per event)
│ │ │
FieldNotify ▶ widget FieldNotify ▶ widget RegisterExtensionAsData(SlotTag)
via CRIMSON_BIND_FIELD via CRIMSON_BIND_FIELD │
UCrimsonUIExtensionPointWidget
instantiates UCrimsonInventoryToastWidget
and routes data via IUserObjectListEntry
timer fires → UnregisterExtension

Mutation flow (client → server):

text
Slot widget / action handler / drag-drop
UCrimsonInventoryEntryViewModel::RequestMoveTo / RequestSplit / RequestDrop / RequestToggleLock
UCrimsonInventoryRoutingComponent (on PlayerController)
│ Server_* RPC
UCrimsonInventoryManagerComponent (on PlayerState, server-only mutation)
▼ FastArray + message broadcast
replication → clients → UCrimsonInventoryViewModel.Refresh()

Setup

These steps assume CrimsonCore is already installed per Integration above, and the player has a UCrimsonInventoryManagerComponent on the PlayerState and a UCrimsonInventoryRoutingComponent on the PlayerController (UGFA_AddInventory handles both — see the CrimsonInventory wiki page for the constraint/currency table setup).

1. Define the HUD slot tags

In your project's tags ini (or via the Gameplay Tag editor), add three child tags under HUD.Slot.*:

ini
[/Script/GameplayTags.GameplayTagsList]
GameplayTagList=(Tag="HUD.Slot.Inventory.Capacity")
GameplayTagList=(Tag="HUD.Slot.Inventory.Currency")
GameplayTagList=(Tag="HUD.Slot.Inventory.Toasts")
Names are conventions
Any tag under HUD.Slot.* works. The names above match the rest of the docs; you can change them as long as the GFA, settings, and extension-point widget all agree.

2. Create BP subclasses of the abstract widgets

Under Content/UI/Inventory/, create Blueprint subclasses for every abstract widget. Each one needs only its OnXVisual events implemented — the C++ base wires the VM bindings.

BP classParentWhat to do in BP
BP_InventoryHUDLayerUCrimsonInventoryHUDLayerBind two UCrimsonListViews to CapacityListView and CurrencyListView in the widget tree.
BP_InventorySlotWidgetUCrimsonInventorySlotWidgetImplement OnEntryBound, OnStackCountVisual, OnIconVisual, OnNameVisual, OnLockVisual, OnHighlightVisual. Set DefaultTooltipWidgetClass in defaults.
BP_InventoryGridSlotWidgetUCrimsonInventoryGridSlotWidgetSame as flat slot, plus footprint / rotation visual events.
BP_CapacityBarWidgetUCrimsonCapacityBarWidgetImplement OnNameVisual, OnCurrentVisual, OnMaxVisual, OnPercentVisual.
BP_CurrencyEntryWidgetUCrimsonCurrencyEntryWidgetImplement OnNameVisual, OnAmountVisual, OnIconVisual.
BP_InventoryToastWidgetUCrimsonInventoryToastWidgetImplement OnTitleVisual, OnSubtitleVisual, OnIconVisual, OnAmountVisual, OnSeverityVisual. Play a fade animation on OnToastBound.
BP_InventoryScreenUCrimsonInventoryScreenBind a UCrimsonTileView to EntryTileView. Set the tile view's EntryWidgetClass via a UCrimsonInventorySlotFactory (FlatSlotWidgetClass → BP_InventorySlotWidget; GridSlotWidgetClass → BP_InventoryGridSlotWidget).
BP_InventoryContainerScreenUCrimsonInventoryContainerScreenBind PlayerEntryTileView and ContainerEntryTileView. Same factory pattern.
BP_WeaponTooltip etc.UCrimsonItemTooltipWidgetOne subclass per item category (weapon / consumable / crafting / ...). Implement OnEntryBound + the title/icon/type/description/rarity visual events.

3. Build the HUD layout

Create BP_HUDLayout : UCrimsonHUDLayout (in CrimsonUI). Place three UCrimsonUIExtensionPointWidgets in the layout, one per slot tag from step 1:

SlotExtensionPointTagDataClassesGetWidgetClassForData
CapacityHUD.Slot.Inventory.CapacityUUserWidget(BP-registered)
CurrencyHUD.Slot.Inventory.CurrencyUUserWidget(BP-registered)
ToastsHUD.Slot.Inventory.ToastsUCrimsonInventoryToastViewModelReturn BP_InventoryToastWidget

4. Wire the HUD widgets via UGFA_AddWidget

In your experience definition's actions, configure a UGFA_AddWidget instance:

text
GFA_AddWidget
Layout:
[0] LayerID = HUD.Layer.Game
LayoutClass = BP_HUDLayout // UCrimsonHUDLayout subclass
Widgets:
[0] SlotID = HUD.Slot.Inventory.Capacity
WidgetClass = BP_InventoryHUDLayer
[1] SlotID = HUD.Slot.Inventory.Currency
WidgetClass = BP_InventoryHUDLayer // same widget — owns both lists
// No entry for the toast slot — the spawner populates it at runtime.
One widget or two?
BP_InventoryHUDLayer queries both capacity and currency from the same VM. Slotting it into one slot is enough. If your HUD design wants them in physically separate places, split it into two BP subclasses (each binding only the relevant list view) and register each into its own slot.

5. Configure toasts

Open Project Settings → Crimson → Crimson Inventory Toasts and set:

SettingValue
ToastSlotTagHUD.Slot.Inventory.Toasts — must match the extension point tag from step 3.
DefaultDurationSecondsLifetime for info / pickup / drop toasts. Default 3.0.
ErrorDurationSecondsLifetime for error toasts (constraint failed). Default 4.5.
bShowCurrencyToastsOff by default — currency change is usually represented by the dedicated currency HUD widget.
RegistrationPriorityHigher = displayed first in the slot. Default 0.
Tag mismatch is silent
If ToastSlotTag doesn't match a UCrimsonUIExtensionPointWidget in your HUD layout, RegisterExtensionAsData still succeeds — but no widget is ever instantiated. Verify both tags match at setup time. The UCrimsonInventoryToastSpawner ULocalPlayerSubsystem is auto-created when the local player joins; no manual instantiation is needed.

6. Wire the inventory screen open action

Pick a key (typically I) and a UInputAction for inventory toggle. From your player controller or a CommonUI bound action, push the screen onto the menu layer:

cpp
UCrimsonUIExtensions::PushContentToLayer_ForPlayer(
PC->GetLocalPlayer(),
FGameplayTag::RequestGameplayTag(TEXT("UI.Layer.Menu")),
BP_InventoryScreen);

The screen finds the player's inventory + routing components automatically and binds. In BP_InventoryScreen's defaults, set UIInputConfig to a UCrimsonInputConfig containing UInputActions tagged UIInputTag.Inventory.Drop, Split, Lock, Rotate, QuickTransfer (names are conventions). Then set the matching DropTag / SplitTag / LockTag / RotateTag / QuickTransferTag properties to those gameplay tags.

7. Wire the container screen

When UCrimsonInventoryRoutingComponent::OpenContainer fires OnContainerOpened(UCrimsonInventoryManagerComponent* ContainerInv) on the requesting client, push the container screen and inject the container inventory before initialization completes:

cpp
UCommonActivatableWidget* Pushed = UCrimsonUIExtensions::PushContentToLayer_ForPlayer(
LP, MenuLayerTag, BP_InventoryContainerScreen);
if (auto* Screen = Cast<UCrimsonInventoryContainerScreen>(Pushed))
{
Screen->SetContainerInventory(ContainerInv); // MUST run before NativeOnInitialized fires
}
SetContainerInventory ordering
PushContentToLayer_ForPlayer returns the widget instance before NativeOnInitialized runs. Call SetContainerInventory(...) immediately after the push call — if it runs later, the right pane stays empty.

8. Wire per-item tooltips

On each UCrimsonInventoryItemDefinition asset, add an instanced UCrimsonInventoryItemFragment_Tooltip fragment and set:

FieldPurpose
TooltipWidgetClassThe tooltip BP for this item category (e.g. BP_WeaponTooltip for swords, BP_ConsumableTooltip for potions). TSoftClassPtr<UUserWidget> — sync-loaded by the slot on bind.
DescriptionLong-form item description rendered via OnDescriptionVisual.
RarityColorAccent color rendered via OnRarityColorVisual.

Set DefaultTooltipWidgetClass on each of your slot BPs as a fallback used when an item def has no Tooltip fragment.

Preload heavy tooltip widgets
The slot calls LoadSynchronous() on the soft class ptr. For frequently-shown items this is fine; for rarely-shown items consider preloading via your asset manager's primary-asset bundle to avoid hitches on first hover.

9. (Optional) Slot focus tracking

The inventory screen and container screen track which entry is "focused" so their input actions (Drop / Split / Lock / Rotate / QuickTransfer) know which entry to act on. Slot widgets call Screen->SetFocusedEntry(...) on their parent screen — wire this from your BP slot's OnHovered event (single-pane screen) or OnFocused event (container screen, which also takes a ECrimsonContainerPane argument).

Customization knobs

ClassOverride pointWhat it controls
UCrimsonInventorySlotWidgetResolveTooltipWidgetClass (BlueprintNativeEvent)Per-slot tooltip class resolution. Default = fragment → DefaultTooltipWidgetClass. Override for rarity-tiered or faction-tiered tooltip classes.
UCrimsonInventorySlotWidgetGetDragQuantity (BlueprintNativeEvent)Quantity carried by a drag from this slot. Default = full stack. Override for Ctrl+drag = 1.
UCrimsonInventorySlotFactoryGetWidgetClassForData (BlueprintNativeEvent)Mapping from entry VM → list-entry widget class. Default = footprint > 1×1 → grid widget, else flat widget. Override for additional slot widget variants.
UCrimsonInventoryToastSpawnerSubclass + override OnStackChanged / OnConstraintFailed / OnCurrencyChanged / OnItemDroppedCustom toast filter logic (rarity-gated, type-gated, dedupe).
UCrimsonItemTooltipWidgetOnEntryBound (BlueprintImplementableEvent)Read additional fragments off the item def to render category-specific data (weapon stats, consumable effects, etc.).
UCrimsonInventoryContainerScreenSetActivePane, SetFocusedEntry (BlueprintCallable)Active-pane + focused-entry tracking for QuickTransfer. Call from BP slot focus events.

Notes & pitfalls

UIInputConfig shadow
The inventory screen's UIInputConfig (a UCrimsonInputConfig* data asset) is intentionally not named InputConfig because the parent UCrimsonActivatableWidget already has an InputConfig property of a different type (the ECrimsonWidgetInputMode enum that controls input mode push on activation). The two serve unrelated jobs.
Source location
Plugins/CrimsonCore/Source/CrimsonCore/Public/UserInterface/Inventory/ (headers) and Plugins/CrimsonCore/Source/CrimsonCore/Private/UserInterface/Inventory/ (sources). Item-fragment header is in CrimsonInventory: Plugins/CrimsonInventory/Source/CrimsonInventory/Public/ItemFragments/CrimsonInventoryItemFragment_Tooltip.h.