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,117 +0,0 @@
////////////////////////////////////////////////////////////
//
// InputModeRequest.cpp
//
////////////////////////////////////////////////////////////
#include "InputModeRequest.h"
#include "Common.h"
bool FlxInputModeRequest::operator<(const FlxInputModeRequest &Other) const
{
// The highest priority request goes to the front of the array.
// Therefore, in this context, 'less than' means 'higher priority'.
// It's a little confusing.
if (ShowPointer != Other.ShowPointer) return ShowPointer > Other.ShowPointer;
return SequenceNumber > Other.SequenceNumber;
}
bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const
{
return (Widget == Other.Widget) &&
(Focus == Other.Focus) &&
(ShowPointer == Other.ShowPointer) &&
(BlockInput == Other.BlockInput) &&
(EnableInputComponent == Other.EnableInputComponent);
}
bool FlxInputModeRequests::SanityCheck(const FlxInputModeRequest &Request)
{
if (Request.Widget == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
return false;
}
return true;
}
int32 FlxInputModeRequests::FindWidget(UUserWidget *Widget)
{
for (const FlxInputModeRequest &Req : Requests)
{
if (Req.Widget == Widget) return &Req - Requests.GetData();
}
return Requests.Num();
}
void FlxInputModeRequests::BubbleItem(int32 Index)
{
while ((Index > 0) && (Requests[Index] < Requests[Index - 1]))
{
Swap(Requests[Index], Requests[Index - 1]);
--Index;
}
while ((Index < Requests.Num() - 1) && (Requests[Index + 1] < Requests[Index]))
{
Swap(Requests[Index], Requests[Index + 1]);
++Index;
}
}
void FlxInputModeRequests::Request(const FlxInputModeRequest &NewRequest, bool UpdateSequence)
{
int32 Index = FindWidget(NewRequest.Widget);
if (Index == Requests.Num())
{
Requests.Emplace(NewRequest);
Requests[Index].SequenceNumber = ++NextSequenceNumber;
}
else
{
int32 SequenceNumber = Requests[Index].SequenceNumber;
if (UpdateSequence) SequenceNumber = ++NextSequenceNumber;
Requests[Index] = NewRequest;
Requests[Index].SequenceNumber = SequenceNumber;
}
BubbleItem(Index);
}
void FlxInputModeRequests::SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent)
{
int32 Index = FindWidget(Widget);
if (Index == Requests.Num())
{
FlxInputModeRequest NewReq;
NewReq.Widget = Widget;
NewReq.EnableInputComponent = EnableInputComponent;
NewReq.SequenceNumber = ++NextSequenceNumber;
Requests.Emplace(NewReq);
}
else
{
Requests[Index].EnableInputComponent = EnableInputComponent;
}
BubbleItem(Index);
}
void FlxInputModeRequests::Remove(UUserWidget *Widget)
{
Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry)
{
return Entry.Widget == Widget;
});
}
void FlxInputModeRequests::GarbageCollect()
{
Requests.RemoveAll([](const FlxInputModeRequest &Entry)
{
return !IsValid(Entry.Widget);
});
}

View File

