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.wrapFocus(boolean) — list mode only: does moving past the last (or first) registered widgetfocusOrder(Array.<UiWidget>) — Every focusable widget registered across every focusEnabled containercurrentlyFocusedIndex(integer) — list mode only: index of currentlyFocused within focusOrder, or -1cursorPosition(BGE.Math.Vector) — pointer mode only: virtual cursor position, in UI-canvas coordinatecursorStep(float) — pointer mode only: pixels the cursor moves per press/held-frame.analogAxisName(dynamic) — Opt-in: name of a BGE.Controller.ControlMap axis (bound viacursorAnalogSpeed(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 resetrepeatThrottle(BGE.UI.RepeatThrottle) — Throttles repeat-while-held navigation to one step peranalogListDirectionActive(boolean) — list mode only, analog stick navigation (see updateAnalogList()): wasanalogListRepeatTimer(BGE.CountdownTimer) — list mode only: counts down analogListRepeatDelay between repeat stepsanalogListSuppressed(boolean) — list mode only: set by focusWidget() so a direction already held then is ignored until released.analogListRepeatDelay(float) — Seconds to hold the stick deflected before updateAnalogList() repeats a
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 scene'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
updateAnalogList(
controls: BGE.Controller.ControlMap,
dt: float,
): void
list mode: continuously drives focus from a bound analog stick axis, the list-mode counterpart to updateAnalogCursor() (pointer mode) - see that method's own doc comment for why this needs to run every frame (Game.processUiInput()) rather than waiting for a discrete input event. No-op unless navigationMode is list, analogAxisName is set, and controls has at least one binding - zero cost for a game that doesn't use this.
Crossing CURSOR_ANALOG_DEADZONE moves focus by one step immediately (edge-triggered, like a fresh button press); continuing to hold past it repeats one step every analogListRepeatDelay seconds until the stick returns to neutral, at which point the next push acts immediately again rather than inheriting a stale repeat delay. Reuses the pointer mode's same analogAxisName field - only one navigationMode is ever active at a time, so there's no ambiguity in sharing it.
Direction picks whichever axis component has the larger magnitude (dominant axis) - a d-pad's remote fallback (see ControlMap.getAxis()) always reports a pure cardinal direction, but a real analog stick rarely reads as perfectly on-axis, and always requiring y to win (the way the discrete d-pad path does, since a d-pad tap can't be diagonal anyway) would make a mostly-horizontal push on a vertical list feel unresponsive.
Parameters
controls(BGE.Controller.ControlMap) — Game.controlsdt(float) — Game.getDeltaTime()
Returns
void
focusWidget(widget: BGE.UI.UiWidget): void
Focuses a specific registered widget directly (list mode) - e.g. a freshly-shown panel wanting a particular button focused by default, rather than whatever seedList()/navigateList() would otherwise leave focused (seedList() only ever seeds once per FocusManager, not once per panel, so a panel rebuilt and reshown later - see BGE.UI.MessagePanel's own doc comment on why it rebuilds fresh each time - doesn't get this for free). A no-op if widget isn't currently registered (e.g. its container was hidden/torn down already).
Parameters
widget(BGE.UI.UiWidget)
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