Home / Specs / TrackPoint Navigation
Gestures v1.2.4

TrackPoint Navigation

Joystick-style cursor control on navigation keys

TrackPoint Navigation Mode

Overview

TrackPoint Navigation Mode provides joystick-style cursor control by holding arrow keys (without initial swipe movement). When activated, finger movement in any direction moves the cursor proportionally, with speed scaling based on distance from the activation point. Supports all 8 directions including diagonals for fluid text navigation.

Key Files

FileClass/FunctionPurpose
src/main/kotlin/tribixbite/cleverkeys/Pointers.kthandleTrackPointRepeat()Main cursor movement logic
src/main/kotlin/tribixbite/cleverkeys/Pointers.ktFLAG_P_TRACKPOINT_MODEMode state flag (value: 64)
src/main/kotlin/tribixbite/cleverkeys/VibratorCompat.ktCLOCK_TICK patternDistinct haptic on activation
src/main/kotlin/tribixbite/cleverkeys/Config.ktHaptic toggleTrackPoint haptic setting

Architecture

User Input (arrow key touch + hold without movement)
       |
       v
+------------------+
| onTouchDown()    | -- Records initial touch position
+------------------+
       |
       v (no initial movement + hold time exceeded)
+------------------+
| TrackPoint Mode  | -- FLAG_P_TRACKPOINT_MODE activated
| Activation       | -- CLOCK_TICK haptic feedback
+------------------+
       |
       v
+------------------+
| handleTrackPoint | -- Repeating handler tracks position
| Repeat()         | -- Calculates direction and distance
+------------------+
       |
       v
+------------------+
| Cursor Movement  | -- Sends DPAD_* key events
| Handler          | -- Supports all 8 directions
+------------------+

Data Flow

Configuration

KeyTypeDefaultDescription
trackpoint_haptic_enabledBooleantrueEnable CLOCK_TICK on mode activation
short_gesture_min_distanceInt15Pixels before short swipe detection (30px for nav keys)

Public API

Pointers.kt

// State flag constant
private const val FLAG_P_TRACKPOINT_MODE = 64

// Timer identifier for repeat handler
private val trackpointWhat = Any()

// Main TrackPoint handler - called repeatedly while mode active
private fun handleTrackPointRepeat(ptr: Pointer) {
    val dx = ptr.currentX - ptr.activationX
    val dy = ptr.currentY - ptr.activationY

    // Calculate 8-direction from delta
    val direction = getTrackPointDirection(dx, dy)

    // Send cursor movement(s)
    sendCursorMovement(direction)
}

// Get direction enum from delta coordinates
private fun getTrackPointDirection(dx: Float, dy: Float): TrackPointDirection

// Send arrow key event(s) for direction
private fun sendCursorMovement(direction: TrackPointDirection)

// Trigger activation haptic
private fun triggerTrackPointHaptic()

Direction Enumeration

enum class TrackPointDirection {
    NORTH,      // Up only
    NORTHEAST,  // Up + Right
    EAST,       // Right only
    SOUTHEAST,  // Down + Right
    SOUTH,      // Down only
    SOUTHWEST,  // Down + Left
    WEST,       // Left only
    NORTHWEST   // Up + Left
}

Implementation Details

Activation vs Short Swipe

TrackPoint mode activates when:

If finger moves >30px before timeout, it's a short swipe (single cursor move).

// Nav keys have increased tolerance to allow hold activation
val isNavKey = key.kind in listOf(Kind.Arrow_Up, Kind.Arrow_Down, Kind.Arrow_Left, Kind.Arrow_Right)
val movementThreshold = if (isNavKey) 30 else 15

if (distance < movementThreshold && holdTime > LONGPRESS_TIMEOUT) {
    activateTrackPointMode(ptr)
}

Direction Calculation

Direction determined by angle from activation point:

private fun getTrackPointDirection(dx: Float, dy: Float): TrackPointDirection {
    val angle = atan2(dy.toDouble(), dx.toDouble())
    val degrees = Math.toDegrees(angle)

    // Map angle to 8 sectors (45 degrees each)
    return when {
        degrees in -22.5..22.5 -> EAST
        degrees in 22.5..67.5 -> SOUTHEAST
        degrees in 67.5..112.5 -> SOUTH
        degrees in 112.5..157.5 -> SOUTHWEST
        degrees > 157.5 || degrees < -157.5 -> WEST
        degrees in -157.5..-112.5 -> NORTHWEST
        degrees in -112.5..-67.5 -> NORTH
        degrees in -67.5..-22.5 -> NORTHEAST
        else -> EAST // fallback
    }
}

Diagonal Movement

Diagonal directions send two key events:

private fun sendCursorMovement(direction: TrackPointDirection) {
    when (direction) {
        NORTH -> sendArrowKey(KEYCODE_DPAD_UP)
        NORTHEAST -> {
            sendArrowKey(KEYCODE_DPAD_UP)
            sendArrowKey(KEYCODE_DPAD_RIGHT)
        }
        EAST -> sendArrowKey(KEYCODE_DPAD_RIGHT)
        SOUTHEAST -> {
            sendArrowKey(KEYCODE_DPAD_DOWN)
            sendArrowKey(KEYCODE_DPAD_RIGHT)
        }
        SOUTH -> sendArrowKey(KEYCODE_DPAD_DOWN)
        SOUTHWEST -> {
            sendArrowKey(KEYCODE_DPAD_DOWN)
            sendArrowKey(KEYCODE_DPAD_LEFT)
        }
        WEST -> sendArrowKey(KEYCODE_DPAD_LEFT)
        NORTHWEST -> {
            sendArrowKey(KEYCODE_DPAD_UP)
            sendArrowKey(KEYCODE_DPAD_LEFT)
        }
    }
}

Speed Scaling

Cursor movement speed scales with finger distance from activation center:

val distance = sqrt(dx * dx + dy * dy)
val repeatDelay = when {
    distance < 50 -> 200   // Slow (5 moves/sec)
    distance < 100 -> 100  // Medium (10 moves/sec)
    distance < 150 -> 50   // Fast (20 moves/sec)
    else -> 25             // Very fast (40 moves/sec)
}
handler.postDelayed(trackpointRunnable, repeatDelay)

Haptic Feedback

Distinct CLOCK_TICK pattern on activation:

private fun triggerTrackPointHaptic() {
    if (config.trackpointHapticEnabled) {
        VibratorCompat.vibrate(HapticEvent.CLOCK_TICK)
    }
}

CLOCK_TICK provides a subtle, distinct feel different from normal key press vibration.

Nav Key Exclusion from Gesture Collection

Arrow keys are excluded from short gesture path collection to prevent accidental gesture triggering:

private fun shouldCollectGesturePath(key: KeyValue): Boolean {
    // Nav keys excluded - they have TrackPoint mode instead
    if (key.kind in listOf(Kind.Arrow_Up, Kind.Arrow_Down, Kind.Arrow_Left, Kind.Arrow_Right)) {
        return false
    }
    return true
}

Error Handling