How-To: Show a Context Menu

Goal: pop an action menu (Use / Drop / Inspect) at the cursor or anchored to a world object, and run the chosen action. Set Request.Context.Target and the menu gathers its UCrimsonContextAction list from the target itself via CrimsonCommon's UCrimsonContextActionGatherer - the exact gather the server re-runs when validating the pick. The same widgets serve inventory items, doors, NPCs, anything.

Prerequisites
Your root layout registers the UI.Layer.ContextMenu layer (the tag ships natively in CrimsonCommon; the shipped W_CrimsonOverallUILayout already has a stack for it). The plugin ships ready-made menu widgets - W_ContextMenu_Modal, W_ContextMenu_World, W_ContextMenu_Radial, and W_ContextMenuEntry - assign them as DefaultModalContextMenuClass / DefaultWorldContextMenuClass / DefaultRadialContextMenuClass in Project Settings -> Crimson -> Crimson UI (or pass them per request; subclass only to customize - see step 1). For the confirm glyph on entries to resolve, your IMC_UI (mapping keys to Crimson_IA_UI_Confirm/Back) must be added to the player at a higher priority with Consume Input off on overlapping keys, and the platform CommonInput_* controller-brush data (also shipped) must be registered - all set up in Quick Start step 4. The player controller has a UCrimsonContextActionPlayerComponent (server dispatch).

1. The menu widgets (shipped - subclass to customize)

The plugin ships W_ContextMenu_Modal, W_ContextMenu_World, W_ContextMenu_Radial, and W_ContextMenuEntry ready to use - if they are assigned as the Crimson UI defaults (see Prerequisites) you can skip to step 2. Build your own, or subclass the shipped ones, only for custom layout; the parents and required child widgets are:

WidgetParentNotes
Entry buttonUCrimsonContextMenuEntryWidgetA UCommonButtonBase + IUserObjectListEntry. Implement its OnEntryViewModelSet event and bind visuals to the entry VM's FieldNotify properties: DisplayName, Tooltip, DisabledReason, Icon, bIsEnabled, bIsDestructive, bIsFocused. Hover-to-focus and click-to-confirm are wired natively - no graph needed. To show the confirm key glyph, add a CrimsonActionWidget named exactly InputActionWidget (CommonButtonBase's managed slot); leave every Triggering action empty and tick bShouldUseFallbackDefaultInputAction so it visualizes the project's default click action, bHideInputActionWithKeyboard = false to show it on mouse/keyboard too. The base class shows it only on the focused/hovered entry - no per-widget input action needed.
Modal menuUCrimsonContextMenuWidget_ModalMust contain a widget named exactly ContentPanel (required - the BP fails to compile without it), anchored top-left with auto size; C++ moves it to the cursor / anchor via render translation, flips + clamps at viewport edges, and closes on outside clicks. Inside it, add a Dynamic Entry Box named exactly EntryBox with Entry Widget Class = your entry BP - C++ creates the entries automatically (no virtualization, sizes to content). A UListView named EntryList is also supported for long scrollable menus, but it must be given a bounded height.
World menuUCrimsonContextMenuWidget_WorldSame required ContentPanel + EntryList content as the modal, but each tick C++ projects a USceneComponent's world position to the screen and moves ContentPanel there; closes itself when the anchor dies. Runs in Menu input mode - the menu owns input while open, so player movement/camera are held (the world keeps ticking) and only the D-pad navigates (thumbstick is suppressed by a per-menu navigation config; it stays free for gameplay). Back closes it automatically.
Reference implementation
All four widgets live in /CrimsonUI/UI/Foundation/ContextMenu/. Open W_ContextMenu_World to see the required structure in a working asset: a top-left anchored, auto-sized ContentPanel (named exactly) containing a header text and a Dynamic Entry Box named exactly EntryBox whose Entry Widget Class is W_ContextMenuEntry. W_ContextMenu_Modal is the cursor-anchored variant, and W_ContextMenuEntry shows the entry-VM bindings and the InputActionWidget glyph setup from the table above.

Modal and Radial run in GameAndMenu input mode with GameMouseCaptureMode = NoCapture, so gameplay input keeps flowing while the mouse drives the menu immediately (with viewport capture, the first click would only release capture instead of hitting a button). World runs in Menu mode instead: the menu owns input, so player movement/camera are cleanly held while it is open (the standard multi-choice-menu behavior; the world keeps ticking). Mouse interaction needs no wiring on any of them. Back closes the menu automatically (the bases are back-handlers - bIsBackHandler routes to RequestClose), and the D-pad navigates via CommonUI's focus chain (thumbstick is suppressed on World). For any custom navigation, the BlueprintCallable methods remain: ConfirmFocusedEntry, RequestClose, CycleNext, CyclePrevious.

