Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public class LibGDXMouseDevice implements MouseDevice {
// The maximum number of touches that LibGDX supports on Android is 20.
// See https://github.com/libgdx/libgdx/blob/5eac848925d6e1f24070f887cbfaf99bb8bc4a63/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/AndroidInput.java#L99
private static final int MAX_POINTERS = 20;
// The default value has been calibrated by trial and error, mostly. You can set your own stickiness in the constructor.
private static final int DEFAULT_POINTER_STICKINESS = 4;
/**
* Defines how "sticky" a pointer is, meaning how many updates it should be retained for after the touch is released.
* This value can be fine-tuned based on the desired general responsiveness of the UI.
*/
private final int pointerStickiness;
/**
* Flags a pointer for "removal" when it is no longer present on the screen.
*
Expand All @@ -43,11 +50,20 @@ public class LibGDXMouseDevice implements MouseDevice {
* The removePointer variable is used to delay this removal by a single update, so that UI widgets have time
* to register the removal first (e.g. for button de-presses).
*/
private boolean[] removePointer;
private final int[] pointerCooldowns;

public LibGDXMouseDevice() {
this(DEFAULT_POINTER_STICKINESS);
}

/**
* @param pointerStickiness Defines how "sticky" a pointer is,
* meaning how many updates it should be retained for after the touch is released.
*/
public LibGDXMouseDevice(int pointerStickiness) {
NUIInputProcessor.init();
removePointer = new boolean[MAX_POINTERS];
this.pointerStickiness = pointerStickiness;
pointerCooldowns = new int[MAX_POINTERS];
Comment on lines +63 to +66

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.

}

@Override
Expand All @@ -72,15 +88,12 @@ public Vector2i getPosition(int pointer) {

if (Gdx.app.getType() == Application.ApplicationType.Android) {
if (Gdx.input.isTouched(pointer)) {
removePointer[pointer] = false;
} else {
if (removePointer[pointer]) {
// Since touches are mapped to pointers on Android, reset the pointer when not currently touching.
// Set the pointer to an off-screen location, so it acts as if it were not present.
return new Vector2i(Integer.MAX_VALUE, Integer.MAX_VALUE);
} else {
removePointer[pointer] = true;
}
pointerCooldowns[pointer] = pointerStickiness;
} else if (pointerCooldowns[pointer] <= 0) {
pointerCooldowns[pointer] = 0;
// Since touches are mapped to pointers on Android, reset the pointer when not currently touching.
// Set the pointer to an off-screen location, so it acts as if it were not present.
return new Vector2i(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
}

Expand All @@ -106,6 +119,11 @@ public boolean isButtonDown(int button) {

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

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.

}

/**
Expand Down