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

@@ -0,0 +1,129 @@
# Keyboard Focus and Input Modes
Luprex provides a "window management system" that takes full ownership of: keyboard focus, mouse capture, pointer visibility, enhanced input routing, and input mode. Top-level widgets don't call `AddToViewport` or configure input modes directly — instead, they are added to a single root canvas, and the player controller arbitrates the per-tick state based on which widget is in front.
## Key types
- **`AlxPlayerControllerBase`** — owns the top-level `RootWidget`, builds the input stack, and reconciles focus / capture / pointer state every tick.
- **`UlxRootCanvasPanel`** — the one `UCanvasPanel` subclass that lives inside `RootWidget`. Every top-level UI widget in the game is a child of this canvas.
- **`UlxRootCanvasSlot`** — the slot type used for children of `UlxRootCanvasPanel`. Extends `UCanvasPanelSlot` with input-mode fields. A widget's slot carries **both** layout config (anchors, offsets, ZOrder) **and** its window-management declarations.
These types collapse what would otherwise be two parallel systems (layout + input-mode requests) into one. Adding a widget to the canvas = declaring its window-management intent.
## The core rule: ZOrder is authoritative
**The widget with the highest ZOrder wins everything.** It dictates:
- Whether the mouse pointer is shown.
- Whether the viewport captures the mouse.
- Which widget (or sub-widget) receives keyboard focus.
- Which input components end up on top of the input stack.
Draw order and input priority are the same fact. A widget you can see on top is the one that gets input.
## Slot fields (`UlxRootCanvasSlot`)
Inherited from `UCanvasPanelSlot`:
- **`ZOrder`** (int32) — Higher = drawn on top / higher input priority. Can be updated at runtime via `Slot->SetZOrder(N)`.
- **`Anchors`, `Offsets`, `Alignment`** — standard UMG canvas layout.
Added by `UlxRootCanvasSlot`:
- **`ShowPointer`** (bool) — When this widget is in front, the mouse cursor is visible and the viewport releases capture. When false, the cursor is hidden and the viewport captures the mouse (game-style input).
- **`BlockInput`** (bool) — When this widget's input component is pushed onto the stack, it blocks lower-priority components from receiving events.
- **`EnableEnhancedInput`** (bool, default true) — Whether this widget's `UInputComponent` is included on the input stack at all. Corresponds to "Enhanced Input events fire on this widget."
One property that lives on the **widget** rather than the slot:
- **`UUserWidget::DesiredFocusWidget`** — the sub-widget to receive keyboard focus when this widget is in front. Null means "focus the viewport."
## API
### Adding a widget
```cpp
// Static, BP-callable with DefaultToSelf + HideSelfPin,
// so in Blueprint it looks like a method on UUserWidget.
AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget);
```
Adds the widget as a child of the player controller's `RootCanvas` at ZOrder 0 with default flags. If the widget is already a child, does nothing.
### Updating a widget's settings
```cpp
UlxRootCanvasPanel::SetWidgetWindowManagement(
UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput,
bool BringToFront, UWidget *DesiredFocusWidget);
```
Updates an already-added widget's slot. Writes all the input-mode flags, sets `DesiredFocusWidget`, and optionally bumps ZOrder to `GetMaxZOrder() + 1` if `BringToFront` is true. Logs an error if the widget isn't yet a root widget.
### Forcing a focus re-apply
```cpp
AlxPlayerControllerBase::RestoreFocusToFrontWidget(const UObject *Context);
```
Clears `LastWidgetGrantedFocus`, which causes the next tick of `UpdateInputMode` to re-apply focus based on the current front widget. Use after mutating `DesiredFocusWidget` or when a modal is dismissed and focus should return to the underlying widget.
## Per-tick flow
On every tick, the player controller runs two passes:
### `BuildInputStack`
Assembles the list of `UInputComponent`s that will receive input events this frame:
1. Controlled pawn's input component(s).
2. Level script actors.
3. The player controller's own input component.
4. `CurrentInputStack` (anything pushed via `PushInputComponent`, filtered to exclude input components already managed by a root canvas slot).
5. Root canvas widget input components, pushed in ZOrder-ascending order (so highest ZOrder ends up on top of the stack, which gets processed first).
Each widget-managed IC has its `bBlockInput` flag set from its slot's `BlockInput` before being pushed.
### `UpdateInputMode`
Reads the front widget's slot and applies the declared state:
1. Sorts the canvas slots by ZOrder; picks the front widget.
2. Calls `SetShowMouseCursor(Slot->ShowPointer)`.
3. Configures viewport capture mode based on `ShowPointer`:
- **Pointer on**: releases capture (only if held), unlocks mouse, sets `CaptureDuringMouseDown`.
- **Pointer off**: high-precision mouse movement, locks mouse to viewport, `CapturePermanently`.
4. Resolves focus:
- If `!ShowPointer`, re-applies focus every tick (keeps stealing focus back from anything that steals it).
- If `ShowPointer` and the front widget (identity) hasn't changed since the last grant, lets click-to-focus take over.
- Otherwise grants focus once to the widget's `DesiredFocusWidget` (or the viewport if none).
## Focus arbitration
`LastWidgetGrantedFocus` (a `TObjectKey<UWidget>`) tracks the last focus target the system applied. The comparison is identity-based: if the current front widget's `DesiredFocusWidget` differs from `LastWidgetGrantedFocus`, focus is re-granted.
To force a re-grant without a widget change (e.g., after mutating `DesiredFocusWidget`), call `RestoreFocusToFrontWidget`. This sets the key to null, so the next tick's comparison will always re-grant.
## Click-to-focus
When the user clicks on a focusable widget, the `LuprexViewportClient` notifies the player controller via `ClickToFocus(Widget)`. On the next `UpdateInputMode` tick, if the system isn't going to re-grant focus for other reasons, it forwards focus to the clicked widget.
This only takes effect in pointer mode. In non-pointer mode, focus is continuously re-applied to the front widget's `DesiredFocusWidget`, overriding click behavior.
## What NOT to do
- **Don't call `AddToViewport` on top-level game widgets.** All top-level widgets must be children of `RootCanvas`. Going directly to the viewport bypasses the entire system.
- **Don't set input mode via `SetInputModeGameOnly` / `SetInputModeUIOnly` / `SetInputModeGameAndUI`.** The window management system owns these. Adjust `ShowPointer` / `BlockInput` on the slot instead.
- **Don't call `SetShowMouseCursor` directly.** Same reason — driven by the front widget's `ShowPointer`.
- **Don't set focus manually.** Set `DesiredFocusWidget` on the widget; the system will pick it up.
## Design notes
**Why slot fields, not separate requests?** Earlier versions of this system kept a parallel `FlxInputModeRequests` array of per-widget state. Tying that to slot lifecycle (widget added = entry exists; widget removed = entry gone) eliminated a whole class of stale-entry and lifecycle bugs.
**Why ZOrder as the priority key?** The alternative is a separate priority field, which can diverge from visual order. Using ZOrder means "the widget you see on top is the one with input priority" is true by construction, not by convention.
**Why identity-based focus invalidation?** Tracking a sequence number worked but required callers to call an explicit re-request API whenever `DesiredFocusWidget` changed. Using `TObjectKey<UWidget>` lets the system notice any change in the intended focus target automatically.
**Why `ShowPointer` and not a whole `EInputMode` enum?** Two flags cover the behaviors that actually matter (`ShowPointer`, `BlockInput`). A three-state enum with `GameOnly` / `UIOnly` / `GameAndUI` would force callers into categories that don't map cleanly to how this system layers widgets.