Three Weird Tricks To Get Around That Gray Disabled Effect In Slate/UMG (Tim Sweeney Hates Him!)

Unreal’s UMG framework defines a property bIsEnabled on all of their widgets, which when set to false will block inputs on the widget and apply a unique disabled style. Button widgets have a configuration for the style that is applied when they are disabled, but for most other widgets it simply darkens the contents by a certain hard-coded amount, and if you don’t want that then you can go use a different engine. To make matters worse, this disabled style is carried all the way down the hierarchy, so even if your button’s disabled state has a unique style, the text or image you put inside the button will still be darkened.

But fear not, I have devised a slew of (three) workarounds for this behavior, so you can disable UI elements with full control of the style.

Option 1: Custom Enabled/Disabled Method

Pros:

  • Can be trivially implemented in blueprints or in C++
  • Can be implemented as bespoke BP logic or integrated into a user widget

Cons:

  • Need to make sure you use the correct version of “SetEnabled”

The key intuition for this solution is that, if you are disabling a UI element, it’s probably because you don’t want it to do anything when the user interacts with it. Setting ESlateVisibility::HitTestInvisible will accomplish this goal, and then it’s up to you to pair any style changes with that.

In Blueprints:

In C++:

// MyUserWidget.cpp

void UMyUserWidget::SetButtonDisabled(UButton* button, bool bInIsDisabled) {
	if (bInIsDisabled) {
		button->SetVisibility(ESlateVisibility::HitTestInvisible);
		button->SetStyle(ButtonStyle_Disabled);
	} else {
		button->SetVisibility(ESlateVisibility::Visible);
		button->SetStyle(ButtonStyle_Enabled);
	}
}

In these examples I am interacting with a UButton widget, and controlling its style with FButtonStyle structs, but this pattern will work for any widget type. I’m also not maintaining any kind of unique property for my pseudo-enabled state – that exercise is left to the reader.

Option 2: Custom Wrapper Widget

Pros:

  • Can be easily dropped anywhere in your UMG hierarchy
  • Extensible

Cons:

  • C++ only

The intuition for this solution is that, if you follow the call stack, the “disabled” state is propagated down the SWidget::OnPaint() chain with code like this:

ESlateDrawEffect DrawEffects = (bParentEnabled && IsEnabled()) ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;

Where bParentEnabled is simply a function parameter for the parent to pass down its own value of IsEnabled(). So, if we put a widget in the hierarchy that always says what we want it to say, the children will be none the wiser when they go to draw themselves.

For this example, I opted to extend UCanvasPanel since it’s the most general-purpose panel widget, but there’s no reason this couldn’t be accomplished with any other panel widget (including e.g. buttons).

Extended Implementation

// EnabledWrapperCanvasPanel.h


class MY_API SEnabledWrapperConstraintCanvas : public SConstraintCanvas {
	bool bOverrideEnabled;
public:
	SLATE_BEGIN_ARGS(SEnabledWrapperConstraintCanvas)
		: _OverrideEnabled()
	{
	}
		/** Whether children should be displayed as enabled */
		SLATE_ARGUMENT( bool, OverrideEnabled )

		SLATE_SUPPORTS_SLOT( SConstraintCanvas::FSlot )

	SLATE_END_ARGS()

	/**
	 * Construct this widget
	 *
	 * @param	InArgs	The declaration data for this widget
	 */
	void Construct( const FArguments& InArgs );

	virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
};

UCLASS(meta = (ShortTooltip = "A canvas panel with an extra checkbox for halting propagation of the enabled state"))
class MY_API UEnabledWrapperCanvasPanel : public UCanvasPanel {
	GENERATED_UCLASS_BODY()

protected:
	// UWidget interface
	virtual TSharedRef<SWidget> RebuildWidget() override;
	// End of UWidget interface

public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Behavior")
	uint8 bOverrideEnabled :1;

	/** A bindable delegate for bOverrideEnabled */
	UPROPERTY()
	FGetBool bOverrideEnabledDelegate;

	/** Gets the current override enabled status of the widget */
	UFUNCTION(BlueprintCallable, Category="Widget")
	bool GetOverrideEnabled() const;

	/** Sets the current override enabled status of the widget */
	UFUNCTION(BlueprintCallable, Category="Widget")
	virtual void SetOverrideEnabled(bool bInOverrideEnabled);

private:
	PROPERTY_BINDING_IMPLEMENTATION(bool, bOverrideEnabled);
};
// EnabledWrapperCanvasPanel.cpp

void SEnabledWrapperConstraintCanvas::Construct(const SEnabledWrapperConstraintCanvas::FArguments& InArgs) {
	const int32 NumSlots = InArgs.Slots.Num();
	for (int32 SlotIndex = 0; SlotIndex < NumSlots; ++SlotIndex) {
		Children.Add(InArgs.Slots[SlotIndex]);
	}

	bOverrideEnabled = InArgs._OverrideEnabled;
}

int32 SEnabledWrapperConstraintCanvas::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const {
	bool isEnabled = bOverrideEnabled || ShouldBeEnabled(bParentEnabled);
	return SConstraintCanvas::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, isEnabled);
}