Empty menu? Wrong place?
Only the first entry appears - you put a virtualizing list view inside the auto-sized ContentPanel: it only generates the rows that fit its near-zero initial geometry. Use a Dynamic Entry Box named `EntryBox` instead (or give the list a bounded height via a Size Box). Completely empty - the entry host is not named exactly EntryBox / EntryList (C++ only auto-populates a bound one; a warning is logged at activation), or you used a CrimsonListView without Factory Rules. Stuck at the top-left / offset from the target - ContentPanel is not anchored top-left with auto size; C++ positions it via render translation, so any other anchoring adds an offset.

2. Build the request and show it

Node chain: Make CrimsonContextMenuRequest (Context -> Target = the item/actor, Context -> Instigator Pawn/Controller = the local player's, Presentation Mode = Modal, bAnchorToCursor = true; leave Actions empty for auto-gather) -> Get CrimsonContextMenuSubsystem (from the local player) -> Show Context Menu.

Other request fields: Header (title text), InputModeOverride (per-menu ECrimsonWidgetInputMode), ScreenAnchor (fixed position when not anchoring to cursor), bCloseOnClickOutside (default true), WorldScreenOffset, and Actions (optional explicit list - only for menus not backed by a provider; auto-gather is the recommended path). CloseAll() dismisses any open menu; GetActiveWidget() returns it.

Verify
The menu opens on UI.Layer.ContextMenu at the cursor (or over the target), entries show your action names grouped by section (destructive last), clicking an entry runs it on the server, clicking outside closes the menu, and disabled entries render greyed with their DisabledReason and are skipped by Cycle Next/Previous.

3. Optional: confirmation, refresh, payloads

  • Confirmation friction - set an action's ConfirmationMode to Dialog and confirming routes through UCrimsonMessagingSubsystem (Yes/No) before executing. HoldToConfirm entries should implement a hold in the entry BP and call ConfirmEntryBypassingConfirmation when it completes; a plain click falls back to the dialog.
  • Refresh while open - call RefreshEntries on the menu widget when relevant state changes (e.g. from an inventory-changed handler). Auto-gathered menus re-gather and preserve focus; explicit-list menus re-evaluate availability. The menu also closes itself if the target is destroyed.
  • Payloads - ConfirmEntryWithPayload(Index, FInstancedStruct) sends per-invocation arguments (e.g. a split quantity from your own quantity dialog) to ExecuteAction on the server.
  • Post-execute behavior - each action's CloseBehavior picks CloseMenu (default), KeepOpen, or RefreshMenu.
  • Suppress gameplay while open - set EffectWhileOpen (an Infinite GE granting e.g. Camera.Blocked; applied to the owning pawn's ASC on open, removed on any close path) or LooseTagsWhileOpen on the menu widget's class defaults. Pair with ActivationBlockedTags on the abilities to block (camera turn, attack...).
  • World-menu lifetime & feel - per-asset options on UCrimsonContextMenuWidget_World: AutoCloseDistance (also gates opening - an interaction from farther than this never shows the menu, so there is no one-frame flash before it closes; and it still closes when the pawn walks away), bCloseOnClickOutside (dialog-border pattern), bScaleWithDistance, bFadeWhenOccluded.
Full integration lives in CrimsonCore
On the full stack, CrimsonCore -> Context Menu documents the end-to-end flow: its UCrimsonCoreContextMenuSubsystem fills the request context for items/actors and adds the default-action fast path (ExecuteDefaultActionForItem / ForActor for double-click / tap). This page is the plugin-agnostic UI half.

4. Optional: radial (gamepad wheel) presentation

Set PresentationMode = Radial for the hold-open / flick-stick / release-confirm wheel (Rust / RDR2 world interaction). Subclass UCrimsonContextMenuWidget_Radial (derives from _Modal, so click-outside and clamping come along): center ContentPanel, leave bAnchorToCursor = false, and place each entry widget at GetEntryDirectionByIndex(Index) * Radius - entry 0 sits at 12 o'clock, proceeding clockwise. Feed the left stick into SelectEntryByDirection each tick (dead-zone and boundary hysteresis are handled in C++) and call ConfirmFocusedEntry on button release. Mouse click/hover on wedges works with no extra wiring. Set the class as DefaultRadialContextMenuClass in Crimson UI settings.

Reference implementation
The shipped W_ContextMenu_Radial (/CrimsonUI/UI/Foundation/ContextMenu/) implements all of this: entries placed on a circle via GetEntryDirectionByIndex, the left stick wired to SelectEntryByDirection, and release wired to ConfirmFocusedEntry. Open it before building your own wheel.
Keep radials small
Radial selection degrades above ~8 options; the widget logs a warning past MaxRadialEntries. Use the radial for world interaction and the Modal list for inventories.

See also

  • How-To: World-Space Indicators - passive (non-focusable) world-anchored UI.
  • Concept: Input Routing & Activatable Widgets - how the GameAndMenu vs Menu input modes route input.
  • How-To: Show Confirmation Dialogs - the dialog system ConfirmationMode = Dialog routes through.