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): FocusManager

Parameters


Instance Methods

register(widget: UiWidget): void

Registers a focusable widget - called by UiContainer.addChild().

Parameters

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

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

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.controls
  • dt (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

Returns

  • void