@@ -1,98 +0,0 @@
////////////////////////////////////////////////////////////
//
// InputModeRequest.h
//
// Custom input event dispatching system. Uses Unreal's
// built-in input modes (GameOnly / UIOnly) with an
// enhanced input component for GameOnly mode hotkeys.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InputModeRequest.generated.h"
////////////////////////////////////////////////////////////
//
// FlxInputModeRequest
//
// Using this struct, a Widget can express a need for a
// particular input mode. These requests go to the player
// controller, which arbitrates.
//
////////////////////////////////////////////////////////////
USTRUCT(BlueprintType)
struct FlxInputModeRequest
{
GENERATED_BODY()
FlxInputModeRequest() = default;
bool operator == (const FlxInputModeRequest &Other) const;
bool operator < (const FlxInputModeRequest &Other) const;
UPROPERTY(BlueprintReadWrite)
UUserWidget* Widget = nullptr;
UPROPERTY(BlueprintReadWrite)
UWidget* Focus = nullptr;
UPROPERTY(BlueprintReadWrite)
bool ShowPointer = false;
UPROPERTY(BlueprintReadWrite)
bool BlockInput = false;
UPROPERTY(BlueprintReadWrite)
bool EnableInputComponent = true;
UPROPERTY()
int32 SequenceNumber = 0;
};
USTRUCT()
struct FlxInputModeRequests
{
GENERATED_BODY()
private:
UPROPERTY()
// Sorted by highest priority first, then most recent first.
TArray<FlxInputModeRequest> Requests;
UPROPERTY()
int32 NextSequenceNumber = 0;
public:
// Get the requests array.
const TArray<FlxInputModeRequest> &GetRequests() const { return Requests; }
// Sanity check a request to see if it is reasonable.
static bool SanityCheck(const FlxInputModeRequest &Request);
// Apply a request. Replaces any previous request by the same widget.
void Request(const FlxInputModeRequest &NewRequest, bool UpdateSequence = true);
// Find the specified widget, and modify the 'EnableInputComponent'
// flag. Adds the widget if it's not already present.
void SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent);
// Remove all requests by the specified widget.
void Remove(UUserWidget *Widget);
// Remove any requests by dead widgets or widgets with no parents.
void GarbageCollect();
private:
// Find specified widget. If not present, returns Requests.Num()
int32 FindWidget(UUserWidget *Widget);
// Move item at Index to its proper place in the array by priority.
void BubbleItem(int32 Index);
};

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())

View File

