Skip to content

CommonUI Integration

Summary: Project Eternal uses CommonUI for gamepad support and consistent input handling. Panels and screens extend UEternalActivatablePanel (activatable Back handling, focus targets, input configs); interactive buttons extend UEternalButtonBase (one WBP + UCommonButtonStyle asset per visual family). The IToolTip interface integrates with Unreal's native tooltip system. Input routing uses Enhanced Input through UEternalInputSubsystem, with mouse events broadcast as delegates for widget subscription.

Table of Contents


Why CommonUI

Design Goals

  1. Input Consistency: Same interaction patterns across mouse, keyboard, gamepad
  2. Platform Parity: Widgets work correctly on PC, console, and handheld
  3. Native Tooltip Support: IToolTip interface hooks into Unreal's tooltip system
  4. Focus Management: CommonUI handles focus chains and navigation

Key Tradeoffs

Decision Benefit Cost
CommonUI for Interactive Consistent input handling Additional dependency
UUserWidget for Containers Lighter weight for layout-only Manual input handling if needed
IToolTip Interface Native tooltip timing and positioning Must implement all interface methods
Delegate Broadcasts Widgets decouple from input system Extra subscription boilerplate

Gamepad Architecture

Built for the Steam Deck / gamepad effort (plan: ImplementationDocs/archive/GamepadSupport.plan.md). Two project base classes carry the whole scheme:

UEternalActivatablePanel (panels and screens)

UCommonActivatableWidget subclass; base for every gamepad-operable panel (game menu, main menu, character select, dialogue).

  • Controllers keep the existing slot model and drive ActivateWidget() after Show / DeactivateWidget() before Hide — no activatable containers.
  • Activation registers the panel's Back handler (ESC / gamepad B → NativeOnHandleBackAction) and applies GetDesiredInputConfig() (mandatory: a lingering Menu config after close kills game input). bIsBackHandler must be set in the constructor, before Super::NativeConstruct.
  • Gamepad focus lands on DefaultFocusWidget (BindWidgetOptional) or a NativeGetDesiredFocusTarget() override.
  • Overlay discipline: an overlay deactivates the screen underneath on open and reactivates it on close (two active activatables fight over focus navigation). The base fences directional nav with Stop rules on the root in NativeOnInitialized (Slate nav is geometric and unscoped); bFenceDirectionalNavigation = false opts a panel out. Full-screen menu surfaces route Back through HandleBackByOpeningGameMenu() so the modal-guard rule has one owner.

UEternalButtonBase (buttons)

UCommonButtonBase subclass; plain UButton is retired for interactive UI.

  • One WBP subclass + one UCommonButtonStyle asset per visual family: WBP_Btn_MenuPlate, WBP_Btn_MainMenu, WBP_Btn_StripIcon (icon-only, via UEternalIconButton), W_DialogueButton (via UDialogueOptionButton), preset cards (the widget IS the button).
  • CommonButtonBase cannot nest per-instance child widgets, so per-instance data goes through reflected properties the WBP tree consumes in NativePreConstruct: ButtonText → ButtonLabel, IconTexture → ButtonIcon.
  • Labels must use a CommonTextStyle asset (MainMenuText, DialogueOptionText, …): a CommonTextBlock without a Style asset falls back to CommonUI's default style and ignores inline font/color.
  • The style brush draws behind the widget tree; opaque plates hide it. Hover/focus feedback on rich widgets is an explicit overlay toggled in NativeOnHovered/Unhovered — CommonUI simulates hover on gamepad focus, so it doubles as the focus visual.

Sliders

USlider with IsFocusable=true and RequiresControllerLock=false: nav left/right steers the value in place (SSlider::OnNavigation) by StepSize, up/down leaves the row. Used by the game menu settings sliders.

Smart Cursor

