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 extendUEternalButtonBase(one WBP +UCommonButtonStyleasset per visual family). TheIToolTipinterface integrates with Unreal's native tooltip system. Input routing uses Enhanced Input throughUEternalInputSubsystem, with mouse events broadcast as delegates for widget subscription.
Table of Contents¶
- Why CommonUI
- Gamepad Architecture
- Widget Hierarchy
- Input Handling
- Tooltip System
- Drag-Drop Support
- Widget Binding Patterns
- API Reference
- Source References
- Related Systems
- Recent Changes
Why CommonUI¶
Design Goals¶
- Input Consistency: Same interaction patterns across mouse, keyboard, gamepad
- Platform Parity: Widgets work correctly on PC, console, and handheld
- Native Tooltip Support: IToolTip interface hooks into Unreal's tooltip system
- 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 appliesGetDesiredInputConfig()(mandatory: a lingering Menu config after close kills game input).bIsBackHandlermust be set in the constructor, beforeSuper::NativeConstruct. - Gamepad focus lands on
DefaultFocusWidget(BindWidgetOptional) or aNativeGetDesiredFocusTarget()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
Stoprules on the root inNativeOnInitialized(Slate nav is geometric and unscoped);bFenceDirectionalNavigation = falseopts a panel out. Full-screen menu surfaces route Back throughHandleBackByOpeningGameMenu()so the modal-guard rule has one owner.
UEternalButtonBase (buttons)¶
UCommonButtonBase subclass; plain UButton is retired for interactive UI.
- One WBP subclass + one
UCommonButtonStyleasset per visual family:WBP_Btn_MenuPlate,WBP_Btn_MainMenu,WBP_Btn_StripIcon(icon-only, viaUEternalIconButton),W_DialogueButton(viaUDialogueOptionButton), 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
CommonTextStyleasset (MainMenuText,DialogueOptionText, …): aCommonTextBlockwithout 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.Debugdraws 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 |
Related Systems¶
- UI Architecture - Subsystem and controller setup
- MVVM Framework - ViewModel binding
- Widget Library - Widget implementations
- Inventory System - Item drag-drop
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 |