Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Handling Input

Viso provides an InputProcessor that translates raw mouse/keyboard events into VisoCommand values. The engine executes commands via engine.execute(cmd).

Architecture

Platform events (winit, web, etc.)
    │
    ▼
InputEvent (platform-agnostic)
    │
    ▼
InputProcessor
    │  translates to
    ▼
VisoCommand
    │
    ▼
engine.execute(cmd)

InputProcessor is optional convenience. Consumers who handle their own input can construct VisoCommand values directly and skip InputProcessor entirely.

InputEvent

The platform-agnostic input enum:

#![allow(unused)]
fn main() {
pub enum InputEvent {
    CursorMoved { x: f32, y: f32 },
    MouseButton { button: MouseButton, pressed: bool },
    Scroll { delta: f32 },
    ModifiersChanged { shift: bool },
}
}

Your windowing layer converts platform events into these variants.

Wiring Input (winit example)

From viso’s standalone viewer:

#![allow(unused)]
fn main() {
struct ViewerApp {
    engine: Option<VisoEngine>,
    input: InputProcessor,
    // ...
}

impl ViewerApp {
    fn dispatch_input(&mut self, event: InputEvent) {
        let Some(engine) = &mut self.engine else { return };

        // Update cursor for GPU picking
        if let InputEvent::CursorMoved { x, y } = event {
            engine.set_cursor_pos(x, y);
        }

        // Translate event to command and execute
        if let Some(cmd) = self.input.handle_event(event, engine.hovered_target()) {
            let _ = engine.execute(cmd);
        }
    }
}
}

Mouse movement

#![allow(unused)]
fn main() {
WindowEvent::CursorMoved { position, .. } => {
    dispatch_input(InputEvent::CursorMoved {
        x: position.x as f32,
        y: position.y as f32,
    });
}
}

Scroll (zoom)

#![allow(unused)]
fn main() {
WindowEvent::MouseWheel { delta, .. } => {
    let scroll_delta = match delta {
        MouseScrollDelta::LineDelta(_, y) => y,
        MouseScrollDelta::PixelDelta(pos) => pos.y as f32 * 0.01,
    };
    dispatch_input(InputEvent::Scroll { delta: scroll_delta });
}
}

Click (selection)

#![allow(unused)]
fn main() {
WindowEvent::MouseInput { button, state, .. } => {
    dispatch_input(InputEvent::MouseButton {
        button: MouseButton::from(button),
        pressed: state == ElementState::Pressed,
    });
}
}

Modifier keys

#![allow(unused)]
fn main() {
WindowEvent::ModifiersChanged(modifiers) => {
    dispatch_input(InputEvent::ModifiersChanged {
        shift: modifiers.state().shift_key(),
    });
}
}

Keyboard Input

Keyboard events go through InputProcessor::handle_key_press, which looks up the key in the configurable KeyBindings map:

#![allow(unused)]
fn main() {
WindowEvent::KeyboardInput { event, .. } => {
    if event.state == ElementState::Pressed {
        let key_str = format!("{:?}", event.physical_key);
        if let Some(cmd) = input.handle_key_press(&key_str) {
            let _ = engine.execute(cmd);
        }
    }
}
}

Display toggles (waters, ions, solvent, lipids) are VisoOptions mutations, not commands. The viewer handles them directly:

#![allow(unused)]
fn main() {
match code {
    KeyCode::KeyU => engine.toggle_waters(),
    KeyCode::KeyI => engine.toggle_ions(),
    KeyCode::KeyO => engine.toggle_solvent(),
    KeyCode::KeyL => engine.toggle_lipids(),
    _ => { /* check keybindings */ }
}
}

VisoCommand

The full action vocabulary:

#![allow(unused)]
fn main() {
pub enum VisoCommand {
    // Camera
    RecenterCamera,
    RotateCamera { delta: Vec2 },
    PanCamera { delta: Vec2 },
    Zoom { delta: f32 },
    ToggleAutoRotate,

    // Focus
    CycleFocus,
    ResetFocus,

    // Selection
    ClearSelection,
    SelectResidue { index: i32, extend: bool },
    SelectSegment { index: i32, extend: bool },
    SelectChain { index: i32, extend: bool },

    // Visualization
    UpdateBands { bands: Vec<BandInfo> },
    UpdatePull { pull: Option<PullInfo> },

    // Playback
    ToggleTrajectory,
}
}

Click Detection

InputProcessor tracks click timing internally to distinguish:

Click TypeResulting Command
Single click on residueSelectResidue { index, extend: false }
Shift + click on residueSelectResidue { index, extend: true }
Double clickSelectSegment { index, extend }
Triple clickSelectChain { index, extend }
Click on backgroundClearSelection
Drag (moved after press)RotateCamera or PanCamera (no selection)

Shift + drag produces PanCamera instead of RotateCamera.

KeyBindings

Customizable key-to-command mapping, serde-serializable:

#![allow(unused)]
fn main() {
let mut input = InputProcessor::new();
// Default bindings are pre-loaded:
// Q → RecenterCamera, Tab → CycleFocus, R → ToggleAutoRotate, etc.
}

Default keybindings:

KeyAction
QRecenter camera
TToggle trajectory playback
TabCycle focus
RToggle auto-rotate
`Reset focus to session
EscapeClear selection

Skipping InputProcessor

For web embeds or custom hosts, construct commands directly:

#![allow(unused)]
fn main() {
// Rotate camera by 5 degrees
engine.execute(VisoCommand::RotateCamera {
    delta: Vec2::new(5.0, 0.0),
});

// Select residue 42
engine.execute(VisoCommand::SelectResidue {
    index: 42,
    extend: false,
});
}