FEternalSmartCursor (Source/ProjectEternal/Public/Input/EternalSmartCursor.h, an FAnalogCursor input processor owned by UEternalInputSubsystem) drives the real OS cursor over the spatial grid surfaces (inventory, equipment, vendor, crafting). It injects synthesized pointer-move events rather than calling SetCursorPos, so the shipped hover/tooltip/click/held-item pipeline runs unmodified (bare position writes fire hover but never arm Slate tooltips). It renders its own cursor visual: under gamescope/Proton the hardware cursor is never drawn for programmatic moves and writes are unclamped, so clamping to the game window is correctness, not polish.

  • Activation: UEternalInputSubsystem::ShouldSmartCursorDrive() — gamepad is the active device AND a spatial panel is open. Menu-shaped UI stays focus-navigated. Any real mouse input flips the device and suspends the drive.
  • Magnetism: item grids register via RegisterSnapGrid(); the cursor is pulled toward the nearest cell centroid (weakens with stick deflection). Seed point on activation = main container grid centre snapped to the nearest target (GetSmartCursorSeedPoint, retried next tick until geometry is valid).
  • Verbs while driving (left stick is consumed — it must not also steer the character): A = left click, X = right click (quick action; synthesized with a held Ctrl so the mouse quick-action path is reused), B = cancel held item (falls through to the panel's Back handler when nothing is held), Y = warp to the next grid group (GetSmartCursorWarpPoint, cycles bag / equipment / other container).
  • Device-flip shielding: moves and synthesized clicks go through CommonInput's shielded path so the injected events can't flip the active device back to mouse every tick.
  • Tuning knobs live in UEternalInputSettings; Eternal.SmartCursor.Debug draws the position/clamp-rect readout.

Loot focus and nameplates (world-space, pad-aware)

ULootFocusComponent (on the AEternalPlayer PlayerController) keeps a registry of nearby AItemActors and picks one focused item with hysteresis; the right stick flicks browse items in screen-space direction when no target is locked (DetectAimFlick). The focused nameplate (UItemNameplateComponent, a screen-space UWidgetComponent) shows the pad pickup glyph + gold border only on gamepad (mouse hover keeps the authored button tint), and ULootTooltipDockWidget docks the focused item's tooltip in the viewport corner. Nameplates are de-overlapped upward by ULootLabelLayoutSubsystem. Full detail: Loot System — Pickup Interaction.


Widget Hierarchy

+---------------------------+
|       UUserWidget         |   Unreal Base
+---------------------------+
            |
            +---------------------------------------+
            |                                       |
            v                                       v
+---------------------------+           +---------------------------+
|    UCommonUserWidget      |           |       UUserWidget         |
+---------------------------+           |       (Standard)          |
     |                  |               +---------------------------+
     v                  v               |  - Layout-only containers |
+------------------+ +------------------+  - HUD chrome (MenuStrip, |
| UCommonActivatab | | UCommonButtonBase|    PlayerHUD, Skillbar)   |
| leWidget         | +------------------+  - Grid/tooltip widgets   |
+------------------+ | UEternalButtonBa |    predating conversion   |
| UEternalActivata | | se               |    (ItemWidget, GlyphWall)|
| blePanel         | |  - EternalIconBtn|+---------------------------+
|  - GameMenu      | |  - DialogueOptBtn|
|  - MainMenu      | |  - PresetCard    |
|  - CharSelect    | +------------------+
|  - Dialogue      |
+------------------+

Selection Criteria

Use UEternalActivatablePanel Use UEternalButtonBase Use Standard UserWidget
Full panel/screen the pad must operate Any clickable/confirmable element Layout-only containers
Needs Back (ESC / pad B) handling Needs gamepad focus + Confirm HUD chrome without focus
Owns a focus target for pad entry Rich widget acting as a button Purely visual widgets

Input Handling

Input Flow Architecture

Player Input Device
        |
        v
+------------------------+
| Enhanced Input System  |
+------------------------+
        |
        v
+------------------------+
| UEternalInputSubsystem |
| (LocalPlayerSubsystem) |
+------------------------+
        |
        +---> UI Toggles --> Controllers --> Widgets
        |
        +---> Mouse Clicks --> Broadcast Delegates
        |                           |
        |                           v
        |                    [Subscribed Widgets]
        |                      - ItemWidget
        |                      - CraftingWidget
        |                      - etc.
        |
        +---> Gameplay --> PlayerController/Pawn

Mouse Event Broadcasting

Why Broadcasts? Widgets don't need direct coupling to input system. They subscribe to what they care about.

Input Event Flow:
+---------------------------+
| Player clicks LMB         |
+---------------------------+
            |
            v
+---------------------------+
| EnhancedInputComponent    |
| triggers LMB action       |
+---------------------------+
            |
            v
+---------------------------+
| EternalInputSubsystem     |
| HandleLeftMouseClick()    |
+---------------------------+
            |
            v
+---------------------------+
| OnLeftMouseClick          |
| .Broadcast()              |
+---------------------------+
            |
     +------+------+
     |             |
     v             v
[Widget A]    [Widget B]
OnLeftClick   OnLeftClick

Widget Mouse Subscription

Widgets subscribe in NativeConstruct, unsubscribe in NativeDestruct:

Widget Subscription Lifecycle:
+---------------------------+
| NativeConstruct()         |
|   Get LocalPlayer         |
|   Get InputSubsystem      |
|   AddDynamic(OnLeftClick) |
|   AddDynamic(OnRightClick)|
+---------------------------+
            |
            v
      [Widget Active]
            |
            v
+---------------------------+
| NativeDestruct()          |
|   RemoveDynamic(...)      |
+---------------------------+

Tooltip System

Anchored Tooltips

Tooltips do not follow the cursor. UEternalGameViewportClient owns an SAnchoredTooltipPresenter (a non-ticking Slate overlay) that places the tooltip beside the element that owns it and keeps it there while the element stays hovered. Key pieces (all under UI/Tooltip/):

Piece Role
ITooltipAnchor (AnchoredTooltipTypes.h) Implemented by the UMG widget that owns the tooltip. Returns an ETooltipSide preference (Auto, Right, Left, Above, Below). Lives on the UObject because SObjectWidgets are rebuilt whenever a widget re-enters the tree. Implementors: UEternalButtonBase, UItemWidget (right of the slot — grids read left to right), USkillSlotWidget (above — the bar hugs the bottom edge).
EternalTooltipPlacement::Compute (TooltipPlacement.h) Pure placement in one coordinate space: preferred side → opposite → the remaining two; slides along the free axis; clamps over the anchor only when nothing fits. Never covers the cursor rect while any side fits (a covered neighbour would re-hover). Keeps the current side while it still fits so detail-peek growth does not flap. Unit-tested in Tests/UI/TooltipPlacement.spec.cpp.
SAnchoredTooltipPresenter (AnchoredTooltipSlate.cpp) Hosts the content, runs a SlatePrepass on fresh content so the first placement uses a real desired size, and re-places on layout change.

Cvar: Eternal.UI.TooltipGap (pixels between tooltip and anchor, default 8). There is no follow-cursor fallback switch any more; the anchored presenter is the only tooltip path.

To give a new widget an anchored tooltip: implement ITooltipAnchor on the UMG class, return the side that reads naturally for where the widget lives on screen, and set the tooltip widget as usual — the presenter handles the rest. Prefer Auto unless the widget sits on a screen edge.

Tooltip Widget Lifecycle

The three tooltip widgets (UItemTooltipWidget, USkillTooltipWidget, UStatusEffectTooltipWidget) are plain UCommonUserWidgets. They are handed to UMG through UWidget::SetToolTip(Widget), which wraps them in the engine's SToolTip; the widget itself does not implement IToolTip (an earlier version did, and pinned its own Slate widget in a member, which leaked the Slate tree — the engine never consulted that implementation).

Step Who What
Create owning widget (item slot, skill slot, status icon) CreateWidget, InitializeFromData(TooltipData) builds the view model and binds fields
Show engine SToolTip via the anchored presenter delay, placement (see Anchored Tooltips)
Hide engine SetToolTip(nullptr) on mouse leave releases the wrapper
Destroy NativeDestruct → OnClosed() unbinds the view model and unregisters the instance from its controller

A tooltip widget is single-use: OnClosed() tears down its bindings and the widget is not re-shown afterwards.

While an item is held on the cursor every tooltip is suppressed (UEternalGameViewportClient::ShouldSuppressTooltips, consulted by SAnchoredTooltipScope): the held visual is the thing under the cursor, not the slot beneath it.

Tooltip Display Flow

Mouse enters widget
        |
        v
+---------------------------+
| UItemWidget               |
| NativeOnMouseEnter()      |
+---------------------------+
        |
        v
+---------------------------+
| UpdateTooltip()           |
|   Create TooltipWidget    |
|   InitializeFromData()    |
|   SetToolTip(Widget)      |
+---------------------------+
        |
        v
+---------------------------+
| Unreal Tooltip System     |
|   Applies delay           |
|   Anchored presenter      |
|   places beside the slot  |
+---------------------------+
        |
        v
+---------------------------+
| TooltipWidget visible     |
+---------------------------+

Mouse leaves widget
        |
        v
+---------------------------+
| NativeOnMouseLeave()      |
|   SetToolTip(nullptr)     |
+---------------------------+
        |
        v
+---------------------------+
| Unreal calls OnClosed()   |
+---------------------------+

Drag-Drop Support

Drag-Drop Architecture

+---------------------------+
|     Drag Source           |  (e.g., ItemWidget)
+---------------------------+
            |
            | Create UDragDropOperation
            | with Payload
            v
+---------------------------+
|   UDragDropOperation      |
|   - Payload: UItemDragDropPayload
|   - DefaultDragVisual     |
|   - Pivot                 |
+---------------------------+
            |
            v
+---------------------------+
|     Drop Target           |  (e.g., GlyphSocketWidget,
|                           |   ItemDropCatcherWidget)
+---------------------------+
            |
            v
+---------------------------+
| NativeOnDrop()            |
|   Extract payload         |
|   Validate drop           |
|   Execute action          |
+---------------------------+

UItemDragDropPayload

Carries item data during drag operations:

Property Type Purpose
Item UItemObject* The dragged item
ItemContainerComponent UItemContainerComponent* Source container
TopLeftIndex int32 Grid position in source

Drop Targets

Widget Accepts Action
UItemDropCatcherWidget Any item Drop item to world
UGlyphSocketWidget Glyph items Socket glyph in plate
InventorySlot Any item Move/swap items
EquipmentSlot Equipment Equip item

Full-Screen Drop Catcher

The ItemDropCatcherWidget covers the viewport to catch drops that miss valid targets:

+------------------------------------------+
|           ItemDropCatcherWidget           |
|  (Full viewport, lowest Z-order)         |
|                                          |
|    +---------------------------+         |
|    |    Inventory Panel       |         |
|    |    (Higher Z-order)      |         |
|    |                          |         |
|    |  [Item] [Item] [Item]    |         |
|    |  [Item] [Item] [Item]    |         |
|    +---------------------------+         |
|                                          |
|  Drop here = item goes to world          |
+------------------------------------------+

Widget Binding Patterns

BindWidget Meta Specifier

Widgets declare child widget references with automatic UMG binding:

Meta Specifier Behavior
BindWidget Required - widget must exist in UMG
BindWidgetOptional Optional - can be null

Child Widget Binding

Widget Class Hierarchy:
+--------------------------------+
| C++ Widget Class              |
|   UPROPERTY(BindWidget)       |
|   UTextBlock* ItemNameText;   |
+--------------------------------+
            |
            v
+--------------------------------+
| Blueprint Widget (BP_MyWidget)|
|   [ItemNameText] TextBlock    |
|   (Name matches property)     |
+--------------------------------+

Button Event Binding Pattern

Binding Flow:
+--------------------------------+
| NativeConstruct()              |
|   If (CraftButton)             |
|     CraftButton->OnClicked     |
|       .AddDynamic(this,        |
|         &OnCraftButtonClicked) |
+--------------------------------+
            |
            v
+--------------------------------+
| User clicks button             |
+--------------------------------+
            |
            v
+--------------------------------+
| OnCraftButtonClicked()         |
|   Call controller method       |
+--------------------------------+
            |
            v
+--------------------------------+
| NativeDestruct()               |
|   (Delegates auto-cleaned)     |
+--------------------------------+

API Reference

UCommonUserWidget Overrides

Method When Called Purpose
NativeOnMouseEnter() Mouse enters bounds Start hover effects, show tooltip
NativeOnMouseLeave() Mouse exits bounds End hover, hide tooltip
NativeOnMouseButtonDown() Mouse button pressed Start drag, hide tooltip
NativeOnMouseButtonUp() Mouse button released End drag
NativeOnDrop() Drop operation completes Handle dropped item
NativeOnDragEnter() Drag enters bounds Show drop preview
NativeOnDragLeave() Drag exits bounds Hide drop preview

Tooltip Widget Methods

Method Return Purpose
InitializeFromData(TooltipData) void Build the view model and bind fields for one item / ability / effect
OnClosed() void Unbind and unregister from the controller; called from NativeDestruct

UEternalInputSubsystem Delegates

Delegate Signature Purpose
OnLeftMouseClick FOnLeftMouseClick LMB press
OnRightMouseClick FOnRightMouseClick RMB press

Common Widget Properties

Type Properties
UCommonTextBlock Text rendering with CommonUI styling
UCommonRichTextBlock Formatted text with styling support
UCommonButtonBase Button with CommonUI input handling

Source References

Class File Line
UItemWidget Source/ProjectEternal/Public/UI/Widgets/Items/ItemWidget.h 1
UItemDropCatcherWidget Source/ProjectEternal/Public/UI/Widgets/Items/ItemDropCatcherWidget.h 1
UItemTooltipWidget Source/ProjectEternal/Public/UI/Widgets/Tooltip/ItemTooltipWidget.h 1
USkillTooltipWidget Source/ProjectEternal/Public/UI/Widgets/Tooltip/SkillTooltipWidget.h 1
UGlyphSocketWidget Source/ProjectEternal/Public/UI/Widgets/Glyph/GlyphSocketWidget.h 1
UEternalInputSubsystem Source/ProjectEternal/Public/Input/EternalInputSubsystem.h 1
UItemDragDropPayload Source/ProjectEternal/Public/UI/Widgets/Items/ItemDragDropPayload.h 1


Recent Changes

Date Change Impact
2026-08-20 Smart cursor, loot focus/nameplates, Compact scaling documented FEternalSmartCursor section; pad-facing HUD chrome (UPadHintBarWidget, UPanelTabStripWidget, ULootTooltipDockWidget) is UCommonUserWidget so it can react to device changes; Compact text scale reaches CommonTextBlock only through its style asset
2026-08 Gamepad Phase 1: UEternalActivatablePanel + UEternalButtonBase family; menus, dialogue, menu strip converted New panels/buttons must follow the Gamepad Architecture section; plain UButton retired for interactive UI
2024-12 SkillTooltipController uses SetCombatComponent() for dependency injection Controller no longer discovers CombatComponent from pawn; must be explicitly provided
2024-12 SkillTooltipViewModel RefreshDynamicValues() Dynamic data (damage, costs) recalculated on demand, not on every hover