Fix some issues with new root canvas stuff

This commit is contained in:
2026-04-22 17:02:24 -04:00
parent d985a6bc55
commit a964211cc8
5 changed files with 128 additions and 101 deletions

Binary file not shown.

View File

@@ -163,8 +163,7 @@ void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
if (Widget->GetParent() == PC->RootCanvas) return;
UlxRootCanvasSlot *Slot = PC->RootCanvas->AddChildToRootCanvas(Widget);
Slot->SlotUnderConstruction = false;
PC->RootCanvas->AddChildToRootCanvas(Widget);
}
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
@@ -239,7 +238,7 @@ void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputSta
}
// Now add the widget-managed input components.
for (UlxRootCanvasSlot *Slot : ReverseIterate(WidgetSlots))
for (UlxRootCanvasSlot *Slot : WidgetSlots)
{
if (Slot->EnableEnhancedInput)
{
@@ -269,15 +268,14 @@ void AlxPlayerControllerBase::UpdateInputMode()
if (!SlateUser.IsValid()) return;
// Get the desired configuration from the first widget.
// TODO: Maybe we don't have to sort the whole array.
TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
RootCanvas->UpdateZOrders();
// Get the desired configuration from the top widget.
UUserWidget *Widget = nullptr;
UWidget *Focus = nullptr;
bool ShowPointer = false;
if (!WidgetSlots.IsEmpty())
if (UlxRootCanvasSlot *Top = RootCanvas->GetTopWidget())
{
UlxRootCanvasSlot *Top = WidgetSlots[0];
Widget = Cast<UUserWidget>(Top->GetContent());
Focus = Widget->GetDesiredFocusWidget();
ShowPointer = Top->ShowPointer;

View File

@@ -18,33 +18,6 @@ UlxRootCanvasSlot::UlxRootCanvasSlot(const FObjectInitializer& ObjectInitializer
SetOffsets(FMargin(0.0f, 0.0f, 0.0f, 0.0f));
}
void UlxRootCanvasSlot::SetZOrderReliable(int32 Order)
{
if (SlotUnderConstruction)
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
ZOrder = Order;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
else
{
SetZOrder(Order);
}
}
int32 UlxRootCanvasSlot::GetZOrderReliable()
{
if (SlotUnderConstruction)
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
return ZOrder;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
else
{
return GetZOrder();
}
}
UClass* UlxRootCanvasPanel::GetSlotClass() const
{
@@ -70,16 +43,24 @@ UlxRootCanvasSlot* UlxRootCanvasPanel::AddChildToRootCanvas(UWidget* Content)
return Cast<UlxRootCanvasSlot>(Super::AddChild(Content));
}
int32 UlxRootCanvasPanel::GetMaxZOrder() const
void UlxRootCanvasPanel::OnSlotAdded(UPanelSlot* InSlot)
{
int32 MaxZOrder = 0;
for (UPanelSlot *PanelSlot : Slots)
UlxRootCanvasSlot *Slot = CastChecked<UlxRootCanvasSlot>(InSlot);
Slot->BringToFrontCount = ++BringToFrontCounter;
Super::OnSlotAdded(InSlot);
}
UlxRootCanvasSlot* UlxRootCanvasPanel::GetTopWidget()
{
UlxRootCanvasSlot* Top = nullptr;
for (UPanelSlot* PanelSlot : Slots)
{
UlxRootCanvasSlot *TypedSlot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(TypedSlot);
MaxZOrder = FMath::Max(MaxZOrder, TypedSlot->GetZOrderReliable());
UlxRootCanvasSlot* Slot = CastChecked<UlxRootCanvasSlot>(PanelSlot);
if (Cast<UUserWidget>(Slot->Content) == nullptr) continue;
if ((Top == nullptr) || (*Top < *Slot)) Top = Slot;
}
return MaxZOrder;
return Top;
}
TArray<UlxRootCanvasSlot*> UlxRootCanvasPanel::GetSortedUserWidgets()
@@ -95,12 +76,33 @@ TArray<UlxRootCanvasSlot*> UlxRootCanvasPanel::GetSortedUserWidgets()
}
Result.StableSort([](const UlxRootCanvasSlot &A, const UlxRootCanvasSlot &B)
{
return A.GetZOrder() > B.GetZOrder();
return A < B;
});
return Result;
}
void UlxRootCanvasPanel::UpdateZOrders()
{
for (UPanelSlot *PanelSlot : Slots)
{
UlxRootCanvasSlot *Slot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(Slot);
int32 ZOrder = (Slot->ShowPointer ? 1000000 : 0) + Slot->BringToFrontCount;
Slot->SetZOrder(ZOrder);
}
}
void UlxRootCanvasPanel::BringToFront(UUserWidget *Widget)
{
if (!IsValid(Widget)) return;
UlxRootCanvasSlot *Slot = Cast<UlxRootCanvasSlot>(Widget->Slot);
if (!Slot) return;
UlxRootCanvasPanel *Panel = Cast<UlxRootCanvasPanel>(Slot->Parent);
if (!Panel) return;
Slot->BringToFrontCount = ++Panel->BringToFrontCounter;
}
void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput, bool BringToFront, UWidget *DesiredFocusWidget)
{
@@ -120,7 +122,15 @@ void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget,
Slot->ShowPointer = ShowPointer;
Slot->BlockInput = BlockInput;
Slot->EnableEnhancedInput = EnableEnhancedInput;
Widget->SetDesiredFocusWidget(DesiredFocusWidget);
if (BringToFront) Slot->SetZOrderReliable(Panel->GetMaxZOrder() + 1);
if (DesiredFocusWidget)
{
if (!Widget->SetDesiredFocusWidget(DesiredFocusWidget))
UE_LOG(LogLuprexIntegration, Error, TEXT("SetWidgetWindowManagement: focus widget must be a child of widget"));
}
else
{
Widget->SetDesiredFocusWidget(NAME_None);
}
if (BringToFront) Slot->BringToFrontCount = ++Panel->BringToFrontCounter;
}

