Reference · Updated 2552.09.10.16.15

Command Bar — Back & Accept (Hardcoded Approach)

How SystemLink's on-screen prompt strip (◀ Back, Ⓐ Select) is built. Decision (2026-07-20): hardcode it. The bar only ever needs Back and maybe Accept, so we skip CommonUI's dynami

How SystemLink's on-screen prompt strip (◀ Back, Ⓐ Select) is built. Decision (2026-07-20): hardcode it. The bar only ever needs Back and maybe Accept, so we skip CommonUI's dynamic CommonBoundActionBar and build a fixed strip. The dynamic bar is reframed as a MenuKit-product upgrade (see the last section).

### ⚠ Update 2026-08-28 — two corrections and one open contradiction
1. CD_SL_KBM exists and is registered. Config/DefaultGame.ini lists both CD_SL_Xbox and
CD_SL_KBM under [CommonInputPlatformSettings_Windows …], with a comment recording that the
2026-07-20 gamepad-only decision was reversed — the bar is meant to show on both input types, with
CommonActionWidget swapping between the Xbox glyphs and the KBM key-caps. Every "gamepad-only" and
"no CD_SL_KBM is needed" statement below predates that reversal.
2. ⚠ The Blueprint has not caught up. WBP_SL_CommandBar still contains the KBM-hiding graph
(CommonInputSubsystem → OnInputMethodChanged → collapse). So the config says "show on both" and the
widget says "gamepad only". Resolve this before trusting either. If the reversal stands, delete the
collapse logic; if hardcoding to gamepad stands, drop CD_SL_KBM from the ini.
3. WBP_SL_BoundActionButton no longer exists — see Cleanup.

#1. The key fact: the bar is display-only

Back and Accept already work without any command bar:

  • B closes the menu via bIsBackHandler (set in the USLScreenWidget constructor).
  • A clicks the focused button via CommonUI's default click action.

The command bar changes nothing about behavior — it only shows the player which buttons do what. That's why hardcoding the display is safe: we're not reimplementing input, just drawing two labels + glyphs.

#2. Design: two building blocks

AssetTypeRole
WBP_SL_CommandPromptUserWidgetOne prompt = glyph + label. Reusable, two exposed vars.
WBP_SL_CommandBarUserWidgetA HorizontalBox holding the prompts (Back, Accept).

Use a CommonActionWidget for the glyph — not a plain image or the letter "B". The CommonActionWidget auto-resolves the correct controller glyph for the current gamepad from the input action. That is the one thing worth keeping from CommonUI; everything else (the router, dynamic spawning) we drop.

The command bar is gamepad-only (decision 2026-07-20). On mouse/keyboard the whole prompt strip is
hidden (WBP_SL_CommandBar collapses its prompts when the active device is KBM), so no keyboard glyphs are
needed — there is no CD_SL_KBM. See "Gamepad-only visibility" below. (We briefly authored a CD_SL_KBM
key-cap set, then removed it once the bar was hidden on KBM — the glyphs would never render.)

#Step 1 — Build WBP_SL_CommandPrompt (the reusable prompt)

  1. Create WBP_SL_CommandPrompt (/Game/SystemLink/UI/Menus/, parent UserWidget).
  1. Tree:

   HorizontalBox (root)

   ├─ Glyph   [CommonActionWidget]

   ├─ Spacer  (size ~8)

   └─ Label   [CommonTextBlock]     (use Style_… text style to taste)

  1. Add two instance-editable variables (eye icon open):
    • InputAction — type Input Action (object reference, i.e. UInputAction).
    • PromptLabel — type Text.
  1. In the PreConstruct graph:
    • Glyph → Set Enhanced Input Action (InputAction) — resolves the glyph brush.
    • Label → Set Text (PromptLabel).
  1. Compile + Save.
Doing this in PreConstruct means the designer preview updates per instance, and the runtime widget sets
itself up on construction — see the timing note in Step 3.
Why no DT_SL_InputActions row? CommonActionWidget resolves the glyph either from an
EnhancedInputAction (a UInputAction) or from the legacy InputActions DataTable array — never both.
UCommonActionWidget::GetIcon() is a ternary on both conditions:
```cpp
return EnhancedInputAction && CommonUI::IsEnhancedInputSupportEnabled()
? GetIconForEnhancedInputAction(Subsystem, EnhancedInputAction)
: GetIconForInputActions(Subsystem, InputActions);
```
So a set EnhancedInputAction wins and the InputActions array is ignored — if you ever see a prompt
with both set, the row is inert. But the DataTable branch is the fallback, not dead code: leave
EnhancedInputAction null and DT_SL_InputActions resolves the glyph normally.
Both paths are live in the project as of 2026-08-28. Prompts here use the UInputAction path;
WBP_SL_Button.TriggeringInputAction and USLModalWidget.ConfirmInputAction use the DataTable path
(DT_SL_InputActions :: Confirm). Neither is wrong — just never set both on one widget.

