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 UCrimsonContextActionGatherer - the exact gather the server re-runs when validating the pick. The same widgets serve inventory items, doors, NPCs, anything.
UI.Layer.ContextMenu layer (the tag ships natively in CrimsonUI; 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 (dispatch: Server actions go over its RPC, OwningClient actions run locally - see How-To: Add Context Actions for Inventory Items). Row keys need no setup: ShowContextMenu applies the mapping context from Project Settings -> Crimson UI -> Context Menu -> Input for the menu's lifetime, and the row glyphs show them.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:
| Widget | Parent | Notes |
|---|---|---|
| Entry button | UCrimsonContextMenuEntryWidget | Base row: a UCommonButtonBase + IUserObjectListEntry (hover focuses, click confirms). A custom row implements OnEntryViewModelSet and binds the entry VM's FieldNotify properties: DisplayName, Tooltip, DisabledReason, Icon, bIsEnabled, bIsDestructive, bIsFocused, BlockedRemaining01, BlockedRemainingSeconds, bIsHeldByKey. |
Sweep row (shipped W_ContextMenuEntry) | UCrimsonContextMenuSweepEntryWidget | No graph needed: C++ drives Text_Label (state colors), Image_Sweep (a MI_UI_CtxMenu_Sweep brush whose Progress it sets: a blocked action's remaining cooldown, or the hold of a HoldToConfirm row - press starts, release cancels, completion dispatches, a tap does nothing; the hold lasts the action's HoldToConfirmSeconds, or the row's HoldSeconds (0.6 s) when that is 0), Text_Reason / Text_Remaining on disabled rows, and SizeBox_Confirm > Action_Confirm (a UCrimsonActionWidget showing the menu's ConfirmInputAction on the focused row). Style Crimson_ButtonStyle_CtxMenuRow; hovered / selected plate is MI_UI_CtxMenu_Row_Focus. Image_SectionRule is a hairline above the first row of each section. |
Choice row (shipped W_ContextMenuEntry_Choice) | UCrimsonContextMenuChoiceEntryWidget | The rotator row for a UCrimsonContextAction_Choice, on top of the sweep row: Text_Value between Text_Previous / Text_Next chevrons and Box_Segments > Image_Segments, the settings rotators' segment strip (SettingsRotator_M; C++ drives Options, SelectedOption, DefaultOption = -1 and OptionTint from the row's label color and sizes the box from SegmentWidth / SegmentGap). With the row focused, Left / Right rotate the value (a custom navigation reply that never leaves the row, the UCommonRotator pattern); a click advances; every change dispatches at once with a choice payload and the menu stays open. Menus pick this row through EntryFactoryRules on the menu widget: the shipped W_ContextMenu_Modal / _World carry one Class rule, UCrimsonContextAction_Choice -> W_ContextMenuEntry_Choice; add rules the same way to give any action type its own row. |
| Modal menu | UCrimsonContextMenuWidget_Modal | Must 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. Optional Text_Header / Text_SubHeader are filled from the request (an item's name in its rarity color, its type). Each row gets a direct key while the menu is open from Project Settings, Crimson UI, Context Menu, Input (ContextMenuEntryActions + ContextMenuMappingContext, shipped with the plugin); the subsystem applies the mapping context around each request, so no widget declares it. The window brush is MI_UI_CtxMenu_Window (M_UI_ChamferBox with its shadow on; give the Border 10 px padding for it). An optional Panel_Footer (the Back glyph) shows only on gamepad. |
| World menu | UCrimsonContextMenuWidget_World | Same 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. |
/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.
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.
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 grayed with their DisabledReason and are skipped by Cycle Next/Previous (an informational interaction option, such as a failed requirement's "No Access", lists this way with its reason).3. Optional: confirmation, refresh, payloads
- Confirmation friction - set an action's
ConfirmationModetoDialogand confirming routes throughUCrimsonMessagingSubsystem(Yes/No) before executing.HoldToConfirmentries should implement a hold in the entry BP and callConfirmEntryBypassingConfirmationwhen it completes; a plain click falls back to the dialog. The shipped sweep row does this for you, holding for the action'sHoldToConfirmSeconds(0 = the row'sHoldSeconds); a custom row reads the same value from the entry view model (HoldToConfirmSeconds). - Refresh while open - call
RefreshEntrieson 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. an amount picked in your own quantity dialog) toExecuteActionon the server. - Ask before dispatching -
SetConfirmInterceptor(C++) gives an owning system first refusal on an entry confirm, so it can collect an argument first. Returntrueto mean "handled, do not dispatch yet" and later callConfirmEntryWithPayload; returnfalseto let the entry take its normalConfirmationModepath. It is consulted inConfirmEntry(so a prompt REPLACES the confirmation dialog instead of stacking a second modal on it) and again inDispatchEntry(so a Blueprint hold callingConfirmEntryBypassingConfirmationcannot skip it). An already-populated payload suppresses the second check, so it is never asked twice for one dispatch. - Post-execute behavior - each action's
CloseBehaviorpicksCloseMenu(default),KeepOpen, orRefreshMenu. - 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) orLooseTagsWhileOpenon the menu widget's class defaults. Pair withActivationBlockedTagson 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.
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.
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.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
GameAndMenuvsMenuinput modes route input. - How-To: Show Confirmation Dialogs - the dialog system
ConfirmationMode = Dialogroutes through.