More work on focus, and good docs
This commit is contained in:
323
Docs/Luprex-Window-Management.md
Normal file
323
Docs/Luprex-Window-Management.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 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.
|
||||
|
||||
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."
|
||||
|
||||
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.
|
||||
|
||||
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".
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
- `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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The function SetWidgetWindowManagement can set all of these
|
||||
properties in a single operation. That one function is all
|
||||
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.
|
||||
|
||||
# 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 pointer is visible, the pointer is
|
||||
inside the rectangle of a widget, and the widget is marked
|
||||
hit-testable.
|
||||
|
||||
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 handle the mouse button using
|
||||
an enhanced input handler.
|
||||
|
||||
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.
|
||||
|
||||
# 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, set
|
||||
the DesiredFocusWidget inside a top-level widget, and our
|
||||
window management system will decide who gets focus.
|
||||
|
||||
- DO NOT use SetShowMouseCursor, or set the bShowMouseCursor
|
||||
flag. Instead, set the ShowPointer flag in the configuration
|
||||
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
|
||||
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
|
||||
widgets should be inserted into the root canvas using
|
||||
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 window lower in the z-order.
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user