Pointing Processor

Use PointingProcessor to turn sensor movement into mouse reports or arrow key taps. Choose one of the four modes below, then switch modes when a layer or key changes.

Modes

Choose the mode for the input you want to send to the host:

ModeHost input
CursorMouse movement.
SniperMouse movement at reduced sensitivity.
ScrollVertical wheel and horizontal pan.
CaretArrow key taps.

Cursor

Maps X/Y deltas directly to mouse cursor movement with optional scaling.

rmk::input_device::pointing::CursorConfig {
    multiplier_x: 1, // scales X delta. 0 disables X.
    multiplier_y: 1, // scales Y delta. 0 disables Y.
    divisor_x: 1,    // divides X motion. 0 disables X.
    divisor_y: 1,    // divides Y motion. 0 disables Y.
    invert_x: false,
    invert_y: false,
}

Each sample produces output = delta * multiplier / divisor, keeping the remainder for the next sample as scroll mode does, so a cursor slower than the sensor, such as multiplier: 2, divisor: 3, loses no movement. Set an axis multiplier or divisor to 0 to disable that axis.

Cursor mode can also accelerate: PointingProcessorConfig::acceleration, or acceleration in keyboard.toml, scales up motion faster than a threshold before the multiplier and divisor apply, so a slow, precise cursor still crosses the screen with a quick move.

Scroll

Maps X to horizontal pan and Y to vertical wheel scrolling.

use rmk::input_device::pointing::{ScrollAxis, ScrollConfig};

ScrollConfig {
    wheel: ScrollAxis {
        multiplier: 1,
        divisor: 8,
        invert: false,
    },
    pan: ScrollAxis {
        multiplier: 1,
        divisor: 8,
        invert: false,
    },
}

Set an output's multiplier or divisor to 0 to disable it. By default, positive Y produces a negative wheel delta, and positive X produces a positive pan delta. Set invert to true to reverse an output.

Scroll mode keeps motion between samples. It computes total = remainder + delta * multiplier, emits total / divisor, and keeps the remainder for the next sample. A larger divisor requires more movement for each scroll step:

Sampledeltatotaloutput (total/8)remainder
150+5=505
255+5=10110−1×8=2
352+5=707

Sniper

Maps X/Y to cursor at reduced sensitivity for precision aiming.

rmk::input_device::pointing::SniperConfig {
    multiplier: 1, // scales delta before divisor (same for both axes)
    divisor: 4,    // divides motion. 0 disables both axes.
    invert_x: false,
    invert_y: false,
}

Like Scroll mode, Sniper mode keeps fractional movement between samples. Increase divisor to reduce cursor sensitivity.

Caret

Maps X/Y to arrow key taps (up/down/left/right). No mouse report is generated.

use rmk::types::keycode::HidKeyCode;

rmk::input_device::pointing::CaretConfig {
    disable_x: false,  // disable horizontal caret taps
    disable_y: false,  // disable vertical caret taps
    invert_x: false,
    invert_y: false,
    threshold: 100,    // minimum accumulated motion (|dx|+|dy|) to trigger a tap
    keycode_up: HidKeyCode::Up,
    keycode_down: HidKeyCode::Down,
    keycode_left: HidKeyCode::Left,
    keycode_right: HidKeyCode::Right,
}

Caret mode accumulates movement until it exceeds threshold, then sends the configured arrow key taps. Increase the threshold to require more movement before each tap.

Note

Caret mode preserves held keys and modifiers, so holding Shift or Ctrl also modifies the arrow key taps. The configured keycodes bypass keymap, combo, and fork lookup. Direct inputs of the same keycode share press/release state.

Mode switching

To change modes when a layer or key changes, create a controller that receives those events and publishes PointingProcessorEvent. Set the event's device_id to match the sensor's ID. The example below targets device 0; change it if your sensor uses another ID.

Add the following module to your project. It selects Cursor on layer 0, Sniper on layer 1, Scroll on layer 2, and Caret on layer 3. Pressing a key bound to User0 also selects Caret.

Note

Reserve a subscriber slot for ActionEvent in keyboard.toml. Its default subscriber count is zero:

[event.action]
subs = 1
src/pointing_processor_controller.rs
use rmk::event::{ActionEvent, LayerChangeEvent, PointingProcessorEvent, publish_event};
use rmk::input_device::pointing::{PointingMode, ScrollAxis, ScrollConfig};
use rmk::macros::processor;
use rmk::types::action::Action;
use rmk::types::keycode::HidKeyCode;

#[processor(subscribe = [LayerChangeEvent, ActionEvent])]
pub struct PointingProcessorController;

impl PointingProcessorController {
    pub fn new() -> Self {
        Self
    }

    async fn on_layer_change_event(&mut self, event: LayerChangeEvent) {
        match event.0 {
            0 => {
                publish_event(PointingProcessorEvent {
                    device_id: 0,
                    mode: PointingMode::Cursor(rmk::input_device::pointing::CursorConfig::default()),
                });
            }
            1 => {
                publish_event(PointingProcessorEvent {
                    device_id: 0,
                    mode: PointingMode::Sniper(rmk::input_device::pointing::SniperConfig {
                        multiplier: 1, // scales sensor delta before divisor
                        divisor: 8, // divides motion for precision aiming
                        invert_x: false,
                        invert_y: false,
                    }),
                });
            }
            2 => {
                publish_event(PointingProcessorEvent {
                    device_id: 0,
                    mode: PointingMode::Scroll(ScrollConfig {
                        wheel: ScrollAxis {
                            divisor: 16,
                            ..Default::default()
                        },
                        pan: ScrollAxis {
                            divisor: 16,
                            ..Default::default()
                        },
                    }),
                });
            }
            3 => {
                publish_event(PointingProcessorEvent {
                    device_id: 0,
                    mode: PointingMode::Caret(rmk::input_device::pointing::CaretConfig {
                        disable_x: false, // disable horizontal caret taps
                        disable_y: false, // disable vertical caret taps
                        invert_x: false,
                        invert_y: false,
                        threshold: 100, // minimum accumulated motion (|dx|+|dy|) to trigger a tap
                        keycode_up: HidKeyCode::Up,
                        keycode_down: HidKeyCode::Down,
                        keycode_left: HidKeyCode::Left,
                        keycode_right: HidKeyCode::Right,
                    }),
                });
            }

            _ => {}
        }
    }

    async fn on_action_event(&mut self, event: ActionEvent) {
        if event.keyboard_event.pressed && event.action == Action::User(0) {
            publish_event(PointingProcessorEvent {
                device_id: 0,
                mode: PointingMode::Caret(rmk::input_device::pointing::CaretConfig::default()),
            });
        }
    }
}

Run the controller on the central, including when the sensor is connected to a peripheral. For a unibody keyboard, run it on the keyboard itself. Choose the setup that matches your project:

Toml
Rust
src/main.rs
#![no_main]
#![no_std]

mod pointing_processor_controller;

use pointing_processor_controller::PointingProcessorController;
use rmk::macros::rmk_keyboard;

#[rmk_keyboard]
mod keyboard {
    #[register_processor]
    fn pointing_processor_controller() -> PointingProcessorController {
        PointingProcessorController::new()
    }
}

Add the constructor to your existing keyboard module. For a split keyboard, use the central's #[rmk_central] module in src/central.rs instead. See running a processor.