UEnabledWrapperCanvasPanel::UEnabledWrapperCanvasPanel(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer) {

	bOverrideEnabled = true;
}

TSharedRef<SWidget> UEnabledWrapperCanvasPanel::RebuildWidget() {
	MyCanvas = SNew(SEnabledWrapperConstraintCanvas)
		.OverrideEnabled(bOverrideEnabled);

	//Copypasta from UCanvasPanel
	for (UPanelSlot* PanelSlot : Slots) {
		if (UCanvasPanelSlot* TypedSlot = Cast<UCanvasPanelSlot>(PanelSlot)) {
			TypedSlot->Parent = this;
			TypedSlot->BuildSlot(MyCanvas.ToSharedRef());
		}
	}

	return MyCanvas.ToSharedRef();
}

void UEnabledWrapperCanvasPanel::SetOverrideEnabled(bool bInOverrideEnabled) {
	bOverrideEnabled = bInOverrideEnabled;
}

bool UEnabledWrapperCanvasPanel::GetOverrideEnabled() const {
	return bOverrideEnabled;
}

The key here is overriding OnPaint in the underlying Slate widget so we can pass a different value for bParentEnabled. Much of the code here is boilerplate to allow this override behavior to be toggled at will. If you don’t care about that, then the code can be pared down considerably:

Simple Implementation

// ForceEnabledWrapperCanvasPanel.h

class MY_API SForceEnabledWrapperConstraintCanvas : public SConstraintCanvas {
	virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
};

UCLASS(meta = (ShortTooltip = "A canvas panel that always tells its children that it's enabled"))
class MY_API UForceEnabledWrapperCanvasPanel : public UCanvasPanel {
	GENERATED_BODY()

protected:
	// UWidget interface
	virtual TSharedRef<SWidget> RebuildWidget() override;
	// End of UWidget interface
};
// ForceEnabledWrapperCanvasPanel.cpp

int32 SForceEnabledWrapperConstraintCanvas::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const {
	return SConstraintCanvas::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, true);
}

TSharedRef<SWidget> UForceEnabledWrapperCanvasPanel::RebuildWidget() {
	MyCanvas = SNew(SForceEnabledWrapperConstraintCanvas);

	//Copypasta from UCanvasPanel
	for (UPanelSlot* PanelSlot : Slots) {
		if (UCanvasPanelSlot* TypedSlot = Cast<UCanvasPanelSlot>(PanelSlot)) {
			TypedSlot->Parent = this;
			TypedSlot->BuildSlot(MyCanvas.ToSharedRef());
		}
	}

	return MyCanvas.ToSharedRef();
}

Option 3: Engine Modification

Pros:

  • Brings the sweet release that only cold vengenance can offer

Cons:

  • Could have unforeseen consequences if done naively

If it’s time to go John Wick on this stupid feature, I can tell you where the code lives. The buck stops at Engine\Shaders\Private\SlateElementPixelShader.usf, where this mind-bogglingly hard-coded snippet lives:

#if USE_MATERIALS
	const half DrawDisabledEffect = DrawFlags.x;
#else
	#if DRAW_DISABLED_EFFECT
		const half DrawDisabledEffect = 1;
	#else
		const half DrawDisabledEffect = 0;
	#endif
#endif

if (DrawDisabledEffect != 0)
{
	#if SOURCE_IN_LINEAR_SPACE
		half3 LumCoeffs = half3( 0.3, 0.59, .11 );
		half3 Grayish = half3(.1, .1, .1);
	#else
		// These are just the linear space values above converted to sRGB space.
		half3 LumCoeffs = float3( 0.5843, 0.7921, 0.3647 );
		half3 Grayish = float3(0.349, 0.349, 0.349);
	#endif

	//desaturate
	half Lum = dot( LumCoeffs, OutColor.rgb );
	OutColor.rgb = lerp( OutColor.rgb, float3(Lum,Lum,Lum), .8 );
	OutColor.rgb = lerp( OutColor.rgb, Grayish, clamp( distance( OutColor.rgb, Grayish ), 0, .8)  );
}

Ensuring that DRAW_DISABLED_EFFECT or DrawDisabledEffect != 0 never evaluates to true will do the trick.

If you don’t want to go quite to the shader level, Engine\Source\Runtime\SlateRHIRenderer\Private\SlateShaders.h contains the engine code that connects to the above shader, and in that file TSlateElementPS::ModifyCompilationEnvironment is responsible for setting the DRAW_DISABLED_EFFECT flag and can be headed off there.

If you would like to take a more surgical approach, you can follow the flag up or down the callstack by searching for ESlateDrawEffect::DisabledEffect. It is used almost exclusively in Slate’s OnPaint() calls. With enough plumbing you could pass whatever you wanted down to the shaders for DrawDisabledEffect.

Conclusion

I’ve found myself surprised that there isn’t more discussion about this issue online. I guess at the end of the day automatically darkening an element when it’s disabled is generally the desirable result, and anyone who doesn’t want that is either using a third party UI framework, or figured out a workaround on their own. I hope this blog post was helpful to you if you don’t fall into any of those camps.

As always for all things UI related, my thanks go to @_benui for his awesome work making sense of Unreal’s UI systems.

Thanks for reading! Follow me on Twitter: https://twitter.com/animawish