#Step 2 — Build WBP_SL_CommandBar (the strip)

  1. Open WBP_SL_CommandBar (already exists as the old placeholder — replace its contents).
2. Delete the placeholder TextBlocks (`B BACK= OPTIONSR SOCIAL`).
  1. Tree:

   HorizontalBox (root, Right-aligned or Fill to taste)

   ├─ WBP_SL_CommandPrompt  "Accept"   InputAction = IA_SL_UI_Confirm,  PromptLabel = "Select"

   ├─ Spacer (size ~24)

   └─ WBP_SL_CommandPrompt  "Back"      InputAction = IA_SL_UI_Back,     PromptLabel = "Back"

  • Order right→left is a Halo convention (Back rightmost). Flip if you prefer.
    • Set each prompt's two exposed vars in the Details panel per the table above.
  1. Style the root (padding, background) with the angled Halo panel material if desired.
  1. Compile + Save.
Labels: IA_SL_UI_Back.ActionDescription is "Back" and IA_SL_UI_Confirm.ActionDescription is
"Select". You can either type the label into PromptLabel (simplest, done above) or drive it from the
action's description later. Hardcoding the text here is fine.

#Step 3 — Place it so it appears with a menu

The bar must be present/constructed while a menu is up (that's when the glyph can resolve — see the footgun). Two options; A is recommended for reliability and simplicity.

Drop WBP_SL_CommandBar into the bottom of each screen's tree (WBP_SL_PauseMenu, WBP_SL_Settings, …), anchored bottom-center or bottom-right.

  • Pro: constructed exactly when the screen activates → the UI input context (IMC_SL_UI) is already
  • applied, so the glyph resolves correctly. Visible iff the screen is visible — no visibility logic.

  • Pro: per-screen freedom — a confirm modal can show Select + Back, a pure info screen just Back.
  • Con: one instance per screen (trivial — it's a single dropped widget; the vars are already set inside
  • WBP_SL_CommandBar).

#Option B — one shared bar in the layout

Place WBP_SL_CommandBar in WBP_SL_PrimaryGameLayout's root Overlay (Overlay_54), as the last child (top z-order), Vertical = Bottom. Then in the layout graph:

  • Bind MenuStack → On Displayed Widget Changed: set WBP_SL_CommandBar visibility to Visible when a widget
  • is active, Collapsed when none.

  • Also re-resolve the glyph on show (call the prompt's Glyph → Set Enhanced Input Action again, or a
  • small RefreshGlyph function) — a bar constructed before IMC_SL_UI is applied may cache an empty glyph. This extra refresh is the price of the shared approach, and the reason A is recommended.

#Step 4 — Verify in PIE (the gate)

  1. PIE with a gamepad.
  1. Press Start → pause menu opens.
  1. Expected: the strip shows Ⓐ Select and Ⓑ Back at the bottom.
  1. Press B → menu closes → strip goes away (with the screen, Option A).
  1. On keyboard/mouse (move the mouse to flip the active device): see the note below — this step is what
  2. the 2026-08-28 contradiction is about. As written, the strip hides; as configured, it should swap to key-caps.

Pass criteria: strip appears with the menu on a gamepad (both glyphs render) and B closes. The KBM half of the criteria is unsettled — resolve the contradiction at the top of this doc first.


#Gamepad-only visibility

⚠ Superseded in config, still present in the Blueprint (2026-08-28). The ini reversed this decision and
registered CD_SL_KBM so the bar can show key-caps on mouse/keyboard, but WBP_SL_CommandBar still runs the
collapse graph described here. This section documents what the widget currently does, not what the project
has decided it should do. Read it as the thing to delete if the reversal stands.

The original reasoning: the command bar is meaningless on mouse/keyboard (the player clicks directly and Esc is obvious), so it's hidden whenever the active device is KBM. This is driven off the UCommonInputSubsystem — not the input actions or the CommonActionWidget:

  • OnInputMethodChanged — a BlueprintAssignable delegate on the subsystem; fires with the new
  • ECommonInputType when the device switches.

  • GetCurrentInputType() — reads the device right now (for the initial state).

WBP_SL_CommandBar graph (Event Construct):

  1. Get Owning Local Player → Get Local Player Subsystem (Class = CommonInputSubsystem). *(That node is
  2. hidden if the palette's Context Sensitive box is checked — uncheck it, or hold Ctrl while searching.)*

  1. Is Valid guard → Bind Event to On Input Method Changed → a custom event.
  1. Both the custom event and a one-time Get Current Input Type on construct funnel through a single
  2. SetInputType/refresh function that sets the prompt visibility: Gamepad → Visible, everything else → Collapsed. Call it from both paths so the initial state is right (the delegate only fires on change).

Do the bind on Event Construct, not Pre-Construct — Pre-Construct also runs in the designer with no valid
local player. Make the refresh a symmetric if/else (re-shows on returning to a pad, not just hide-on-KBM).

Because the bar never draws on KBM, no CD_SL_KBM glyph set is needed — only CD_SL_Xbox is registered. No longer true. CD_SL_KBM was re-authored and both controller data assets are registered in Config/DefaultGame.ini. If the collapse graph above is removed, the KBM key-caps resolve on their own — CommonActionWidget picks the glyph set from the active input type with no extra wiring.


#Troubleshooting / footguns

  • Glyph blank on gamepad too → the CommonActionWidget resolved before IMC_SL_UI was live. CommonUI
  • resolves glyphs via QueryKeysMappedToAction, which only sees currently-applied input contexts. Option A (embed in screen) avoids this because the screen applies Menu input on activate before its children resolve. If on Option B, add the RefreshGlyph call from Step 3B.

  • Designer preview is not proof. UCommonActionWidget::GetIcon() has an editor-only branch that renders
  • DesignTimeKey and bypasses the runtime path — it will show a perfect glyph even when the runtime one is broken. Validate in PIE only.

  • Glyph is the wrong button → check the prompt's InputAction var points at the right IA_SL_UI_* action,
  • and that action is mapped in IMC_SL_UI.

  • Nothing shows at all (Option B) → visibility binding not firing, or the bar is under an opaque menu
  • background (it's not the last child of the root Overlay).

  • Don't wire click/behavior into these prompts. They are pure display. B and A already function via
  • bIsBackHandler / the default click action. A command prompt is not a button.

#Cleanup

  • ~~WBP_SL_BoundActionButton (built for the dynamic bar) is not used by this approach. Keep it parked
  • for the MenuKit upgrade below.~~ It is gone — no asset by that name exists under Content/ as of 2026-08-28. The MenuKit upgrade below has to rebuild it; see the note in that section for what it must be.

  • The old placeholder text in WBP_SL_CommandBar is replaced in Step 2.

#Later: the dynamic bar as a MenuKit upgrade

For SystemLink the game, hardcoded Back/Accept is the right call — simpler, and it moves us toward the Settings-menu gate instead of re-fighting the CommonUI glyph/router plumbing.

For MenuKit the Fab product, the dynamic CommonBoundActionBar is the more valuable, expected form — buyers' screens have varied actions and want the bar to populate itself. Swapping the hardcoded strip for the dynamic bar is a self-contained later task, not a rewrite. Treat it as a MenuKit feature, not a SystemLink blocker.

What the upgrade actually costs (WBP_SL_BoundActionButton is gone, so this is from scratch):

  • UCommonBoundActionBar is a UDynamicEntryBoxBase. It asks CommonUI's action router what is bound right
  • now and spawns one entry per binding registered with bDisplayInActionBar = true, rebuilding on binding changes and on input-device changes.

  • Its ActionButtonClass carries meta = (MustImplement = CommonBoundActionButtonInterface), so the entry
  • must derive from UCommonBoundActionButton — a plain UserWidget like WBP_SL_CommandPrompt will not compile into that slot. The entry needs a Text_ActionName (CommonTextBlock) BindWidget; the bar calls SetRepresentedAction() on it to fill in glyph and label.

  • Useful defaults: bDisplayOwningPlayerActionsOnly (true — split-screen hygiene) and
  • bIgnoreDuplicateActions.

The rule of thumb for when it's worth it: hardcode while a screen's action set is fixed forever (a two-button confirm dialog), go dynamic when actions appear and disappear at runtime (a Settings page whose Reset prompt only exists while something is dirty).

Prior revision of this doc described that dynamic-bar wiring in full; it's preserved in git history if the
MenuKit upgrade needs it.