Skip to content

feat(nui-libgdx): replace touch pointer removal logic with configurable stickiness - #67

Open
BenjaminAmos wants to merge 1 commit into
MovingBlocks:masterfrom
BenjaminAmos:libgdx-touch-cooldowns
Open

feat(nui-libgdx): replace touch pointer removal logic with configurable stickiness#67
BenjaminAmos wants to merge 1 commit into
MovingBlocks:masterfrom
BenjaminAmos:libgdx-touch-cooldowns

Conversation

@BenjaminAmos

Copy link
Copy Markdown
Contributor

This is a fix that makes the UI on Android when using nui-libgdx a bit more responsive. I found that with my previous implementation of the touch removal logic some button release events were not being processed. This was due to the pointer being removed before the button had registered the mouse release event, so the pointer was no longer being registered as hovering over the button.

The new logic makes the system more configurable by employing a "stickiness" value, which is the number of updates to retain the touch pointer for at it's former position after the touch has been released. A lower stickiness makes the UI more responsive but risks missing input events. A higher stickiness means that the UI will register mouse hover events for longer after the touch has been released.

@soloturn soloturn left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review. Filed by agent driven by @soloturn via GDD.

Traced the state machine: pointerCooldowns[pointer] is set to pointerStickiness on touch, decremented once per update() call while > 0, and the pointer is only pushed off-screen once it hits 0 - a straightforward generalization of the original single-update grace period (which this replaces) into a configurable N-update one. Verified getPosition/update() still fall through to the same real-position read as before when the cooldown hasn't expired, so behavior for desktop (non-Android) callers and touched pointers is unchanged.

Compiled LibGDXMouseDevice.java plus its full dependency chain directly against gdx 1.9.14 and guava 23.0 (the versions this module actually pins to) - clean, no errors. The touched file hasn't changed on master since this PR's base commit, so it's still current despite the PR showing as "behind."

@soloturn

soloturn commented Aug 5, 2026

Copy link
Copy Markdown

@BenjaminAmos you want to have this still ?

@soloturn
soloturn force-pushed the libgdx-touch-cooldowns branch from 87e428e to 46cd966 Compare August 5, 2026 18:03
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added configurable pointer stickiness for improved handling of released touch pointers.
    • Released Android pointers now remain active for a configurable number of updates before being reset.
    • Preserved the default behavior through the existing no-argument setup.

Walkthrough

LibGDXMouseDevice adds configurable pointer stickiness. Released Android pointers remain active for the configured update count before moving off-screen. Per-pointer cooldowns replace removal flags.

Changes

Pointer stickiness

Layer / File(s) Summary
Pointer cooldown lifecycle
nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java
The device adds a configurable stickiness value and constructor. It tracks per-pointer cooldowns, refreshes active pointers, resets expired pointers, and decrements cooldowns during update().

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a rabbit watching pointers stay,
Cooldowns count each update away.
Touches refresh, expired ones flee,
Off-screen they hop so quietly.
A sticky little input spree!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the replacement of touch pointer removal with configurable pointer stickiness.
Description check ✅ Passed The description explains the Android UI responsiveness issue and the configurable stickiness change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java (1)

53-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Refresh the pointer lifecycle documentation.

The documentation immediately above pointerCooldowns still names removePointer and describes a one-update delay. The implementation now stores a per-pointer countdown and decrements it in update(). Update the comment to describe the current cooldown behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java`
at line 53, Update the documentation immediately above
LibGDXMouseDevice.pointerCooldowns to describe the per-pointer cooldown
countdown and its decrement during update(), removing the outdated removePointer
reference and one-update-delay description.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java`:
- Around line 63-66: Update the public LibGDXMouseDevice constructor to validate
that pointerStickiness is non-negative before assigning it, rejecting negative
values while preserving zero as valid for immediate removal.
- Around line 122-126: Update the cooldown handling in
LibGDXMouseDevice.update() so active pointer positions are refreshed before
their cooldowns are decremented, or otherwise move decrementing until after
sampling. Ensure pointer stickiness no longer depends on getPosition(int
pointer) being called for every pointer before each update, while preserving the
existing cooldown behavior.

---

Nitpick comments:
In
`@nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java`:
- Line 53: Update the documentation immediately above
LibGDXMouseDevice.pointerCooldowns to describe the per-pointer cooldown
countdown and its decrement during update(), removing the outdated removePointer
reference and one-update-delay description.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 28fb3222-ab1c-46db-9790-e64be4e2273a

📥 Commits

