Working on new root canvas stuff

This commit is contained in:
2026-04-21 21:26:06 -04:00
parent ec983951fe
commit 8e5d43fd24
13 changed files with 492 additions and 333 deletions

View File

@@ -1,8 +1,10 @@
#include "PlayerControllerBase.h"
#include "Common.h"
#include "RootCanvas.h"
#include "Tangible.h"
#include "TangibleManager.h"
#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/InputComponent.h"
#include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
@@ -63,7 +65,17 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
void AlxPlayerControllerBase::BeginPlay()
{
// Build the root UMG stack BEFORE Super::BeginPlay. Super calls
// ReceiveBeginPlay, which fires the Blueprint Event BeginPlay;
// BP code there may immediately try to add widgets to RootCanvas,
// so the canvas must already exist.
RootWidget = CreateWidget<UlxRootWidget>(this);
RootCanvas = RootWidget->WidgetTree->ConstructWidget<UlxRootCanvasPanel>();
RootWidget->WidgetTree->RootWidget = RootCanvas;
RootWidget->AddToViewport(0);
Super::BeginPlay();
if (FSlateApplication::IsInitialized())
{
FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject(
@@ -78,6 +90,14 @@ void AlxPlayerControllerBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle);
FocusChangingHandle.Reset();
}
if (IsValid(RootWidget))
{
RootWidget->RemoveFromParent();
}
RootWidget = nullptr;
RootCanvas = nullptr;
Super::EndPlay(EndPlayReason);
}
@@ -111,11 +131,18 @@ UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *W
return Cast<UInputComponent>(Value);
}
void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus, bool EnableInputComponent)
void AlxPlayerControllerBase::RestoreFocusToFrontWidget(const UObject *Context)
{
// This will trigger UpdateInputMode to shift focus back to
// the front window, if the front window wants focus.
FromContext(Context)->LastWidgetGrantedFocus = nullptr;
}
void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
{
if (!IsValid(Widget))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("WidgetRequestInputMode called with an invalid widget."));
UE_LOG(LogLuprexIntegration, Error, TEXT("AddWidgetToRoot called with an invalid widget."));
return;
}
APlayerController *OwningPC = Widget->GetOwningPlayer();
@@ -123,53 +150,21 @@ void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool S
if (PC == nullptr)
{
UE_LOG(LogLuprexIntegration, Error,
TEXT("WidgetRequestInputMode: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
TEXT("AddWidgetToRoot: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
*Widget->GetName(), *GetNameSafe(OwningPC));
return;
}
FlxInputModeRequest Req;
Req.Widget = Widget;
Req.Focus = Focus;
Req.ShowPointer = ShowPointer;
Req.BlockInput = BlockInput;
Req.EnableInputComponent = EnableInputComponent;
PC->InputModeRequests.Request(Req);
}
void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent)
{
if (InInputComponent)
if (PC->RootCanvas == nullptr)
{
// Widgets don't go on the current input stack. Instead, they're
// tracked in InputModeRequests, where the PlayerController can arbitrate
// focus, pointer visibility, and input blocking across them.
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
{
InputModeRequests.SetEnableInputComponent(Widget, true);
return;
}
CurrentInputStack.RemoveSingle(InInputComponent);
CurrentInputStack.Add(InInputComponent);
UE_LOG(LogLuprexIntegration, Error,
TEXT("AddWidgetToRoot: root canvas is not initialized, this is probably an initialization order issue"));
return;
}
}
bool AlxPlayerControllerBase::PopInputComponent(UInputComponent* InInputComponent)
{
if (InInputComponent)
{
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
{
InputModeRequests.SetEnableInputComponent(Widget, false);
InInputComponent->ClearBindingValues();
return true;
}
if (CurrentInputStack.RemoveSingle(InInputComponent) > 0)
{
InInputComponent->ClearBindingValues();
return true;
}
}
return false;
if (Widget->GetParent() == PC->RootCanvas) return;
UlxRootCanvasSlot *Slot = PC->RootCanvas->AddChildToRootCanvas(Widget);
Slot->SetZOrder(0);
}
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
@@ -216,51 +211,51 @@ void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputSta
InputStack.Push(InputComponent);
}
// The current input stack is unlikely to have anything,
// given that we've moved widgets to their own mechanism.
// But, if there's anything here, sort it and push it.
if (!CurrentInputStack.IsEmpty())
// Get the widget slots.
TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
// Generate a set of input components that are being managed by the WidgetSlots.
TSet<UInputComponent*> WidgetManagedComponents;
for (UlxRootCanvasSlot *Slot : WidgetSlots)
{
TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
{
UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC)) Pushed.Add(IC);
else CurrentInputStack.RemoveAt(Idx--);
}
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B)
{
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput;
return A.Priority < B.Priority;
});
InputStack.Append(Pushed);
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
UInputComponent *IC = GetWidgetInputComponent(Widget);
if (IC) WidgetManagedComponents.Add(IC);
}
// Push the widget input components from InputModeRequests. Requests
// are ordered high-priority first; the input stack is processed
// from top (last index) down, so we push back-to-front: low
// priority first, high priority last (ending up on top).
const TArray<FlxInputModeRequest> &Requests = InputModeRequests.GetRequests();
for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx)
// Add components in the CurrentInputStack, *unless* they are being managed
// by widgets. If they're being managed by widgets, they get added later.
for (int32 Idx=0; Idx<CurrentInputStack.Num(); ++Idx)
{
const FlxInputModeRequest &Req = Requests[Idx];
if (!Req.EnableInputComponent) continue;
UUserWidget *Widget = Req.Widget;
if (!Widget->IsInViewport()) continue;
if (UInputComponent *IC = GetWidgetInputComponent(Widget))
UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC))
{
IC->bBlockInput = Req.BlockInput;
InputStack.Push(IC);
if (!WidgetManagedComponents.Contains(IC)) InputStack.Push(IC);
}
else
{
CurrentInputStack.RemoveAt(Idx--);
}
}
// Now add the widget-managed input components.
for (UlxRootCanvasSlot *Slot : ReverseIterate(WidgetSlots))
{
if (Slot->EnableEnhancedInput)
{
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
UInputComponent *IC = GetWidgetInputComponent(Widget);
if (IC)
{
IC->bBlockInput = Slot->BlockInput;
InputStack.Push(IC);
}
}
}
}
void AlxPlayerControllerBase::UpdateInputMode()
{
InputModeRequests.GarbageCollect();
// Get all the various objects we need to be able to manipulate
// the input mode.
UGameViewportClient *GameViewportClient = GetWorld()->GetGameViewport();
@@ -273,19 +268,24 @@ void AlxPlayerControllerBase::UpdateInputMode()
TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser();
if (!SlateUser.IsValid()) return;
// The first active entry in InputModeRequests dictates the
// pointer / capture / focus state. If there are no requests at all,
// fall back to a static default-constructed request.
static const FlxInputModeRequest EmptyRequest;
const FlxInputModeRequest *Top = &EmptyRequest;
for (const FlxInputModeRequest &Req : InputModeRequests.GetRequests())
// Get the desired configuration from the first widget.
// TODO: Maybe we don't have to sort the whole array.
TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
UUserWidget *Widget = nullptr;
UWidget *Focus = nullptr;
bool ShowPointer = false;
if (!WidgetSlots.IsEmpty())
{
if (Req.Widget->IsInViewport()) { Top = &Req; break; }
UlxRootCanvasSlot *Top = WidgetSlots[0];
Widget = Cast<UUserWidget>(Top->GetContent());
Focus = Widget->GetDesiredFocusWidget();
ShowPointer = Top->ShowPointer;
}
SetShowMouseCursor(Top->ShowPointer);
SetShowMouseCursor(ShowPointer);
if (Top->ShowPointer)
if (ShowPointer)
{
// Only release capture if the viewport is currently holding it
// (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture
@@ -321,20 +321,20 @@ void AlxPlayerControllerBase::UpdateInputMode()
// viewport client notifies us of that fact. We then focus the
// widget if possible.
//
if ((!Top->ShowPointer) || (LastRequestGrantedFocus != Top->SequenceNumber))
if ((!ShowPointer) || (LastWidgetGrantedFocus != Focus))
{
if (Top->Focus)
if (Focus)
{
if (TSharedPtr<SWidget> SlateFocus = Top->Focus->GetCachedWidget())
if (TSharedPtr<SWidget> SlateFocus = Focus->GetCachedWidget())
{
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
LastRequestGrantedFocus = Top->SequenceNumber;
LastWidgetGrantedFocus = Focus;
}
}
else
{
SlateOperations.SetUserFocus(ViewportWidgetRef);
LastRequestGrantedFocus = Top->SequenceNumber;
LastWidgetGrantedFocus = nullptr;
}
}
else if (TSharedPtr<SWidget> ClickedWidget = ClickToFocusTarget.Pin())