@@ -3,9 +3,12 @@
#include "CoreMinimal.h"
#include "Engine/HitResult.h"
#include "GameFramework/PlayerController.h"
#include "InputModeRequest.h"
#include "UObject/ObjectKey.h"
#include "PlayerControllerBase.generated.h"
class UlxRootCanvasPanel;
class UWidget;
UCLASS(BlueprintType, Blueprintable)
class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
{
@@ -55,9 +58,6 @@ private:
public:
// Input stack overrides: unsorted, append-on-push.
virtual void PushInputComponent(UInputComponent* InInputComponent) override;
virtual bool PopInputComponent(UInputComponent* InInputComponent) override;
virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
// Read UUserWidget::InputComponent via reflection. The field is
@@ -65,12 +65,14 @@ public:
// FProperty so we always see the current value without caching it.
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
// Blueprint-facing entry point. Looks like a method on UUserWidget
// (thanks to DefaultToSelf + HideSelfPin): the widget self-binds,
// we find its owning PlayerController, and register the request.
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true", EnableInputComponent = "true"))
static void WidgetRequestInputMode(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, class UWidget *Focus, bool EnableInputComponent);
// Restore focus back to the window that is in front, if it wants focus.
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Root Canvas")
static void RestoreFocusToFrontWidget(const UObject *Context);
// Add a widget to the root canvas at ZOrder 0 with default slot flags.
UFUNCTION(BlueprintCallable, Category = "Luprex|Root Canvas",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true"))
static void AddWidgetToRoot(class UUserWidget *Widget);
// Get the player controller, cast to AlxPlayerControllerBase.
static AlxPlayerControllerBase *FromContext(const UObject *Context);
@@ -78,13 +80,19 @@ public:
UPROPERTY()
FHitResult CurrentLookAt;
// Input mode requests - see InputModeRequest.h for an explanation.
UPROPERTY()
FlxInputModeRequests InputModeRequests;
// The last widget whose focus request was granted.
TObjectKey<UWidget> LastWidgetGrantedFocus;
// The last input mode request whose focus request was granted.
// The single top-level UUserWidget added to the viewport. All
// top-level UI widgets are children of RootCanvas inside it.
UPROPERTY()
int32 LastRequestGrantedFocus = 0;
UUserWidget *RootWidget = nullptr;
// The root canvas panel inside RootWidget. Children of this
// canvas are the top-level widgets; their slots carry both
// layout and input-mode configuration.
UPROPERTY()
UlxRootCanvasPanel *RootCanvas = nullptr;
// The viewport client uses this to notify us that the user
// clicked on a focusable widget.

View File

@@ -0,0 +1,101 @@
////////////////////////////////////////////////////////////
//
// RootCanvas.cpp
//
////////////////////////////////////////////////////////////
#include "RootCanvas.h"
#include "Common.h"
#include "Blueprint/UserWidget.h"
UlxRootCanvasSlot::UlxRootCanvasSlot(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Children of the root canvas default to filling the parent,
// matching the viewport's "stretch" behavior. Individual widgets
// can still override anchors/offsets after being added.
SetAnchors(FAnchors(0.0f, 0.0f, 1.0f, 1.0f));
SetOffsets(FMargin(0.0f, 0.0f, 0.0f, 0.0f));
}
UClass* UlxRootCanvasPanel::GetSlotClass() const
{
return UlxRootCanvasSlot::StaticClass();
}
UlxRootCanvasSlot *UlxRootCanvasPanel::GetRootCanvasSlot(UUserWidget *Widget, ElxSuccessOrWrongType &Result)
{
if (IsValid(Widget))
{
if (UlxRootCanvasSlot *Slot = Cast<UlxRootCanvasSlot>(Widget->Slot))
{
Result = ElxSuccessOrWrongType::Success;
return Slot;
}
}
Result = ElxSuccessOrWrongType::WrongType;
return nullptr;
}
UlxRootCanvasSlot* UlxRootCanvasPanel::AddChildToRootCanvas(UWidget* Content)
{
return Cast<UlxRootCanvasSlot>(Super::AddChild(Content));
}
int32 UlxRootCanvasPanel::GetMaxZOrder() const
{
int32 MaxZOrder = 0;
for (UPanelSlot *PanelSlot : Slots)
{
UlxRootCanvasSlot *TypedSlot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(TypedSlot);
MaxZOrder = FMath::Max(MaxZOrder, TypedSlot->GetZOrder());
}
return MaxZOrder;
}
TArray<UlxRootCanvasSlot*> UlxRootCanvasPanel::GetSortedUserWidgets()
{
TArray<UlxRootCanvasSlot*> Result;
Result.Reserve(Slots.Num());
for (UPanelSlot *PanelSlot : Slots)
{
UlxRootCanvasSlot *TypedSlot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(TypedSlot);
if (Cast<UUserWidget>(TypedSlot->Content) == nullptr) continue;
Result.Add(TypedSlot);
}
Result.StableSort([](const UlxRootCanvasSlot &A, const UlxRootCanvasSlot &B)
{
return A.GetZOrder() > B.GetZOrder();
});
return Result;
}
void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput, bool BringToFront, UWidget *DesiredFocusWidget)
{
if (!IsValid(Widget))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("ManageRootWidget called with an invalid widget."));
return;
}
UlxRootCanvasSlot *Slot = Cast<UlxRootCanvasSlot>(Widget->Slot);
if (Slot == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("Widget is not yet a root widget, use 'AddWidgetToRoot' first"));
return;
}
UlxRootCanvasPanel *Panel = Cast<UlxRootCanvasPanel>(Slot->Parent);
check(Panel);
Slot->ShowPointer = ShowPointer;
Slot->BlockInput = BlockInput;
Slot->EnableEnhancedInput = EnableEnhancedInput;
Widget->SetDesiredFocusWidget(DesiredFocusWidget);
if (BringToFront)
{
Slot->SetZOrder(Panel->GetMaxZOrder() + 1);
}
}

View File

