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 { 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 { 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, events: VecDeque, } #[derive(Default)] pub(crate) struct InputCapture { sessions: HashMap, zones: Vec, zone_set: u32, next_session: u64, next_activation: u32, active: Option, last_absolute: Option<(f64, f64)>, pub(crate) restore_focus: bool, waiters: HashMap)>, } impl InputCapture { pub(crate) fn update_zones(&mut self, mut zones: Vec) { 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::>(); 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 { 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 { 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::>()})) } 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) -> Result, 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) -> 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::>(); 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) { 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::>(); 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::>()}) } 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::>(); 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, to: smithay::utils::Point, 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::>(); 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 { 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()); } }