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 - add a stack for it exactly like the Quick Start layers). Menu widget Blueprints exist (step 1) and are set as project defaults in Project Settings -> Crimson -> Crimson UI (DefaultModalContextMenuClass / DefaultWorldContextMenuClass), or passed per request. The player controller has a UCrimsonContextActionPlayerComponent (server dispatch).

1. Build the menu widgets once

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.
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.
Screenshot pendingImages/CrimsonUI/howto-contextmenu-widgets.png
WBP_ContextMenu_World (parent: Crimson Context Menu Widget World) hierarchy: 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 WBP_ContextMenuEntry.

Both menu bases derive from UCrimsonActivatableWidget with InputConfig = GameAndMenu and 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; note mouse-look is suspended while a menu is open). Mouse interaction needs no wiring. For gamepad/keyboard, bind the UI.Action.ContextMenu.* tags (or your own input events) to the BlueprintCallable methods: ConfirmFocusedEntry, RequestClose, CycleNext, CyclePrevious. Up/Down navigation is CommonUI's normal focus chain.

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

Screenshot pendingImages/CrimsonUI/howto-contextmenu-show-bp.png
Make CrimsonContextMenuRequest (Context -> Target = the item/actor, Context -> Instigator Pawn/Controller = the local player's, Presentation Mode = Modal, bAnchorToCursor = true; leave Actions empty) -> Get CrimsonContextMenuSubsystem (from 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 (close 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.

Screenshot pendingImages/CrimsonUI/howto-contextmenu-radial.png
WBP_ContextMenu_Radial: entries placed on a circle via GetEntryDirectionByIndex, left stick wired to SelectEntryByDirection, release wired to ConfirmFocusedEntry.
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 - why the menu stays GameAndMenu.
  • How-To: Show Confirmation Dialogs - the dialog system ConfirmationMode = Dialog routes through.