@@ -0,0 +1,136 @@
////////////////////////////////////////////////////////////
//
// RootCanvas.h
//
// UlxRootCanvasPanel is a UCanvasPanel subclass whose
// slots (UlxRootCanvasSlot) carry input-mode configuration
// in addition to layout. The PlayerController scans these
// slots, sorted by ZOrder, to arbitrate pointer visibility,
// capture, focus, and input-component blocking for
// top-level widgets. ZOrder therefore serves double duty:
// it determines draw order AND input priority.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "Common.h"
#include "Blueprint/UserWidget.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "RootCanvas.generated.h"
class UWidget;
////////////////////////////////////////////////////////////
//
// UlxRootCanvasSlot
//
// Luprex provides a "window management system" for root widgets.
// This system is documented in Docs/Keyboard-Focus-and-Input-Modes.md
// The Root Canvas Slot is how widgets ask the window management system
// to engage certain behaviors.
//
////////////////////////////////////////////////////////////
UCLASS()
class INTEGRATION_API UlxRootCanvasSlot : public UCanvasPanelSlot
{
GENERATED_BODY()
public:
UlxRootCanvasSlot(const FObjectInitializer& ObjectInitializer);
// When this window is in front, the mouse pointer is shown and the
// viewport does not capture the mouse.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool ShowPointer = false;
// When this window is in front, this window's input component blocks
// lower-priority input components in the input stack.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool BlockInput = false;
// If true, this widget's input component is enabled, which is to say,
// that enhanced input events are enabled. If false, enhanced input
// events in the Event Graph are deactivated.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool EnableEnhancedInput = true;
};
////////////////////////////////////////////////////////////
//
// UlxRootCanvasPanel
//
// A UCanvasPanel that uses UlxRootCanvasSlot for its
// children instead of the plain UCanvasPanelSlot. Layout
// behavior is identical to UCanvasPanel; only the slot
// type differs.
//
////////////////////////////////////////////////////////////
UCLASS()
class INTEGRATION_API UlxRootCanvasPanel : public UCanvasPanel
{
GENERATED_BODY()
public:
// Convenience wrapper around AddChild that returns the
// derived slot type, so callers don't have to cast.
UFUNCTION()
UlxRootCanvasSlot* AddChildToRootCanvas(UWidget* Content);
// Find children of type UserWidget. Return them in a sorted
// order, with the highest Zorder first.
TArray<UlxRootCanvasSlot*> GetSortedUserWidgets();
// Return the largest ZOrder across all slots, or 0 if empty.
// Used as the basis for placing new widgets on top.
int32 GetMaxZOrder() const;
// This function updates several window-management-related properties
// which are stored in the UserWidget and the lxRootCanvasSlot. Note that
// it is perfectly legal to edit these properties by other means: this
// function is one of many valid setters. See the documentation in
// Docs/Keyboard-Focus-and-Input-Modes.md for information about how Luprex
// window management works.
UFUNCTION(BlueprintCallable, Category = "Luprex|Window Management",
meta = (DefaultToSelf = "Widget", EnableEnhancedInput = "true"))
static void SetWidgetWindowManagement(class UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput,
bool BringToFront, UWidget *DesiredFocusWidget);
// Fetch the UlxRootCanvasSlot for a widget that is parented to a
// UlxRootCanvasPanel. Returns nullptr via the WrongType exec pin
// if the widget isn't a root widget (no slot, or slot is not a
// UlxRootCanvasSlot).
UFUNCTION(BlueprintCallable, Category = "Luprex|Window Management",
meta = (DefaultToSelf = "Widget", ExpandEnumAsExecs = "Result"))
static UlxRootCanvasSlot *GetRootCanvasSlot(class UUserWidget *Widget, ElxSuccessOrWrongType &Result);
protected:
// UPanelWidget
virtual UClass* GetSlotClass() const override;
};
////////////////////////////////////////////////////////////
//
// UlxRootWidget
//
// A trivial concrete UUserWidget subclass used by the
// PlayerController to host the root UlxRootCanvasPanel.
// UUserWidget itself is marked Abstract in UMG, so we need
// a non-abstract class to instantiate.
//
////////////////////////////////////////////////////////////
UCLASS()
class INTEGRATION_API UlxRootWidget : public UUserWidget
{
GENERATED_BODY()
};