View File

@@ -2,13 +2,16 @@
//
// 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.
// Luprex provides a "window management system" for root
// widgets. In this system, all top-level widgets have to go
// into the root canvas (instead of the viewport). The
// window management system monitors the widgets within the
// root canvas and continuously updates the z-orders, the
// pointer visibility, the mouse capture mode, the keyboard
// focus, and enhanced input event routing based on hints
// and directives given by the widgets.
//
// To learn more, read Docs/Luprex-Window-Management.md
//
////////////////////////////////////////////////////////////
@@ -29,11 +32,6 @@ 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
@@ -60,32 +58,24 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool EnableEnhancedInput = true;
// Knowing whether the slot is under construction helps us to work
// around some bugs in Unreal. See below.
bool SlotUnderConstruction = true;
// Reliable version of SetZOrder. There is a bug in the normal version of
// SetZOrder: it crashes if you use it during OnConstruct. We have a
// workaround, but it requires using a different getter and setter.
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode")
void SetZOrderReliable(int32 Order);
// Sequence number assigned when this slot was last brought to front.
int32 BringToFrontCount = 0;
// Reliable version of GetZOrder. There is a bug in the normal version of
// SetZOrder: it crashes if you use it during OnConstruct. We have a
// workaround, but it requires using a different getter and setter.
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode")
int32 GetZOrderReliable();
// Widget Z Ordering: widgets that show the pointer go on top of windows
// that don't, and within a group, widgets that have greater BringToFrontCount
// go on top. Sorting an array with this puts the top windows at the end
// of the array.
bool operator<(const UlxRootCanvasSlot &Other) const
{
if (ShowPointer != Other.ShowPointer) return !ShowPointer;
return BringToFrontCount < Other.BringToFrontCount;
}
};
////////////////////////////////////////////////////////////
//
// 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
@@ -94,18 +84,18 @@ class INTEGRATION_API UlxRootCanvasPanel : public UCanvasPanel
public:
// Convenience wrapper around AddChild that returns the
// derived slot type, so callers don't have to cast.
// Add a child to the canvas. Also brings the child to the front
// for the first time, ensuring that every widget has a unique
// BringToFront counter.
UFUNCTION()
UlxRootCanvasSlot* AddChildToRootCanvas(UWidget* Content);
// Find children of type UserWidget. Return them in a sorted
// order, with the highest Zorder first.
// order, with the bottom window first and the top window last.
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;
// Return the highest-priority UserWidget slot, or nullptr if there are none.
UlxRootCanvasSlot* GetTopWidget();
// This function updates several window-management-related properties
// which are stored in the UserWidget and the lxRootCanvasSlot. Note that
@@ -119,6 +109,17 @@ public:
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput,
bool BringToFront, UWidget *DesiredFocusWidget);
// Bring the widget to the front of the Z order within its group.
// This is implemented using the BringToFront counter.
UFUNCTION(BlueprintCallable, Category = "Luprex|Window Management",
meta = (DefaultToSelf = "Widget"))
static void BringToFront(class UUserWidget *Widget);
// Recompute and apply ZOrder values for all slots based on ShowPointer and
// BringToFrontCount. Must be called from a context where SetZOrder is safe -
// i.e. not during OnConstruct.
void UpdateZOrders();
// 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
@@ -129,8 +130,17 @@ public:
protected:
// UPanelWidget
// We inherit most of our code from CanvasPanel. This causes the
// CanvasPanel code to allocate slots of type UlxRootCanvasSlot.
virtual UClass* GetSlotClass() const override;
// We override OnSlotAdded in order to be able to be able to
// fully finish initializing the slot before the widget
// OnConstruct method has a chance to execute.
virtual void OnSlotAdded(UPanelSlot* InSlot) override;
// Monotonic counter incremented each time any slot is brought to front.
int32 BringToFrontCounter = 0;
};

