diff --git a/wayvr/src/backend/wayvr/client.rs b/wayvr/src/backend/wayvr/client.rs index a972bfd0..788ddf0d 100644 --- a/wayvr/src/backend/wayvr/client.rs +++ b/wayvr/src/backend/wayvr/client.rs @@ -3,10 +3,13 @@ use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc}; use anyhow::Context; use glam::Vec2; use smithay::{ - backend::input::Keycode, - input::{keyboard::KeyboardHandle, pointer::PointerHandle}, - reexports::wayland_server::{self, protocol::wl_surface::WlSurface}, - utils::SerialCounter, + backend::input::{ButtonState, Keycode}, + input::{ + keyboard::KeyboardHandle, + pointer::{ButtonEvent, MotionEvent, PointerHandle}, + }, + reexports::wayland_server::{self, Resource, protocol::wl_surface::WlSurface}, + utils::{Logical, Point, SerialCounter}, }; use xkbcommon::xkb; @@ -211,29 +214,80 @@ impl WayVRCompositor { .context("Failed to set keymap") } + pub fn send_mouse_move_unfocused(&mut self, global_pos: Vec2) { + let location: Point = (global_pos.x as f64, global_pos.y as f64).into(); + + self.seat_pointer.motion( + &mut self.state, + None, + &MotionEvent { + location, + serial: self.serial_counter.next_serial(), + time: super::time::get_millis() as u32, + }, + ); + + self.seat_pointer.frame(&mut self.state); + } + pub fn send_mouse_move_to_surface( &mut self, surface: WlSurface, global_pos: Vec2, surface_origin: Vec2, ) { - use smithay::input::pointer::MotionEvent; - use smithay::utils::{Logical, Point}; - let location: Point = (global_pos.x as f64, global_pos.y as f64).into(); let focus_location: Point = (surface_origin.x as f64, surface_origin.y as f64).into(); + let serial = self.serial_counter.next_serial(); + let time = super::time::get_millis() as u32; + self.seat_pointer.motion( &mut self.state, Some((surface, focus_location)), &MotionEvent { location, - serial: self.serial_counter.next_serial(), - time: 0, + serial, + time, }, ); + + self.seat_pointer.frame(&mut self.state); + } + + pub fn send_pointer_button(&mut self, index: super::MouseIndex, pressed: bool) { + let state = if pressed { + ButtonState::Pressed + } else { + ButtonState::Released + }; + + let serial = self.serial_counter.next_serial(); + let time = super::time::get_millis() as u32; + let button = match index { + super::MouseIndex::Left => 0x110, + super::MouseIndex::Center => 0x112, + super::MouseIndex::Right => 0x111, + }; + + log::trace!( + "pointer button: button={button:#x} pressed={pressed} focus={:?}", + self.seat_pointer.current_focus().map(|s| s.id()) + ); + + self.seat_pointer.button( + &mut self.state, + &ButtonEvent { + serial, + time, + button, + state, + }, + ); + + self.seat_pointer.frame(&mut self.state); } } diff --git a/wayvr/src/backend/wayvr/comp.rs b/wayvr/src/backend/wayvr/comp.rs index fd6261a2..938b5ed5 100644 --- a/wayvr/src/backend/wayvr/comp.rs +++ b/wayvr/src/backend/wayvr/comp.rs @@ -1,7 +1,11 @@ use anyhow::Context; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::renderer::{BufferType, buffer_type}; -use smithay::desktop::{PopupKind, PopupManager}; +use smithay::desktop::{ + PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, + find_popup_root_surface, +}; +use smithay::input::pointer::Focus; use smithay::input::{Seat, SeatHandler, SeatState}; use smithay::output::Output; use smithay::reexports::rustix::fs::{OFlags, fcntl_setfl}; @@ -400,8 +404,59 @@ impl XdgShellHandler for Application { self.popup_manager.cleanup(); } - fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { - // Handle popup grab here + fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) { + log::info!( + "xdg_popup.grab: surface={:?} serial={:?}", + surface.wl_surface().id(), + serial + ); + + let popup = PopupKind::Xdg(surface.clone()); + + let Ok(root_surface) = find_popup_root_surface(&popup) else { + log::warn!("xdg_popup.grab: could not find popup root surface"); + return; + }; + + let Some(seat) = Seat::::from_resource(&seat) else { + log::warn!("xdg_popup.grab: unknown seat"); + return; + }; + + let root_focus = root_surface; + + let Ok(mut grab) = self + .popup_manager + .grab_popup::(root_focus, popup, &seat, serial) + else { + log::debug!("xdg_popup.grab denied"); + return; + }; + + if let Some(keyboard) = seat.get_keyboard() { + if keyboard.is_grabbed() + && !(keyboard.has_grab(serial) + || keyboard.has_grab(grab.previous_serial().unwrap_or(serial))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + + keyboard.set_focus(self, grab.current_grab(), serial); + keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); + } + + if let Some(pointer) = seat.get_pointer() { + if pointer.is_grabbed() + && !(pointer.has_grab(serial) + || pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial()))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + + pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep); + } } fn reposition_request( diff --git a/wayvr/src/backend/wayvr/mod.rs b/wayvr/src/backend/wayvr/mod.rs index 9ba63cc5..7433579b 100644 --- a/wayvr/src/backend/wayvr/mod.rs +++ b/wayvr/src/backend/wayvr/mod.rs @@ -119,6 +119,7 @@ pub struct WvrServerState { overlay_to_window: SecondaryMap, } +#[derive(Clone, Copy, PartialEq)] pub enum MouseIndex { Left, Center, @@ -581,6 +582,10 @@ impl WvrServerState { self.window_to_overlay.get(&window).copied() } + pub fn pointer_is_grabbed(&self) -> bool { + self.manager.seat_pointer.is_grabbed() + } + pub fn send_mouse_move_to_surface( &mut self, surface: WlSurface, @@ -604,6 +609,50 @@ impl WvrServerState { }); } + pub fn send_mouse_button_to_surface( + &mut self, + surface: WlSurface, + global_pos: Vec2, + surface_origin: Vec2, + hover_window: window::WindowHandle, + index: MouseIndex, + pressed: bool, + ) { + self.manager + .send_mouse_move_to_surface(surface, global_pos, surface_origin); + + self.wm.mouse = Some(window::MouseState { + hover_window, + x: global_pos.x as u32, + y: global_pos.y as u32, + }); + + self.manager.send_pointer_button(index, pressed); + } + + pub fn send_mouse_button_main_surface( + &mut self, + click_freeze: i32, + hover_window: window::WindowHandle, + global_pos: Vec2, + index: MouseIndex, + pressed: bool, + ) { + if pressed { + self.mouse_freeze = Instant::now() + Duration::from_millis(click_freeze.max(0) as u64); + } + + self.manager.send_mouse_move_unfocused(global_pos); + + self.wm.mouse = Some(window::MouseState { + hover_window, + x: global_pos.x.max(0.0) as u32, + y: global_pos.y.max(0.0) as u32, + }); + + self.manager.send_pointer_button(index, pressed); + } + pub fn send_mouse_move(&mut self, handle: window::WindowHandle, x: u32, y: u32) { if self.mouse_freeze > Instant::now() { return; diff --git a/wayvr/src/overlays/wayvr.rs b/wayvr/src/overlays/wayvr.rs index 4823444f..8c9918fd 100644 --- a/wayvr/src/overlays/wayvr.rs +++ b/wayvr/src/overlays/wayvr.rs @@ -129,6 +129,7 @@ pub struct WvrWindowBackend { icon: Arc, pipeline: Option, popups_pipeline: Arc>, + popup_outside_button: Option, interaction_transform: Option, window: WindowHandle, popups: Vec, @@ -230,6 +231,7 @@ impl WvrWindowBackend { window, popups: vec![], popups_pipeline, + popup_outside_button: None, interaction_transform: None, just_resumed: false, meta: None, @@ -317,6 +319,11 @@ impl WvrWindowBackend { ) } + fn unclamped_client_pos_from_hit(&self, hit: &input::PointerHit) -> Vec2 { + let transformed = self.transformed_uv_from_hit(hit); + self.client_pos_from_transformed_uv(transformed) + } + fn is_inside_client_area(&self, transformed: Vec2) -> bool { self.uv_range.contains(&transformed.x) && self.uv_range.contains(&transformed.y) } @@ -724,20 +731,89 @@ impl OverlayBackend for WvrWindowBackend { } fn on_pointer(&mut self, app: &mut state::AppState, hit: &input::PointerHit, pressed: bool) { - match self.hit_target(hit) { + let Some(index) = Self::mouse_index_from_mode(hit.mode) else { + return; + }; + + let target = self.hit_target(hit); + let outside_pos = self.unclamped_client_pos_from_hit(hit); + + // if the press was consumed to dismiss a popup, consume the matching release too. + if !pressed && self.popup_outside_button == Some(index) { + self.popup_outside_button = None; + + let click_freeze = app.session.config.click_freeze_time_ms; + app.wvr_server + .as_mut() + .unwrap() + .send_mouse_button_main_surface( + click_freeze, + self.window, + outside_pos, + index, + false, + ); + return; + } + + let popup_grab_active = !self.popups.is_empty() + && app + .wvr_server + .as_ref() + .is_some_and(|server| server.pointer_is_grabbed()); + + let outside_grabbed_popup = + popup_grab_active && !matches!(&target, Some(WvrHitTarget::Popup { .. })); + + if outside_grabbed_popup { + if pressed { + self.popup_outside_button = Some(index); + } + + let click_freeze = app.session.config.click_freeze_time_ms; + app.wvr_server + .as_mut() + .unwrap() + .send_mouse_button_main_surface( + click_freeze, + self.window, + outside_pos, + index, + pressed, + ); + return; + } + + match target { Some(WvrHitTarget::Panel(hit2)) => { self.panel_hovered = true; self.panel.on_pointer(app, &hit2, pressed); } - Some(WvrHitTarget::Popup { .. }) | Some(WvrHitTarget::Toplevel { .. }) => { - let Some(index) = Self::mouse_index_from_mode(hit.mode) else { - return; - }; + Some(WvrHitTarget::Popup { + surface, + global_pos, + surface_origin, + }) => { + let wvr_server = app.wvr_server.as_mut().unwrap(); + wvr_server.send_mouse_button_to_surface( + surface, + global_pos, + surface_origin, + self.window, + index, + pressed, + ); + } + + Some(WvrHitTarget::Toplevel { pos }) => { let click_freeze = app.session.config.click_freeze_time_ms; let wvr_server = app.wvr_server.as_mut().unwrap(); + // normal toplevel click path, only when no popup grab is active. + wvr_server.send_mouse_move(self.window, pos.x as u32, pos.y as u32); + if pressed { wvr_server.send_mouse_down(click_freeze, self.window, index); } else {