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 ViewerShell {
    engine: Option<VisoEngine>,
    input: InputProcessor,
    // ...
}

impl ViewerShell {
    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 physical key in the configurable KeyBindings map:

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

Display toggles for ambient types (water/ion/solvent) and lipid mode go through VisoCommand::SetTypeVisibility and VisoCommand::CycleLipidMode respectively. The default key bindings already wire these up — pressing U, I, O, or L produces the right command without any extra code in the viewer.

VisoCommand

The full action vocabulary:

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

    // Focus
    CycleFocus,
    ResetFocus,

    // Playback
    ToggleTrajectory,

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

    // Entity management
    FocusEntity { id: u32 },
    ToggleEntityVisibility { id: u32 },
    RemoveEntity { id: u32 },

    // Display toggles
    SetTypeVisibility { mol_type: MoleculeType, visible: Option<bool> },
    CycleLipidMode,
}
}

engine.execute(cmd) returns a CommandOutcome describing what changed (SelectionChanged, FocusChanged, VisibilityChanged, NoEffect, or Unhandled). RemoveEntity is Unhandled when sent to the engine directly — it must be routed through VisoApp (or the host that owns the Assembly) so the assembly is mutated and the new snapshot is pushed via engine.set_assembly.

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. Keys use the winit::keyboard::KeyCode debug format ("KeyQ", "Tab", "Backquote", etc.):

#![allow(unused)]
fn main() {
let mut input = InputProcessor::new();
// Default bindings are pre-loaded.

// Or with a custom map:
let bindings: KeyBindings = toml::from_str(&toml_str)?;
let mut input = InputProcessor::with_key_bindings(bindings);
}

Default keybindings:

KeyAction
KeyQRecenter camera on focus
KeyTToggle trajectory playback
TabCycle focus
KeyRToggle auto-rotate
BackquoteReset focus to session
EscapeClear selection
KeyIToggle ion visibility
KeyUToggle water visibility
KeyOToggle solvent visibility
KeyLCycle lipid display mode

Skipping InputProcessor

For web embeds or custom hosts, construct commands directly:

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

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