UI Showcases

CrimsonCore is the aggregator layer - the only place allowed to mix CrimsonInput with a feature plugin's UI in one class. The widgets here are reference implementations that demonstrate the recommended wiring pattern; subclass them in Blueprint to use as-is, or copy them as a starting point for project-specific UIs.

Cardinal rule preserved
CrimsonSkillTree (and every other Tier-1 plugin) is free of any CrimsonInput dependency. The cross-plugin wiring lives here, where it's allowed.

UCrimsonCoreSkillTreeScreen

The ready-made skill tree screen. It is a UCrimsonActivatableWidget (CrimsonUI) that hosts a UCrimsonCoreSkillTreeGraph canvas, resolves the player's UCrimsonSkillTreeManager, drives a UCrimsonCoreSkillTreeViewModel for the detail panel, and owns all input. It lives here rather than in CrimsonSkillTree because it needs CrimsonUI, CrimsonInput and the MVVM stack at once - which only CrimsonCore is allowed to combine.

Input binds on Enhanced Input, not CommonUI UI-actions
Every binding goes on the owning controller's UEnhancedInputComponent. That gives one consistent path for analog axis values and press/hold events while the menu holds mouse capture. CommonUI remains the activation/screen framework but is out of the input path, and the screen deliberately does not take focus - CapturePermanently keeps focus on the viewport so the capture cannot collapse on a click.

Configurable properties

PropertyCategoryPurpose
UIInputConfigInputTagged input config. Its UIInputActions array must contain an entry per tag below.
ZoomTagInputAxis1D action - wheel delta.
PanMoveTagInputAxis2D action - pointer delta. Only applied while pan is active.
PanToggleTagInputDigital action - held to enable panning.
PrimaryTagInputDigital action - runs RequestPrimaryActionOnTarget.
SecondaryTagInputDigital action - runs RequestSecondaryActionOnTarget.
DefaultSkillTreeTypeTagSkill TreeWhich configured tree to display on activation.
ZoomSpeedFactorZoomPer-notch zoom step (default 0.07).
MinZoomScale / MaxZoomScaleZoomClamp range (defaults 0.75 / 1.75).
InitialCanvasScaleZoomScale applied once the graph reports ready. 0 leaves the engine default.
Required BindWidget
The Blueprint's widget tree must contain a UCrimsonCoreSkillTreeGraph named `SkillTreeGraph`. It is a hard BindWidget, so the Blueprint will not compile without it.

Step 1 - Author UI input actions

In your UCrimsonInputConfig asset, add five entries to the UI Input Actions array, each tagged with a UIInputTag. The screen resolves its UInputAction objects from these tags at activation, so rebinding is pure data - any key, mouse button, wheel axis or gamepad control works.

text
DA_InputConfig_PlayerUI
UIInputActions[0].InputTag = UIInputTag.SkillTree.Zoom # Axis1D (mouse wheel)
UIInputActions[0].InputAction = IA_SkillTree_Zoom
UIInputActions[1].InputTag = UIInputTag.SkillTree.PanMove # Axis2D (mouse XY)
UIInputActions[1].InputAction = IA_SkillTree_PanMove
UIInputActions[2].InputTag = UIInputTag.SkillTree.PanToggle # digital (RMB hold)
UIInputActions[2].InputAction = IA_SkillTree_PanToggle
UIInputActions[3].InputTag = UIInputTag.SkillTree.Primary # digital (LMB / gamepad accept)
UIInputActions[3].InputAction = IA_SkillTree_Primary
UIInputActions[4].InputTag = UIInputTag.SkillTree.Secondary # digital (RMB click / gamepad back)
UIInputActions[4].InputAction = IA_SkillTree_Secondary

Step 2 - Create the widget Blueprint

Right-click in the Content Browser -> User Widget -> parent class UCrimsonCoreSkillTreeScreen. Place a UCrimsonCoreSkillTreeGraph in the widget tree and name it SkillTreeGraph. In Class Defaults, set the Input Config plus the five tags, and the default skill tree type tag. Push the screen onto your CommonUI Menu layer.

