Files
feresaria/compositor/src/input_capture.rs
T
2026-10-01 13:24:21 +01:00

662 lines
22 KiB
Rust

use std::collections::{HashMap, VecDeque};
use std::sync::mpsc::SyncSender;
use ferese_ipc::Response;
use serde::Deserialize;
use serde_json::{Value, json};
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
const MAX_SESSIONS: usize = 8;
const MAX_BARRIERS: usize = 64;
const MAX_EVENTS: usize = 512;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Zone {
pub width: u32,
pub height: u32,
pub x: i32,
pub y: i32,
}
impl Zone {
fn right(self) -> f64 {
f64::from(self.x) + f64::from(self.width)
}
fn bottom(self) -> f64 {
f64::from(self.y) + f64::from(self.height)
}
fn contains(self, point: (f64, f64)) -> bool {
point.0 >= f64::from(self.x)
&& point.0 < self.right()
&& point.1 >= f64::from(self.y)
&& point.1 < self.bottom()
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Barrier {
pub id: u32,
pub position: [i32; 4],
}
#[derive(Clone, Copy, Debug)]
enum Edge {
Left,
Right,
Top,
Bottom,
}
fn edge(barrier: &Barrier, zones: &[Zone]) -> Option<Edge> {
if barrier.id == 0 {
return None;
}
let [x1, y1, x2, y2] = barrier.position.map(f64::from);
let horizontal = y1 == y2 && x1 <= x2;
let vertical = x1 == x2 && y1 <= y2;
if !horizontal && !vertical {
return None;
}
for zone in zones {
let found = if horizontal && x1 >= f64::from(zone.x) && x2 < zone.right() {
if y1 == f64::from(zone.y) {
Some(Edge::Top)
} else if y1 == zone.bottom() {
Some(Edge::Bottom)
} else {
None
}
} else if vertical && y1 >= f64::from(zone.y) && y2 < zone.bottom() {
if x1 == f64::from(zone.x) {
Some(Edge::Left)
} else if x1 == zone.right() {
Some(Edge::Right)
} else {
None
}
} else {
None
};
let Some(found) = found else {
continue;
};
let intersects_neighbor = zones.iter().any(|other| {
if other == zone {
return false;
}
match found {
Edge::Top | Edge::Bottom => {
let outside = y1 + if matches!(found, Edge::Top) { -0.5 } else { 0.5 };
outside >= f64::from(other.y)
&& outside < other.bottom()
&& x1 < other.right()
&& x2 + 1.0 > f64::from(other.x)
}
Edge::Left | Edge::Right => {
let outside = x1 + if matches!(found, Edge::Left) { -0.5 } else { 0.5 };
outside >= f64::from(other.x)
&& outside < other.right()
&& y1 < other.bottom()
&& y2 + 1.0 > f64::from(other.y)
}
}
});
if !intersects_neighbor {
return Some(found);
}
}
None
}
fn crossed(barrier: &Barrier, edge: Edge, from: (f64, f64), to: (f64, f64)) -> Option<f64> {
let [x1, y1, x2, y2] = barrier.position.map(f64::from);
let (distance, movement, start, end) = match edge {
Edge::Left if from.0 >= x1 && to.0 < x1 => (x1 - from.0, to.0 - from.0, y1, y2 + 1.0),
Edge::Right if from.0 < x1 && to.0 >= x1 => (x1 - from.0, to.0 - from.0, y1, y2 + 1.0),
Edge::Top if from.1 >= y1 && to.1 < y1 => (y1 - from.1, to.1 - from.1, x1, x2 + 1.0),
Edge::Bottom if from.1 < y1 && to.1 >= y1 => (y1 - from.1, to.1 - from.1, x1, x2 + 1.0),
_ => return None,
};
let fraction = distance / movement;
let along = if matches!(edge, Edge::Left | Edge::Right) {
from.1 + (to.1 - from.1) * fraction
} else {
from.0 + (to.0 - from.0) * fraction
};
((0.0..=1.0).contains(&fraction) && along >= start && along < end).then_some(fraction)
}
struct Session {
owner: u64,
capabilities: u32,
barriers: Vec<(Barrier, Edge)>,
enabled: bool,
closed: bool,
activation: Option<u32>,
events: VecDeque<Value>,
}
#[derive(Default)]
pub(crate) struct InputCapture {
sessions: HashMap<u64, Session>,
zones: Vec<Zone>,
zone_set: u32,
next_session: u64,
next_activation: u32,
active: Option<u64>,
last_absolute: Option<(f64, f64)>,
pub(crate) restore_focus: bool,
waiters: HashMap<u64, (u64, u64, SyncSender<Response>)>,
}
impl InputCapture {
pub(crate) fn update_zones(&mut self, mut zones: Vec<Zone>) {
zones.sort_by_key(|zone| (zone.x, zone.y, zone.width, zone.height));
zones.dedup();
if self.zones == zones {
return;
}
self.zones = zones;
self.zone_set = self.zone_set.wrapping_add(1);
self.disable_all();
let ids = self.sessions.keys().copied().collect::<Vec<_>>();
for id in ids {
self.sessions.get_mut(&id).unwrap().barriers.clear();
self.enqueue(id, json!({"type":"zones", "zone_set":self.zone_set}));
}
self.flush();
}
pub(crate) fn register(&mut self, owner: u64, capabilities: u32) -> Result<u64, String> {
if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 {
return Err("Unsupported capabilities or input capture session limit reached".into());
}
self.next_session = self.next_session.checked_add(1).ok_or("Input capture IDs exhausted")?;
let id = self.next_session;
self.sessions.insert(
id,
Session {
owner,
capabilities,
barriers: Vec::new(),
enabled: false,
closed: false,
activation: None,
events: VecDeque::new(),
},
);
Ok(id)
}
pub(crate) fn zones(&self, id: u64) -> Result<Value, String> {
self.session(id)?;
Ok(json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}))
}
fn session(&self, id: u64) -> Result<&Session, String> {
self.sessions.get(&id).ok_or("Unknown input capture session".into())
}
pub(crate) fn set_barriers(&mut self, id: u64, zone_set: u32, barriers: Vec<Barrier>) -> Result<Vec<u32>, String> {
self.session(id)?;
if barriers.len() > MAX_BARRIERS {
return Err("Too many pointer barriers".into());
}
self.disable(id)?;
let mut accepted = Vec::new();
let mut failed = Vec::new();
let mut counts = HashMap::new();
for barrier in &barriers {
*counts.entry(barrier.id).or_insert(0) += 1;
}
for barrier in barriers {
let kind = edge(&barrier, &self.zones);
if zone_set == self.zone_set
&& counts[&barrier.id] == 1
&& let Some(kind) = kind
{
accepted.push((barrier, kind));
} else {
failed.push(barrier.id);
}
}
self.sessions.get_mut(&id).unwrap().barriers = accepted;
Ok(failed)
}
pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> {
let session = self.sessions.get_mut(&id).ok_or("Unknown input capture session")?;
if session.closed {
return Err("Input capture session is closed".into());
}
session.enabled = true;
Ok(())
}
pub(crate) fn has_sessions(&self) -> bool {
!self.sessions.is_empty()
}
pub(crate) fn active(&self) -> bool {
self.active.is_some()
}
pub(crate) fn captures(&self, capability: u32) -> bool {
self.active
.and_then(|id| self.sessions.get(&id))
.is_some_and(|session| session.capabilities & capability != 0)
}
pub(crate) fn motion(&mut self, from: (f64, f64), to: (f64, f64), allowed: bool) -> bool {
if self.active() {
return true;
}
if !allowed || ![from.0, from.1, to.0, to.1].iter().all(|value| value.is_finite()) {
return false;
}
let hit = self
.sessions
.iter()
.filter(|(_, session)| session.enabled && !session.closed)
.flat_map(|(id, session)| {
session.barriers.iter().filter_map(move |(barrier, kind)| {
crossed(barrier, *kind, from, to).map(|fraction| (*id, barrier.id, fraction))
})
})
.min_by(|left, right| left.2.total_cmp(&right.2).then(left.0.cmp(&right.0)));
let Some((id, barrier, _)) = hit else {
return false;
};
self.next_activation = self.next_activation.wrapping_add(1);
let activation = self.next_activation;
self.active = Some(id);
let session = self.sessions.get_mut(&id).unwrap();
session.activation = Some(activation);
self.enqueue(
id,
json!({"type":"activated", "activation_id":activation,
"barrier_id":barrier, "cursor_position":to}),
);
self.flush();
true
}
fn enqueue(&mut self, id: u64, event: Value) {
let Some(session) = self.sessions.get_mut(&id) else {
return;
};
if session.closed {
return;
}
if session.events.len() >= MAX_EVENTS {
session.events.clear();
session.events.push_back(json!({"type":"closed"}));
session.closed = true;
session.enabled = false;
session.activation = None;
if self.active == Some(id) {
self.active = None;
self.last_absolute = None;
self.restore_focus = true;
}
return;
}
session.events.push_back(event);
}
pub(crate) fn push(&mut self, event: Value) {
if let Some(id) = self.active {
self.enqueue(id, event);
self.flush();
}
}
pub(crate) fn absolute_delta(&mut self, point: (f64, f64)) -> (f64, f64) {
let previous = self.last_absolute.replace(point).unwrap_or(point);
(point.0 - previous.0, point.1 - previous.1)
}
pub(crate) fn close_all(&mut self) {
if self.active.take().is_some() {
self.restore_focus = true;
}
self.last_absolute = None;
self.sessions.clear();
self.flush();
}
pub(crate) fn release(&mut self, id: u64, activation: Option<u32>) -> Result<(), String> {
let session = self.session(id)?;
if activation.is_some() && session.activation != activation {
return Err("Stale input capture activation".into());
}
if self.active == Some(id) {
self.active = None;
self.last_absolute = None;
self.restore_focus = true;
let session = self.sessions.get_mut(&id).unwrap();
let activation = session.activation.take();
self.enqueue(id, json!({"type":"deactivated", "activation_id":activation}));
self.flush();
}
Ok(())
}
pub(crate) fn disable(&mut self, id: u64) -> Result<(), String> {
self.release(id, None)?;
let session = self.sessions.get_mut(&id).unwrap();
if session.enabled {
session.enabled = false;
self.enqueue(id, json!({"type":"disabled"}));
}
self.flush();
Ok(())
}
pub(crate) fn disable_all(&mut self) {
let ids = self.sessions.keys().copied().collect::<Vec<_>>();
for id in ids {
let _ = self.disable(id);
}
}
pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool {
point.0.is_finite() && point.1.is_finite() && self.zones.iter().any(|zone| zone.contains(point))
}
pub(crate) fn watch(&mut self, owner: u64, request: u64, id: u64, response: SyncSender<Response>) {
if !self.sessions.contains_key(&id)
|| self.waiters.len() >= MAX_SESSIONS
|| self.waiters.values().any(|(_, token, _)| *token == id)
{
let _ = response.try_send(Response::error(
request,
"invalid_capture",
"Unknown or already watched input capture session",
));
return;
}
self.waiters.insert(owner, (request, id, response));
self.flush();
}
fn flush(&mut self) {
let ready = self
.waiters
.iter()
.filter_map(|(owner, (_, id, _))| {
self.sessions
.get(id)
.is_none_or(|session| !session.events.is_empty())
.then_some(*owner)
})
.collect::<Vec<_>>();
for owner in ready {
let (request, id, response) = self.waiters.remove(&owner).unwrap();
let result = if let Some(session) = self.sessions.get_mut(&id) {
json!({"events":session.events.drain(..).collect::<Vec<_>>()})
} else {
json!({"events":[{"type":"closed"}]})
};
let _ = response.try_send(Response::success(request, result));
}
}
pub(crate) fn remove_owner(&mut self, owner: u64) {
self.waiters.remove(&owner);
let ids = self
.sessions
.iter()
.filter_map(|(id, session)| (session.owner == owner).then_some(*id))
.collect::<Vec<_>>();
for id in ids {
let _ = self.release(id, None);
self.sessions.remove(&id);
}
self.flush();
}
}
impl crate::Ferese {
pub(crate) fn refresh_input_capture_zones(&mut self) {
if !self.input_capture.has_sessions() {
return;
}
let active = self.input_capture.active();
let zones = self
.space
.outputs()
.filter_map(|output| {
let rect = self.space.output_geometry(output)?;
Some(Zone {
width: u32::try_from(rect.size.w).ok()?,
height: u32::try_from(rect.size.h).ok()?,
x: rect.loc.x,
y: rect.loc.y,
})
})
.collect();
self.input_capture.update_zones(zones);
if active && !self.input_capture.active() {
self.restore_input_capture_focus();
}
}
pub(crate) fn restore_input_capture_focus(&mut self) {
self.input_capture.restore_focus = false;
self.cursor_redraw_pending = true;
if !self.session_lock.active {
self.restore_keyboard_focus();
if let Some(pointer) = self.seat.get_pointer() {
let location = pointer.current_location();
pointer.motion(
self,
self.surface_under(location),
&smithay::input::pointer::MotionEvent {
location,
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
time: 0,
},
);
pointer.frame(self);
}
}
}
pub(crate) fn capture_pointer_motion(
&mut self,
from: smithay::utils::Point<f64, smithay::utils::Logical>,
to: smithay::utils::Point<f64, smithay::utils::Logical>,
absolute: bool,
) -> bool {
let pointer = self.seat.get_pointer().expect("seat has pointer");
let active = self.input_capture.active();
let allowed = !self.session_lock.active
&& !self.overview.is_presenting()
&& !pointer.is_grabbed()
&& !self.swipe.active()
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed())
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed())
&& !self.seat.keyboard_shortcuts_inhibited();
if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) {
return false;
}
let delta = if absolute {
self.input_capture.absolute_delta((to.x, to.y))
} else {
(to.x - from.x, to.y - from.y)
};
if !active {
self.cursor_redraw_pending = true;
pointer.motion(
self,
None,
&smithay::input::pointer::MotionEvent {
location: from,
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
time: 0,
},
);
pointer.frame(self);
if self.input_capture.captures(1) {
let keyboard = self.seat.get_keyboard().expect("seat has keyboard");
let keys = keyboard
.pressed_keys()
.iter()
.filter(|key| !self.intercepted_keys.contains(key))
.map(|key| key.raw().saturating_sub(8))
.collect::<Vec<_>>();
keyboard.set_focus(self, None, smithay::utils::SERIAL_COUNTER.next_serial());
self.input_capture.push(json!({"type":"keys", "keys":keys}));
}
} else if self.input_capture.captures(2) {
self.input_capture
.push(json!({"type":"motion", "x":delta.0, "y":delta.1}));
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn zones() -> Vec<Zone> {
vec![
Zone {
width: 1920,
height: 1080,
x: 0,
y: 0,
},
Zone {
width: 1920,
height: 1080,
x: 1920,
y: 0,
},
]
}
#[test]
fn barriers_use_inclusive_pixel_span_and_only_outer_union_edges() {
let zones = zones();
for position in [
[0, 0, 1919, 0],
[0, 1080, 1919, 1080],
[1920, 0, 3839, 0],
[3840, 0, 3840, 1079],
[0, 0, 0, 1079],
] {
assert!(edge(&Barrier { id: 1, position }, &zones).is_some(), "{position:?}");
}
for position in [
[1920, 0, 1920, 1079],
[0, 0, 1920, 0],
[5, 10, 8, 13],
[50, 50, 50, 200],
[0, 5, 0, 4],
] {
assert!(edge(&Barrier { id: 1, position }, &zones).is_none(), "{position:?}");
}
let barrier = Barrier {
id: 1,
position: [3840, 0, 3840, 1079],
};
assert!(crossed(&barrier, Edge::Right, (3830.0, 1079.5), (3900.0, 1079.5)).is_some());
assert!(crossed(&barrier, Edge::Right, (3830.0, 1080.0), (3900.0, 1080.0)).is_none());
}
#[test]
fn absolute_samples_report_incremental_motion_and_reset_on_release() {
let mut capture = InputCapture::default();
assert_eq!(capture.absolute_delta((100.0, 50.0)), (0.0, 0.0));
assert_eq!(capture.absolute_delta((110.0, 55.0)), (10.0, 5.0));
assert_eq!(capture.absolute_delta((120.0, 60.0)), (10.0, 5.0));
capture.close_all();
assert_eq!(capture.absolute_delta((600.0, 700.0)), (0.0, 0.0));
}
#[test]
fn duplicate_ids_are_rejected_without_installing_a_barrier() {
let mut capture = InputCapture::default();
capture.update_zones(zones());
let id = capture.register(1, 3).unwrap();
let barrier = Barrier {
id: 9,
position: [3840, 0, 3840, 1079],
};
let failed = capture
.set_barriers(id, capture.zone_set, vec![barrier.clone(), barrier])
.unwrap();
assert_eq!(failed, vec![9, 9]);
assert!(capture.sessions[&id].barriers.is_empty());
}
#[test]
fn stalled_control_and_input_queues_close_and_restore_local_focus() {
let mut capture = InputCapture::default();
capture.update_zones(zones());
let id = capture.register(1, 3).unwrap();
capture
.set_barriers(
id,
capture.zone_set,
vec![Barrier {
id: 1,
position: [3840, 0, 3840, 1079],
}],
)
.unwrap();
capture.enable(id).unwrap();
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
for _ in 0..MAX_EVENTS + 10 {
capture.push(json!({"type":"motion", "x":1.0, "y":0.0}));
}
assert!(!capture.active());
assert!(capture.restore_focus);
assert!(capture.enable(id).is_err());
assert_eq!(capture.sessions[&id].events.len(), 1);
assert_eq!(capture.sessions[&id].events[0]["type"], "closed");
let id = capture.register(2, 2).unwrap();
for _ in 0..MAX_EVENTS + 10 {
capture.enable(id).ok();
capture.disable(id).unwrap();
}
assert_eq!(capture.sessions[&id].events.len(), 1);
assert!(capture.sessions[&id].closed);
}
#[test]
fn release_preserves_enable_but_topology_changes_disable_and_invalidate() {
let mut capture = InputCapture::default();
capture.update_zones(zones());
let id = capture.register(9, 3).unwrap();
let barrier = Barrier {
id: 1,
position: [3840, 0, 3840, 1079],
};
assert!(
capture
.set_barriers(id, capture.zone_set, vec![barrier])
.unwrap()
.is_empty()
);
capture.enable(id).unwrap();
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
let activation = capture.sessions[&id].activation.unwrap();
assert!(capture.release(id, Some(activation.wrapping_add(1))).is_err());
capture.release(id, Some(activation)).unwrap();
assert!(capture.sessions[&id].enabled);
capture.update_zones(vec![zones()[0]]);
assert!(!capture.active());
assert!(!capture.sessions[&id].enabled);
assert!(capture.sessions[&id].barriers.is_empty());
}
}