diff --git a/Cargo.lock b/Cargo.lock index 8eae3b99..4b59f73e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,6 +689,18 @@ dependencies = [ "no_std_io2", ] +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1779,6 +1791,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "evdev" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b686663ba7f08d92880ff6ba22170f1df4e83629341cba34cf82cd65ebea99" +dependencies = [ + "bitvec", + "cfg-if", + "libc", + "nix", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1971,6 +1995,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -4349,6 +4379,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -5628,6 +5664,12 @@ dependencies = [ "slotmap", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.13.5" @@ -6492,6 +6534,7 @@ dependencies = [ "config", "dash-frontend", "dbus", + "evdev", "futures", "glam", "idmap", @@ -7084,6 +7127,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/wayvr/Cargo.toml b/wayvr/Cargo.toml index 25857c12..eb5228a7 100644 --- a/wayvr/Cargo.toml +++ b/wayvr/Cargo.toml @@ -121,6 +121,7 @@ xkbcommon = { calloop = { version = "0.14.4", optional = true } calloop-wayland-source = { version = "0.4.1", optional = true } +evdev = "0.13.2" [build-dependencies] regex.workspace = true diff --git a/wayvr/src/backend/input.rs b/wayvr/src/backend/input.rs index f0633645..d46cb66c 100644 --- a/wayvr/src/backend/input.rs +++ b/wayvr/src/backend/input.rs @@ -18,7 +18,7 @@ use crate::overlays::keyboard::KEYBOARD_NAME; use crate::overlays::watch::WATCH_NAME; use crate::state::{AppSession, AppState}; use crate::subsystem::hid::WheelDelta; -use crate::subsystem::input::KeyboardFocus; +use crate::subsystem::input::InputFocus; use crate::windowing::backend::OverlayEventData; use crate::windowing::manager::OverlayWindowManager; use crate::windowing::window::{self, OverlayWindowData, realign, scalar_scale}; @@ -407,12 +407,13 @@ fn populate_lines( } } -fn update_focus(focus: &mut KeyboardFocus, overlay_keyboard_focus: Option) { - if let Some(f) = &overlay_keyboard_focus - && *focus != *f +fn update_focus(app: &mut AppState, overlay_input_focus: Option) { + if let Some(f) = &overlay_input_focus + && app + .hid_provider + .set_input_focus(app.wvr_server.as_mut(), *f) { log::debug!("Setting keyboard focus to {:?}", *f); - *focus = *f; } } @@ -537,10 +538,7 @@ where // grab if pointer.now.grab && !pointer.before.grab && hovered_state.grabbable { - update_focus( - &mut app.hid_provider.keyboard_focus, - hovered.config.keyboard_focus, - ); + update_focus(app, hovered.config.input_focus); start_grab( idx, hit.overlay, @@ -567,10 +565,7 @@ where let pointer = &mut app.input_state.pointers[hit.pointer]; if pointer.now.click && !pointer.before.click { pointer.interaction.clicked_id = Some(hit.overlay); - update_focus( - &mut app.hid_provider.keyboard_focus, - hovered.config.keyboard_focus, - ); + update_focus(app, hovered.config.input_focus); hovered.config.backend.on_pointer(app, &hit, true); } else if !pointer.now.click && pointer.before.click { // send release event to overlay that was originally clicked diff --git a/wayvr/src/backend/wayvr/client.rs b/wayvr/src/backend/wayvr/client.rs index b32e8f8a..9c78f675 100644 --- a/wayvr/src/backend/wayvr/client.rs +++ b/wayvr/src/backend/wayvr/client.rs @@ -232,11 +232,26 @@ impl WayVRCompositor { Keycode::new(virtual_key), state, self.serial_counter.next_serial(), - 0, + super::time::get_millis() as u32, |_, _, _| smithay::input::keyboard::FilterResult::Forward, ); } + pub fn release_all_keys(&mut self) { + let pressed = self.seat_keyboard.pressed_keys(); + + for keycode in pressed { + self.seat_keyboard.input::<(), _>( + &mut self.state, + keycode, + smithay::backend::input::KeyState::Released, + self.serial_counter.next_serial(), + super::time::get_millis() as u32, + |_, _, _| smithay::input::keyboard::FilterResult::Forward, + ); + } + } + pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> { // Smithay only accepts keymaps in a string form due to thread safety concerns self.seat_keyboard diff --git a/wayvr/src/backend/wayvr/input_capture.rs b/wayvr/src/backend/wayvr/input_capture.rs new file mode 100644 index 00000000..25a35de9 --- /dev/null +++ b/wayvr/src/backend/wayvr/input_capture.rs @@ -0,0 +1,604 @@ +use evdev::{AttributeSetRef, Device, EventType, KeyCode, RelativeAxisCode}; +use std::{ + collections::HashSet, + io, + os::fd::AsRawFd, + path::{Path, PathBuf}, + sync::mpsc::{self, Receiver, SyncSender, TryRecvError}, + thread::{self, JoinHandle}, + time::{Duration, Instant}, +}; + +pub const IGNORE_PREFIX: &str = "WayVR"; + +const WATCHDOG_TIMEOUT: Duration = Duration::from_millis(5000); +const POLL_TIMEOUT_MS: i32 = 20; +const SYN_REPORT_CODE: u16 = 0; + +#[derive(Debug, Clone)] +pub enum CapturedEvent { + Key { + code: u16, + pressed: bool, + }, + PointerButton { + button: u32, + pressed: bool, + }, + PointerMotion { + dx: f64, + dy: f64, + }, + PointerAxis { + horizontal_v120: i32, + vertical_v120: i32, + }, + UngrabbedAll, +} + +pub struct InputCapture { + command_tx: SyncSender, + event_rx: Receiver, + worker: Option>, +} + +impl InputCapture { + pub fn new() -> io::Result { + let (command_tx, command_rx) = mpsc::sync_channel(8); + let (event_tx, event_rx) = mpsc::sync_channel(64); + + let worker = thread::Builder::new() + .name("wayvr-input-capture".into()) + .spawn(move || worker_main(command_rx, event_tx))?; + + Ok(Self { + command_tx, + event_rx, + worker: Some(worker), + }) + } + + /// Returns every currently queued event without blocking. + pub fn drain_events(&self) -> Vec { + let _ = self.command_tx.try_send(Command::ResetWatchdog); + self.event_rx.try_iter().collect() + } + + /// Exclusively grabs every currently detected keyboard and mouse. + /// Newly connected matching devices are grabbed automatically. + pub fn set_grabbed(&self, grabbed: bool) -> anyhow::Result<()> { + if let Err(e) = self.command_tx.send(Command::SetGrabbed { grabbed }) { + anyhow::bail!("Worker thread unreachable: {e:?}"); + } + Ok(()) + } +} + +impl Drop for InputCapture { + fn drop(&mut self) { + let _ = self.command_tx.send(Command::Shutdown); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +enum Command { + ResetWatchdog, + SetGrabbed { grabbed: bool }, + Shutdown, +} + +struct OpenDevice { + path: PathBuf, + device: Device, + is_keyboard: bool, + is_mouse: bool, + pending: Vec, + pressed_keys: HashSet, +} + +#[derive(Debug)] +enum PendingEvent { + Key { + code: u16, + pressed: bool, + }, + Button { + button: u32, + pressed: bool, + }, + Motion { + dx: i32, + dy: i32, + }, + Axis { + horizontal: i32, + vertical: i32, + horizontal_hi_res: i32, + vertical_hi_res: i32, + }, +} + +fn worker_main(command_rx: Receiver, event_tx: SyncSender) { + let mut devices = Vec::::new(); + let mut grabbed = false; + let mut emergency_ungrab = false; + + let mut want_rescan = true; + let mut watchdog = Instant::now(); // in case wayvr main thread gets blocked + + 'worker: loop { + if Instant::now() >= watchdog && grabbed { + log::warn!("Watchdog timed out, ungrabbing all input devices."); + emergency_ungrab = true; + } else if want_rescan { + scan_for_devices(&mut devices, grabbed); + want_rescan = false; + } + + loop { + match command_rx.try_recv() { + Ok(Command::SetGrabbed { grabbed: requested }) => { + let result = if requested { + grab_existing_devices(&mut devices) + } else { + ungrab_existing_devices(&mut devices) + }; + + grabbed = requested && result.is_ok(); + watchdog = Instant::now() + WATCHDOG_TIMEOUT; + + for entry in &mut devices { + entry.pending.clear(); + entry.pressed_keys.clear(); + discard_available_events(&mut entry.device); + } + } + Ok(Command::ResetWatchdog) => watchdog = Instant::now() + WATCHDOG_TIMEOUT, + Ok(Command::Shutdown) => break 'worker, + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => break 'worker, + } + } + + let mut poll_fds: Vec = devices + .iter() + .map(|entry| libc::pollfd { + fd: entry.device.as_raw_fd(), + events: libc::POLLIN, + revents: 0, + }) + .collect(); + + // SAFETY: poll_fds is a valid contiguous array for the duration of the call. + let poll_result = unsafe { + libc::poll( + poll_fds.as_mut_ptr(), + poll_fds.len() as libc::nfds_t, + POLL_TIMEOUT_MS, + ) + }; + + if poll_result < 0 { + let error = io::Error::last_os_error(); + if error.kind() != io::ErrorKind::Interrupted { + thread::sleep(Duration::from_millis(20)); + } + continue; + } + + let mut dead = vec![false; devices.len()]; + + for (index, poll_fd) in poll_fds.iter().enumerate() { + if poll_fd.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0 { + dead[index] = true; + continue; + } + + if poll_fd.revents & libc::POLLIN == 0 { + continue; + } + + match read_device_events(&mut devices[index], grabbed, &event_tx) { + ReadResult::Continue => {} + ReadResult::DeviceGone => dead[index] = true, + ReadResult::EmergencyUngrab => { + emergency_ungrab = true; + break; + } + ReadResult::ReceiverGone => break 'worker, + } + } + + if emergency_ungrab { + // dropping the descriptors forcibly releases EVIOCGRAB + devices.clear(); + + grabbed = false; + emergency_ungrab = false; + + // rediscover the devices immediately, but leave them ungrabbed + want_rescan = true; + + let e = event_tx.send(CapturedEvent::UngrabbedAll); // send this synchronously + if e.is_err() { + break 'worker; + } + + continue; + } + + for index in (0..dead.len()).rev() { + if dead[index] { + devices.swap_remove(index); + } + } + } +} + +fn scan_for_devices(devices: &mut Vec, grabbed: bool) { + let known_paths: HashSet = devices.iter().map(|entry| entry.path.clone()).collect(); + + for (path, mut device) in evdev::enumerate() { + if known_paths.contains(&path) { + continue; + } + + if device + .name() + .is_some_and(|name| name.starts_with(IGNORE_PREFIX)) + { + continue; + } + + let Some(keys) = device.supported_keys() else { + continue; + }; + + let is_keyboard = looks_like_keyboard(keys); + let is_mouse = looks_like_mouse(&device, keys); + if !is_keyboard && !is_mouse { + continue; + } + + if device.set_nonblocking(true).is_err() { + continue; + } + + if grabbed && device.grab().is_err() { + continue; + } + + discard_available_events(&mut device); + devices.push(OpenDevice { + path, + device, + is_keyboard, + is_mouse, + pending: Vec::new(), + pressed_keys: HashSet::with_capacity(8), + }); + } +} + +fn looks_like_keyboard(keys: &AttributeSetRef) -> bool { + let full_keyboard = (keys.contains(KeyCode::KEY_A) || keys.contains(KeyCode::KEY_Z)) + && [KeyCode::KEY_ESC, KeyCode::KEY_ENTER, KeyCode::KEY_SPACE] + .into_iter() + .filter(|key| keys.contains(*key)) + .count() + >= 2; + + let numeric_keypad = keys.contains(KeyCode::KEY_KP0) + && keys.contains(KeyCode::KEY_KP1) + && keys.contains(KeyCode::KEY_KPENTER); + + full_keyboard || numeric_keypad +} + +fn looks_like_mouse(device: &Device, keys: &AttributeSetRef) -> bool { + let Some(relative_axes) = device.supported_relative_axes() else { + return false; + }; + + relative_axes.contains(RelativeAxisCode::REL_X) + && relative_axes.contains(RelativeAxisCode::REL_Y) + && mouse_buttons() + .into_iter() + .any(|button| keys.contains(button)) +} + +fn mouse_buttons() -> [KeyCode; 8] { + [ + KeyCode::BTN_LEFT, + KeyCode::BTN_RIGHT, + KeyCode::BTN_MIDDLE, + KeyCode::BTN_SIDE, + KeyCode::BTN_EXTRA, + KeyCode::BTN_FORWARD, + KeyCode::BTN_BACK, + KeyCode::BTN_TASK, + ] +} + +fn grab_existing_devices(devices: &mut [OpenDevice]) -> io::Result<()> { + let mut grabbed_now = Vec::new(); + + for index in 0..devices.len() { + if devices[index].device.is_grabbed() { + continue; + } + + if let Err(error) = devices[index].device.grab() { + let error = with_device_context("grab", &devices[index].path, error); + for previous in grabbed_now { + let dev: &mut OpenDevice = &mut devices[previous]; + let _ = dev.device.ungrab(); + } + return Err(error); + } + + grabbed_now.push(index); + } + + Ok(()) +} + +fn ungrab_existing_devices(devices: &mut [OpenDevice]) -> io::Result<()> { + let mut first_error = None; + + for entry in devices { + if !entry.device.is_grabbed() { + continue; + } + + if let Err(error) = entry.device.ungrab() { + if first_error.is_none() { + first_error = Some(with_device_context("ungrab", &entry.path, error)); + } + } + } + + match first_error { + Some(error) => Err(error), + None => Ok(()), + } +} + +fn discard_available_events(device: &mut Device) { + loop { + match device.fetch_events() { + Ok(events) => { + if events.count() == 0 { + break; + } + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => break, + Err(_) => break, + } + } +} + +enum ReadResult { + Continue, + DeviceGone, + EmergencyUngrab, + ReceiverGone, +} + +fn read_device_events( + entry: &mut OpenDevice, + emit: bool, + event_tx: &SyncSender, +) -> ReadResult { + loop { + let events = match entry.device.fetch_events() { + Ok(events) => events.collect::>(), + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + return ReadResult::Continue; + } + Err(_) => return ReadResult::DeviceGone, + }; + + if events.is_empty() { + return ReadResult::Continue; + } + + if !emit { + entry.pending.clear(); + continue; + } + + for event in events { + if event.event_type() == EventType::KEY { + match event.value() { + 0 | 1 => { + let code = event.code(); + let pressed = event.value() == 1; + + if entry.is_mouse && is_mouse_button(code) { + entry.pending.push(PendingEvent::Button { + button: u32::from(code), + pressed, + }); + } else if entry.is_keyboard { + entry.pending.push(PendingEvent::Key { code, pressed }); + + if !is_mouse_button(code) { + if pressed { + entry.pressed_keys.insert(code); + if emergency_chord_active(&entry.pressed_keys) { + // do not forward delete or any pending events to wayvr + entry.pending.clear(); + log::info!( + "Ctrl+Alt+Del pressed, ungrabbing all input devices" + ); + return ReadResult::EmergencyUngrab; + } + } else { + entry.pressed_keys.remove(&code); + } + } + } + } + 2 => { + // kernel autorepeat. smithay/xkb performs repeat itself + } + _ => {} + } + } else if event.event_type() == EventType::RELATIVE && entry.is_mouse { + push_relative_event(entry, event.code(), event.value()); + } else if event.event_type() == EventType::SYNCHRONIZATION + && event.code() == SYN_REPORT_CODE + && !flush_pending(entry, event_tx) + { + return ReadResult::ReceiverGone; + } + } + } +} + +fn push_relative_event(entry: &mut OpenDevice, code: u16, value: i32) { + if code == RelativeAxisCode::REL_X.0 || code == RelativeAxisCode::REL_Y.0 { + let (dx, dy) = if code == RelativeAxisCode::REL_X.0 { + (value, 0) + } else { + (0, value) + }; + + if let Some(PendingEvent::Motion { + dx: pending_dx, + dy: pending_dy, + .. + }) = entry.pending.last_mut() + { + *pending_dx = pending_dx.saturating_add(dx); + *pending_dy = pending_dy.saturating_add(dy); + } else { + entry.pending.push(PendingEvent::Motion { dx, dy }); + } + return; + } + + let axis_field = if code == RelativeAxisCode::REL_WHEEL.0 { + AxisField::Vertical + } else if code == RelativeAxisCode::REL_HWHEEL.0 { + AxisField::Horizontal + } else if code == RelativeAxisCode::REL_WHEEL_HI_RES.0 { + AxisField::VerticalHiRes + } else if code == RelativeAxisCode::REL_HWHEEL_HI_RES.0 { + AxisField::HorizontalHiRes + } else { + return; + }; + + if !matches!(entry.pending.last(), Some(PendingEvent::Axis { .. })) { + entry.pending.push(PendingEvent::Axis { + horizontal: 0, + vertical: 0, + horizontal_hi_res: 0, + vertical_hi_res: 0, + }); + } + + let Some(PendingEvent::Axis { + horizontal, + vertical, + horizontal_hi_res, + vertical_hi_res, + .. + }) = entry.pending.last_mut() + else { + unreachable!(); + }; + + let target = match axis_field { + AxisField::Horizontal => horizontal, + AxisField::Vertical => vertical, + AxisField::HorizontalHiRes => horizontal_hi_res, + AxisField::VerticalHiRes => vertical_hi_res, + }; + *target = target.saturating_add(value); +} + +enum AxisField { + Horizontal, + Vertical, + HorizontalHiRes, + VerticalHiRes, +} + +fn flush_pending(entry: &mut OpenDevice, event_tx: &SyncSender) -> bool { + for pending in entry.pending.drain(..) { + let event = match pending { + PendingEvent::Key { code, pressed } => CapturedEvent::Key { code, pressed }, + PendingEvent::Button { button, pressed } => { + CapturedEvent::PointerButton { button, pressed } + } + PendingEvent::Motion { dx, dy } => { + if dx == 0 && dy == 0 { + continue; + } + CapturedEvent::PointerMotion { + dx: f64::from(dx), + dy: f64::from(dy), + } + } + PendingEvent::Axis { + horizontal, + vertical, + horizontal_hi_res, + vertical_hi_res, + } => { + let horizontal_v120 = if horizontal_hi_res != 0 { + horizontal_hi_res + } else { + horizontal.saturating_mul(120) + }; + let vertical_v120_raw = if vertical_hi_res != 0 { + vertical_hi_res + } else { + vertical.saturating_mul(120) + }; + + if horizontal_v120 == 0 && vertical_v120_raw == 0 { + continue; + } + + CapturedEvent::PointerAxis { + horizontal_v120, + // REL_WHEEL is positive for wheel-up + vertical_v120: vertical_v120_raw.saturating_neg(), + } + } + }; + + if event_tx.send(event).is_err() { + return false; + } + } + + true +} + +fn is_mouse_button(code: u16) -> bool { + mouse_buttons().into_iter().any(|button| button.0 == code) +} + +fn with_device_context(action: &str, path: &Path, error: io::Error) -> io::Error { + io::Error::new( + error.kind(), + format!("failed to {action} {}: {error}", path.display()), + ) +} + +fn emergency_chord_active(pressed: &HashSet) -> bool { + let ctrl = + pressed.contains(&KeyCode::KEY_LEFTCTRL.0) || pressed.contains(&KeyCode::KEY_RIGHTCTRL.0); + + let alt = + pressed.contains(&KeyCode::KEY_LEFTALT.0) || pressed.contains(&KeyCode::KEY_RIGHTALT.0); + + ctrl && alt && pressed.contains(&KeyCode::KEY_DELETE.0) +} diff --git a/wayvr/src/backend/wayvr/mod.rs b/wayvr/src/backend/wayvr/mod.rs index 724981eb..29e05881 100644 --- a/wayvr/src/backend/wayvr/mod.rs +++ b/wayvr/src/backend/wayvr/mod.rs @@ -2,9 +2,11 @@ pub mod client; mod comp; mod handle; mod image_importer; +mod input_capture; pub mod process; mod time; pub mod window; + use anyhow::Context; use comp::Application; use glam::Vec2; @@ -52,7 +54,7 @@ use std::{ use vulkano::image::view::ImageView; use wayland_protocols::xdg::shell::server::xdg_toplevel; use wayvr_ipc::{packet_client::PositionMode, packet_server}; -use wgui::gfx::WGfx; +use wgui::{gfx::WGfx, log::LogErr}; use wlx_capture::frame::Transform; use wlx_common::desktop_finder::DesktopFinder; use xkbcommon::xkb; @@ -62,6 +64,7 @@ use crate::{ task::{OverlayTask, SpawnPos, TaskContainer, TaskType, ToggleMode}, wayvr::{ image_importer::ImageImporter, + input_capture::InputCapture, process::{KillSignal, Process}, }, }, @@ -69,7 +72,10 @@ use crate::{ ipc::{event_queue::SyncEventQueue, ipc_server, signal::WayVRSignal}, overlays::wayvr::create_wl_window_overlay, state::AppState, - subsystem::hid::{MODS_TO_KEYS, WheelDelta}, + subsystem::{ + dbus::DbusConnector, + hid::{MODS_TO_KEYS, WheelDelta}, + }, windowing::{OverlayID, OverlaySelector}, }; @@ -125,6 +131,9 @@ pub struct WvrServerState { window_to_overlay: HashMap, overlay_to_window: SecondaryMap, process_overlays: HashMap>, + input_capture: Option, + has_input_focus: bool, + grab_toast_sent: bool, } #[derive(Clone, Copy, PartialEq)] @@ -246,6 +255,10 @@ impl WvrServerState { let dma_importer = ImageImporter::new(gfx); + let input_capture = InputCapture::new() + .log_err("Could not initialize evdev input capture") + .ok(); + let state = Application { output, image_importer: dma_importer, @@ -281,6 +294,9 @@ impl WvrServerState { window_to_overlay: HashMap::new(), overlay_to_window: SecondaryMap::new(), process_overlays: HashMap::new(), + input_capture, + has_input_focus: false, + grab_toast_sent: false, }) } @@ -589,6 +605,8 @@ impl WvrServerState { wvr_server.manager.cleanup_handles(); } + wvr_server.process_input_capture(); + wvr_server.ticks += 1; Ok(tasks) @@ -661,6 +679,59 @@ impl WvrServerState { self.manager.seat_pointer.is_grabbed() } + pub fn process_input_capture(&mut self) { + if !self.has_input_focus { + return; + } + let Some(input_capture) = self.input_capture.as_mut() else { + return; + }; + + #[allow(unused_variables)] //TODO: remove this + for ev in input_capture.drain_events() { + match ev { + input_capture::CapturedEvent::Key { code, pressed } => { + self.manager.send_key((code as u32) + 8, pressed); + } + input_capture::CapturedEvent::PointerButton { button, pressed } => {} + input_capture::CapturedEvent::PointerMotion { dx, dy } => {} + input_capture::CapturedEvent::PointerAxis { + horizontal_v120, + vertical_v120, + } => {} + input_capture::CapturedEvent::UngrabbedAll => { + self.manager.release_all_keys(); + self.has_input_focus = false; + } + } + } + } + + pub fn set_input_focus(&mut self, has_focus: bool) { + let Some(input_capture) = self.input_capture.as_mut() else { + return; + }; + + let res = input_capture + .set_grabbed(has_focus) + .log_err("Could not grab input."); + + if res.is_ok() && !self.grab_toast_sent { + self.grab_toast_sent = true; + //TODO: localize + let _ = DbusConnector::notify_send( + "WayVR has your keyboard and mouse!", + "Ctrl+Alt+Del to release", + 1, + 5000, + 0, + true, + ); + } + + self.has_input_focus = has_focus; + } + pub fn send_mouse_move( &mut self, target: PointerFocusTarget, diff --git a/wayvr/src/overlays/screen/mod.rs b/wayvr/src/overlays/screen/mod.rs index cbfe93eb..acb05ec7 100644 --- a/wayvr/src/overlays/screen/mod.rs +++ b/wayvr/src/overlays/screen/mod.rs @@ -6,7 +6,7 @@ use wlx_common::windowing::{OverlayWindowState, Positioning}; use crate::{ state::{AppSession, AppState, ScreenMeta}, - subsystem::input::KeyboardFocus, + subsystem::input::InputFocus, windowing::{ backend::OverlayBackend, window::{OverlayCategory, OverlayWindowConfig}, @@ -56,7 +56,7 @@ fn create_screen_from_backend( ), ..OverlayWindowState::default() }, - keyboard_focus: Some(KeyboardFocus::PhysicalScreen), + input_focus: Some(InputFocus::PhysicalScreen), ..OverlayWindowConfig::from_backend(backend) } } diff --git a/wayvr/src/overlays/wayvr.rs b/wayvr/src/overlays/wayvr.rs index ea2dff97..61a3adb7 100644 --- a/wayvr/src/overlays/wayvr.rs +++ b/wayvr/src/overlays/wayvr.rs @@ -51,7 +51,7 @@ use crate::{ }, overlays::screen::capture::ScreenPipeline, state::{self, AppState}, - subsystem::{hid::WheelDelta, input::KeyboardFocus}, + subsystem::{hid::WheelDelta, input::InputFocus}, windowing::{ OverlayID, OverlaySelector, backend::{ @@ -112,7 +112,7 @@ pub fn create_wl_window_overlay( ), ..OverlayWindowState::default() }, - keyboard_focus: Some(KeyboardFocus::WayVR), + input_focus: Some(InputFocus::WayVR), category: OverlayCategory::WayVR, show_on_spawn: true, ..OverlayWindowConfig::from_backend(Box::new(WvrWindowBackend::new( diff --git a/wayvr/src/overlays/whisper.rs b/wayvr/src/overlays/whisper.rs index bfc9373d..70f8a575 100644 --- a/wayvr/src/overlays/whisper.rs +++ b/wayvr/src/overlays/whisper.rs @@ -29,7 +29,7 @@ use crate::{ subsystem::{ clipboard::{self, ClipboardProvider}, hid::VirtualKey, - input::KeyboardFocus, + input::InputFocus, whisper_stt::WhisperStt, }, windowing::{ @@ -51,14 +51,14 @@ impl WhisperState { return false; }; - match app.hid_provider.keyboard_focus { - KeyboardFocus::WayVR => { + match app.hid_provider.get_input_focus() { + InputFocus::WayVR => { if let Some(wvr) = app.wvr_server.as_mut() { wvr.set_clipboard(text); return true; } } - KeyboardFocus::PhysicalScreen => { + InputFocus::PhysicalScreen => { if let Some(clip) = self.clipboard_provider.as_mut() { clip.set_clipboard_utf8(text); return true; diff --git a/wayvr/src/subsystem/input.rs b/wayvr/src/subsystem/input.rs index 3b8d82e8..b852e145 100644 --- a/wayvr/src/subsystem/input.rs +++ b/wayvr/src/subsystem/input.rs @@ -7,13 +7,13 @@ use crate::subsystem::hid::provider::dummy::DummyProvider; use crate::{backend::wayvr::WvrServerState, overlays::toast::Toast, subsystem::hid::XkbKeymap}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum KeyboardFocus { +pub enum InputFocus { PhysicalScreen, WayVR, // (wayland window id data is handled internally), } pub struct HidWrapper { - pub keyboard_focus: KeyboardFocus, + input_focus: InputFocus, pub inner: Box, pub keymap: Option, } @@ -32,7 +32,7 @@ impl HidWrapper { ( Self { - keyboard_focus: KeyboardFocus::PhysicalScreen, + input_focus: InputFocus::PhysicalScreen, inner: provider, keymap: None, }, @@ -40,15 +40,37 @@ impl HidWrapper { ) } + pub fn set_input_focus( + &mut self, + wvr_server: Option<&mut WvrServerState>, + value: InputFocus, + ) -> bool { + if self.input_focus != value { + self.input_focus = value; + + if let Some(wvr_server) = wvr_server { + wvr_server.set_input_focus(matches!(value, InputFocus::WayVR)); + } + + true + } else { + false + } + } + + pub fn get_input_focus(&self) -> InputFocus { + self.input_focus + } + pub fn send_key_routed( &self, wvr_server: Option<&mut WvrServerState>, key: VirtualKey, down: bool, ) { - match self.keyboard_focus { - KeyboardFocus::PhysicalScreen => self.inner.send_key(key, down), - KeyboardFocus::WayVR => { + match self.input_focus { + InputFocus::PhysicalScreen => self.inner.send_key(key, down), + InputFocus::WayVR => { if let Some(wvr_server) = wvr_server { wvr_server.send_key(key as u32, down); } @@ -73,9 +95,9 @@ impl HidWrapper { } pub fn set_modifiers_routed(&mut self, wvr_server: Option<&mut WvrServerState>, mods: u8) { - match self.keyboard_focus { - KeyboardFocus::PhysicalScreen => self.inner.set_modifiers(mods), - KeyboardFocus::WayVR => { + match self.input_focus { + InputFocus::PhysicalScreen => self.inner.set_modifiers(mods), + InputFocus::WayVR => { if let Some(wvr_server) = wvr_server { wvr_server.set_modifiers(mods); } diff --git a/wayvr/src/windowing/window.rs b/wayvr/src/windowing/window.rs index 5c9fdc73..9b674010 100644 --- a/wayvr/src/windowing/window.rs +++ b/wayvr/src/windowing/window.rs @@ -5,7 +5,7 @@ use wlx_common::windowing::{OverlayWindowState, Positioning}; use crate::{ state::AppState, - subsystem::input::KeyboardFocus, + subsystem::input::InputFocus, windowing::{ backend::{FrameMeta, OverlayBackend, RenderResources, ShouldRender}, snap_upright, @@ -76,7 +76,7 @@ pub struct OverlayWindowConfig { /// Order to draw overlays in. Overlays with higher numbers will be drawn over ones with lower numbers. pub z_order: u32, /// If set, hovering this overlay will cause the HID provider to switch focus. - pub keyboard_focus: Option, + pub input_focus: Option, /// Category of the overlay, used by toolbox on the watch. pub category: OverlayCategory, /// Should the overlay be displayed on the next frame? @@ -104,7 +104,7 @@ impl OverlayWindowConfig { }, active_state: None, z_order: 0, - keyboard_focus: None, + input_focus: None, category: OverlayCategory::Internal, show_on_spawn: false, global: false,