mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
Six comments across nescope, neswire and nescapture still named nestri-guest-hub, plus one still naming nestri-protocol. Deferred from the import commits so the rename would read as one change rather than six unexplained edits inside otherwise-verbatim trees. Comments only. Realigned an ASCII box in encode.rs that the shorter name knocked crooked.
395 lines
14 KiB
Rust
395 lines
14 KiB
Rust
//! Programmatic input injection into the Smithay seat.
|
|
//!
|
|
//! Unlike the proxy version, nescope does **not** receive input from a host
|
|
//! compositor. Instead callers (e.g. a streaming server, a test harness, or
|
|
//! a control socket handler) send [`InputEvent`]s over a
|
|
//! [`calloop::channel::Channel`] that is registered in the event loop.
|
|
//!
|
|
//! # Usage
|
|
//!
|
|
//! ```ignore
|
|
//! // Obtained when calling NescopeState::new()
|
|
//! let input_tx: calloop::channel::Sender<InputEvent> = ...;
|
|
//!
|
|
//! // From any thread:
|
|
//! input_tx.send(InputEvent::KeyDown { keycode: 28 }).unwrap(); // evdev KEY_ENTER
|
|
//! ```
|
|
//!
|
|
//! Keycodes follow the Linux evdev convention (the same as used in
|
|
//! Moonshine's `CompositorInputEvent`). The compositor adds 8 to convert
|
|
//! them to X11/xkbcommon keycodes before passing them to Smithay.
|
|
|
|
#![allow(dead_code)]
|
|
use smithay::backend::input::{Axis, AxisSource, ButtonState, KeyState};
|
|
use smithay::input::keyboard::{FilterResult, Keycode};
|
|
use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent};
|
|
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
|
use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_constraint};
|
|
use smithay::wayland::seat::WaylandFocus;
|
|
|
|
use nesprotocol::input::{self as nestri_input, DecodedInput};
|
|
|
|
use crate::focus::KeyboardFocusTarget;
|
|
use crate::state::NescopeState;
|
|
|
|
// ── Wire protocol constants (mirrors nesprotocol/src/input.rs) ──
|
|
const WIRE_INPUT_KEY: u8 = 0;
|
|
const WIRE_INPUT_MOUSE_MOVE: u8 = 1;
|
|
const WIRE_INPUT_MOUSE_BUTTON: u8 = 2;
|
|
const WIRE_INPUT_MOUSE_WHEEL: u8 = 3;
|
|
const WIRE_KEY_DOWN: u8 = 1;
|
|
const WIRE_BTN_LEFT: u32 = 0x110; // BTN_LEFT
|
|
const WIRE_BTN_MIDDLE: u32 = 0x112; // BTN_MIDDLE
|
|
const WIRE_BTN_RIGHT: u32 = 0x111; // BTN_RIGHT
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public event type
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Events that can be injected into the compositor seat programmatically.
|
|
///
|
|
/// All coordinates are in logical (output-space) pixels.
|
|
/// Keycodes are Linux evdev keycodes (NOT X11 keycodes).
|
|
#[derive(Debug, Clone)]
|
|
pub enum InputEvent {
|
|
// ── Keyboard ─────────────────────────────────────────────────────────
|
|
/// Key pressed. `keycode` is a Linux evdev keycode (8 will be added
|
|
/// internally to produce an X11 keycode).
|
|
KeyDown { keycode: u32 },
|
|
/// Key released.
|
|
KeyUp { keycode: u32 },
|
|
|
|
// ── Pointer — absolute ───────────────────────────────────────────────
|
|
/// Absolute pointer position. Coordinates are in the Moonlight client's
|
|
/// coordinate space; they are scaled to the compositor output.
|
|
MouseMoveAbsolute {
|
|
x: f64,
|
|
y: f64,
|
|
/// Client screen width used for coordinate mapping.
|
|
screen_width: f64,
|
|
/// Client screen height used for coordinate mapping.
|
|
screen_height: f64,
|
|
},
|
|
|
|
// ── Pointer — relative ───────────────────────────────────────────────
|
|
/// Relative pointer delta in logical pixels.
|
|
MouseMoveRelative { dx: f64, dy: f64 },
|
|
|
|
// ── Pointer — buttons ────────────────────────────────────────────────
|
|
/// Mouse button pressed. `button` is a Linux button code
|
|
/// (e.g. `BTN_LEFT = 0x110`).
|
|
MouseButtonDown { button: u32 },
|
|
/// Mouse button released.
|
|
MouseButtonUp { button: u32 },
|
|
|
|
// ── Pointer — scroll ─────────────────────────────────────────────────
|
|
/// Vertical scroll. Positive = up, negative = down.
|
|
ScrollVertical { amount: f64 },
|
|
/// Horizontal scroll. Positive = right, negative = left.
|
|
ScrollHorizontal { amount: f64 },
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Injection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Process a single [`InputEvent`] injected from outside the compositor.
|
|
///
|
|
/// Called from the calloop idle callback after draining the input channel.
|
|
pub fn process_input(event: InputEvent, state: &mut NescopeState) {
|
|
let serial = SERIAL_COUNTER.next_serial();
|
|
let time = state.clock.now().as_millis();
|
|
|
|
// Track pointer activity for cursor inactivity timer.
|
|
match event {
|
|
InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } => {}
|
|
_ => state.last_pointer_activity = std::time::Instant::now(),
|
|
}
|
|
|
|
// One-time X11 focus reset when the gamescope WSI surface is active.
|
|
if state.override_surface.is_some() && state.x11_focus_needs_reset {
|
|
state.sync_x11_focus();
|
|
}
|
|
|
|
match event {
|
|
InputEvent::KeyDown { keycode } => {
|
|
if let Some(kb) = state.seat.get_keyboard() {
|
|
// Auto-focus the topmost window if nothing has focus yet.
|
|
if kb.current_focus().is_none() {
|
|
state.determine_and_apply_focus();
|
|
}
|
|
// evdev → X11/xkbcommon keycode: add 8.
|
|
kb.input::<(), _>(
|
|
state,
|
|
Keycode::from(keycode + 8),
|
|
KeyState::Pressed,
|
|
serial,
|
|
time,
|
|
|_, _, _| FilterResult::Forward,
|
|
);
|
|
}
|
|
}
|
|
|
|
InputEvent::KeyUp { keycode } => {
|
|
if let Some(kb) = state.seat.get_keyboard() {
|
|
if kb.current_focus().is_none() {
|
|
state.determine_and_apply_focus();
|
|
}
|
|
kb.input::<(), _>(
|
|
state,
|
|
Keycode::from(keycode + 8),
|
|
KeyState::Released,
|
|
serial,
|
|
time,
|
|
|_, _, _| FilterResult::Forward,
|
|
);
|
|
}
|
|
}
|
|
|
|
InputEvent::MouseMoveAbsolute {
|
|
x,
|
|
y,
|
|
screen_width,
|
|
screen_height,
|
|
} => {
|
|
let output_size = state
|
|
.output
|
|
.current_mode()
|
|
.map(|m| m.size)
|
|
.unwrap_or((state.width as i32, state.height as i32).into());
|
|
|
|
let new_x = if screen_width > 0.0 {
|
|
x / screen_width * output_size.w as f64
|
|
} else {
|
|
x
|
|
};
|
|
let new_y = if screen_height > 0.0 {
|
|
y / screen_height * output_size.h as f64
|
|
} else {
|
|
y
|
|
};
|
|
|
|
state.cursor_position = Point::from((new_x, new_y));
|
|
clamp_cursor(state);
|
|
|
|
let under = surface_under(state);
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
pointer.motion(
|
|
state,
|
|
under,
|
|
&MotionEvent {
|
|
location: state.cursor_position,
|
|
serial,
|
|
time,
|
|
},
|
|
);
|
|
pointer.frame(state);
|
|
}
|
|
|
|
InputEvent::MouseMoveRelative { dx, dy } => {
|
|
if !state.cursor_initialized {
|
|
let size = state
|
|
.output
|
|
.current_mode()
|
|
.map(|m| m.size)
|
|
.unwrap_or((state.width as i32, state.height as i32).into());
|
|
state.cursor_position = Point::from((size.w as f64 / 2.0, size.h as f64 / 2.0));
|
|
state.cursor_initialized = true;
|
|
tracing::debug!("cursor initialized to center: {:?}", state.cursor_position);
|
|
}
|
|
|
|
let delta = Point::from((dx, dy));
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
|
|
// Check for a pointer lock constraint.
|
|
let mut locked = false;
|
|
let under = surface_under(state);
|
|
if let Some((ref target, _)) = under {
|
|
if let Some(surf) = target.wl_surface() {
|
|
with_pointer_constraint(&surf, &pointer, |c| {
|
|
if let Some(c) = c {
|
|
if c.is_active() {
|
|
if let PointerConstraint::Locked(_) = &*c {
|
|
locked = true;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
pointer.relative_motion(
|
|
state,
|
|
under.clone(),
|
|
&RelativeMotionEvent {
|
|
delta,
|
|
delta_unaccel: delta,
|
|
utime: time as u64,
|
|
},
|
|
);
|
|
|
|
state.cursor_position += delta;
|
|
clamp_cursor(state);
|
|
|
|
if locked {
|
|
pointer.frame(state);
|
|
return;
|
|
}
|
|
|
|
pointer.motion(
|
|
state,
|
|
under.clone(),
|
|
&MotionEvent {
|
|
location: state.cursor_position,
|
|
serial,
|
|
time,
|
|
},
|
|
);
|
|
pointer.frame(state);
|
|
}
|
|
|
|
InputEvent::MouseButtonDown { button } => {
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
pointer.button(
|
|
state,
|
|
&ButtonEvent {
|
|
serial,
|
|
time,
|
|
button,
|
|
state: ButtonState::Pressed,
|
|
},
|
|
);
|
|
pointer.frame(state);
|
|
}
|
|
|
|
InputEvent::MouseButtonUp { button } => {
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
pointer.button(
|
|
state,
|
|
&ButtonEvent {
|
|
serial,
|
|
time,
|
|
button,
|
|
state: ButtonState::Released,
|
|
},
|
|
);
|
|
pointer.frame(state);
|
|
}
|
|
|
|
InputEvent::ScrollVertical { amount } => {
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
pointer.axis(
|
|
state,
|
|
AxisFrame::new(time)
|
|
.source(AxisSource::Wheel)
|
|
.value(Axis::Vertical, -amount),
|
|
);
|
|
pointer.frame(state);
|
|
}
|
|
|
|
InputEvent::ScrollHorizontal { amount } => {
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
pointer.axis(
|
|
state,
|
|
AxisFrame::new(time)
|
|
.source(AxisSource::Wheel)
|
|
.value(Axis::Horizontal, amount),
|
|
);
|
|
pointer.frame(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Wire-protocol decoder
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Decode a single input event from the neshub wire protocol
|
|
/// and convert it to an [`InputEvent`] for the compositor seat.
|
|
///
|
|
/// Uses the shared [`nesprotocol::input::decode_input_event`] and maps:
|
|
/// - Button 0 → `BTN_LEFT` (0x110), 1 → `BTN_MIDDLE` (0x112), 2 → `BTN_RIGHT` (0x111)
|
|
/// - Mouse wheel `dy` → `ScrollVertical`
|
|
///
|
|
/// Returns `None` if the buffer cannot be decoded.
|
|
pub fn decode_wire_event(data: &[u8]) -> Option<InputEvent> {
|
|
match nestri_input::decode_input_event(data)? {
|
|
DecodedInput::Key { down, keycode } => Some(if down {
|
|
InputEvent::KeyDown {
|
|
keycode: keycode as u32,
|
|
}
|
|
} else {
|
|
InputEvent::KeyUp {
|
|
keycode: keycode as u32,
|
|
}
|
|
}),
|
|
DecodedInput::MouseMove { dx, dy } => Some(InputEvent::MouseMoveRelative {
|
|
dx: dx as f64,
|
|
dy: dy as f64,
|
|
}),
|
|
DecodedInput::MouseButton { button, down } => {
|
|
let btn = match button {
|
|
0 => 0x110, // BTN_LEFT
|
|
1 => 0x112, // BTN_MIDDLE
|
|
2 => 0x111, // BTN_RIGHT
|
|
_ => return None,
|
|
};
|
|
Some(if down {
|
|
InputEvent::MouseButtonDown { button: btn }
|
|
} else {
|
|
InputEvent::MouseButtonUp { button: btn }
|
|
})
|
|
}
|
|
DecodedInput::MouseWheel { dx: _dx, dy } => {
|
|
Some(InputEvent::ScrollVertical { amount: dy as f64 })
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Clamp the cursor to the output bounds.
|
|
fn clamp_cursor(state: &mut NescopeState) {
|
|
let size = state
|
|
.output
|
|
.current_mode()
|
|
.map(|m| m.size)
|
|
.unwrap_or((state.width as i32, state.height as i32).into());
|
|
state.cursor_position.x = state.cursor_position.x.clamp(0.0, (size.w - 1) as f64);
|
|
state.cursor_position.y = state.cursor_position.y.clamp(0.0, (size.h - 1) as f64);
|
|
}
|
|
|
|
/// Find the focused target under the current cursor position.
|
|
pub fn surface_under(state: &NescopeState) -> Option<(KeyboardFocusTarget, Point<f64, Logical>)> {
|
|
if state.override_surface.is_some() {
|
|
if let Some(wid) = state.focused_x11_window {
|
|
for window in state.space.elements() {
|
|
if let Some(x11) = window.x11_surface() {
|
|
if x11.window_id() == wid {
|
|
let loc = state.space.element_geometry(window)?.loc;
|
|
return Some((KeyboardFocusTarget::Window(window.clone()), loc.to_f64()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let (window, loc) = state.space.element_under(state.cursor_position)?;
|
|
return Some((KeyboardFocusTarget::Window(window.clone()), loc.to_f64()));
|
|
}
|
|
|
|
// Try element_under first
|
|
if let Some((window, loc)) = state.space.element_under(state.cursor_position) {
|
|
return Some((KeyboardFocusTarget::Window(window.clone()), loc.to_f64()));
|
|
}
|
|
|
|
// Fallback: use the keyboard-focused window if any window is mapped
|
|
if let Some(w) = state.space.elements().next() {
|
|
if let Some(geo) = state.space.element_geometry(w) {
|
|
return Some((KeyboardFocusTarget::Window(w.clone()), geo.loc.to_f64()));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|