Home / Specs / Cursor Navigation
Navigation v1.1.0

Cursor Navigation

Spacebar slider and arrow key navigation

Cursor Navigation System

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.

Key Files

FileClass/FunctionPurpose
src/main/kotlin/tribixbite/cleverkeys/Pointers.ktSliding inner classTouch tracking and movement calculation
src/main/kotlin/tribixbite/cleverkeys/KeyValue.ktSlider enumCursor key definitions
src/main/kotlin/tribixbite/cleverkeys/KeyEventHandler.kthandleSlider(), moveCursor()Cursor movement execution
src/main/kotlin/tribixbite/cleverkeys/Config.ktswipe_scaling, slide_step_pxConfiguration and scaling
res/xml/bottom_row.xmlKey layoutSwipe 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:

Configuration

KeyTypeDefaultDescription
slider_sensitivityInt30% - Pixels per cursor movement (lower = more sensitive)
slider_speed_smoothingFloat0.6Exponential smoothing factor (0.0-1.0)
slider_speed_maxFloat6.0Maximum speed multiplier
SLIDING_SPEED_VERTICAL_MULTFloat0.5Vertical movement reduction factor

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:

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 = 0.5f        // Current speed multiplier (0.5 - max)
    var last_move_ms: Long  // Timestamp for speed calculation
}

fun onTouchMove(ptr: Pointer, x: Float, y: Float) {
    val travelled = abs(x - last_x) + abs(y - last_y)

    // 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

    // 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)
    }

    update_speed(travelled, x, y)
}

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

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.5 (capped at 6)27-28 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 6x multiplier)No
Distance-sensitiveYes (proportional)No
Output methodInputConnection.setSelection()KeyEvent.KEYCODE_DPAD_*
Use caseQuick navigation through textPrecise single-char movement

Cursor Movement Execution

// KeyEventHandler.kt
private fun handleSlider(s: KeyValue.Slider, r: Int, keyDown: Boolean) {
    when (s) {
        Slider.Cursor_left -> moveCursor(-r)
        Slider.Cursor_right -> moveCursor(r)
        Slider.Cursor_up -> moveCursorVertical(-r)
        Slider.Cursor_down -> moveCursorVertical(r)
        Slider.Selection_cursor_left -> moveCursorSel(r, true, keyDown)
        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
    }
}