wvr_server: rework popup mouse handling

This commit is contained in:
galister 2026-07-04 14:36:49 +09:00
parent 052253f7f4
commit f0f3402650
3 changed files with 272 additions and 66 deletions

View File

@ -1,10 +1,11 @@
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,
reexports::wayland_server::{self, protocol::wl_surface::WlSurface},
utils::SerialCounter,
};
use xkbcommon::xkb;
@ -209,6 +210,31 @@ impl WayVRCompositor {
)
.context("Failed to set keymap")
}
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<f64, Logical> = (global_pos.x as f64, global_pos.y as f64).into();
let focus_location: Point<f64, Logical> =
(surface_origin.x as f64, surface_origin.y as f64).into();
self.seat_pointer.motion(
&mut self.state,
Some((surface, focus_location)),
&MotionEvent {
location,
serial: self.serial_counter.next_serial(),
time: 0,
},
);
}
}
const STARTING_WAYLAND_ADDR_IDX: u32 = 20;

View File

@ -7,6 +7,7 @@ mod time;
pub mod window;
use anyhow::Context;
use comp::Application;
use glam::Vec2;
use process::ProcessVec;
use slotmap::SecondaryMap;
use smallvec::SmallVec;
@ -16,7 +17,11 @@ use smithay::{
output::{Mode, Output},
reexports::{
wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager as kde_decoration,
wayland_server::{self, backend::ClientId, protocol::wl_buffer},
wayland_server::{
self,
backend::ClientId,
protocol::{wl_buffer, wl_surface::WlSurface},
},
},
utils::{Logical, Size},
wayland::{
@ -576,6 +581,29 @@ impl WvrServerState {
self.window_to_overlay.get(&window).copied()
}
pub fn send_mouse_move_to_surface(
&mut self,
surface: WlSurface,
global_pos: Vec2,
surface_origin: Vec2,
hover_window: window::WindowHandle,
) {
if self.mouse_freeze > Instant::now() {
return;
}
self.manager
.send_mouse_move_to_surface(surface, global_pos, surface_origin);
self.mouse_freeze = Instant::now() + Duration::from_millis(1);
self.wm.mouse = Some(window::MouseState {
hover_window,
x: global_pos.x as u32,
y: global_pos.y as u32,
});
}
pub fn send_mouse_move(&mut self, handle: window::WindowHandle, x: u32, y: u32) {
if self.mouse_freeze > Instant::now() {
return;

View File

@ -1,7 +1,7 @@
use glam::{Affine2, Affine3A, Quat, Vec2, Vec3, vec2, vec3};
use smithay::{
desktop::PopupManager,
reexports::wayland_server::{Resource, backend::ObjectId},
reexports::wayland_server::{Resource, backend::ObjectId, protocol::wl_surface::WlSurface},
wayland::{compositor::with_states, shell::xdg::XdgPopupSurfaceData},
};
use std::{mem, ops::RangeInclusive, sync::Arc};
@ -101,6 +101,29 @@ pub fn create_wl_window_overlay(
})
}
#[derive(Clone)]
struct RenderedPopup {
surface: WlSurface,
surface_id: ObjectId,
image: Arc<ImageView>,
pos: Vec2,
size: Vec2,
}
enum WvrHitTarget {
Panel(input::PointerHit),
Toplevel {
pos: Vec2,
},
Popup {
surface: WlSurface,
global_pos: Vec2,
surface_origin: Vec2,
},
}
pub struct WvrWindowBackend {
name: Arc<str>,
icon: Arc<str>,
@ -108,7 +131,7 @@ pub struct WvrWindowBackend {
popups_pipeline: Arc<WGfxPipeline<Vert2Uv>>,
interaction_transform: Option<Affine2>,
window: WindowHandle,
popups: Vec<(ObjectId, Arc<ImageView>, Vec2)>,
popups: Vec<RenderedPopup>,
just_resumed: bool,
meta: Option<FrameMeta>,
mouse: Option<MouseMeta>,
@ -261,6 +284,106 @@ impl WvrWindowBackend {
Ok(())
}
fn surface_point_from_hit(&self, hit: &input::PointerHit) -> Option<Vec2> {
if self.inner_extent == [0, 0] {
return None;
}
let transformed = self.mouse_transform.transform_point2(hit.uv);
Some(vec2(
transformed.x * self.inner_extent[0] as f32,
transformed.y * self.inner_extent[1] as f32,
))
}
fn popup_at(&self, p: Vec2) -> Option<&RenderedPopup> {
self.popups.iter().rev().find(|popup| {
let local = p - popup.pos;
local.x >= 0.0 && local.y >= 0.0 && local.x < popup.size.x && local.y < popup.size.y
})
}
fn transformed_uv_from_hit(&self, hit: &input::PointerHit) -> Vec2 {
self.mouse_transform.transform_point2(hit.uv)
}
fn client_pos_from_transformed_uv(&self, transformed: Vec2) -> Vec2 {
vec2(
transformed.x * self.inner_extent[0] as f32,
transformed.y * self.inner_extent[1] as f32,
)
}
fn is_inside_client_area(&self, transformed: Vec2) -> bool {
self.uv_range.contains(&transformed.x) && self.uv_range.contains(&transformed.y)
}
fn panel_hit_from_hit(&self, hit: &input::PointerHit) -> Option<input::PointerHit> {
let meta = self.meta.as_ref()?;
let panel_height = meta.extent[1].checked_sub(self.inner_extent[1])?;
if panel_height == 0 {
return None;
}
let mut hit2 = *hit;
hit2.uv.y *= meta.extent[1] as f32 / panel_height as f32;
Some(hit2)
}
fn popup_hit_at_client_pos(&self, pos: Vec2) -> Option<(WlSurface, Vec2)> {
self.popups.iter().rev().find_map(|popup| {
let local = pos - popup.pos;
let hit = local.x >= 0.0
&& local.y >= 0.0
&& local.x < popup.size.x
&& local.y < popup.size.y;
if hit {
Some((popup.surface.clone(), popup.pos))
} else {
None
}
})
}
fn hit_target(&self, hit: &input::PointerHit) -> Option<WvrHitTarget> {
let transformed = self.transformed_uv_from_hit(hit);
let client_pos = self.client_pos_from_transformed_uv(transformed);
// Important: popups are checked before the panel/client bounds.
// Real xdg_popups may extend outside the parent toplevel rect.
if let Some((surface, surface_origin)) = self.popup_hit_at_client_pos(client_pos) {
return Some(WvrHitTarget::Popup {
surface,
global_pos: client_pos,
surface_origin,
});
}
if !self.is_inside_client_area(transformed) {
return self.panel_hit_from_hit(hit).map(WvrHitTarget::Panel);
}
let clamped = transformed.clamp(Vec2::ZERO, Vec2::ONE);
let pos = self.client_pos_from_transformed_uv(clamped);
Some(WvrHitTarget::Toplevel { pos })
}
fn mouse_index_from_mode(mode: input::PointerMode) -> Option<wayvr::MouseIndex> {
match mode {
input::PointerMode::Left => Some(wayvr::MouseIndex::Left),
input::PointerMode::Middle => Some(wayvr::MouseIndex::Center),
input::PointerMode::Right => Some(wayvr::MouseIndex::Right),
_ => None,
}
}
}
impl OverlayBackend for WvrWindowBackend {
@ -320,11 +443,16 @@ impl OverlayBackend for WvrWindowBackend {
}
if let Some(surf) = SurfaceBufWithImage::get_from_surface(states) {
Some((
popup.wl_surface().id(),
surf.image,
vec2(point.x as _, point.y as _),
))
let image = surf.image;
let size = image.extent_f32();
Some(RenderedPopup {
surface: popup.wl_surface().clone(),
surface_id: popup.wl_surface().id(),
image,
pos: vec2(point.x as _, point.y as _),
size: vec2(size[0], size[1]),
})
} else {
None
}
@ -397,7 +525,7 @@ impl OverlayBackend for WvrWindowBackend {
y: (m.y as f32) / (inner_extent[1] as f32),
});
let dirty = self.mouse != mouse || self.popups != popups;
let dirty = self.mouse != mouse || rendered_popups_dirty(&self.popups, &popups);
self.mouse = mouse;
self.popups = popups;
self.meta = Some(meta);
@ -451,10 +579,10 @@ impl OverlayBackend for WvrWindowBackend {
.unwrap()
.render(image, self.mouse.as_ref(), app, rdr)?;
for (popup_surface_id, popup_img, point) in &self.popups {
for popup in &self.popups {
let meta = self.meta.as_ref().unwrap();
let extentf = [meta.extent[0] as f32, meta.extent[1] as f32];
let popup_extentf = popup_img.extent_f32();
let mut buf_vert = app
.gfx
.empty_buffer(BufferUsage::TRANSFER_DST | BufferUsage::VERTEX_BUFFER, 4)?;
@ -463,15 +591,15 @@ impl OverlayBackend for WvrWindowBackend {
&mut buf_vert,
extentf[0],
extentf[1],
point.x,
point.y,
popup_extentf[0],
popup_extentf[1],
popup.pos.x,
popup.pos.y,
popup.size.x,
popup.size.y,
)?;
let set0 = self.popups_pipeline.uniform_sampler(
0,
popup_img.clone(),
popup.image.clone(),
app.gfx.texture_filter,
)?;
let pass = self.popups_pipeline.create_pass(
@ -493,7 +621,7 @@ impl OverlayBackend for WvrWindowBackend {
.unwrap()
.manager
.state
.send_frame_callbacks_for_surface_id(popup_surface_id);
.send_frame_callbacks_for_surface_id(&popup.surface_id);
}
if let Some(wvr_server) = app.wvr_server.as_mut() {
@ -540,33 +668,51 @@ impl OverlayBackend for WvrWindowBackend {
}
fn on_hover(&mut self, app: &mut state::AppState, hit: &input::PointerHit) -> HoverResult {
let transformed = self.mouse_transform.transform_point2(hit.uv);
match self.hit_target(hit) {
Some(WvrHitTarget::Panel(hit2)) => {
self.panel_hovered = true;
self.panel.on_hover(app, &hit2)
}
Some(WvrHitTarget::Popup {
surface,
global_pos,
surface_origin,
}) => {
if self.panel_hovered {
self.panel.on_left(app, hit.pointer);
self.panel_hovered = false;
}
if !self.uv_range.contains(&transformed.x) || !self.uv_range.contains(&transformed.y) {
let Some(meta) = self.meta.as_ref() else {
return HoverResult::default();
};
let wvr_server = app.wvr_server.as_mut().unwrap();
let mut hit2 = *hit;
hit2.uv.y *= meta.extent[1] as f32 / (meta.extent[1] - self.inner_extent[1]) as f32;
self.panel_hovered = true;
return self.panel.on_hover(app, &hit2);
} else if self.panel_hovered {
self.panel.on_left(app, hit.pointer);
self.panel_hovered = false;
}
wvr_server.send_mouse_move_to_surface(
surface,
global_pos,
surface_origin,
self.window,
);
let clamped = transformed.clamp(Vec2::ZERO, Vec2::ONE);
HoverResult {
haptics: None,
consume: true,
}
}
Some(WvrHitTarget::Toplevel { pos }) => {
if self.panel_hovered {
self.panel.on_left(app, hit.pointer);
self.panel_hovered = false;
}
let x = (clamped.x * (self.inner_extent[0] as f32)) as u32;
let y = (clamped.y * (self.inner_extent[1] as f32)) as u32;
let wvr_server = app.wvr_server.as_mut().unwrap();
let wvr_server = app.wvr_server.as_mut().unwrap(); //never None
wvr_server.send_mouse_move(self.window, x, y);
wvr_server.send_mouse_move(self.window, pos.x as u32, pos.y as u32);
HoverResult {
haptics: None, // haptics are handled via task
consume: true,
HoverResult {
haptics: None,
consume: true,
}
}
None => HoverResult::default(),
}
}
@ -578,35 +724,28 @@ impl OverlayBackend for WvrWindowBackend {
}
fn on_pointer(&mut self, app: &mut state::AppState, hit: &input::PointerHit, pressed: bool) {
let transformed = self.mouse_transform.transform_point2(hit.uv);
if !self.uv_range.contains(&transformed.x) || !self.uv_range.contains(&transformed.y) {
let Some(meta) = self.meta.as_ref() else {
return;
};
let mut hit2 = *hit;
hit2.uv.y *= meta.extent[1] as f32 / (meta.extent[1] - self.inner_extent[1]) as f32;
self.panel_hovered = true;
return self.panel.on_pointer(app, &hit2, pressed);
}
if let Some(index) = match hit.mode {
input::PointerMode::Left => Some(wayvr::MouseIndex::Left),
input::PointerMode::Middle => Some(wayvr::MouseIndex::Center),
input::PointerMode::Right => Some(wayvr::MouseIndex::Right),
_ => {
// Unknown pointer event, ignore
None
match self.hit_target(hit) {
Some(WvrHitTarget::Panel(hit2)) => {
self.panel_hovered = true;
self.panel.on_pointer(app, &hit2, pressed);
}
} {
let click_freeze = app.session.config.click_freeze_time_ms;
let wvr_server = app.wvr_server.as_mut().unwrap(); //never None
if pressed {
wvr_server.send_mouse_down(click_freeze, self.window, index);
} else {
wvr_server.send_mouse_up(index);
Some(WvrHitTarget::Popup { .. }) | Some(WvrHitTarget::Toplevel { .. }) => {
let Some(index) = Self::mouse_index_from_mode(hit.mode) else {
return;
};
let click_freeze = app.session.config.click_freeze_time_ms;
let wvr_server = app.wvr_server.as_mut().unwrap();
if pressed {
wvr_server.send_mouse_down(click_freeze, self.window, index);
} else {
wvr_server.send_mouse_up(index);
}
}
None => {}
}
}
@ -667,3 +806,16 @@ impl OverlayBackend for WvrWindowBackend {
}
}
}
fn rendered_popups_dirty(a: &[RenderedPopup], b: &[RenderedPopup]) -> bool {
if a.len() != b.len() {
return true;
}
a.iter().zip(b).any(|(a, b)| {
a.surface_id != b.surface_id
|| !Arc::ptr_eq(&a.image, &b.image)
|| a.pos != b.pos
|| a.size != b.size
})
}