That is the whole integration. NativeOnActivated binds the Enhanced Input actions and NativeOnDeactivated unbinds them; the screen resolves the manager, populates the canvas and binds the view model on its own.

How pan works

Pan is a held gesture expressed as two bindings rather than a polling timer. PanToggle (Started / Completed) flips an active flag, and PanMove delivers an Axis2D pointer delta which is applied only while that flag is set. Because the delta arrives with the input event, there is no cursor polling and no tick rate to tune.

BindingTriggerHandlerEffect
PanToggleTagStartedHandlePanPressedMarks pan active.
PanToggleTagCompletedHandlePanReleasedMarks pan inactive.
PanMoveTagTriggered (Axis2D)HandlePanMoveEIApplies the delta via PanByViewportDelta, ignored unless pan is active.

How zoom works

UCrimsonCoreSkillTreeGraph::ApplyZoom is a pure math primitive - a multiplier plus a screen-space anchor, with no speed or clamp policy. The screen owns the policy: it converts the wheel delta into a multiplier using ZoomSpeedFactor, clamps the resulting scale between MinZoomScale and MaxZoomScale, then passes the effective multiplier down.

cpp
const float WheelDelta = Value.Get<float>();
const float CurrentScale = SkillTreeGraph->GetCanvasRenderScale();
const float RawMultiplier = (WheelDelta > 0.f) ? (1.f + ZoomSpeedFactor)
: (1.f / (1.f + ZoomSpeedFactor));
const float Desired = FMath::Clamp(CurrentScale * RawMultiplier, MinZoomScale, MaxZoomScale);
const float Effective = Desired / CurrentScale;
ZoomAtScreenPosition(Effective, MousePos);

InitialCanvasScale is applied separately through SetCanvasRenderScale when the graph broadcasts OnGraphReady.

Node widgets: any base class you like

The graph spawns node and decoration widgets as plain UUserWidget and talks to them only through CrimsonSkillTree's ICrimsonSkillTreeNodeWidget / ICrimsonSkillTreeDecorationWidget contracts, so a project may build nodes on any widget base. See CrimsonSkillTree -> How-To: Use Your Own Node Widget Class.

`UCrimsonCoreSkillTreeNodeButton` is the worked example shipped here: a node built on UCrimsonButtonBase instead of on any CrimsonSkillTree class, which is what gives it gamepad focus, navigation and per-input-method styling for free. Assign it like any other node widget class, via GraphConfig.DefaultNodeWidgetClass or NodeTypeToWidgetClassMap.

It reports, the screen decides
The button broadcasts OnSkillNodeSelected / OnSkillNodeActivated and resolves no action itself. The graph forwards those to OnNodeWidgetSelected / OnNodeWidgetActivated, and the screen turns them into SetTargetNode + RequestPrimaryActionOnTarget. Action resolution stays in one place, so the mouse and gamepad paths cannot drift apart.

Non-CommonUI projects

The action API is input-agnostic and public: ZoomAtScreenPosition, PanByViewportDelta, RecenterOnRoot, RecenterOnContent, SetTargetNode, RequestPrimaryActionOnTarget, RequestSecondaryActionOnTarget and RequestActionOnNode are all BlueprintCallable. If you would rather not use this screen, host UCrimsonCoreSkillTreeGraph in your own widget and drive that same API from wherever your input lives.

Source location
Plugins/CrimsonCore/Source/CrimsonCore/Public/UserInterface/SkillTree/ - Screens/CrimsonCoreSkillTreeScreen.h, Graph/CrimsonCoreSkillTreeGraph.h, Nodes/CrimsonCoreSkillTreeNodeButton.h, ViewModels/CrimsonCoreSkillTreeViewModel.h, with the matching .cpp files under Private/UserInterface/SkillTree/.

UCrimsonCoreCastBarWidget

