From 3f6ef4b56c34b4b06e66a3ca4152ccb4a71d7b6b Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 24 Apr 2026 20:17:08 -0400 Subject: [PATCH] Simplify keyboard focus rule to just 'widget in front', full stop. --- Docs/Luprex-Window-Management.md | 160 +++++++++----------- Source/Integration/PlayerControllerBase.cpp | 39 ++--- Source/Integration/PlayerControllerBase.h | 4 - Source/Integration/RootCanvas.cpp | 2 - Source/Integration/RootCanvas.h | 3 - 5 files changed, 84 insertions(+), 124 deletions(-) diff --git a/Docs/Luprex-Window-Management.md b/Docs/Luprex-Window-Management.md index 5bac441f..27bf4906 100644 --- a/Docs/Luprex-Window-Management.md +++ b/Docs/Luprex-Window-Management.md @@ -28,72 +28,53 @@ Our window management system, in order to keep things simple, has to make some assumptions about how Luprex games work. So, here are the rules. -The presumption is that most of the time, you're interacting -with the 3D world, and importantly, we assume that if you're -using keyboard and mouse, you're using the mouse to control -the camera - aka "mouselook." +Top-level UserWidgets get inserted into a "Root Canvas", +instead of into the viewport. The root canvas implements most +of the functionality of our window management system. -We also assume that as you interact with the 3D world, you -will occasionally be popping up GUI widgets that can -coexist with mouselook. These mouselook-compatible -widgets don't need a mouse pointer, they don't need you to -click on anything. They rely on buttons alone. We assume -that most of the GUI elements you interact with will be -mouselook-compatible, in order to allow you to stay -immersed in the 3D world. +The keyboard focus rule is simple: the UserWidget in front +according to the z-order gets keyboard focus. The window +management system will put focus on the front widget and +will keep it there. The *only* way to give a UserWidget +keyboard focus is to raise it to the front of the z-order. -But we also assume that there may be moments when you want -to pop up a very complicated widget, for example, a big -inventory management screen, for which a mouse pointer would -be very helpful. For occasions like these, a widget can -declare "ShowPointer". +Mouse movements events are handled in two different ways: +the system can shift between "mouselook" mode and +"point-and-click" mode. Every top-level UserWidget declares +whether it wants a mouse pointer or not. If the front +UserWidget wants a pointer, the system shifts into +point-and-click mode. -When one of these ShowPointer widgets is on the screen, -the entire system switches into point-and-click mode. -In point-and-click mode, the pointer is visible. Mouse -movements move the pointer. Mouse movements do *not* get -translated into mouselook. +In point-and-click mode, enhanced input mouse move events +cannot happen. In mouselook mode, widget OnMouseDown and +OnMouseMove events cannot happen. In both modes, you can +track mouse movement, but you have to use different +mechanisms. -Widgets have a z-order: one widget is always "in front." In -mouselook mode, only the front widget can get keyboard -focus. In mouselook mode, the window management system will -put focus on the front widget, and it will keep it there. -If you want some other widget to have focus, you'll have to -bring that widget to the front. +Widgets that declare that they want a pointer are +automatically put in front of widgets that don't want a +pointer. Because of this rule, the system essentially +separates into the "mouselook" layer underneath, and the +"point-and-click" layer on top. When the point-and-click +layer gets out of the way, then you can drive the 3D world. -In point-and-click mode, the keyboard focus rules differ. -When you raise a widget to the front, the window management -system will give it focus. But, if you click the mouse -pointer on a different widget - say, on a text box in a -different widget - keyboard focus can get transferred. +# State Variables of the Window Management System -When a ShowPointer window is on the screen, not only -does the system shift to point-and-click mode, but it -also keeps all ShowPointer windows in front of any -non-ShowPointer window. - -Basically, you can think of the system as a 3D world with -its mouselook-compatible widgets as one layer, and the -point-and-click stuff as a second layer on top of that. -When the point-and-click layer gets out of the way, then you -can drive the 3D world. - -# The Root Canvas +I have made an effort to keep the number of state variables +that you have to control to an absolute minimum, and to +concentrate them all in one place. That place is the "Root +Canvas Slot." Typically, in Unreal, when you create a new top-level -widget, you insert it into the viewport using -AddToViewport. But to use our window management -system, you must instead insert top-level widgets into a -'root canvas', using AddWidgetToRoot. +widget, you insert it into the viewport using AddToViewport. +But to use our window management system, you must instead +insert top-level widgets into a 'root canvas', using +AddWidgetToRoot. -The main reason for the creation of the root canvas is that -it gives us a place to store window-management related -hints, and window-management related state. - -The root canvas object attaches a RootCanvasSlot to each +The root canvas object associates a RootCanvasSlot to each top-level widget. The RootCanvasSlot is a place where we can -store management-related hints for that widget. The contents -of the RootCanvasSlot include the following: +store window management-related hints for that widget. The +contents of the RootCanvasSlot include the following: - `ShowPointer`: If true, this is a point-and-click widget. When this widget is in front, the pointer is visible, @@ -106,20 +87,23 @@ of the RootCanvasSlot include the following: *this* widget are disabled. - `BringToFrontCount`: Effectively, a timestamp indicating - the last time this window was brought to the front. + the last time this window was brought to the front. This + is the main factor determining the z-order of the widgets. In addition, the top-level widget itself contains some window-management related properties. Currently, these are: - `DesiredFocusWidget`: Indicates which sub-widget, if any, - should be given focus. + should be given focus. When the system grants focus to + the frontmost UserWidget, the focus actually goes here. -That is all the state variables that control our new window -management system. If your blueprint is managing these -properties, then it is doing everything it needs to do. +There are deliberately *no other variables* that control our +new window management system. If your blueprint is managing +these properties, then it is doing everything it needs to +do. The function SetWidgetWindowManagement can set all of these -properties in a single operation. That one function is all +variables in a single operation. That one function is all you need to control the entire window management system. # Handling Keyboard and Gamepad Buttons @@ -177,7 +161,8 @@ This is all almost entirely unchanged from Unreal's default behavior. We've only made two tiny tweaks: we send enhanced input to widgets in front-to-back order, and, widgets disable enhanced input by setting a flag instead of by -unregistering their input component. +unregistering their input component. Other than that, this +is all just stock unreal. # Handling mouse buttons @@ -185,9 +170,9 @@ Mouse buttons behave differently than keyboard buttons. Widgets have an OnMouseDown handler. This is only active in point-and-click mode. OnMouseDown only fires when three -things are true: the pointer is visible, the pointer is -inside the rectangle of a widget, and the widget is marked -hit-testable. +things are true: the system must be in point-and-click mode, +the pointer must be inside the rectangle of a widget, and +the widget must be marked hit-testable. If no OnMouseDown event fires, or if OnMouseDown declares the mouse down to be "not handled," then the mouse down @@ -199,18 +184,18 @@ buttons. It can be mapped to an enhanced input event by the input mapping context, and then from there, it can be handled by any enhanced input event handler in a blueprint. -The upshot of all this is: if you want to think of a -mouse button as "just another button," then the -way to achieve that is to handle the mouse button using -an enhanced input handler. +The upshot of all this is: if you want to think of a mouse +button as "just another button," then the way to achieve +that is to *not* write an OnMouseDown handler, but instead, +to deal with it through enhanced input. -We have very slightly tweaked the default behavior of -unreal. If the pointer is visible, and you click on a -widget that is hit-testable, but which has no OnMouseDown -handler, we provide a default OnMouseDown behavior: we -bring the widget to the front. Because our system -grants keyboard focus to the widget in front, this -will grant focus, if the widget can accept it. +We have tweaked the default behavior of unreal. If the +system is in point-and-click mode, and you click on a widget +that is hit-testable, but which has no OnMouseDown handler, +we provide a default OnMouseDown behavior: we bring the +top-level UserWidget to the front. Because our system grants +keyboard focus to the widget in front, this will also grant +focus. # Handling Mouse Movement @@ -251,17 +236,19 @@ If you're using our Luprex window management system, there are several things your blueprint should *NOT* do: - DO NOT use SetKeyboardFocus, SetUserFocus, or any other - function with Set-Focus in the name. Instead, set - the DesiredFocusWidget inside a top-level widget, and our - window management system will decide who gets focus. + function with Set-Focus in the name. Instead, just + be aware that the frontmost UserWidget will get focus. + It can delegate that focus to one of its components by + setting DesiredFocusWidget. - DO NOT use SetShowMouseCursor, or set the bShowMouseCursor - flag. Instead, set the ShowPointer flag in the configuration - of any top-level widget. + flag. Instead, set the ShowPointer flag in the + RootCanvasSlot of any top-level widget. - DO NOT use UserWidget::RegisterInputComponent or - UserWidget::UnregisterInputComponent. These will be ignored. - Instead, set or unset the flag EnableEnhancedInput, which + UserWidget::UnregisterInputComponent. These will be + ignored. Instead, set or unset the flag + EnableEnhancedInput in the RootCanvasSlot, which effectively does the same thing. - DO NOT use SetZOrder. If you try, you will be overridden @@ -279,7 +266,7 @@ several things your blueprint should *NOT* do: window management system. - DO NOT use AddToViewport or AddToPlayerScreen. Top level - widgets should be inserted into the root canvas using + UserWidgets should be inserted into the root canvas using AddWidgetToRoot. - DO NOT use SetIgnoreInput. You will be overridden. Our @@ -287,7 +274,8 @@ several things your blueprint should *NOT* do: system being active, turning it off would cause everything to fail. However, a widget can handle keyboard or character events, causing them not to be propagated, it - can also block events to any window lower in the z-order. + can also block events to any widget lower in the z-order, + and to the player controller and character. - DO NOT use SetInputModeXXX. Be aware that there is no "input mode" enum or "input mode" variable anywhere in diff --git a/Source/Integration/PlayerControllerBase.cpp b/Source/Integration/PlayerControllerBase.cpp index cfdd2e19..cf2d65ea 100644 --- a/Source/Integration/PlayerControllerBase.cpp +++ b/Source/Integration/PlayerControllerBase.cpp @@ -131,13 +131,6 @@ UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *W return Cast(Value); } -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)->RootCanvas->LastWidgetGrantedFocus = nullptr; -} - void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget) { if (!IsValid(Widget)) @@ -309,32 +302,20 @@ void AlxPlayerControllerBase::UpdateInputMode() GameViewportClient->SetIgnoreInput(false); - // How we handle focus depends on whether we're showing the pointer. - // In pointer mode, we set focus to the desired state just once, - // and then we let the pointer control it from there on. In - // no-pointer mode, we set focus to the desired state and - // keep putting it back forever. - // - // If the user clicks the mouse on a focusable widget, the - // viewport client notifies us of that fact. We then focus the - // widget if possible. - // - if ((!ShowPointer) || (RootCanvas->LastWidgetGrantedFocus != Focus)) + // We always put keyboard focus on whatever user widget is in + // front. If the front widget doesn't want keyboard focus, + // then we put keyboard focus on the viewport. + if (Focus) { - if (Focus) + if (TSharedPtr SlateFocus = Focus->GetCachedWidget()) { - if (TSharedPtr SlateFocus = Focus->GetCachedWidget()) - { - SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); - RootCanvas->LastWidgetGrantedFocus = Focus; - } - } - else - { - SlateOperations.SetUserFocus(ViewportWidgetRef); - RootCanvas->LastWidgetGrantedFocus = nullptr; + SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); } } + else + { + SlateOperations.SetUserFocus(ViewportWidgetRef); + } } void AlxPlayerControllerBase::UpdateLookAt() diff --git a/Source/Integration/PlayerControllerBase.h b/Source/Integration/PlayerControllerBase.h index b4018af6..9b769e05 100644 --- a/Source/Integration/PlayerControllerBase.h +++ b/Source/Integration/PlayerControllerBase.h @@ -65,10 +65,6 @@ public: // FProperty so we always see the current value without caching it. static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget); - // 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")) diff --git a/Source/Integration/RootCanvas.cpp b/Source/Integration/RootCanvas.cpp index 456652bd..1849de4c 100644 --- a/Source/Integration/RootCanvas.cpp +++ b/Source/Integration/RootCanvas.cpp @@ -101,8 +101,6 @@ void UlxRootCanvasPanel::BringToFront(UUserWidget *Widget) UlxRootCanvasPanel *Panel = Cast(Slot->Parent); if (!Panel) return; Slot->BringToFrontCount = ++Panel->BringToFrontCounter; - // This refocuses the widget, even if it was already in front. - Panel->LastWidgetGrantedFocus = Panel; } void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget, diff --git a/Source/Integration/RootCanvas.h b/Source/Integration/RootCanvas.h index 044483d9..9b560549 100644 --- a/Source/Integration/RootCanvas.h +++ b/Source/Integration/RootCanvas.h @@ -129,9 +129,6 @@ public: meta = (DefaultToSelf = "Widget", ExpandEnumAsExecs = "Result")) static UlxRootCanvasSlot *GetRootCanvasSlot(class UUserWidget *Widget, ElxSuccessOrWrongType &Result); - // The last widget whose focus request was granted. - TObjectKey LastWidgetGrantedFocus; - protected: // We inherit most of our code from CanvasPanel. This causes the