wip: UI Button Base + content/config in-progress

Snapshot do trabalho em andamento na branch feat/ui-system: refinamentos do
UIButton_Base (header+impl), atualizacao de styles/themes do botao, mapa de
teste L_ZeusIATest, e demais .uasset/Config tocados durante a sessao de
playtest do meshing. Commit conjunto a pedido do usuario pra deixar a branch
limpa antes do proximo batch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:56:28 -03:00
parent 5e32cb0757
commit e771aec259
125 changed files with 810 additions and 24 deletions

View File

@@ -1,8 +1,456 @@
#include "UIButton_Base.h"
#include "CommonTextBlock.h"
#include "Components/Image.h"
#include "Components/OverlaySlot.h"
#include "Engine/DataTable.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "Styling/SlateBrush.h"
#include "Styling/SlateTypes.h"
#include "UObject/ConstructorHelpers.h"
#include "UI/UIStyleRow.h"
#include "UI/UIStyleTokens.h"
#include "Engine/DataTable.h"
namespace
{
constexpr float MinTransition = 0.001f;
// Params do material UI_M_Button (Bg).
static const FName P_TopColour("TopColour");
static const FName P_BottomColour("BottomColour");
static const FName P_GlowColour("GlowColour");
static const FName P_GlowIntensity("GlowIntensity");
static const FName P_CornerColour("CornerColour");
static const FName P_CornerLength("CornerLength");
static const FName P_CornerThickness("CornerThickness");
static const FName P_FrameColour("FrameColour");
static const FName P_AspectRatio("AspectRatio");
static const FName P_GlowEdge("GlowEdge");
static const FName P_GlowSoftness("GlowSoftness");
FORCEINLINE float EaseOutCubic(float Alpha)
{
return 1.f - FMath::Pow(1.f - Alpha, 3.f);
}
}
UUIButton_Base::UUIButton_Base(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Materiais default (fallback quando o brush da imagem não traz material).
static ConstructorHelpers::FObjectFinder<UMaterialInterface> BgMatFinder(
TEXT("/Game/ZMMO/UI/Themes/Default/Button/UI_M_Button.UI_M_Button"));
if (BgMatFinder.Succeeded()) { BgMaterial = BgMatFinder.Object; }
static ConstructorHelpers::FObjectFinder<UMaterialInterface> GlowMatFinder(
TEXT("/Game/ZMMO/UI/Themes/Default/Button/UI_M_Button_Glow.UI_M_Button_Glow"));
if (GlowMatFinder.Succeeded()) { GlowMaterial = GlowMatFinder.Object; }
static ConstructorHelpers::FObjectFinder<UMaterialInterface> CornerMatFinder(
TEXT("/Game/ZMMO/UI/Themes/Default/Button/UI_M_CornerBrackets.UI_M_CornerBrackets"));
if (CornerMatFinder.Succeeded()) { CornerMaterial = CornerMatFinder.Object; }
static ConstructorHelpers::FObjectFinder<UMaterialInterface> FrameMatFinder(
TEXT("/Game/ZMMO/UI/Themes/Default/Button/UI_M_Button_Frame.UI_M_Button_Frame"));
if (FrameMatFinder.Succeeded()) { FrameMaterial = FrameMatFinder.Object; }
// Defaults = btn-primary (buttons_showcase.html). Cores em valores sRGB
// diretos (mesma convenção do UUIProgressBar_Base — UI sem gamma).
// Default (repouso)
DefaultVisual.TopColour = FLinearColor(0.910f, 0.753f, 0.376f, 0.18f);
DefaultVisual.BottomColour = FLinearColor(0.659f, 0.471f, 0.188f, 0.10f);
DefaultVisual.BorderColour = FLinearColor(0.831f, 0.659f, 0.314f, 0.28f);
DefaultVisual.BorderWidth = 1.f;
DefaultVisual.GlowColour = FLinearColor(0.f, 0.f, 0.f, 0.5f); // drop shadow (escura)
DefaultVisual.GlowIntensity = 0.7f;
DefaultVisual.TextColour = FLinearColor(1.f, 0.847f, 0.533f, 1.f);
DefaultVisual.Scale = 1.f;
DefaultVisual.RenderOpacity = 1.f;
// Hover — gradient mais forte, borda gold, glow aceso, leve zoom.
HoveredVisual.TopColour = FLinearColor(0.910f, 0.753f, 0.376f, 0.32f);
HoveredVisual.BottomColour = FLinearColor(0.659f, 0.471f, 0.188f, 0.18f);
HoveredVisual.BorderColour = FLinearColor(0.910f, 0.753f, 0.439f, 1.f);
HoveredVisual.BorderWidth = 1.f;
HoveredVisual.GlowColour = FLinearColor(0.f, 0.f, 0.f, 0.55f); // sombra mais forte (elevado)
HoveredVisual.GlowIntensity = 0.9f;
HoveredVisual.TextColour = FLinearColor(1.f, 0.847f, 0.533f, 1.f);
HoveredVisual.Scale = 1.02f;
HoveredVisual.RenderOpacity = 1.f;
// Pressed/Active — gradient invertido, encolhe.
PressedVisual.TopColour = FLinearColor(0.659f, 0.471f, 0.188f, 0.25f);
PressedVisual.BottomColour = FLinearColor(0.910f, 0.753f, 0.376f, 0.15f);
PressedVisual.BorderColour = FLinearColor(0.910f, 0.753f, 0.439f, 1.f);
PressedVisual.BorderWidth = 1.f;
PressedVisual.GlowColour = FLinearColor(0.f, 0.f, 0.f, 0.5f); // sombra menor (afundou)
PressedVisual.GlowIntensity = 0.5f;
PressedVisual.TextColour = FLinearColor(1.f, 0.847f, 0.533f, 1.f);
PressedVisual.Scale = 0.98f;
PressedVisual.RenderOpacity = 1.f;
// Disabled — quase apagado, texto escuro, opacity 0.6.
DisabledVisual.TopColour = FLinearColor(1.f, 1.f, 1.f, 0.02f);
DisabledVisual.BottomColour = FLinearColor(1.f, 1.f, 1.f, 0.02f);
DisabledVisual.BorderColour = FLinearColor(1.f, 1.f, 1.f, 0.06f);
DisabledVisual.BorderWidth = 1.f;
DisabledVisual.GlowColour = FLinearColor(0.f, 0.f, 0.f, 0.25f); // sombra fraca
DisabledVisual.GlowIntensity = 0.3f;
DisabledVisual.TextColour = FLinearColor(0.227f, 0.220f, 0.188f, 1.f);
DisabledVisual.Scale = 1.f;
DisabledVisual.RenderOpacity = 0.6f;
// Selected (tabs/toggles) — gold suave persistente, sem zoom.
SelectedVisual.TopColour = FLinearColor(0.910f, 0.753f, 0.376f, 0.20f);
SelectedVisual.BottomColour = FLinearColor(0.659f, 0.471f, 0.188f, 0.12f);
SelectedVisual.BorderColour = FLinearColor(0.831f, 0.659f, 0.314f, 0.55f);
SelectedVisual.BorderWidth = 1.f;
SelectedVisual.GlowColour = FLinearColor(0.f, 0.f, 0.f, 0.5f);
SelectedVisual.GlowIntensity = 0.75f;
SelectedVisual.TextColour = FLinearColor(1.f, 0.847f, 0.533f, 1.f);
SelectedVisual.Scale = 1.f;
SelectedVisual.RenderOpacity = 1.f;
CurrentVisual = DefaultVisual;
}
void UUIButton_Base::NativePreConstruct()
{
Super::NativePreConstruct();
EnsureMID();
// Padding/centralização do texto via slot do Overlay (design-time + runtime).
if (ButtonText)
{
if (UOverlaySlot* OS = Cast<UOverlaySlot>(ButtonText->Slot))
{
OS->SetPadding(ContentPadding);
OS->SetHorizontalAlignment(HAlign_Center);
OS->SetVerticalAlignment(VAlign_Center);
}
}
// GlowFx (drop shadow): vaza pra fora via padding negativo + deslocado pra
// baixo por GlowOffsetY (a sombra aparece mais embaixo, estilo box-shadow).
if (GlowFx)
{
if (UOverlaySlot* OS = Cast<UOverlaySlot>(GlowFx->Slot))
{
OS->SetPadding(FMargin(-GlowExtent, -GlowExtent + GlowOffsetY,
-GlowExtent, -GlowExtent - GlowOffsetY));
OS->SetHorizontalAlignment(HAlign_Fill);
OS->SetVerticalAlignment(VAlign_Fill);
}
}
// Corners puxado pra dentro (CornerInset>0) ou pra fora/abraçando (<0).
if (Corners)
{
if (UOverlaySlot* OS = Cast<UOverlaySlot>(Corners->Slot))
{
OS->SetPadding(FMargin(CornerInset));
OS->SetHorizontalAlignment(HAlign_Fill);
OS->SetVerticalAlignment(VAlign_Fill);
}
}
CurrentVisual = DefaultVisual;
bTransitioning = false;
ApplyVisual(CurrentVisual);
}
void UUIButton_Base::NativeConstruct()
{
Super::NativeConstruct();
EnsureMID();
bHoveredState = bPressedState = bSelectedState = false;
bDisabledState = !GetIsEnabled();
// Estado inicial sem animar.
const FUIButtonStateVisual& Target =
bDisabledState ? DisabledVisual
: bSelectedState ? SelectedVisual
: DefaultVisual;
CurrentVisual = Target;
bTransitioning = false;
ApplyVisual(CurrentVisual);
}
void UUIButton_Base::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
if (!bTransitioning)
{
return;
}
TransitionElapsed += InDeltaTime;
const float Duration = FMath::Max(TransitionSeconds, MinTransition);
const float Alpha = FMath::Clamp(TransitionElapsed / Duration, 0.f, 1.f);
CurrentVisual = LerpVisual(FromVisual, ToVisual, EaseOutCubic(Alpha));
ApplyVisual(CurrentVisual);
if (Alpha >= 1.f)
{
CurrentVisual = ToVisual;
ApplyVisual(CurrentVisual);
bTransitioning = false;
}
}
// === Hooks de estado ===
void UUIButton_Base::NativeOnHovered()
{
Super::NativeOnHovered();
bHoveredState = true;
RefreshState();
}
void UUIButton_Base::NativeOnUnhovered()
{
Super::NativeOnUnhovered();
bHoveredState = false;
RefreshState();
}
void UUIButton_Base::NativeOnPressed()
{
Super::NativeOnPressed();
bPressedState = true;
RefreshState();
}
void UUIButton_Base::NativeOnReleased()
{
Super::NativeOnReleased();
bPressedState = false;
RefreshState();
}
void UUIButton_Base::NativeOnEnabled()
{
Super::NativeOnEnabled();
bDisabledState = false;
RefreshState();
}
void UUIButton_Base::NativeOnDisabled()
{
Super::NativeOnDisabled();
bDisabledState = true;
RefreshState();
}
void UUIButton_Base::NativeOnSelected(bool bBroadcast)
{
Super::NativeOnSelected(bBroadcast);
bSelectedState = true;
RefreshState();
}
void UUIButton_Base::NativeOnDeselected(bool bBroadcast)
{
Super::NativeOnDeselected(bBroadcast);
bSelectedState = false;
RefreshState();
}
// === Internos ===
void UUIButton_Base::RefreshState()
{
const FUIButtonStateVisual& Target =
bDisabledState ? DisabledVisual
: bPressedState ? PressedVisual
: bHoveredState ? HoveredVisual
: bSelectedState ? SelectedVisual
: DefaultVisual;
GoToState(Target);
}
void UUIButton_Base::GoToState(const FUIButtonStateVisual& Target)
{
if (TransitionSeconds <= MinTransition)
{
CurrentVisual = Target;
bTransitioning = false;
ApplyVisual(CurrentVisual);
return;
}
FromVisual = CurrentVisual;
ToVisual = Target;
TransitionElapsed = 0.f;
bTransitioning = true;
}
void UUIButton_Base::ApplyVisual(const FUIButtonStateVisual& V)
{
// Fundo: só gradient (o glow agora é a camada externa GlowFx).
if (BgMID)
{
BgMID->SetVectorParameterValue(P_TopColour, V.TopColour);
BgMID->SetVectorParameterValue(P_BottomColour, V.BottomColour);
}
// Halo externo (sai pra fora).
if (GlowMID)
{
GlowMID->SetVectorParameterValue(P_GlowColour, V.GlowColour);
GlowMID->SetScalarParameterValue(P_GlowIntensity, V.GlowIntensity);
}
// Cantos: cor acompanha a borda do estado (acende no hover); tamanho/peso
// vêm das UPROPERTYs (estáticos por instância).
if (CornerMID)
{
CornerMID->SetVectorParameterValue(P_CornerColour, V.BorderColour);
CornerMID->SetScalarParameterValue(P_CornerLength, CornerLength);
CornerMID->SetScalarParameterValue(P_CornerThickness, CornerThickness);
}
// Frame (material): mesma cor de borda do estado.
if (BorderMID)
{
BorderMID->SetVectorParameterValue(P_FrameColour, V.BorderColour);
}
const bool bSimple = (BorderStyle == EUIButtonBorderStyle::Simple);
const bool bFrame = (BorderStyle == EUIButtonBorderStyle::Frame);
const bool bCorners = bShowCorners; // INDEPENDENTE da borda
// Bg: canto arredondado sempre; a linha (outline) só no modo Simple.
if (Bg)
{
FSlateBrush B = Bg->GetBrush();
B.DrawAs = ESlateBrushDrawType::RoundedBox;
FSlateBrushOutlineSettings Outline = B.OutlineSettings;
Outline.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius);
Outline.RoundingType = ESlateBrushRoundingType::FixedRadius;
if (bSimple)
{
Outline.Color = FSlateColor(V.BorderColour);
Outline.Width = FMath::Max(V.BorderWidth, 0.f);
}
else
{
Outline.Color = FSlateColor(FLinearColor::Transparent);
Outline.Width = 0.f;
}
B.OutlineSettings = Outline;
Bg->SetBrush(B);
}
// Frame (moldura material) visível só no modo Frame.
if (BorderFx)
{
BorderFx->SetVisibility(bFrame ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed);
}
// Cantos visíveis só no modo Corners.
if (Corners)
{
Corners->SetVisibility(bCorners ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed);
}
if (ButtonText)
{
ButtonText->SetColorAndOpacity(FSlateColor(V.TextColour));
}
SetRenderScale(FVector2D(V.Scale, V.Scale));
SetRenderOpacity(V.RenderOpacity);
}
void UUIButton_Base::EnsureMID()
{
// Prioriza o material do próprio brush (se o WBP definiu um); senão usa o
// material default da classe. Idempotente por imagem.
auto Ensure = [this](UImage* Img, UMaterialInterface* Fallback) -> UMaterialInstanceDynamic*
{
if (!Img) { return nullptr; }
UMaterialInterface* Mat = Cast<UMaterialInterface>(Img->GetBrush().GetResourceObject());
if (!Mat) { Mat = Fallback; }
if (!Mat) { return nullptr; }
UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Mat, this);
Img->SetBrushFromMaterial(MID);
return MID;
};
if (!BgMID) { BgMID = Ensure(Bg, BgMaterial); }
if (!GlowMID) { GlowMID = Ensure(GlowFx, GlowMaterial); }
if (!CornerMID) { CornerMID = Ensure(Corners, CornerMaterial); }
if (!BorderMID) { BorderMID = Ensure(BorderFx, FrameMaterial); }
}
void UUIButton_Base::ApplyAspectToLayers() const
{
// Cada camada usa o aspect da SUA geometry (Corners/BorderFx têm padding
// próprio, então tamanho ≠ do botão). Sem isso, os traços esticam.
auto SetAspect = [](UMaterialInstanceDynamic* MID, const UImage* Img)
{
if (!MID || !Img) { return; }
const FVector2D S = Img->GetCachedGeometry().GetLocalSize();
if (S.Y > 1.f)
{
MID->SetScalarParameterValue(P_AspectRatio, static_cast<float>(S.X / S.Y));
}
};
SetAspect(CornerMID, Corners);
SetAspect(BorderMID, BorderFx);
SetAspect(GlowMID, GlowFx);
// Glow (drop shadow): ancora o anel da sombra na borda do BOTÃO — fração =
// margem (GlowExtent) / meia-altura do GlowFx. Assim a sombra esmaece pros
// dois lados a partir da borda, sem aresta dura na borda externa do quad.
if (GlowMID && GlowFx)
{
const FVector2D S = GlowFx->GetCachedGeometry().GetLocalSize();
if (S.Y > 1.f)
{
// Edge = fração (em inY=min(V,1-V)) onde está a borda do botão = margem/altura.
const float Edge = GlowExtent / static_cast<float>(S.Y);
GlowMID->SetScalarParameterValue(P_GlowEdge, Edge);
// Largura do esmaecimento ~ a margem (escala com o tamanho do botão).
GlowMID->SetScalarParameterValue(P_GlowSoftness, Edge * 1.2f);
}
}
}
int32 UUIButton_Base::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
// NativePaint roda no Designer E em runtime (o Tick não roda no Designer) —
// é aqui que garantimos o aspect correto dos materiais em ambos.
ApplyAspectToLayers();
return Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements,
LayerId, InWidgetStyle, bParentEnabled);
}
FUIButtonStateVisual UUIButton_Base::LerpVisual(const FUIButtonStateVisual& A, const FUIButtonStateVisual& B, float T)
{
FUIButtonStateVisual R;
R.TopColour = FMath::Lerp(A.TopColour, B.TopColour, T);
R.BottomColour = FMath::Lerp(A.BottomColour, B.BottomColour, T);
R.GlowColour = FMath::Lerp(A.GlowColour, B.GlowColour, T);
R.GlowIntensity = FMath::Lerp(A.GlowIntensity, B.GlowIntensity, T);
R.BorderColour = FMath::Lerp(A.BorderColour, B.BorderColour, T);
R.BorderWidth = FMath::Lerp(A.BorderWidth, B.BorderWidth, T);
R.TextColour = FMath::Lerp(A.TextColour, B.TextColour, T);
R.Scale = FMath::Lerp(A.Scale, B.Scale, T);
R.RenderOpacity = FMath::Lerp(A.RenderOpacity, B.RenderOpacity, T);
return R;
}
void UUIButton_Base::SetButtonText(const FText& InText)
{
if (ButtonText)
{
ButtonText->SetText(InText);
}
}
// === Acesso ao estilo ativo (DT_UI_Styles) — fase final ===
const FUIStyle* UUIButton_Base::GetActiveUIStyle(FName ThemeId) const
{

View File

@@ -4,53 +4,278 @@
#include "CommonButtonBase.h"
#include "UIButton_Base.generated.h"
class USizeBox;
class UImage;
class UCanvasPanel;
class UCommonTextBlock;
class UTextBlock;
class UMaterialInterface;
class UMaterialInstanceDynamic;
struct FUIStyle;
struct FUIStyleButton;
/** Tipo de borda do botão (fixo por instância; a COR acende por estado). */
UENUM(BlueprintType)
enum class EUIButtonBorderStyle : uint8
{
None UMETA(DisplayName = "Sem borda"),
Simple UMETA(DisplayName = "Linha simples (Slate)"),
Frame UMETA(DisplayName = "Frame (material)")
};
/**
* Botão base do ZMMO (CommonUI). Hoje expõe BindWidgetOptional dos widgets
* que o WBP UI_Button_Master_New consome, e helpers pra ler o estilo ativo
* de DT_UI_Styles. A aplicação visual (pintar brushes/cores/fonte) entra
* depois — esta camada só prepara o acesso.
* Snapshot visual de UM estado do botão (Default / Hover / Pressed / Disabled).
* As cores de gradient/glow vão pro material (UI_M_Button via MID); a borda usa
* o RoundedBox nativo do Slate (OutlineSettings) e o scale/opacity são
* RenderTransform do widget. Transições entre estados interpolam esta struct
* inteira (ver UUIButton_Base::NativeTick).
*/
USTRUCT(BlueprintType)
struct ZMMO_API FUIButtonStateVisual
{
GENERATED_BODY()
/** Topo do gradient vertical do fundo (param TopColour do material). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State")
FLinearColor TopColour = FLinearColor(0.910f, 0.753f, 0.376f, 0.18f);
/** Base do gradient vertical (param BottomColour). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State")
FLinearColor BottomColour = FLinearColor(0.659f, 0.471f, 0.188f, 0.10f);
/** Cor do halo externo (camada GlowFx, material UI_M_Button_Glow). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State")
FLinearColor GlowColour = FLinearColor(0.910f, 0.753f, 0.376f, 1.f);
/** Intensidade do halo externo (GlowFx). 0 = sem glow; hover acende (sai pra fora). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State",
meta = (ClampMin = "0", ClampMax = "1"))
float GlowIntensity = 0.f;
/** Cor da borda/cantos (Slate outline no modo Simple, ou material no modo Corners). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State")
FLinearColor BorderColour = FLinearColor(0.831f, 0.659f, 0.314f, 0.28f);
/** Espessura da borda em pixels (Slate OutlineSettings.Width). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State",
meta = (ClampMin = "0", ClampMax = "6"))
float BorderWidth = 1.f;
/** Cor do texto (ButtonText->SetColorAndOpacity). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State")
FLinearColor TextColour = FLinearColor(1.f, 0.847f, 0.533f, 1.f);
/** Scale do botão (CSS transform: scale). 1 = normal, 1.02 = hover, 0.98 = pressed. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State",
meta = (ClampMin = "0.5", ClampMax = "1.5"))
float Scale = 1.f;
/** Opacidade geral do botão (CSS opacity — disabled usa 0.6). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button|State",
meta = (ClampMin = "0", ClampMax = "1"))
float RenderOpacity = 1.f;
};
/**
* Botão base do ZMMO (CommonUI). Mesmo padrão do UUIProgressBar_Base: um UImage
* (`Bg`) com material custom (UI_M_Button) controlado por MID + RoundedBox nativo
* do Slate pra borda/canto. Os estados do UCommonButtonBase (Hover/Pressed/
* Disabled/Selected) trocam o FUIButtonStateVisual alvo; a transição interpola
* suave no Tick (estilo CSS `transition`).
*
* Modo atual = manual (os 4 FUIButtonStateVisual editáveis no Details, já com
* defaults do `btn-primary`). A leitura via DT_UI_Styles (bUseTheme + Variant)
* entra na fase final, reusando esta struct.
*
* WBP filho (UI_Button_Master_New): `Bg` (UImage, obrigatório, material no brush)
* + `ButtonText` (UCommonTextBlock, opcional).
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIButton_Base : public UCommonButtonBase
{
GENERATED_BODY()
public:
UUIButton_Base(const FObjectInitializer& ObjectInitializer);
/** Define o texto do botão (ButtonText). */
UFUNCTION(BlueprintCallable, Category = "UI Button")
void SetButtonText(const FText& InText);
// === Visual por estado (modo manual; defaults = btn-primary) ===
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
FUIButtonStateVisual DefaultVisual;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
FUIButtonStateVisual HoveredVisual;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
FUIButtonStateVisual PressedVisual;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
FUIButtonStateVisual DisabledVisual;
/** Estado "selecionado" (tabs/toggles). Default = igual ao Hovered. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
FUIButtonStateVisual SelectedVisual;
/** Raio do canto em pixels (Slate RoundedBox). Comum a todos os estados. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual",
meta = (ClampMin = "0", ClampMax = "32"))
float CornerRadius = 2.f;
/** Tipo de borda (independente dos cantos): sem borda, linha Slate ou frame material. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
EUIButtonBorderStyle BorderStyle = EUIButtonBorderStyle::Frame;
/** Liga os cantos (corner brackets) — INDEPENDENTE da borda. Pode ter os dois,
* só um, ou nenhum. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
bool bShowCorners = true;
/** Material do fundo (UI_M_Button). Fallback quando o brush do Bg não traz
* material próprio. Defaults resolvidos no construtor. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Button|Visual")
TObjectPtr<UMaterialInterface> BgMaterial;
/** Material do halo externo (UI_M_Button_Glow), aplicado no GlowFx. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Button|Visual")
TObjectPtr<UMaterialInterface> GlowMaterial;
/** Material dos cantos (UI_M_CornerBrackets), aplicado no Corners. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Button|Visual")
TObjectPtr<UMaterialInterface> CornerMaterial;
/** Material da moldura (UI_M_Button_Frame), aplicado no BorderFx. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Button|Visual")
TObjectPtr<UMaterialInterface> FrameMaterial;
/** Padding do conteúdo (texto) dentro do botão. btn-primary = 28px horiz /
* 12px vert. Aplicado no slot do ButtonText em PreConstruct (o texto fica
* centralizado). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual")
FMargin ContentPadding = FMargin(28.f, 12.f, 28.f, 12.f);
/** Quanto o halo externo (GlowFx) vaza pra fora do botão, em pixels. Aplicado
* como padding negativo no slot do GlowFx em PreConstruct. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual",
meta = (ClampMin = "0", ClampMax = "64"))
float GlowExtent = 16.f;
/** Desloca os cantos. Negativo = pra FORA (abraçando o botão, estilo CSS);
* positivo = pra dentro. Aplicado no slot do Corners em PreConstruct. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual",
meta = (ClampMin = "-32", ClampMax = "32"))
float CornerInset = -5.f;
/** Comprimento de cada braço do "L" (fração da altura). Maior = L mais longo. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual",
meta = (ClampMin = "0.05", ClampMax = "1.0"))
float CornerLength = 0.35f;
/** Espessura/peso do traço dos cantos (fração da altura). Menor = mais fino. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual",
meta = (ClampMin = "0.005", ClampMax = "0.3"))
float CornerThickness = 0.04f;
/** Deslocamento da sombra (GlowFx) pra baixo, em px — dá o look de drop shadow. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Visual",
meta = (ClampMin = "0", ClampMax = "32"))
float GlowOffsetY = 4.f;
/** Duração da transição entre estados (s). 0 = troca instantânea. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Button|Animation",
meta = (ClampMin = "0", ClampMax = "1"))
float TransitionSeconds = 0.12f;
protected:
// ---- Widgets BindWidgetOptional (espelham a árvore do UI_Button_Master_New) ----
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<USizeBox> SizeBox;
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UImage> ImgStoke;
// === Hooks de estado do UCommonButtonBase ===
virtual void NativeOnHovered() override;
virtual void NativeOnUnhovered() override;
virtual void NativeOnPressed() override;
virtual void NativeOnReleased() override;
virtual void NativeOnEnabled() override;
virtual void NativeOnDisabled() override;
virtual void NativeOnSelected(bool bBroadcast) override;
virtual void NativeOnDeselected(bool bBroadcast) override;
// === BindWidget (espelham a árvore do UI_Button_Master_New) ===
// Todos BindWidgetOptional pra não quebrar WBPs que herdam UUIButton_Base sem
// essas peças — sem elas o visual degrada (não pinta), mas não quebra.
/** Halo externo (atrás do Bg, com padding negativo pra vazar pra fora). */
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UCanvasPanel> CanvasPanel_Text;
TObjectPtr<UImage> GlowFx;
/** Fundo (gradient). */
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UImage> Bg;
/** Moldura/frame (material), visível só no BorderStyle = Frame. */
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UImage> BorderFx;
/** Cantos (corner brackets), visível só no BorderStyle = Corners. */
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UImage> Corners;
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UCommonTextBlock> ButtonText;
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> Number_Text;
// === Acesso ao estilo ativo (DT_UI_Styles) — usado na fase final ===
UPROPERTY(BlueprintReadOnly, Category = "UI Button", meta = (BindWidgetOptional))
TObjectPtr<UImage> ImgIcon;
// ---- Acesso ao estilo ativo (DT_UI_Styles) — C++ only ----
/** Carrega DT_UI_Styles e devolve o FUIStyle da row pedida (default "Default").
* Retorna nullptr se a DT ou a row não existirem. Ponteiro válido enquanto
* a DT estiver carregada — uso típico é leitura imediata, sem armazenar. */
/** Carrega DT_UI_Styles e devolve o FUIStyle da row pedida (default "Default"). */
const FUIStyle* GetActiveUIStyle(FName ThemeId = TEXT("Default")) const;
/** Atalho: devolve só o bloco Button do FUIStyle ativo. */
const FUIStyleButton* GetActiveButtonStyle(FName ThemeId = TEXT("Default")) const;
private:
/** Cria os MIDs a partir do material no brush de cada imagem (idempotente). */
void EnsureMID();
/** Passa a proporção W/H de CADA camada (cantos/frame/glow) pro respectivo
* material que compensa aspect. Chamado no NativePaint — roda no Designer E
* em runtime (o Tick NÃO roda no Designer, por isso o traço esticava lá). */
void ApplyAspectToLayers() const;
/** Recalcula o estado-alvo (disabled>pressed>hovered>selected>default) e transiciona. */
void RefreshState();
/** Inicia transição pro visual alvo (ou aplica direto se TransitionSeconds≈0). */
void GoToState(const FUIButtonStateVisual& Target);
/** Empurra um visual pros widgets (MID + borda Slate + scale/opacity + texto). */
void ApplyVisual(const FUIButtonStateVisual& V);
static FUIButtonStateVisual LerpVisual(const FUIButtonStateVisual& A, const FUIButtonStateVisual& B, float T);
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> BgMID;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> GlowMID;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> CornerMID;
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> BorderMID;
bool bHoveredState = false;
bool bPressedState = false;
bool bDisabledState = false;
bool bSelectedState = false;
FUIButtonStateVisual FromVisual;
FUIButtonStateVisual ToVisual;
FUIButtonStateVisual CurrentVisual;
float TransitionElapsed = 0.f;
bool bTransitioning = false;
};