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:

Both are accessed via swipe gestures on bottom row keys.

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:

Slider System (Spacebar Cursor)

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

}

Configuration Parameters

| Parameter | Default | Description |

|-----------|---------|-------------|

| slider_sensitivity | 30% | Pixels per cursor movement (lower = more sensitive) |

| slider_speed_smoothing | 0.6 | Exponential smoothing factor (0.0-1.0) |

| slider_speed_max | 6.0 | Maximum speed multiplier |

| SLIDING_SPEED_VERTICAL_MULT | 0.5 | Vertical movement reduction factor |

The swipe_scaling Calculation

The slider system uses device-adaptive scaling to ensure consistent behavior across different screen sizes and DPIs.

// Config.kt lines 353-359

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

Breakdown:

- Square pixels: dpi_ratio = 1.0

- Non-square (e.g., 440x400 DPI): dpi_ratio = 1.1

- Uses smaller dimension (usually width in portrait)

- Divides by 10 to get a reasonable base unit

- Example: 1080px width → swipe_scaling = 108

- Multiplies sensitivity (0.0-1.0) by swipe_scaling

- 30% sensitivity on 1080px screen: 0.30 108 = 32.4px

Example calculations by device:

| Device | Width | DPI Ratio | swipe_scaling | slide_step_px (30%) |

|--------|-------|-----------|---------------|---------------------|

| 1080p phone | 1080px | 1.0 | 108 | 32.4px |

| 1440p phone | 1440px | 1.0 | 144 | 43.2px |

| 720p phone | 720px | 1.0 | 72 | 21.6px |

| Tablet 800px | 800px | 1.0 | 80 | 24.0px |

Sliding Algorithm

Located in Pointers.kt, the Sliding inner class:

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

}

Movement 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)

}

Speed calculation:

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

Short, slow swipe (50px in 200ms):

instant_speed = min(6.0, 50/200 + 1) = 1.25

speed ≈ 1.0 (starts at 0.5, smoothed toward 1.25)

cursor_moves = 50 1.0 / 32.4 ≈ 1-2 positions

Long, slow swipe (200px in 800ms):

instant_speed = min(6.0, 200/800 + 1) = 1.25

speed ≈ 1.2 (smoothed)

cursor_moves = 200 1.2 / 32.4 ≈ 7-8 positions

Fast swipe (150px in 75ms):

instant_speed = min(6.0, 150/75 + 1) = 3.0

speed ≈ 2.5 (smoothed from previous)

cursor_moves = 150 2.5 / 32.4 ≈ 11-12 positions

Very fast swipe (200px in 40ms):

instant_speed = min(6.0, 200/40 + 1) = 6.0 (capped)

speed ≈ 4.5 (smoothed)

cursor_moves = 200 4.5 / 32.4 ≈ 27-28 positions

Arrow Key Navigation (Dedicated Nav Key)

Key Types

// KeyValue.kt - creates DPAD key events

"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)

Behavior

  • • Each swipe triggers exactly ONE key event
  • • No distance or speed scaling
  • • Consistent, predictable movement
  • • Works in all text fields and apps

Comparison Table

| Feature | Spacebar Slider | Nav Key Arrows |

|---------|-----------------|----------------|

| Key values | cursor_left, cursor_right | left, right, up, down |

| Movement type | Continuous (fractional accumulation) | Discrete (one per swipe) |

| Speed-sensitive | Yes (1x to 6x multiplier) | No |

| Distance-sensitive | Yes (proportional) | No |

| Output method | InputConnection.setSelection() | KeyEvent.KEYCODE_DPAD_ |

| Use case | Quick navigation through text | Precise single-char movement |

| Selection support | Yes (selection_cursor_* variants) | With Shift modifier |

Cursor Movement Execution

// KeyEventHandler.kt

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

}

}

Settings UI

Users can configure slider behavior via Settings → Gestures:

Files