Files
integration/Docs/Luprex-Window-Management.md

312 lines
12 KiB
Markdown
Raw Permalink Normal View History

2026-04-22 22:52:04 -04:00
# Introduction
Unreal has several input mode-related subsystems that
interact with each other in complicated ways. These
subsystems include:
- keyboard focus
- mouse capture
- enhanced input routing
- pointer visibility
- window z-order
Unreal is littered with conditionals that cause these bits
of state to affect each other in unpredictable, often
illogical ways. If you set these bits of state in the wrong
order, or to the wrong values, it is all too easy to get
unreal into a non-functioning state. The system is *much*
too fragile.
For this reason, I have implemented a window management
system that orchestrates all of this from a centralized
location, in a way that guarantees reasonably predictable,
sane behavior.
# Core Design Choices
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.
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.
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.
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.
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 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.
# State Variables of the Window Management System
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."
2026-04-22 22:52:04 -04:00
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.
2026-04-22 22:52:04 -04:00
The root canvas object associates a RootCanvasSlot to each
2026-04-22 22:52:04 -04:00
top-level widget. The RootCanvasSlot is a place where we can
store window management-related hints for that widget. The
contents of the RootCanvasSlot include the following:
2026-04-22 22:52:04 -04:00
- `ShowPointer`: If true, this is a point-and-click widget.
When this widget is in front, the pointer is visible,
and the system switches to point-and-click mode.
- `BlockInput`: If this window is in front, all enhanced
input events in *other* objects are blocked.
- `EnableEnhancedInput`: If false, enhanced input events in
*this* widget are disabled.
- `BringToFrontCount`: Effectively, a timestamp indicating
the last time this window was brought to the front. This
is the main factor determining the z-order of the widgets.
2026-04-22 22:52:04 -04:00
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. When the system grants focus to
the frontmost UserWidget, the focus actually goes here.
2026-04-22 22:52:04 -04:00
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.
2026-04-22 22:52:04 -04:00
The function SetWidgetWindowManagement can set all of these
variables in a single operation. That one function is all
2026-04-22 22:52:04 -04:00
you need to control the entire window management system.
# Handling Keyboard and Gamepad Buttons
Here is a summary of how keyboard/gamepad button handling in
unreal works. We have tweaked this slightly, but this is
mostly just ordinary unreal input handling:
When you press a keyboard or gamepad button, the button
first goes to any widget that has keyboard focus. If that
widget doesn't declare the button to be "handled", then
button is offered to other widgets higher in the widget
heirarchy. If no widget handles the button, the button then
goes to the "enhanced input subsystem."
The enhanced input system puts the button through
an "input mapping context." Basically, that's a many-to-one
map that translates buttons into more abstract "enhanced
input events." Here's a fragment of a typical input
mapping context:
Key W --> IA_Move_Forward
Key S --> IA_Move_Backward
Left_Thumbstick_Forward --> IA_Move_Forward
Left_Thumbstick_Backward --> IA_Move_Backward
What the mapping context buys you is that you can handle
events like "IA_Move_Forward" without having to care
whether the player is driving with the WASD keys or with
the gamepad left thumbstick.
Typically, enhanced input events go to *all* of the
following: the player controller, the character, and
user-defined widgets. All of these consumers of enhanced
input are automatically registered to receive enhanced
input, which means that all they have to do is implement a
handler in their event graph, and they're ready. Other
actors can *also* receive enhanced input, but that requires
jumping through some hoops.
It's interesting that a widget can implement a handler for a
raw keyboard button, and then declare the button "not
handled". If the button proceeds to the enhanced input
system, and if the widget has a handler for enhanced input,
the widget can receive the same button again, in a
different form!
There is a priority order among consumers of enhanced input
events: user widgets first (front-to-back), then the player
controller, then the character. A consumer of enhanced
input has the option of blocking input to lower-priority
consumers.
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. Other than that, this
is all just stock unreal.
2026-04-22 22:52:04 -04:00
# Handling mouse buttons
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 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.
2026-04-22 22:52:04 -04:00
If no OnMouseDown event fires, or if OnMouseDown declares
the mouse down to be "not handled," then the mouse down
makes it to the enhanced input subsystem.
Once the mouse down reaches the enhanced input system, it
starts being treated the same as keyboard and gamepad
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 *not* write an OnMouseDown handler, but instead,
to deal with it through enhanced input.
2026-04-22 22:52:04 -04:00
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.
2026-04-22 22:52:04 -04:00
# Handling Mouse Movement
In point-and-click mode, mouse movement moves the pointer
and doesn't generate any events at all.
There is one exception: mouse capture. If you click on a
hit-testable widget, that widget will "capture" the mouse
until you release the mouse button. As long as the widget
has capture, it receives OnMouseMove events. This is
mainly intended to implement click-and-drag, scroll
bar scrolling, and other movements like that.
Unreal has a *lot* of complicated mouse capture and mouse
lock options and modes. We don't support any of that. We
support only the basics: automatic capture when you click.
If you need more, we'll have to improve the Luprex window
management system.
In point-and-click mode, mouse movements do not go to the
enhanced input system at all.
When the system is in mouselook mode, mouse movements go
directly to the enhanced input system. They get mapped by
the input mapping context and turned into enhanced input
events. Handling these events is how mouselook works.
# Handling Analog Joysticks
Analog joysticks (including gamepad thumbsticks) generate
events that go directly to the enhanced input subsystem.
They get mapped to enhanced input events. From there,
they can be handled by any consumer of enhanced input.
# Functions you Should NOT CALL!
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, just
be aware that the frontmost UserWidget will get focus.
It can delegate that focus to one of its components by
setting DesiredFocusWidget.
2026-04-22 22:52:04 -04:00
- DO NOT use SetShowMouseCursor, or set the bShowMouseCursor
flag. Instead, set the ShowPointer flag in the
RootCanvasSlot of any top-level widget.
2026-04-22 22:52:04 -04:00
- DO NOT use UserWidget::RegisterInputComponent or
UserWidget::UnregisterInputComponent. These will be
ignored. Instead, set or unset the flag
EnableEnhancedInput in the RootCanvasSlot, which
2026-04-22 22:52:04 -04:00
effectively does the same thing.
- DO NOT use SetZOrder. If you try, you will be overridden
by our window management code. Currently, the only control
we're giving over window z-order is 'BringToFront'. If
you need more control, we'll have to enhance the window
management system.
- DO NOT use SetMouseCaptureMode, SetMouseLockMode,
SetHideCursorDuringCapture, CaptureMouse, ReleaseMouseCapture,
LockMouseToWidget, ReleaseMouseLock. We simply don't support
controlling mouse capture and mouse lock at this level of
granularity. Trying to use these will fight our window
management code. If you need this, we'll have to enhance the
window management system.
- DO NOT use AddToViewport or AddToPlayerScreen. Top level
UserWidgets should be inserted into the root canvas using
2026-04-22 22:52:04 -04:00
AddWidgetToRoot.
- DO NOT use SetIgnoreInput. You will be overridden. Our
window management system relies on the enhanced input
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 widget lower in the z-order,
and to the player controller and character.
2026-04-22 22:52:04 -04:00
- DO NOT use SetInputModeXXX. Be aware that there is no
"input mode" enum or "input mode" variable anywhere in
Unreal. What these functions actually do is set a large
number of state variables - keyboard focus, mouse capture,
and so forth - from a single call. Naturally, then, these
will fight our window management system.
# Most *local* event-handling functions are allowed
There are many functions that gate or route events locally -
ie, within a single UserWidget, or within a single Actor.
Controlling and gating events within a single localized
entity does not create window-management confusion. Because
of that, all of these are still allowed:
- You CAN use EnableInput/DisableInput on actors, to turn
enhanced input events on/off for that actor.
- You CAN use PushInputComponent/PopInputComponent on the
player controller, if you want to register something that's
NOT a widget to receive enhanced input events. Seems
esoteric, but it still works.
- You CAN use methods of UUserWidget to bind or unbind
input events.
More broadly, functions that an actor or widget uses to
manipulate its *own* input component or input events
are no problem.