FocusManager
Owns the single, global focused-widget/cursor state shared by every UiContainer that opts in (UiContainer.focusEnabled) - one Game instance owns one FocusManager (Game.focusManager). Widget positions are already absolute UI-canvas coordinates (each level of UiContainer.draw() adds its own parent's position - see UiWidget.getWorldPosition()), so one shared cursor/hit-test naturally spans nested containers with no translation.
Properties
game(BGE.Game)navigationMode(BGE.UI.FocusNavigationMode) — How directional input moves focus - see BGE.UI.FocusNavigationMode. ' Defaults to list (discrete next/previous, like a typical menu); a game ' opts into pointer (spatial cursor) explicitly.wrapFocus(boolean) — list mode only: does moving past the last (or first) registered widget ' wrap around to the other end? Off by default - most menus stop at the ' ends rather than cycling.focusOrder(dynamic) — Every focusable widget registered across every focusEnabled container ' (gameUi and any of its focusEnabled descendants) - flat, in register() ' order. In list mode this order IS the navigation order; in pointer mode ' it's used only to seed initial focus and as the hit-test target.currentlyFocusedIndex(integer) — list mode only: index of currentlyFocused within focusOrder, or -1 ' before anything is focused.cursorPosition(BGE.Math.Vector) — pointer mode only: virtual cursor position, in UI-canvas coordinate ' space. Moved by directional input; hit-tested against focusOrder each ' frame to drive hover/focus (cursor-primary - hovering a widget focuses ' it).cursorStep(float) — pointer mode only: pixels the cursor moves per press/held-frame.analogAxisName(dynamic) — Opt-in: name of a BGE.Controller.ControlMap axis (bound via ' ControlMap.bindAxis()) that continuously drives cursorPosition in pointer ' mode, alongside the existing d-pad press/held stepping. invalid (the ' default) means no analog input drives the cursor at all - zero behavior ' change. See updateAnalogCursor(). ' dynamic, not string, so a game can reset it back to invalid (e.g. when ' leaving the room that opted in) - the type checker rejects assigning ' invalid to a string-typed field.cursorAnalogSpeed(float) — Pixels/second the cursor moves at full stick deflection (magnitude 1.0).currentlyFocused(BGE.UI.UiWidget)hasSeededFocus(boolean)consumedThisFrame(boolean) — Latched true as soon as anything consumes during a frame, and reset ' once per frame (see resetFrame()). Game.bs dispatches more than one ' event per frame (a press, plus a synthesized held event while the ' button stays down) - without this latch a later, unconsumed event ' would undo an earlier one's input capture.repeatThrottle(BGE.UI.RepeatThrottle) — Throttles repeat-while-held navigation to one step per ' WIDGET_REPEAT_DELAY_MS, the same way Slider/Select throttle their own ' adjustment - shared between list and pointer mode since only one mode ' is ever active at a time.
Constructor
new FocusManager(game: BGE.Game): FocusManagerParameters
game(BGE.Game)
Instance Methods
register(widget: UiWidget): void
Registers a focusable widget - called by UiContainer.addChild().
Parameters
widget(UiWidget)
Returns
void
registeredCount(): integer
How many focusable widgets are currently registered.
Returns
integer
getRegistered(index: integer): UiWidget
The widget registered at the given index, in register() order.
Parameters
index(integer)
Returns
unregister(widget: UiWidget): void
Unregisters a focusable widget - called by UiContainer.removeChild().
Parameters
widget(UiWidget)
Returns
void
resetFrame(): void
Top-of-frame reset of the input-consumption latch. Safe to call more than once per frame (every focusEnabled UiContainer's onUpdate() calls this) since resetting to false twice is a no-op.
Returns
void
update(input: BGE.GameInput): void
Drives the focus side of input handling for the whole shared registry. First seeds/updates currentlyFocused for whatever this frame's state is (list mode: seeds index 0 once; pointer mode: hit-tests the cursor each call - see seedList()/seedAndHitTestPointer()) - this happens before dispatch, so hover/focus state is current for this frame. Then gives the focused widget first refusal on the event - OK press dispatches onMouseDown()/onClick() and consumes; OK release dispatches onMouseUp() only (does NOT consume - a release is never a discrete "act on this" event, so it's free to also reach GameEntity.onInput()); anything else forwards to the widget's own handleInput() (see UiWidget.handleInput()). Only if the widget did NOT report the event as handled does focus itself move (navigateList()/navigatePointer()) - otherwise the same Left/Right that adjusted a focused Slider would also walk focus away from it. If anything along the way calls input.consume(), Game.setInputEntity() captures all input for the rest of this frame - see GameInput.consume(). "Rest of this frame" is latched in m.consumedThisFrame (see resetFrame()).
Called exactly once per input event by Game.processFocusManagerInput() - not from UiContainer.onInput(), so nesting focusEnabled containers can't cause this to double-fire for one event (e.g. two onClick() calls for one OK press).
Parameters
input(BGE.GameInput) — GameInput object for the last frame
Returns
void
ensureFocusSeeded(): void
Forces initial focus onto the first registered widget immediately, without waiting for the first input event - update() only seeds focus lazily, on its next call (driven by an actual button press/held event or, in pointer mode, updateAnalogCursor()), so a widget added this frame stays unfocused/unhighlighted until the player presses something. Call this once right after adding a room's focusable widgets (e.g. at the end of onCreate()) so the first one already shows focused on screen before any input arrives. Safe to call even with nothing registered yet, and a no-op once focus has already been seeded (e.g. update() got there first).
Returns
void
updateAnalogCursor(
controls: BGE.Controller.ControlMap,
dt: float,
): void
Continuously moves cursorPosition from a bound analog stick axis, then re-runs hit-testing - unlike update(), called every frame regardless of whether a d-pad event fired this frame (see Game.processUiInput()), since analog movement can't wait for a discrete input event. No-op unless navigationMode is pointer, analogAxisName is set, and controls has at least one binding - zero cost for a game that doesn't use this.
Once analogAxisName is set to a name actually bound via bindAxis(), this becomes the ONLY thing moving the cursor on directional input: ControlMap.getAxis() falls back to the remote d-pad when the bound stick is neutral, so a d-pad press already flows through here, and update()'s discrete cursorStep stepping is skipped to avoid double-driving the same input. A single d-pad tap therefore moves the cursor by one frame's worth of cursorAnalogSpeed rather than a full cursorStep - a deliberate tradeoff of this mode. If analogAxisName is unbound (never passed to bindAxis(), e.g. a typo), update() detects that via ControlMap.hasAxisBinding() and falls back to discrete stepping instead of leaving the cursor dead.
Parameters
controls(BGE.Controller.ControlMap) — Game.controlsdt(float) — Game.getDeltaTime()
Returns
void
draw(canvas: BGE.Canvas, theme: BGE.UI.Theme): void
Draws the single global cursor indicator. pointer mode only - list mode has no spatial cursor (widgets self-render their own focus ring off m.focused, e.g. Button/Slider/Select/Checkbox.draw()). Called once per frame by Game.drawUI(), not per-container.
Parameters
canvas(BGE.Canvas)theme(BGE.UI.Theme)
Returns
void