Skip to main content

Cursor

Cursor manages the local client cursor through handle-based ownership.

The same cursor manager is used by:

  • core game UI
  • main menu
  • debug console
  • modal dialogs
  • client-side Lua resources

Use Cursor when a resource needs a visible pointer for interactive UI such as:

  • admin panels
  • inventories
  • custom editors
  • modal tools

Do not toggle the engine cursor directly from scripts.
Use Cursor.Acquire(...) and Cursor.Release(...) so multiple systems can coexist safely.

Error Handling Pattern​

  • Success: err == nil
  • Failure: err is an error code string

Scope Model​

  • Cursor is client-only.
  • Server scripts do not directly control the local cursor.
  • Cursor state is local presentation state, not authoritative gameplay state.

Function List​

FunctionDescriptionScope
Cursor.AcquireAcquire a cursor handle for the current resource.C
Cursor.ReleaseRelease one acquired cursor handle.C
Cursor.GetStateRead the effective cursor state and active owner count.C

Ownership Model​

Cursor is handle-based.

That means:

  • each caller acquires its own cursor handle
  • each caller releases only its own cursor handle
  • the engine resolves the effective cursor state from all active handles

This avoids broken behavior when:

  • a Lua resource opens UI
  • the main menu opens above it
  • the debug console opens above both
  • one layer closes while another still needs the cursor

Typical flow:

local cursorHandle, err = Cursor.Acquire({
visible = true,
capture = "free",
reason = "admin_panel"
})

-- later
Cursor.Release(cursorHandle)

Effective State Rules​

Public scripting rule:

  • if at least one active handle requires a visible cursor, the effective cursor remains visible
  • releasing one handle does not hide the cursor if another active handle still requires it

Engine arbitration details such as core UI priority are internal implementation detail.

Automatic Cleanup​

Cursor handles owned by a resource are automatically released when:

  • the resource stops
  • the client disconnects
  • the client Lua runtime resets

Scripts should still release handles explicitly when no longer needed.

Controller and Pointer Behavior​

When the effective cursor is visible:

  • mouse input moves the same visible cursor on keyboard/mouse setups
  • controller pointer sources may move the same cursor locally

Current client behavior:

  • keyboard/mouse: native mouse movement controls the visible cursor
  • controller: controller-provided pointer movement can drive the same visible cursor while the active device is a controller

Current v1 guarantee:

  • controller ui_accept is translated into a left mouse click while the cursor is visible
  • native pointer movement still controls the same visible cursor, including controller pointer sources such as touchpads when the platform exposes them as pointer motion

Current limitation:

  • controller touchpad pointer movement is not guaranteed yet across all platforms/runtime setups
  • if the platform does not expose controller touchpad motion to Godot as native pointer motion, the visible cursor will not move from the touchpad today

Touchpad-capable controllers use the same cursor model, but exact touchpad motion support remains platform-dependent and is not yet considered production-complete.

Cursor controls cursor visibility/capture.
It does not replace focus navigation:

  • d-pad / stick UI navigation is still handled by UI/input systems
  • pointer movement is only used when a visible pointer is active

Capture Modes​

Supported capture modes:

  • free: visible free cursor for UI interaction
  • captured: cursor hidden/captured for gameplay look input
  • confined: cursor visible but confined to the game window

Resources should usually use:

  • free for UI panels

Functions​

Cursor.Acquire​

C Client Only

local handleId, err = Cursor.Acquire(options)

Parameters:

  • options (table):
  • visible (boolean): requested cursor visibility
  • capture ("free" | "captured" | "confined", default "free")
  • reason (string, optional): diagnostic reason for the request

Returns:

  • handleId (string | nil)
  • err (nil | string)

Example:

local cursorHandle, err = Cursor.Acquire({
visible = true,
capture = "free",
reason = "admin_panel"
})

if err then
Logger.Error("Cursor.Acquire failed: " .. tostring(err))
end

Cursor.Release​

C Client Only

local err = Cursor.Release(handleId)

Parameters:

  • handleId (string)

Returns:

  • err (nil | string)

Example:

if cursorHandle then
local err = Cursor.Release(cursorHandle)
if err then
Logger.Error("Cursor.Release failed: " .. tostring(err))
end
cursorHandle = nil
end

Cursor.GetState​

C Client Only

local state, err = Cursor.GetState()

Returns:

  • state (table | nil)
  • err (nil | string)

State fields:

  • visible (boolean)
  • capture ("free" | "captured" | "confined")
  • activeHandleCount (number)
  • position ({ x, y }) current viewport mouse/pointer position

Example:

local state, err = Cursor.GetState()
if not err and state then
print("visible=" .. tostring(state.visible) .. " capture=" .. tostring(state.capture))
end

UI Relationship​

Cursor and UI are related, but separate:

  • UI creates controls
  • Cursor controls whether the player can point at them

Opening a UI panel does not automatically imply cursor visibility unless the resource explicitly acquires a cursor handle.

Typical UI pattern:

local panelVisible = false
local cursorHandle = nil

local function setPanelVisible(visible)
panelVisible = visible and true or false

if panelVisible and not cursorHandle then
cursorHandle = Cursor.Acquire({
visible = true,
capture = "free",
reason = "admin_panel"
})
elseif not panelVisible and cursorHandle then
Cursor.Release(cursorHandle)
cursorHandle = nil
end
end

Input Relationship​

Cursor is not an input binding API.

Use:

  • Input for binds and action callbacks
  • UI for controls and focusable elements
  • Cursor for pointer visibility/capture ownership

Text input and UI focus rules still belong to UI / Input, not Cursor.