Reviewing files that changed from the base of the PR and between eaff694 and 46cd966.

📒 Files selected for processing (1)
  • nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java

Comment on lines +63 to +66
public LibGDXMouseDevice(int pointerStickiness) {
NUIInputProcessor.init();
removePointer = new boolean[MAX_POINTERS];
this.pointerStickiness = pointerStickiness;
pointerCooldowns = new int[MAX_POINTERS];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject negative pointerStickiness values.

The public constructor accepts a negative update count. For -1, Line 91 stores a negative cooldown, and Line 92 treats it as expired. Invalid input therefore silently becomes zero stickiness. Validate pointerStickiness >= 0; keep zero valid for immediate removal.

Proposed fix
     public LibGDXMouseDevice(int pointerStickiness) {
+        if (pointerStickiness < 0) {
+            throw new IllegalArgumentException("pointerStickiness must be non-negative");
+        }
         NUIInputProcessor.init();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public LibGDXMouseDevice(int pointerStickiness) {
NUIInputProcessor.init();
removePointer = new boolean[MAX_POINTERS];
this.pointerStickiness = pointerStickiness;
pointerCooldowns = new int[MAX_POINTERS];
public LibGDXMouseDevice(int pointerStickiness) {
if (pointerStickiness < 0) {
throw new IllegalArgumentException("pointerStickiness must be non-negative");
}
NUIInputProcessor.init();
this.pointerStickiness = pointerStickiness;
pointerCooldowns = new int[MAX_POINTERS];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java`
around lines 63 - 66, Update the public LibGDXMouseDevice constructor to
validate that pointerStickiness is non-negative before assigning it, rejecting
negative values while preserving zero as valid for immediate removal.

Comment on lines +122 to +126
for (int pointer = 0; pointer < MAX_POINTERS; pointer++) {
if (pointerCooldowns[pointer] > 0) {
pointerCooldowns[pointer]--;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect pointer polling and device update call sites.
ast-grep run --lang java --pattern '$DEVICE.getPosition($_)' .
ast-grep run --lang java --pattern '$DEVICE.update()' .

Repository: MovingBlocks/TeraNUI

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find target file"
fd -a 'LibGDXMouseDevice\.java$' . || true

echo "List Java mouse/device files"
fd -a 'Mouse.*\.java$|.*Mouse.*\.java$' . | sed -n '1,200p'

echo "Search for MouseDevice references"
rg -n "MouseDevice|getPosition\\(|\\.update\\(\\)" --glob '*.java' .

Repository: MovingBlocks/TeraNUI

Length of output: 8662


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target file outline"
ast-grep outline nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java --view expanded || true

echo "Target file relevant content"
cat -n nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java | sed -n '1,180p'

echo "InputDevice contract"
cat -n nui-input/src/main/java/org/terasology/input/device/InputDevice.java | sed -n '1,220p' || true

echo "MouseDevice contract"
cat -n nui-input/src/main/java/org/terasology/input/device/MouseDevice.java | sed -n '1,220p' || true

echo "KeyboardDevice/Device update call sites"
rg -n "extends .*Update|implements .*Update|void update\\(|\\.update\\(\\)|getPointer|getPointers|pointerCooldowns|pointerStickiness|pointerCooldown" --glob '*.java' .

Repository: MovingBlocks/TeraNUI

Length of output: 14410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find update callers with mouse context"
rg -n -C 3 "\.update\(\)" --glob '*.java' . | rg -C 5 "mouse|Mouse|InputDevice|InputProcessor|NUIInputProcessor" || true

echo "Search NUIInputProcessor update and sampling"
fd -a 'NUIInputProcessor\.java$|.*InputProcessor.*\.java$' .
rg -n "NUIInputProcessor|MouseInputQueue|mouse|Pointer|update\\(|getPosition" --glob '*.java' nui nui-input nui-libgdx | sed -n '1,260p'

echo "NullMouseDevice"
cat -n nui-input/src/main/java/org/terasology/input/device/nulldevices/NullMouseDevice.java

Repository: MovingBlocks/TeraNUI

Length of output: 37804


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "CanvasImpl pointer polling/update relevant content"
cat -n nui/src/main/java/org/terasology/nui/canvas/CanvasImpl.java | sed -n '1,260p'

echo "NUIInputProcessor relevant content"
cat -n nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/NUIInputProcessor.java | sed -n '1,200p'

echo "Programmatic check: update decrements before position sampling is required by contract? (source search)"
python3 - <<'PY'
from pathlib import Path
import re

target = Path("nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java").read_text()
iface = Path("nui-input/src/main/java/org/terasology/input/device/MouseDevice.java").read_text()
canvas = Path("nui/src/main/java/org/terasology/nui/canvas/CanvasImpl.java").read_text()

print("LibGDXMouseDevice decres in update:", bool(re.search(r'public void update\(\).*pointerCooldowns\[pointer\]--', target, re.S)))
print("getPosition(int) reloads only on touched:", bool(re.search(r'if \(Gdx\.input\.isTouched\(pointer\)\).*pointerCooldowns\[pointer\] = pointerStickiness', target, re.S)))
print("getPosition(int) returns off-screen after release unless cooldown:", bool(re.search(r'} else if \(pointerCooldowns\[pointer\] <= 0\).*return new Vector2i\(Integer\.MAX_VALUE, Integer\.MAX_VALUE\)', target, re.S)))
print("CanvasImpl update starts with polling:", bool(re.search(r'for \(int ptr = 0; ptr < getMaxPointers\(\); ptr\+\+\) \{[\s\S]*getPosition\(ptr\);', canvas)))
print("MouseDevice contract requires poll-before-update:", bool(re.search(r'Decrement cooldown|sampling order|before calling|always poll', iface)))

# Behavioral probe of local state machine with sampled vs skipped pointer.
MAX_POINTERS = 20
stickiness = 3
cooldowns = [0]*MAX_POINTERS
def getPosition(p):
    pos = (10,10)
    if True:
        if True:
            cooldowns[p] = stickiness
        elif cooldowns[p] <= 0:
            cooldowns[p] = 0
            return (2**31-1, 2**31-1)
    return pos
def update():
    global cooldowns
    cooldowns = [c-1 if c > 0 else 0 for c in cooldowns]

cooldowns[0] = 3
update()                # skipped until release
print("skipped until release position after update:", getPosition(0))
cooldowns = [0]*MAX_POINTERS
cooldowns[0] = 3
getPosition(0)          # sampled touches reset
update()                # after sampling release
print("sampled once after update:", getPosition(0))
PY

Repository: MovingBlocks/TeraNUI

Length of output: 18746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find MouseDevice implementations/update calls with exact Java syntax"
python3 - <<'PY'
from pathlib import Path
import re

paths=[]
for path in Path('.').rglob('*.java'):
    text=path.read_text(errors='ignore')
    if re.search(r'\b(mouse|mousedevice|libgdxmousedevice)\b|getPosition\s*\(|\bupdate\s*\(\s*\)', text, re.I):
        paths.append(path)

for path in paths:
    text=path.read_text(errors='ignore')
    lines=text.splitlines()
    print(f"\n{'='*60}\n{path}")
    for i,line in enumerate(lines,1):
        if any(s in line for s in ['getPosition(', '.update()', '.getMaxPointers()', 'new LibGDXMouseDevice', 'processMouse', 'canvas.update', 'mouse.update']):
            lo=max(1,i-4); hi=min(len(lines),i+4)
            for n in range(lo,hi+1):
                print(f"{n:5d}: {lines[n-1]}")
PY

echo "Search exact CursorAttachment update/call path"
cat -n nui/src/main/java/org/terasology/nui/widgets/CursorAttachment.java | sed -n '45,110p'
rg -n "update\(" nui/src/main/java/org/terasology/nui/widgets/CursorAttachment.java

Repository: MovingBlocks/TeraNUI

Length of output: 24233


Guard pointer stickiness from update() skipping

update() decrements every active cooldown immediately, but getPosition(int pointer) is the only place that refreshes a touched pointer and it returns an off-screen position once the cooldown reaches zero. This makes pointer stickiness depend on external code polling every pointer before each update; update active pointers first, or move the decrement after sampling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXMouseDevice.java`
around lines 122 - 126, Update the cooldown handling in
LibGDXMouseDevice.update() so active pointer positions are refreshed before
their cooldowns are decremented, or otherwise move decrementing until after
sampling. Ensure pointer stickiness no longer depends on getPosition(int
pointer) being called for every pointer before each update, while preserving the
existing cooldown behavior.

@BenjaminAmos

Copy link
Copy Markdown
Contributor Author

Just from re-reading this, I can already see a potential issue. By decrementing always by 1 in the update() method, it ties the pointer cooldown to the frame rate. Somehow Destination Sol has got by without this fix for years, so I do not recall just how much of a difference it makes. I believe it mostly helped in when interacting with more complex NUI screens, like the widget test screen which I often tested against during the NUI migration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants