Skip to content
CleverKeys Wiki
implemented v1.4.0 User guide

Cursor Navigation Technical Specification

Overview

CleverKeys provides two distinct cursor navigation mechanisms: slider-based cursor (spacebar) for continuous movement scaled by distance and speed, and arrow key navigation (dedicated nav key) for discrete single-character movement. Both are accessed via swipe gestures on bottom row keys, with optional repeat acceleration when an arrow key is held.

Key Components

ComponentFilePurpose
Pointerssrc/main/kotlin/tribixbite/cleverkeys/Pointers.ktSliding inner class — touch tracking and movement calculation
KeyValuesrc/main/kotlin/tribixbite/cleverkeys/KeyValue.ktSlider enum — cursor key definitions
KeyEventHandlersrc/main/kotlin/tribixbite/cleverkeys/KeyEventHandler.kthandleSlider(), moveCursor() — cursor movement execution
InputCoordinatorsrc/main/kotlin/tribixbite/cleverkeys/InputCoordinator.ktCursor position tracking
Configsrc/main/kotlin/tribixbite/cleverkeys/Config.ktswipe_scaling, slide_step_px, slider sensitivity / smoothing / max
Layoutres/xml/bottom_row.xmlSwipe direction mappings

Architecture

Layout Configuration

From res/xml/bottom_row.xml:

<!-- Spacebar: Slider-based cursor (continuous) -->
<key width="4.4" key0="space" key5="cursor_left" key6="cursor_right"
     key7="switch_forward" key8="switch_backward"/>

<!-- Navigation key: Arrow keys (discrete) -->
<key key0="loc compose" key5="left" key6="right" key7="up" key8="down"
     key1="loc home" key2="loc page_up" key3="loc end" key4="loc page_down"/>

Swipe direction mapping:

  • key5 = West (left swipe)
  • key6 = East (right swipe)
  • key7 = North (up swipe)
  • key8 = South (down swipe)

Configuration

KeyTypeDefaultDescription
slider_sensitivityInt (% string)30Pixels per cursor movement (lower = more sensitive)
slider_speed_smoothingFloat0.7Exponential smoothing factor (0.0-1.0)
slider_speed_maxFloat4.0Maximum speed multiplier
SLIDING_SPEED_VERTICAL_MULTFloat (const)0.5Vertical movement reduction factor

Default values reflect the current constants in Config.kt:542-543 and Pointers.kt:1747. The previously-documented defaults (0.6 / 6.0) refer to an older configuration.

Implementation Details

Slider Key Types

// KeyValue.kt
enum class Slider(val symbol: String) {
    Cursor_left("\uE008"),
    Cursor_right("\uE006"),
    Cursor_up("\uE005"),
    Cursor_down("\uE007"),
    Selection_cursor_left("\uE008"),   // Extends selection leftward
    Selection_cursor_right("\uE006");  // Extends selection rightward
}

The swipe_scaling Calculation

Device-adaptive scaling ensures consistent behavior across different screen sizes:

// Config.kt
val dpi_ratio = maxOf(dm.xdpi, dm.ydpi) / minOf(dm.xdpi, dm.ydpi)
val swipe_scaling = minOf(dm.widthPixels, dm.heightPixels) / 10f * dpi_ratio

val slider_sensitivity = prefs.getString("slider_sensitivity", "30").toFloat() / 100f
slide_step_px = slider_sensitivity * swipe_scaling

Example calculations by device:

DeviceWidthDPI Ratioswipe_scalingslide_step_px (30%)
1080p phone1080px1.010832.4px
1440p phone1440px1.014443.2px
720p phone720px1.07221.6px

Sliding Algorithm

Located in Pointers.kt:1443:

inner class Sliding(
    x: Float, y: Float,
    val direction_x: Int,  // ±1 based on initial swipe direction
    val direction_y: Int,
    val slider: KeyValue.Slider
) {
    var d = 0f              // Accumulated fractional cursor movements
    var speed = 1.0f        // Current speed multiplier (1.0 - max)
    var last_move_ms: Long = -1  // Timestamp for speed calculation
}

fun onTouchMove(ptr: Pointer, x: Float, y: Float) {
    val travelled = abs(x - last_x) + abs(y - last_y)
    if (last_move_ms == -1L) {
        if (travelled < _config.slide_step_px) {
            return
        }
        last_move_ms = System.currentTimeMillis()
    }
    // Accumulate distance with speed multiplier
    d += ((x - last_x) * speed * direction_x +
          (y - last_y) * speed * SLIDING_SPEED_VERTICAL_MULT * direction_y) /
         _config.slide_step_px
    update_speed(travelled, x, y)
    // Send cursor event for each whole unit accumulated
    val d_ = d.toInt()
    if (d_ != 0) {
        d -= d_
        _handler.onPointerHold(KeyValue.sliderKey(slider, d_), ptr.modifiers)
    }
}

private fun update_speed(travelled: Float, x: Float, y: Float) {
    val now = System.currentTimeMillis()
    val instant_speed = min(getSlidingSpeedMax(), travelled / (now - last_move_ms) + 1f)
    speed = speed + (instant_speed - speed) * getSlidingSpeedSmoothing()
    last_move_ms = now
    last_x = x
    last_y = y
}

Swipe Behavior Examples

Swipe TypeDistanceTimeSpeedCursor Moves
Short, slow50px200ms~1.01-2 positions
Long, slow200px800ms~1.27-8 positions
Fast150px75ms~2.511-12 positions
Very fast200px40ms~4.0 (capped at slider_speed_max)~20 positions

Arrow Key Navigation

Discrete movement via DPAD key events:

// KeyValue.kt
"up" -> keyeventKey(0xE005, KeyEvent.KEYCODE_DPAD_UP, 0)
"right" -> keyeventKey(0xE006, KeyEvent.KEYCODE_DPAD_RIGHT, FLAG_SMALLER_FONT)
"down" -> keyeventKey(0xE007, KeyEvent.KEYCODE_DPAD_DOWN, 0)
"left" -> keyeventKey(0xE008, KeyEvent.KEYCODE_DPAD_LEFT, FLAG_SMALLER_FONT)

Each swipe triggers exactly ONE key event — no distance or speed scaling.

Comparison Table

FeatureSpacebar SliderNav Key Arrows
Key valuescursor_left, cursor_rightleft, right, up, down
Movement typeContinuous (fractional accumulation)Discrete (one per swipe)
Speed-sensitiveYes (1x to slider_speed_max)No
Distance-sensitiveYes (proportional)No
Output methodInputConnection.setSelection()KeyEvent.KEYCODE_DPAD_*
Use caseQuick navigation through textPrecise single-char movement

Cursor Movement Execution

// KeyEventHandler.kt:706
private fun handleSlider(s: KeyValue.Slider, r: Int, keyDown: Boolean) {
    when (s) {
        KeyValue.Slider.Cursor_left -> moveCursor(-r)
        KeyValue.Slider.Cursor_right -> moveCursor(r)
        KeyValue.Slider.Cursor_up -> moveCursorVertical(-r)
        KeyValue.Slider.Cursor_down -> moveCursorVertical(r)
        KeyValue.Slider.Selection_cursor_left -> moveCursorSel(r, true, keyDown)
        KeyValue.Slider.Selection_cursor_right -> moveCursorSel(r, false, keyDown)
    }
}

private fun moveCursor(d: Int) {
    val conn = recv.getCurrentInputConnection() ?: return
    val et = getCursorPos(conn)

    if (et != null && canSetSelection(conn)) {
        var selEnd = et.selectionEnd + d
        var selStart = if ((metaState and KeyEvent.META_SHIFT_ON) == 0) selEnd else et.selectionStart
        conn.setSelection(selStart, selEnd)
    } else {
        moveCursorFallback(d)  // Send arrow key events
    }
}

Holding an Arrow Key — Repeat with Acceleration

When keyrepeat_enabled is set and the user holds a discrete arrow key (rather than swiping on the spacebar slider), the long-press handler engages a repeating timer. Each repeat tick re-sends the same DPAD key event with a progressively shorter delay, producing acceleration similar to a physical keyboard:

  • Initial delay: longpress_timeout (default 600 ms) before the first repeat fires
  • Each subsequent tick: delay is interpolated downward toward TRACKPOINT_MIN_DELAY (30 ms) as the hold persists
  • Releasing the key clears the timer immediately

Holding a nav key on a layout where that key has nav subkeys instead engages TrackPoint Mode — the repeat-accelerate path applies to plain (non-subkey) arrow keys.

Word Navigation

With the Ctrl modifier held, arrow keys jump by word boundary rather than character:

fun handleWordNavigation(direction: Direction, ic: InputConnection) {
    val text = when (direction) {
        Direction.LEFT -> ic.getTextBeforeCursor(100, 0)
        Direction.RIGHT -> ic.getTextAfterCursor(100, 0)
    } ?: return

    val boundary = findWordBoundary(text, direction)
    // Move cursor by boundary offset
}

Key Events Sent

DirectionKeyEventAndroid Constant
LeftDPAD_LEFT21
RightDPAD_RIGHT22
UpDPAD_UP19
DownDPAD_DOWN20
HomeMOVE_HOME122
EndMOVE_END123

InputConnection Methods

// Alternative to key events for precise control
ic.setSelection(newPosition, newPosition)  // Move cursor
ic.getTextBeforeCursor(n, 0)               // Get context
ic.getTextAfterCursor(n, 0)                // Get context