A HUD cast / channel bar. Binds the owning player's ASC OnAbilityProgress (CrimsonAbilitySystem), and interpolates the fill locally from the message's StartWorldTime - the ability broadcasts only a start and a stop, never a per-tick stream. Register it at the HUD.Slot.CastBar extension point through UCrimsonGameFeatureAction_AddWidget.

MemberPurpose
On Cast Bar Shown(Message)A cast or channel began. Read Kind to pick a presentation; Duration <= 0 means an indeterminate channel.
On Progress Visual(Progress01, Elapsed, Remaining)Every frame while active. Progress01 is 0 for an indeterminate channel - use Elapsed.
On Cast Bar Ended(bWasInterrupted)The segment ended and the widget is still visible. Play a finish animation here.
On Cast Bar Outro AbortedA new cast preempted the finish animation. Stop it; On Cast Bar Shown fires immediately after.
Finish Cast Bar Outro()Call when the finish animation completes. Collapses the bar.
Outro Timeout SecondsCeiling on how long the bar waits for that call. 0 hides immediately - raise it to use an outro at all.
bBindInputActionToClickOff by default, and leave it off for an action bar. CrimsonInput already binds ability input on the player's EnhancedInputComponent, so turning this on registers a second binding for the same key and one press activates twice. Off still uses the Input Action - just for the glyph.
GetSlotInputAction()The Input Action the ability is currently bound to, resolved through UCrimsonInputConfig from the spec's tags. Follows a rebind; null when nothing is bound.
Why the outro is bounded
Handing Blueprint an open-ended 'hide me when you are done' pins the bar on screen forever the first time an animation path forgets to call back, and that failure is silent. Outro Timeout Seconds is a safety ceiling, not the intended duration - set it just above your longest finish animation.

Finish Cast Bar Outro is deliberately a no-op outside an outro, because an animation-finished delegate can still fire after a new cast took the bar over.
The owning player's casts only
The progress message is a local broadcast - it reaches the server and the predicting client, never other machines. This widget cannot show another player's or an NPC's cast, and that gap is invisible on a listen-server host. Verify from a remote client.

UCrimsonCoreAbilityFailureWidget

Surfaces 'why did nothing happen' feedback - out of resource, still on cooldown, silenced, out of charges. Listens on TAG_ABILITY_SIMPLE_FAILURE_MESSAGE, filtered to the owning player, and calls On Failure Visual(Reason, FailureTags). Register it at HUD.Slot.Toasts.

Reason can be empty
Reason is the designer-authored string from the ability's FailureTagToUserFacingMessages map. An ability with no message configured for that failure tag sends an empty text - check before showing it. FailureTags lets you style cooldown differently from out-of-resource.

UCrimsonCoreAbilityStateWidget

One action-bar slot: icon, cooldown sweep, cost and block states, charges. Derives from UCrimsonButtonBase (CrimsonUI), so CommonUI focus, hover, gamepad navigation and the input-action-widget hookup come for free rather than being reimplemented.