View File

@@ -16,26 +16,32 @@ _FUObjectItemType = None
_GNameBlocksDebug = None
_GObjectArrayForDebugVisualizers = None
def _init_globals(target):
def _init_globals(frame, bp_loc, dict):
global _FNameEntryType, _FNameEntryStride, _FUObjectItemType
global _GNameBlocksDebug, _GObjectArrayForDebugVisualizers
_FNameEntryType = target.FindFirstType('FNameEntry')
if not _FNameEntryType.IsValid():
print('UEDataFormatter: FNameEntry type not found')
_FNameEntryStride = _FNameEntryType.GetByteAlign()
target = frame.GetThread().GetProcess().GetTarget()
_FUObjectItemType = target.FindFirstType('FUObjectItem')
if not _FUObjectItemType.IsValid():
print('UEDataFormatter: FUObjectItem type not found')
if _FNameEntryType is None:
t = target.FindFirstType('FNameEntry')
if t.IsValid():
_FNameEntryType = t
_FNameEntryStride = t.GetByteAlign()
_GNameBlocksDebug = target.FindFirstGlobalVariable('GNameBlocksDebug')
if not _GNameBlocksDebug.IsValid():
print('UEDataFormatter: GNameBlocksDebug not found')
_GObjectArrayForDebugVisualizers = target.FindFirstGlobalVariable('GObjectArrayForDebugVisualizers')
if not _GObjectArrayForDebugVisualizers.IsValid():
print('UEDataFormatter: GObjectArrayForDebugVisualizers not found')
if _FUObjectItemType is None:
t = target.FindFirstType('FUObjectItem')
if t.IsValid():
_FUObjectItemType = t
if _GNameBlocksDebug is None:
v = target.FindFirstGlobalVariable('GNameBlocksDebug')
if v.IsValid():
_GNameBlocksDebug = v
if _GObjectArrayForDebugVisualizers is None:
v = target.FindFirstGlobalVariable('GObjectArrayForDebugVisualizers')
if v.IsValid():
_GObjectArrayForDebugVisualizers = v
############################################################
@@ -677,8 +683,11 @@ def _register_provider(cat, pattern, summary_fn=None, synth_cls=None):
def __lldb_init_module(debugger, dict):
print("Running lldb_init_module")
_init_globals(debugger.GetSelectedTarget())
debugger.HandleCommand('target stop-hook add --python-function ' + __name__ + '._init_globals')
debugger.HandleCommand('type category delete ' + __name__)
frame = debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
if frame.IsValid():
_init_globals(frame, None, {})
cat = debugger.CreateCategory(__name__)
_register_provider(cat, '^FString$', summary_fn='UEFStringSummaryProvider')