MemberPurpose
SlotInputTagHand-authored fallback for a bar not driven by the Fixed Slot UI. Matched with HasTagExact against the spec's dynamic source tags - the exact key UCrimsonAbilitySet writes at grant and AbilityInputTagPressed matches on, so the button and the keypress cannot point at different abilities.
SetAbilitySpecHandle(Handle)Deliberate override; always wins. For anything not driven by input: a tooltip, a skill tree node, an inspect panel.
bClickToActivateClicking activates through AbilityInputTagPressed, so a click and a keypress take the identical path. Turn it off for a read-only display; interaction is disabled, which also stops the slot taking gamepad focus.
PollRateHzHow often discrete state is re-evaluated (default 10). Polled rather than evented because GAS broadcasts nothing when a cost becomes affordable through regeneration.
GetAbilityState()Returns FCrimsonAbilityStateSnapshot: bIsGranted, bIsActive, bCanActivate, bCostMet, bOnCooldown, CooldownRemaining/Duration/Progress01, bUsesCharges, CurrentCharges, MaxCharges, FailureTags.
OnAbilityStateChangedFires only when discrete state actually moved, plus once on construct. Rebuild visuals here without guarding against per-frame spam.
OnCooldownProgressFires every frame while a cooldown runs. Progress01 rises 0 -> 1; drive a radial sweep with it.
OnIconResolvedFires when the icon becomes known, including after a loadout swap. Icon is null when the ability authored no display data - draw your empty-slot treatment.
OnActivationRefusedThe slot was clicked but the ability refused. Carries the failure tags for a shake or an error beep.
Binding pathWhen it applies
SetAbilitySpecHandleExplicit override. Always wins.
Fixed Slot UI contextThe ability registered the slot itself. Delivered via ICrimsonUIExtensionWidget::SetExtensionPayload. The intended path for an action bar.
SlotInputTagHand-authored bar not using the Fixed Slot UI.
SetFixedSlotContext(Payload)Blueprint escape hatch. Rarely needed - the interface delivers the payload automatically.
Use the Fixed Slot UI, not a hand-built bar
The intended way to place these is CrimsonAbilitySystem's Fixed Slot UI, not dropping slots into a HUD layout by hand. Tick bUseFixedSlotUI on the ability, set FixedSlotExtensionPointTag, and name your slot Blueprint in FixedSlotWidgetClass. The ability then registers a UCrimsonFixedAbilitySlotContext at that tag and the widget is spawned for you - so a granted ability brings its bar entry with it, and a removed one takes it away.

Two setup steps that fail silently if missed: add CrimsonFixedAbilitySlotContext to the extension point's DataClasses array (the payload is filtered out otherwise and nothing spawns), and make sure the ability's tag and the extension point's tag match exactly under ExactMatch.

The context's OnCooldownStarted, OnChargesChanged and OnIdentityChanged delegates drive immediate refreshes on top of the poll. OnIdentityChanged is the one that matters most: an augment can swap which ability backs the slot, and the icon follows.
Charges need no separate widget
A charge ability is this button with a stack count and a recharge sweep that runs while the ability is still usable - 2 of 3 charges means bOnCooldown and bCanActivate are both true. The state model already treats those as independent axes, so charges cost two extra fields rather than a subclass. Check bUsesCharges and show your pip row. Subclass in Blueprint if you want distinct visuals; the state model needs no change.
It reads the instance, not the CDO
Cost and cooldown are resolved from the granted ability instance (GetPrimaryInstance), falling back to the CDO only when there is no instance. Ability-scoped values - an augment-modified cost or cooldown - live in the instance's PropertyCache, so reading the CDO would silently show the unaugmented numbers.
Key glyphs resolve through CrimsonInput
You do not need to build glyph handling. UCommonButtonBase already carries an optional InputActionWidget and UCrimsonButtonBase already refreshes it when the input method changes - all that was missing was the action.

The slot resolves that action dynamically, per poll: it takes the tags on the resolved ability spec and asks UCrimsonInputConfig::FindAbilityInputActionForTag about each one, using the config on the pawn's UCrimsonPawnData - the same config the hero component actually binds. It deliberately does not read the ability's authored FixedSlotInputAction, because that is a fixed value in the asset and cannot follow a hotbar rebind. Rebind the ability and the glyph changes on the next poll.

To use it: add a CrimsonActionWidget to your slot Blueprint named exactly InputActionWidget (a BindWidget with OptionalWidget = true, so omitting it just means no glyph).

The action is pushed with SetEnhancedInputAction, which resolves a glyph and registers no binding - the key keeps activating the ability through CrimsonInput alone. Glyph caveat: an Enhanced Input action only resolves to a key while it is in a currently added Input Mapping Context.
Source location
CrimsonCore/Public/UserInterface/Ability/CrimsonCoreAbilityStateWidget.h. Display data comes from CrimsonAbilitySystem -> API Reference -> Ability display data.