From 521e3c267e77e6b9f97361be2129f5f26a022dd1 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 11:36:25 -0800 Subject: [PATCH 01/13] gestures wip --- api/protobuf/pinnacle/input/v1/input.proto | 43 +++++ api/rust/src/input.rs | 160 ++++++++++++++++- api/rust/src/snowcap.rs | 50 ++++++ src/api/input/v1.rs | 123 +++++++++++++ src/input.rs | 75 ++++++++ src/input/bind.rs | 200 +++++++++++++++++++++ 6 files changed, 650 insertions(+), 1 deletion(-) diff --git a/api/protobuf/pinnacle/input/v1/input.proto b/api/protobuf/pinnacle/input/v1/input.proto index 98249b244..4105871e5 100644 --- a/api/protobuf/pinnacle/input/v1/input.proto +++ b/api/protobuf/pinnacle/input/v1/input.proto @@ -33,6 +33,7 @@ message Bind { oneof bind { Keybind key = 6; Mousebind mouse = 7; + Gesturebind gesture = 8; } } @@ -93,6 +94,44 @@ message MousebindOnPressRequest { uint32 bind_id = 1; } +// Gesturebinds + +enum GestureDirection { + DOWN = 0; + LEFT = 1; + RIGHT = 2; + UP = 3; +} + +enum GestureFingers { + THREE = 0; + FOUR = 1; +} + +message Gesturebind { + GestureDirection direction = 1; + GestureFingers fingers = 2; +} + +message GesturebindStreamRequest { + uint32 bind_id = 1; +} +message GesturebindStreamResponse { + Edge edge = 1; +} + +message GesturebindRequest { + uint32 bind_id = 1; +} + +message GesturebindOnBeginRequest { + uint32 bind_id = 1; +} + +message GesturebindOnFinishRequest { + uint32 bind_id = 1; +} + /////// message GetBindInfosRequest {} @@ -313,9 +352,13 @@ service InputService { rpc KeybindStream(KeybindStreamRequest) returns (stream KeybindStreamResponse); rpc MousebindStream(MousebindStreamRequest) returns (stream MousebindStreamResponse); + rpc GesturebindStream(GesturebindStreamRequest) returns (stream GesturebindStreamResponse); rpc KeybindOnPress(KeybindOnPressRequest) returns (google.protobuf.Empty); rpc MousebindOnPress(MousebindOnPressRequest) returns (google.protobuf.Empty); + rpc Gesturebind(GesturebindRequest) returns (google.protobuf.Empty); + rpc GesturebindOnBegin(GesturebindOnBeginRequest) returns (google.protobuf.Empty); + rpc GesturebindOnFinish(GesturebindOnFinishRequest) returns (google.protobuf.Empty); // Xkb diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index cd54a21d9..ae247a092 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -10,7 +10,8 @@ use num_enum::{FromPrimitive, IntoPrimitive}; use pinnacle_api_defs::pinnacle::input::{ self, v1::{ - BindProperties, BindRequest, EnterBindLayerRequest, GetBindInfosRequest, + BindProperties, BindRequest, EnterBindLayerRequest, GesturebindOnBeginRequest, + GesturebindOnFinishRequest, GesturebindStreamRequest, GetBindInfosRequest, KeybindOnPressRequest, KeybindStreamRequest, MousebindOnPressRequest, MousebindStreamRequest, SetBindPropertiesRequest, SetRepeatRateRequest, SetXcursorRequest, SetXkbConfigRequest, SetXkbKeymapRequest, SwitchXkbLayoutRequest, @@ -30,6 +31,8 @@ pub mod libinput; pub use xkbcommon::xkb::Keysym; +pub use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureFingers}; + /// A mouse button. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)] #[repr(u32)] @@ -176,6 +179,16 @@ impl BindLayer { new_mousebind(mods, button, self).block_on_tokio() } + /// Creates a gesturebind on this layer. + pub fn gesturebind( + &self, + mods: Mod, + direction: GestureDirection, + fingers: GestureFingers, + ) -> Gesturebind { + new_gesturebind(mods, direction, fingers, self).block_on_tokio() + } + /// Enters this layer, causing only its binds to be in effect. pub fn enter(&self) { Client::input() @@ -524,6 +537,138 @@ async fn new_mousebind_stream( send } +// Gesturebinds + +type GesturebindCallback = (Box, Edge); + +/// A Gesturebind. +pub struct Gesturebind { + bind_id: u32, + callback_sender: Option>, +} + +bind_impl!(Gesturebind); + +/// Creates a gesturebind on the [`DEFAULT`][BindLayer::DEFAULT] bind layer. +pub fn gesturebind(mods: Mod, direction: GestureDirection, fingers: GestureFingers) -> Gesturebind { + BindLayer::DEFAULT.gesturebind(mods, direction, fingers) +} + +impl Gesturebind { + /// Runs a closure whenever this mousebind is pressed. + pub fn on_begin(&mut self, on_begin: F) -> &mut Self { + let sender = self + .callback_sender + .get_or_insert_with(|| new_gesturebind_stream(self.bind_id).block_on_tokio()); + let _ = sender.send((Box::new(on_begin), Edge::Press)); + + Client::input() + .gesturebind_on_begin(GesturebindOnBeginRequest { + bind_id: self.bind_id, + }) + .block_on_tokio() + .unwrap(); + + self + } + + /// Runs a closure whenever this mousebind is released. + pub fn on_finish(&mut self, on_finish: F) -> &mut Self { + let sender = self + .callback_sender + .get_or_insert_with(|| new_mousebind_stream(self.bind_id).block_on_tokio()); + let _ = sender.send((Box::new(on_finish), Edge::Release)); + + Client::input() + .gesturebind_on_finish(GesturebindOnFinishRequest { + bind_id: self.bind_id, + }) + .block_on_tokio() + .unwrap(); + + self + } +} + +async fn new_gesturebind( + mods: Mod, + direction: GestureDirection, + fingers: GestureFingers, + layer: &BindLayer, +) -> Gesturebind { + let ignore_mods = mods.api_ignore_mods(); + let mods = mods.api_mods(); + + let bind_id = Client::input() + .bind(BindRequest { + bind: Some(input::v1::Bind { + mods: mods.into_iter().map(|m| m.into()).collect(), + ignore_mods: ignore_mods.into_iter().map(|m| m.into()).collect(), + layer_name: layer.name.clone(), + properties: Some(BindProperties::default()), + bind: Some(input::v1::bind::Bind::Gesture(input::v1::Gesturebind { + direction: direction.into(), + fingers: fingers.into(), + })), + }), + }) + .await + .unwrap() + .into_inner() + .bind_id; + + Gesturebind { + bind_id, + callback_sender: None, + } +} + +async fn new_gesturebind_stream( + bind_id: u32, +) -> UnboundedSender<(Box, Edge)> { + let mut from_server = Client::input() + .gesturebind_stream(GesturebindStreamRequest { bind_id }) + .await + .unwrap() + .into_inner(); + + let (send, mut recv) = unbounded_channel(); + + tokio::spawn(async move { + let mut on_presses = Vec::>::new(); + let mut on_releases = Vec::>::new(); + + loop { + tokio::select! { + Some(Ok(response)) = from_server.next() => { + match response.edge() { + input::v1::Edge::Unspecified => (), + input::v1::Edge::Press => { + for on_press in on_presses.iter_mut() { + on_press(); + } + } + input::v1::Edge::Release => { + for on_release in on_releases.iter_mut() { + on_release(); + } + } + } + } + Some((cb, edge)) = recv.recv() => { + match edge { + Edge::Press => on_presses.push(cb), + Edge::Release => on_releases.push(cb), + } + } + else => break, + } + } + }); + + send +} + /// A struct that lets you define xkeyboard config options. /// /// See `xkeyboard-config(7)` for more information. @@ -699,6 +844,13 @@ pub enum BindInfoKind { /// Which mouse button this bind uses. button: MouseButton, }, + /// This is a gesturebind. + Gesture { + /// Direction of the gesture. + direction: GestureDirection, + /// Fingers used in the gesture. + fingers: GestureFingers, + }, } /// Sets the keyboard's repeat rate. @@ -845,6 +997,12 @@ pub fn bind_infos() -> impl Iterator { input::v1::bind::Bind::Mouse(mousebind) => BindInfoKind::Mouse { button: MouseButton::from(mousebind.button), }, + input::v1::bind::Bind::Gesture(gesturebind) => BindInfoKind::Gesture { + direction: GestureDirection::try_from(gesturebind.direction) + .expect("invalid gesture direction value"), + fingers: GestureFingers::try_from(gesturebind.fingers) + .expect("invalid gesture finger value"), + }, }; let layer = BindLayer { diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index 99064287c..60dbdbb40 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -7,6 +7,7 @@ use std::sync::{Arc, OnceLock}; use indexmap::IndexMap; +use pinnacle_api_defs::pinnacle::input::v1::GestureDirection; use snowcap_api::{ decoration::{DecorationHandle, NewDecorationError}, layer::{ExclusiveZone, KeyboardInteractivity, ZLayer}, @@ -202,12 +203,42 @@ impl Program for BindOverlay { } } + #[derive(PartialEq, Eq, Hash)] + struct GesturebindRepr { + mods: Mod, + direction: String, + fingers: String, + layer: Option, + } + + impl std::fmt::Display for GesturebindRepr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mods = format_mods(self.mods); + + let layer = self + .layer + .as_ref() + .map(|layer| format!("[{layer}] ")) + .unwrap_or_default(); + + let bind = mods + .as_deref() + .into_iter() + .chain([self.direction.as_str(), self.fingers.as_str()]) + .collect::>() + .join(" + "); + write!(f, "{layer}{bind}") + } + } + #[derive(Default)] struct GroupBinds { /// keybinds to descriptions keybinds: IndexMap>, /// mousebinds to descriptions mousebinds: IndexMap>, + /// gesturebinds to descriptions + gesturebinds: IndexMap>, } let bind_infos = crate::input::bind_infos(); @@ -258,6 +289,25 @@ impl Program for BindOverlay { descs.push(desc); } } + BindInfoKind::Gesture { direction, fingers } => { + let repr = GesturebindRepr { + mods, + direction: match direction { + GestureDirection::Down => "Down", + GestureDirection::Left => "Left", + GestureDirection::Right => "Right", + GestureDirection::Up => "Up", + } + .to_string(), + fingers: format!("{fingers:?} fingers"), + layer, + }; + + let descs = group.gesturebinds.entry(repr).or_default(); + if !desc.is_empty() { + descs.push(desc); + } + } } } diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index 241debc5b..fc4f4e6ee 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -2,6 +2,8 @@ use pinnacle_api_defs::pinnacle::input::{ self, v1::{ AccelProfile, BindInfo, BindRequest, BindResponse, ClickMethod, EnterBindLayerRequest, + GestureDirection, GestureFingers, GesturebindOnBeginRequest, GesturebindOnFinishRequest, + GesturebindRequest, GesturebindStreamRequest, GesturebindStreamResponse, GetBindInfosRequest, GetBindInfosResponse, GetBindLayerStackRequest, GetBindLayerStackResponse, GetDeviceCapabilitiesRequest, GetDeviceCapabilitiesResponse, GetDeviceInfoRequest, GetDeviceInfoResponse, GetDeviceTypeRequest, GetDeviceTypeResponse, @@ -37,6 +39,7 @@ use super::InputService; impl input::v1::input_service_server::InputService for InputService { type KeybindStreamStream = ResponseStream; type MousebindStreamStream = ResponseStream; + type GesturebindStreamStream = ResponseStream; async fn bind(&self, request: Request) -> TonicResult { let request = request.into_inner(); @@ -160,6 +163,30 @@ impl input::v1::input_service_server::InputService for InputService { bind_id } + input::v1::bind::Bind::Gesture(gesturebind) => { + let direction = GestureDirection::try_from(gesturebind.direction) + .expect("invalid gesture direction value"); + let fingers = GestureFingers::try_from(gesturebind.fingers) + .expect("invalid gesture fingers value"); + let bind_id = state + .pinnacle + .input_state + .bind_state + .gesturebinds + .add_gesturebind( + direction, + fingers, + mods, + layer, + group, + desc, + quit, + reload_config, + allow_when_locked, + ); + + bind_id + } }; Ok(BindResponse { bind_id }) @@ -235,6 +262,7 @@ impl input::v1::input_service_server::InputService for InputService { match input::v1::bind::Bind::Key(input::v1::Keybind::default()) { input::v1::bind::Bind::Key(_) => (), input::v1::bind::Bind::Mouse(_) => (), + input::v1::bind::Bind::Gesture(_) => (), } let push_mods = |mods: &mut Vec, @@ -517,6 +545,53 @@ impl input::v1::input_service_server::InputService for InputService { .await } + async fn gesturebind_stream( + &self, + request: Request, + ) -> TonicResult { + let request = request.into_inner(); + + let bind_id = request.bind_id; + + run_server_streaming(&self.sender, move |state, sender| { + let Some(bind) = state + .pinnacle + .input_state + .bind_state + .gesturebinds + .id_map + .get(&bind_id) + else { + return Err(Status::not_found(format!("bind {bind_id} was not found"))); + }; + + let Some(mut recv) = bind.borrow_mut().recv.take() else { + return Err(Status::already_exists(format!( + "bind {bind_id} already has a stream set up" + ))); + }; + + tokio::spawn(async move { + while let Some(edge) = recv.recv().await { + let msg = Ok(GesturebindStreamResponse { + edge: match edge { + Edge::Press => input::v1::Edge::Press, + Edge::Release => input::v1::Edge::Release, + } + .into(), + }); + if sender.send(msg).is_err() { + break; + } + tokio::task::yield_now().await; + } + }); + + Ok(()) + }) + .await + } + async fn keybind_on_press(&self, request: Request) -> TonicResult<()> { let bind_id = request.into_inner().bind_id; @@ -548,6 +623,54 @@ impl input::v1::input_service_server::InputService for InputService { .await } + async fn gesturebind_on_begin( + &self, + request: Request, + ) -> TonicResult<()> { + let bind_id = request.into_inner().bind_id; + + run_unary_no_response(&self.sender, move |state| { + state + .pinnacle + .input_state + .bind_state + .gesturebinds + .set_gesturebind_has_on_begin(bind_id); + }) + .await + } + + async fn gesturebind_on_finish( + &self, + request: Request, + ) -> TonicResult<()> { + let bind_id = request.into_inner().bind_id; + + run_unary_no_response(&self.sender, move |state| { + state + .pinnacle + .input_state + .bind_state + .gesturebinds + .set_gesturebind_has_on_begin(bind_id); + }) + .await + } + + async fn gesturebind(&self, request: Request) -> TonicResult<()> { + let bind_id = request.into_inner().bind_id; + + run_unary_no_response(&self.sender, move |state| { + state + .pinnacle + .input_state + .bind_state + .gesturebinds + .set_gesturebind_has_on_begin(bind_id); + }) + .await + } + async fn set_xkb_config(&self, request: Request) -> TonicResult<()> { let request = request.into_inner(); diff --git a/src/input.rs b/src/input.rs index eaf702016..3c606b433 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,11 +8,13 @@ use std::{any::Any, time::Duration}; use crate::{ api::signal::Signal as _, focus::pointer::{PointerContents, PointerFocusTarget}, + input::bind::Edge, state::{Pinnacle, WithState}, window::WindowElement, }; use bind::BindState; use libinput::LibinputState; +use pinnacle_api::input::{GestureDirection, GestureFingers}; use smithay::{ backend::{ input::{ @@ -50,10 +52,17 @@ use tracing::{error, info}; use crate::state::State; +#[derive(Default, Debug)] +pub struct GestureState { + pub delta: Option<(f64, f64)>, + pub fingers: u32, +} + #[derive(Default, Debug)] pub struct InputState { pub bind_state: BindState, pub libinput_state: LibinputState, + pub gesture_state: GestureState, } impl InputState { @@ -909,6 +918,11 @@ impl State { return; }; + self.pinnacle.input_state.gesture_state = GestureState { + delta: Some((0., 0.)), + fingers: event.fingers(), + }; + pointer.gesture_swipe_begin( self, &GestureSwipeBeginEvent { @@ -926,6 +940,10 @@ impl State { use smithay::backend::input::GestureSwipeUpdateEvent as _; + let delta = event.delta(); + + self.pinnacle.input_state.gesture_state.delta = Some((delta.x, delta.y)); + pointer.gesture_swipe_update( self, &GestureSwipeUpdateEvent { @@ -939,6 +957,63 @@ impl State { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; + let Some(keyboard) = self.pinnacle.seat.get_keyboard() else { + return; + }; + + let mods = keyboard.modifier_state(); + + let current_layer = self.pinnacle.input_state.bind_state.current_layer(); + + if let Some((x, y)) = self.pinnacle.input_state.gesture_state.delta { + let direction = if x.abs() > y.abs() { + if x < 0.0 { + Some(GestureDirection::Left) + } else if x > 0.0 { + Some(GestureDirection::Right) + } else { + None + } + } else if x.abs() < y.abs() { + if y < 0.0 { + Some(GestureDirection::Up) + } else if y > 0.0 { + Some(GestureDirection::Down) + } else { + None + } + } else { + None + }; + + info!( + "[pre] on_gesture_swipe_end(): {direction:?}:{}", + self.pinnacle.input_state.gesture_state.fingers + ); + + let fingers = match self.pinnacle.input_state.gesture_state.fingers { + 3 => Some(GestureFingers::Three), + 4 => Some(GestureFingers::Four), + _ => None, + }; + + if let Some(fingers) = fingers + && let Some(direction) = direction + { + let bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + direction, + fingers, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); + + info!("on_gesture_swipe_end(): {direction:?}:{fingers:?} = {bind_action:?}"); + } + } + + self.pinnacle.input_state.gesture_state.delta = None; pointer.gesture_swipe_end( self, diff --git a/src/input/bind.rs b/src/input/bind.rs index 1b038b582..cb3a93d91 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -6,6 +6,7 @@ use std::{ }; use indexmap::{IndexMap, map::Entry}; +use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureFingers}; use smithay::input::keyboard::ModifiersState; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use xkbcommon::xkb::Keysym; @@ -17,6 +18,7 @@ pub struct BindState { pub layer_stack: Vec, pub keybinds: Keybinds, pub mousebinds: Mousebinds, + pub gesturebinds: Gesturebinds, } impl BindState { @@ -525,3 +527,201 @@ impl Mousebinds { mousebind.borrow_mut().has_on_press = true; } } + +// Gesturebinds + +#[derive(Debug)] +pub struct Gesturebind { + pub bind_data: BindData, + pub direction: GestureDirection, + pub fingers: GestureFingers, + sender: UnboundedSender, + pub recv: Option>, + pub has_on_begin: bool, +} + +#[derive(Debug, Default)] +pub struct Gesturebinds { + pub id_map: IndexMap>>, + gesture_map: IndexMap<(GestureDirection, GestureFingers), Vec>>>, + + pub last_pressed_triggered_binds: HashMap<(GestureDirection, GestureFingers), Vec>, +} + +// TODO: may be able to dedup with Keybinds above +impl Gesturebinds { + /// Notifies configs that a gesture was executed. + /// + /// Returns whether the gesture should be suppressed (not sent to the client). + pub fn gesture( + &mut self, + direction: GestureDirection, + fingers: GestureFingers, + mods: ModifiersState, + edge: Edge, + current_layer: Option, + is_locked: bool, + ) -> BindAction { + tracing::info!( + "checking gesture for {direction:?}:{fingers:?} with {mods:?} and edge={edge:?}" + ); + + let Some(gesturebinds) = self.gesture_map.get_mut(&(direction, fingers)) else { + tracing::info!("no gesture found in map"); + return BindAction::Forward; + }; + + if edge == Edge::Release { + let last_triggered_binds_on_press = self + .last_pressed_triggered_binds + .remove(&(direction, fingers)); + let bind_action = if let Some(bind_ids) = last_triggered_binds_on_press { + let mut bind_action = BindAction::Forward; + for bind_id in bind_ids { + let gesturebind = self.id_map.entry(bind_id); + let Entry::Occupied(gb_entry) = gesturebind else { + continue; + }; + if gb_entry.get().borrow().bind_data.is_quit_bind { + return BindAction::Quit; + } + if gb_entry.get().borrow().bind_data.is_reload_config_bind { + return BindAction::ReloadConfig; + } + if is_locked && !gb_entry.get().borrow().bind_data.allow_when_locked { + return BindAction::Forward; + } + if gb_entry.get().borrow().has_on_begin { + bind_action = BindAction::Suppress; + } + let sent = gb_entry.get().borrow().sender.send(Edge::Release).is_ok(); + if !sent { + gb_entry.shift_remove(); + } + } + bind_action + } else { + tracing::info!("not the last triggered bind on press"); + BindAction::Forward + }; + return bind_action; + } + + let mut bind_action = BindAction::Forward; + + let mut should_clear_releases = false; + + gesturebinds.retain(|mousebind| { + let Some(mousebind) = mousebind.upgrade() else { + return false; + }; + + let mousebind = mousebind.borrow(); + + let same_layer = current_layer == mousebind.bind_data.layer; + + if let BindAction::Quit | BindAction::ReloadConfig = bind_action { + return true; + } + + if mousebind.bind_data.mods.matches(mods) { + if mousebind.has_on_begin { + should_clear_releases = true; + } + + match edge { + Edge::Press => { + self.last_pressed_triggered_binds + .entry((direction, fingers)) + .or_default() + .push(mousebind.bind_data.id); + } + Edge::Release => unreachable!(), + } + + let mut retain = true; + + if mousebind.bind_data.is_quit_bind { + bind_action = BindAction::Quit; + } else if mousebind.bind_data.is_reload_config_bind { + bind_action = BindAction::ReloadConfig; + } else if mousebind.has_on_begin + && same_layer + && (!is_locked || mousebind.bind_data.allow_when_locked) + { + retain = mousebind.sender.send(edge).is_ok(); + bind_action = BindAction::Suppress; + }; + + retain + } else { + true + } + }); + + if should_clear_releases { + self.last_pressed_triggered_binds + .retain(|gesture, _| *gesture == (direction, fingers)); + } + + bind_action + } + + pub fn add_gesturebind( + &mut self, + direction: GestureDirection, + fingers: GestureFingers, + mods: ModMask, + layer: Option, + group: String, + desc: String, + is_quit_bind: bool, + is_reload_config_bind: bool, + allow_when_locked: bool, + ) -> u32 { + let id = BIND_ID_COUNTER.fetch_add(1, Ordering::Relaxed); + + let (sender, recv) = tokio::sync::mpsc::unbounded_channel::(); + + let gesturebind = Rc::new(RefCell::new(Gesturebind { + bind_data: BindData { + id, + mods, + layer, + group, + desc, + is_quit_bind, + is_reload_config_bind, + allow_when_locked, + }, + direction, + fingers, + sender, + recv: Some(recv), + has_on_begin: false, + })); + + assert!( + self.id_map.insert(id, gesturebind.clone()).is_none(), + "new keybind should have unique id" + ); + + self.gesture_map + .entry((direction, fingers)) + .or_default() + .push(Rc::downgrade(&gesturebind)); + + id + } + + pub fn remove_gesturebind(&mut self, gesturebind_id: u32) { + self.id_map.shift_remove(&gesturebind_id); + } + + pub fn set_gesturebind_has_on_begin(&self, gesturebind_id: u32) { + let Some(gesturebind) = self.id_map.get(&gesturebind_id) else { + return; + }; + gesturebind.borrow_mut().has_on_begin = true; + } +} From 6421ae9b2c125c53549c4a58d8e0af176d3b444e Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 15:03:16 -0800 Subject: [PATCH 02/13] working gestures and api config --- api/rust/src/input.rs | 2 +- src/api/input/v1.rs | 2 +- src/input.rs | 9 +--- src/input/bind.rs | 117 ++++++++++-------------------------------- 4 files changed, 31 insertions(+), 99 deletions(-) diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index ae247a092..ce2cfec44 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -576,7 +576,7 @@ impl Gesturebind { pub fn on_finish(&mut self, on_finish: F) -> &mut Self { let sender = self .callback_sender - .get_or_insert_with(|| new_mousebind_stream(self.bind_id).block_on_tokio()); + .get_or_insert_with(|| new_gesturebind_stream(self.bind_id).block_on_tokio()); let _ = sender.send((Box::new(on_finish), Edge::Release)); Client::input() diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index fc4f4e6ee..a37355aa0 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -652,7 +652,7 @@ impl input::v1::input_service_server::InputService for InputService { .input_state .bind_state .gesturebinds - .set_gesturebind_has_on_begin(bind_id); + .set_gesturebind_has_on_finish(bind_id); }) .await } diff --git a/src/input.rs b/src/input.rs index 3c606b433..70888ff82 100644 --- a/src/input.rs +++ b/src/input.rs @@ -986,11 +986,6 @@ impl State { None }; - info!( - "[pre] on_gesture_swipe_end(): {direction:?}:{}", - self.pinnacle.input_state.gesture_state.fingers - ); - let fingers = match self.pinnacle.input_state.gesture_state.fingers { 3 => Some(GestureFingers::Three), 4 => Some(GestureFingers::Four), @@ -1000,7 +995,7 @@ impl State { if let Some(fingers) = fingers && let Some(direction) = direction { - let bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( direction, fingers, mods, @@ -1008,8 +1003,6 @@ impl State { current_layer, !self.pinnacle.lock_state.is_unlocked(), ); - - info!("on_gesture_swipe_end(): {direction:?}:{fingers:?} = {bind_action:?}"); } } diff --git a/src/input/bind.rs b/src/input/bind.rs index cb3a93d91..7fe4ad877 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -559,112 +559,44 @@ impl Gesturebinds { fingers: GestureFingers, mods: ModifiersState, edge: Edge, - current_layer: Option, + _current_layer: Option, is_locked: bool, ) -> BindAction { - tracing::info!( - "checking gesture for {direction:?}:{fingers:?} with {mods:?} and edge={edge:?}" - ); - let Some(gesturebinds) = self.gesture_map.get_mut(&(direction, fingers)) else { - tracing::info!("no gesture found in map"); return BindAction::Forward; }; if edge == Edge::Release { - let last_triggered_binds_on_press = self - .last_pressed_triggered_binds - .remove(&(direction, fingers)); - let bind_action = if let Some(bind_ids) = last_triggered_binds_on_press { - let mut bind_action = BindAction::Forward; - for bind_id in bind_ids { - let gesturebind = self.id_map.entry(bind_id); - let Entry::Occupied(gb_entry) = gesturebind else { - continue; - }; - if gb_entry.get().borrow().bind_data.is_quit_bind { - return BindAction::Quit; - } - if gb_entry.get().borrow().bind_data.is_reload_config_bind { - return BindAction::ReloadConfig; - } - if is_locked && !gb_entry.get().borrow().bind_data.allow_when_locked { - return BindAction::Forward; - } - if gb_entry.get().borrow().has_on_begin { - bind_action = BindAction::Suppress; - } - let sent = gb_entry.get().borrow().sender.send(Edge::Release).is_ok(); - if !sent { - gb_entry.shift_remove(); - } - } - bind_action - } else { - tracing::info!("not the last triggered bind on press"); - BindAction::Forward - }; - return bind_action; - } - - let mut bind_action = BindAction::Forward; - - let mut should_clear_releases = false; - - gesturebinds.retain(|mousebind| { - let Some(mousebind) = mousebind.upgrade() else { - return false; - }; - - let mousebind = mousebind.borrow(); + let mut bind_action = BindAction::Forward; - let same_layer = current_layer == mousebind.bind_data.layer; - - if let BindAction::Quit | BindAction::ReloadConfig = bind_action { - return true; - } + for gesturebind in gesturebinds { + let Some(gesturebind) = gesturebind.upgrade() else { + continue; + }; - if mousebind.bind_data.mods.matches(mods) { - if mousebind.has_on_begin { - should_clear_releases = true; + if !gesturebind.borrow().bind_data.mods.matches(mods) { + return BindAction::Forward; } - match edge { - Edge::Press => { - self.last_pressed_triggered_binds - .entry((direction, fingers)) - .or_default() - .push(mousebind.bind_data.id); - } - Edge::Release => unreachable!(), + if gesturebind.borrow().bind_data.is_quit_bind { + return BindAction::Quit; } - - let mut retain = true; - - if mousebind.bind_data.is_quit_bind { - bind_action = BindAction::Quit; - } else if mousebind.bind_data.is_reload_config_bind { - bind_action = BindAction::ReloadConfig; - } else if mousebind.has_on_begin - && same_layer - && (!is_locked || mousebind.bind_data.allow_when_locked) - { - retain = mousebind.sender.send(edge).is_ok(); + if gesturebind.borrow().bind_data.is_reload_config_bind { + return BindAction::ReloadConfig; + } + if is_locked && !gesturebind.borrow().bind_data.allow_when_locked { + return BindAction::Forward; + } + if gesturebind.borrow().has_on_begin { bind_action = BindAction::Suppress; - }; - - retain - } else { - true + } + let _sent = gesturebind.borrow().sender.send(Edge::Release).is_ok(); } - }); - if should_clear_releases { - self.last_pressed_triggered_binds - .retain(|gesture, _| *gesture == (direction, fingers)); + return bind_action; } - bind_action + BindAction::Forward } pub fn add_gesturebind( @@ -724,4 +656,11 @@ impl Gesturebinds { }; gesturebind.borrow_mut().has_on_begin = true; } + + pub fn set_gesturebind_has_on_finish(&self, gesturebind_id: u32) { + let Some(gesturebind) = self.id_map.get(&gesturebind_id) else { + return; + }; + gesturebind.borrow_mut().has_on_begin = false; + } } From 2f63fd433b0647a2e2749dd86853052808fa8761 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 15:48:17 -0800 Subject: [PATCH 03/13] gestures: fix mod check --- src/input/bind.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/bind.rs b/src/input/bind.rs index 7fe4ad877..0765ec646 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -575,7 +575,7 @@ impl Gesturebinds { }; if !gesturebind.borrow().bind_data.mods.matches(mods) { - return BindAction::Forward; + continue; } if gesturebind.borrow().bind_data.is_quit_bind { From 3492c6ce38aff99239f2ba7493a9a3f8b9ab46b1 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 17:55:12 -0800 Subject: [PATCH 04/13] slightly more scientific gesture direction --- api/protobuf/pinnacle/input/v1/input.proto | 11 ++++++ api/rust/src/input.rs | 16 ++++++--- api/rust/src/snowcap.rs | 4 +++ src/input.rs | 41 ++++++++++++---------- src/input/bind.rs | 6 +++- 5 files changed, 54 insertions(+), 24 deletions(-) diff --git a/api/protobuf/pinnacle/input/v1/input.proto b/api/protobuf/pinnacle/input/v1/input.proto index 4105871e5..dd0f7dce1 100644 --- a/api/protobuf/pinnacle/input/v1/input.proto +++ b/api/protobuf/pinnacle/input/v1/input.proto @@ -101,6 +101,10 @@ enum GestureDirection { LEFT = 1; RIGHT = 2; UP = 3; + DOWN_AND_LEFT = 4; + DOWN_AND_RIGHT = 5; + UP_AND_LEFT = 6; + UP_AND_RIGHT = 7; } enum GestureFingers { @@ -108,9 +112,16 @@ enum GestureFingers { FOUR = 1; } +enum GestureType { + HOLD = 0; + PINCH = 1; + SWIPE = 2; +} + message Gesturebind { GestureDirection direction = 1; GestureFingers fingers = 2; + GestureType gesture_type = 3; } message GesturebindStreamRequest { diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index ce2cfec44..0b384b76b 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -31,7 +31,7 @@ pub mod libinput; pub use xkbcommon::xkb::Keysym; -pub use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureFingers}; +pub use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureFingers, GestureType}; /// A mouse button. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)] @@ -185,8 +185,9 @@ impl BindLayer { mods: Mod, direction: GestureDirection, fingers: GestureFingers, + gesture_type: GestureType, ) -> Gesturebind { - new_gesturebind(mods, direction, fingers, self).block_on_tokio() + new_gesturebind(mods, direction, fingers, gesture_type, self).block_on_tokio() } /// Enters this layer, causing only its binds to be in effect. @@ -550,8 +551,13 @@ pub struct Gesturebind { bind_impl!(Gesturebind); /// Creates a gesturebind on the [`DEFAULT`][BindLayer::DEFAULT] bind layer. -pub fn gesturebind(mods: Mod, direction: GestureDirection, fingers: GestureFingers) -> Gesturebind { - BindLayer::DEFAULT.gesturebind(mods, direction, fingers) +pub fn gesturebind( + mods: Mod, + direction: GestureDirection, + fingers: GestureFingers, + gesture_type: GestureType, +) -> Gesturebind { + BindLayer::DEFAULT.gesturebind(mods, direction, fingers, gesture_type) } impl Gesturebind { @@ -594,6 +600,7 @@ async fn new_gesturebind( mods: Mod, direction: GestureDirection, fingers: GestureFingers, + gesture_type: GestureType, layer: &BindLayer, ) -> Gesturebind { let ignore_mods = mods.api_ignore_mods(); @@ -609,6 +616,7 @@ async fn new_gesturebind( bind: Some(input::v1::bind::Bind::Gesture(input::v1::Gesturebind { direction: direction.into(), fingers: fingers.into(), + gesture_type: gesture_type.into(), })), }), }) diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index 60dbdbb40..e73c14df3 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -297,6 +297,10 @@ impl Program for BindOverlay { GestureDirection::Left => "Left", GestureDirection::Right => "Right", GestureDirection::Up => "Up", + GestureDirection::DownAndLeft => "Down and Left", + GestureDirection::DownAndRight => "Down and Right", + GestureDirection::UpAndLeft => "Up and Left", + GestureDirection::UpAndRight => "Up and Right", } .to_string(), fingers: format!("{fingers:?} fingers"), diff --git a/src/input.rs b/src/input.rs index 70888ff82..0129a20a1 100644 --- a/src/input.rs +++ b/src/input.rs @@ -966,24 +966,29 @@ impl State { let current_layer = self.pinnacle.input_state.bind_state.current_layer(); if let Some((x, y)) = self.pinnacle.input_state.gesture_state.delta { - let direction = if x.abs() > y.abs() { - if x < 0.0 { - Some(GestureDirection::Left) - } else if x > 0.0 { - Some(GestureDirection::Right) + let direction = { + use std::f64::consts::PI; + + let angle = y.atan2(x); + let angle_deg = (angle * 180.0 / PI + 360.0) % 360.0; + + if !(22.5..337.5).contains(&angle_deg) { + GestureDirection::Right + } else if (22.5..67.5).contains(&angle_deg) { + GestureDirection::DownAndRight + } else if (67.5..112.5).contains(&angle_deg) { + GestureDirection::Down + } else if (112.5..157.5).contains(&angle_deg) { + GestureDirection::DownAndLeft + } else if (157.5..202.5).contains(&angle_deg) { + GestureDirection::Left + } else if (202.5..247.5).contains(&angle_deg) { + GestureDirection::UpAndLeft + } else if (247.5..292.5).contains(&angle_deg) { + GestureDirection::Up } else { - None + GestureDirection::UpAndRight } - } else if x.abs() < y.abs() { - if y < 0.0 { - Some(GestureDirection::Up) - } else if y > 0.0 { - Some(GestureDirection::Down) - } else { - None - } - } else { - None }; let fingers = match self.pinnacle.input_state.gesture_state.fingers { @@ -992,9 +997,7 @@ impl State { _ => None, }; - if let Some(fingers) = fingers - && let Some(direction) = direction - { + if let Some(fingers) = fingers { let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( direction, fingers, diff --git a/src/input/bind.rs b/src/input/bind.rs index 0765ec646..8488fbae9 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -559,7 +559,7 @@ impl Gesturebinds { fingers: GestureFingers, mods: ModifiersState, edge: Edge, - _current_layer: Option, + current_layer: Option, is_locked: bool, ) -> BindAction { let Some(gesturebinds) = self.gesture_map.get_mut(&(direction, fingers)) else { @@ -578,6 +578,10 @@ impl Gesturebinds { continue; } + if gesturebind.borrow().bind_data.layer != current_layer { + continue; + } + if gesturebind.borrow().bind_data.is_quit_bind { return BindAction::Quit; } From b4a5f5ed7f9f56bae4db31a7b2ab8114581a6aac Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 21:02:52 -0800 Subject: [PATCH 05/13] accept u32 as the gesture finger value --- api/protobuf/pinnacle/input/v1/input.proto | 7 +----- api/rust/src/input.rs | 15 ++++++------- src/api/input/v1.rs | 5 ++--- src/input.rs | 26 +++++++++------------- src/input/bind.rs | 12 +++++----- 5 files changed, 26 insertions(+), 39 deletions(-) diff --git a/api/protobuf/pinnacle/input/v1/input.proto b/api/protobuf/pinnacle/input/v1/input.proto index dd0f7dce1..42788673b 100644 --- a/api/protobuf/pinnacle/input/v1/input.proto +++ b/api/protobuf/pinnacle/input/v1/input.proto @@ -107,11 +107,6 @@ enum GestureDirection { UP_AND_RIGHT = 7; } -enum GestureFingers { - THREE = 0; - FOUR = 1; -} - enum GestureType { HOLD = 0; PINCH = 1; @@ -120,7 +115,7 @@ enum GestureType { message Gesturebind { GestureDirection direction = 1; - GestureFingers fingers = 2; + uint32 fingers = 2; GestureType gesture_type = 3; } diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index 0b384b76b..c33e99cf7 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -31,7 +31,7 @@ pub mod libinput; pub use xkbcommon::xkb::Keysym; -pub use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureFingers, GestureType}; +pub use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureType}; /// A mouse button. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)] @@ -184,7 +184,7 @@ impl BindLayer { &self, mods: Mod, direction: GestureDirection, - fingers: GestureFingers, + fingers: u32, gesture_type: GestureType, ) -> Gesturebind { new_gesturebind(mods, direction, fingers, gesture_type, self).block_on_tokio() @@ -554,7 +554,7 @@ bind_impl!(Gesturebind); pub fn gesturebind( mods: Mod, direction: GestureDirection, - fingers: GestureFingers, + fingers: u32, gesture_type: GestureType, ) -> Gesturebind { BindLayer::DEFAULT.gesturebind(mods, direction, fingers, gesture_type) @@ -599,7 +599,7 @@ impl Gesturebind { async fn new_gesturebind( mods: Mod, direction: GestureDirection, - fingers: GestureFingers, + fingers: u32, gesture_type: GestureType, layer: &BindLayer, ) -> Gesturebind { @@ -615,7 +615,7 @@ async fn new_gesturebind( properties: Some(BindProperties::default()), bind: Some(input::v1::bind::Bind::Gesture(input::v1::Gesturebind { direction: direction.into(), - fingers: fingers.into(), + fingers, gesture_type: gesture_type.into(), })), }), @@ -857,7 +857,7 @@ pub enum BindInfoKind { /// Direction of the gesture. direction: GestureDirection, /// Fingers used in the gesture. - fingers: GestureFingers, + fingers: u32, }, } @@ -1008,8 +1008,7 @@ pub fn bind_infos() -> impl Iterator { input::v1::bind::Bind::Gesture(gesturebind) => BindInfoKind::Gesture { direction: GestureDirection::try_from(gesturebind.direction) .expect("invalid gesture direction value"), - fingers: GestureFingers::try_from(gesturebind.fingers) - .expect("invalid gesture finger value"), + fingers: gesturebind.fingers }, }; diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index a37355aa0..b0f0e973d 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -2,7 +2,7 @@ use pinnacle_api_defs::pinnacle::input::{ self, v1::{ AccelProfile, BindInfo, BindRequest, BindResponse, ClickMethod, EnterBindLayerRequest, - GestureDirection, GestureFingers, GesturebindOnBeginRequest, GesturebindOnFinishRequest, + GestureDirection, GesturebindOnBeginRequest, GesturebindOnFinishRequest, GesturebindRequest, GesturebindStreamRequest, GesturebindStreamResponse, GetBindInfosRequest, GetBindInfosResponse, GetBindLayerStackRequest, GetBindLayerStackResponse, GetDeviceCapabilitiesRequest, GetDeviceCapabilitiesResponse, @@ -166,8 +166,7 @@ impl input::v1::input_service_server::InputService for InputService { input::v1::bind::Bind::Gesture(gesturebind) => { let direction = GestureDirection::try_from(gesturebind.direction) .expect("invalid gesture direction value"); - let fingers = GestureFingers::try_from(gesturebind.fingers) - .expect("invalid gesture fingers value"); + let fingers = gesturebind.fingers; let bind_id = state .pinnacle .input_state diff --git a/src/input.rs b/src/input.rs index 0129a20a1..f2a8a8355 100644 --- a/src/input.rs +++ b/src/input.rs @@ -14,7 +14,7 @@ use crate::{ }; use bind::BindState; use libinput::LibinputState; -use pinnacle_api::input::{GestureDirection, GestureFingers}; +use pinnacle_api::input::GestureDirection; use smithay::{ backend::{ input::{ @@ -991,22 +991,16 @@ impl State { } }; - let fingers = match self.pinnacle.input_state.gesture_state.fingers { - 3 => Some(GestureFingers::Three), - 4 => Some(GestureFingers::Four), - _ => None, - }; + let fingers = self.pinnacle.input_state.gesture_state.fingers; - if let Some(fingers) = fingers { - let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - direction, - fingers, - mods, - Edge::Release, - current_layer, - !self.pinnacle.lock_state.is_unlocked(), - ); - } + let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + direction, + fingers, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); } self.pinnacle.input_state.gesture_state.delta = None; diff --git a/src/input/bind.rs b/src/input/bind.rs index 8488fbae9..ad3254eb2 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -6,7 +6,7 @@ use std::{ }; use indexmap::{IndexMap, map::Entry}; -use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureFingers}; +use pinnacle_api_defs::pinnacle::input::v1::GestureDirection; use smithay::input::keyboard::ModifiersState; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use xkbcommon::xkb::Keysym; @@ -534,7 +534,7 @@ impl Mousebinds { pub struct Gesturebind { pub bind_data: BindData, pub direction: GestureDirection, - pub fingers: GestureFingers, + pub fingers: u32, sender: UnboundedSender, pub recv: Option>, pub has_on_begin: bool, @@ -543,9 +543,9 @@ pub struct Gesturebind { #[derive(Debug, Default)] pub struct Gesturebinds { pub id_map: IndexMap>>, - gesture_map: IndexMap<(GestureDirection, GestureFingers), Vec>>>, + gesture_map: IndexMap<(GestureDirection, u32), Vec>>>, - pub last_pressed_triggered_binds: HashMap<(GestureDirection, GestureFingers), Vec>, + pub last_pressed_triggered_binds: HashMap<(GestureDirection, u32), Vec>, } // TODO: may be able to dedup with Keybinds above @@ -556,7 +556,7 @@ impl Gesturebinds { pub fn gesture( &mut self, direction: GestureDirection, - fingers: GestureFingers, + fingers: u32, mods: ModifiersState, edge: Edge, current_layer: Option, @@ -606,7 +606,7 @@ impl Gesturebinds { pub fn add_gesturebind( &mut self, direction: GestureDirection, - fingers: GestureFingers, + fingers: u32, mods: ModMask, layer: Option, group: String, From 72f7e2a39cba958ea6aa8b8952c7b5bff67f56f5 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 21:05:43 -0800 Subject: [PATCH 06/13] fix rustfmt --- api/rust/src/input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index c33e99cf7..b547afd3c 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -1008,7 +1008,7 @@ pub fn bind_infos() -> impl Iterator { input::v1::bind::Bind::Gesture(gesturebind) => BindInfoKind::Gesture { direction: GestureDirection::try_from(gesturebind.direction) .expect("invalid gesture direction value"), - fingers: gesturebind.fingers + fingers: gesturebind.fingers, }, }; From ba548bc68b0d591aac61a5444651494e412a4521 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 22:54:00 -0800 Subject: [PATCH 07/13] pinch gestures --- src/api/input/v1.rs | 4 ++ src/input.rs | 89 ++++++++++++++++++++++++++++++++------------- src/input/bind.rs | 9 +++++ 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index b0f0e973d..11d619ab4 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -1,3 +1,4 @@ +use pinnacle_api::input::GestureType; use pinnacle_api_defs::pinnacle::input::{ self, v1::{ @@ -167,6 +168,8 @@ impl input::v1::input_service_server::InputService for InputService { let direction = GestureDirection::try_from(gesturebind.direction) .expect("invalid gesture direction value"); let fingers = gesturebind.fingers; + let gesture_type = GestureType::try_from(gesturebind.gesture_type) + .expect("invalid gesture type"); let bind_id = state .pinnacle .input_state @@ -175,6 +178,7 @@ impl input::v1::input_service_server::InputService for InputService { .add_gesturebind( direction, fingers, + gesture_type, mods, layer, group, diff --git a/src/input.rs b/src/input.rs index f2a8a8355..47cca15b4 100644 --- a/src/input.rs +++ b/src/input.rs @@ -14,7 +14,7 @@ use crate::{ }; use bind::BindState; use libinput::LibinputState; -use pinnacle_api::input::GestureDirection; +use pinnacle_api::input::{GestureDirection, GestureType}; use smithay::{ backend::{ input::{ @@ -965,37 +965,15 @@ impl State { let current_layer = self.pinnacle.input_state.bind_state.current_layer(); - if let Some((x, y)) = self.pinnacle.input_state.gesture_state.delta { - let direction = { - use std::f64::consts::PI; - - let angle = y.atan2(x); - let angle_deg = (angle * 180.0 / PI + 360.0) % 360.0; - - if !(22.5..337.5).contains(&angle_deg) { - GestureDirection::Right - } else if (22.5..67.5).contains(&angle_deg) { - GestureDirection::DownAndRight - } else if (67.5..112.5).contains(&angle_deg) { - GestureDirection::Down - } else if (112.5..157.5).contains(&angle_deg) { - GestureDirection::DownAndLeft - } else if (157.5..202.5).contains(&angle_deg) { - GestureDirection::Left - } else if (202.5..247.5).contains(&angle_deg) { - GestureDirection::UpAndLeft - } else if (247.5..292.5).contains(&angle_deg) { - GestureDirection::Up - } else { - GestureDirection::UpAndRight - } - }; + if let Some(delta) = self.pinnacle.input_state.gesture_state.delta { + let direction = delta_to_direction(delta); let fingers = self.pinnacle.input_state.gesture_state.fingers; let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( direction, fingers, + GestureType::Swipe, mods, Edge::Release, current_layer, @@ -1020,6 +998,11 @@ impl State { return; }; + self.pinnacle.input_state.gesture_state = GestureState { + delta: Some((0., 0.)), + fingers: event.fingers(), + }; + pointer.gesture_pinch_begin( self, &GesturePinchBeginEvent { @@ -1037,6 +1020,10 @@ impl State { use smithay::backend::input::GesturePinchUpdateEvent as _; + let delta = event.delta(); + + self.pinnacle.input_state.gesture_state.delta = Some((delta.x, delta.y)); + pointer.gesture_pinch_update( self, &GesturePinchUpdateEvent { @@ -1052,6 +1039,29 @@ impl State { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; + let Some(keyboard) = self.pinnacle.seat.get_keyboard() else { + return; + }; + + let mods = keyboard.modifier_state(); + + let current_layer = self.pinnacle.input_state.bind_state.current_layer(); + + if self.pinnacle.input_state.gesture_state.delta.is_some() { + let fingers = self.pinnacle.input_state.gesture_state.fingers; + + let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + GestureDirection::Up, // Direction doesn't matter for pinches + fingers, + GestureType::Pinch, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); + } + + self.pinnacle.input_state.gesture_state.delta = None; pointer.gesture_pinch_end( self, @@ -1391,6 +1401,33 @@ fn constrain_point_inside_rects( .unwrap_or(pos) } +fn delta_to_direction(delta: (f64, f64)) -> GestureDirection { + use std::f64::consts::PI; + + let (x, y) = delta; + + let angle = y.atan2(x); + let angle_deg = (angle * 180.0 / PI + 360.0) % 360.0; + + if !(22.5..337.5).contains(&angle_deg) { + GestureDirection::Right + } else if (22.5..67.5).contains(&angle_deg) { + GestureDirection::DownAndRight + } else if (67.5..112.5).contains(&angle_deg) { + GestureDirection::Down + } else if (112.5..157.5).contains(&angle_deg) { + GestureDirection::DownAndLeft + } else if (157.5..202.5).contains(&angle_deg) { + GestureDirection::Left + } else if (202.5..247.5).contains(&angle_deg) { + GestureDirection::UpAndLeft + } else if (247.5..292.5).contains(&angle_deg) { + GestureDirection::Up + } else { + GestureDirection::UpAndRight + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/input/bind.rs b/src/input/bind.rs index ad3254eb2..a04b64b18 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -6,6 +6,7 @@ use std::{ }; use indexmap::{IndexMap, map::Entry}; +use pinnacle_api::input::GestureType; use pinnacle_api_defs::pinnacle::input::v1::GestureDirection; use smithay::input::keyboard::ModifiersState; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -535,6 +536,7 @@ pub struct Gesturebind { pub bind_data: BindData, pub direction: GestureDirection, pub fingers: u32, + pub gesture_type: GestureType, sender: UnboundedSender, pub recv: Option>, pub has_on_begin: bool, @@ -557,6 +559,7 @@ impl Gesturebinds { &mut self, direction: GestureDirection, fingers: u32, + gesture_type: GestureType, mods: ModifiersState, edge: Edge, current_layer: Option, @@ -578,6 +581,10 @@ impl Gesturebinds { continue; } + if gesturebind.borrow().gesture_type != gesture_type { + continue; + } + if gesturebind.borrow().bind_data.layer != current_layer { continue; } @@ -607,6 +614,7 @@ impl Gesturebinds { &mut self, direction: GestureDirection, fingers: u32, + gesture_type: GestureType, mods: ModMask, layer: Option, group: String, @@ -632,6 +640,7 @@ impl Gesturebinds { }, direction, fingers, + gesture_type, sender, recv: Some(recv), has_on_begin: false, From a9185ccbaa5d944cffde6dc4da962ba6b0d187cb Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 27 Jan 2026 23:01:41 -0800 Subject: [PATCH 08/13] hold gestures --- src/input.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/input.rs b/src/input.rs index 47cca15b4..a4419aa7c 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1078,6 +1078,11 @@ impl State { return; }; + self.pinnacle.input_state.gesture_state = GestureState { + delta: Some((0., 0.)), + fingers: event.fingers(), + }; + pointer.gesture_hold_begin( self, &GestureHoldBeginEvent { @@ -1092,6 +1097,29 @@ impl State { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; + let Some(keyboard) = self.pinnacle.seat.get_keyboard() else { + return; + }; + + let mods = keyboard.modifier_state(); + + let current_layer = self.pinnacle.input_state.bind_state.current_layer(); + + if self.pinnacle.input_state.gesture_state.delta.is_some() { + let fingers = self.pinnacle.input_state.gesture_state.fingers; + + let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + GestureDirection::Up, // Direction doesn't matter for pinches + fingers, + GestureType::Hold, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); + } + + self.pinnacle.input_state.gesture_state.delta = None; pointer.gesture_hold_end( self, From 324cfd2866675f7b3a0bad450abe038e89141b89 Mon Sep 17 00:00:00 2001 From: rustysec Date: Sun, 1 Feb 2026 15:52:18 -0800 Subject: [PATCH 09/13] clean up enum naming --- api/lua/pinnacle/grpc/defs.lua | 118 ++++++++++++++++++ api/lua/pinnacle/input.lua | 136 +++++++++++++++++++++ api/protobuf/pinnacle/input/v1/input.proto | 22 ++-- api/rust/src/snowcap.rs | 8 +- src/input.rs | 12 +- src/input/bind.rs | 2 + 6 files changed, 276 insertions(+), 22 deletions(-) diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index 6ef38552f..65b4ae834 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -526,6 +526,25 @@ local pinnacle_input_v1_Edge = { EDGE_RELEASE = 2, } +---@enum pinnacle.input.v1.GestureDirection +local pinnacle_input_v1_GestureDirection = { + DOWN = 0, + LEFT = 1, + RIGHT = 2, + UP = 3, + DOWN_AND_LEFT = 4, + DOWN_AND_RIGHT = 5, + UP_AND_LEFT = 6, + UP_AND_RIGHT = 7, +} + +---@enum pinnacle.input.v1.GestureType +local pinnacle_input_v1_GestureType = { + HOLD = 0, + PINCH = 1, + SWIPE = 2, +} + ---@enum pinnacle.input.v1.ClickMethod local pinnacle_input_v1_ClickMethod = { CLICK_METHOD_UNSPECIFIED = 0, @@ -675,6 +694,7 @@ local pinnacle_v1_Backend = { ---@field properties pinnacle.input.v1.BindProperties? ---@field key pinnacle.input.v1.Keybind? ---@field mouse pinnacle.input.v1.Mousebind? +---@field gesture pinnacle.input.v1.Gesturebind? ---@class pinnacle.input.v1.BindRequest ---@field bind pinnacle.input.v1.Bind? @@ -718,6 +738,26 @@ local pinnacle_v1_Backend = { ---@class pinnacle.input.v1.MousebindOnPressRequest ---@field bind_id integer? +---@class pinnacle.input.v1.Gesturebind +---@field direction pinnacle.input.v1.GestureDirection? +---@field fingers integer? +---@field gesture_type pinnacle.input.v1.GestureType? + +---@class pinnacle.input.v1.GesturebindStreamRequest +---@field bind_id integer? + +---@class pinnacle.input.v1.GesturebindStreamResponse +---@field edge pinnacle.input.v1.Edge? + +---@class pinnacle.input.v1.GesturebindRequest +---@field bind_id integer? + +---@class pinnacle.input.v1.GesturebindOnBeginRequest +---@field bind_id integer? + +---@class pinnacle.input.v1.GesturebindOnFinishRequest +---@field bind_id integer? + ---@class pinnacle.input.v1.GetBindInfosRequest ---@class pinnacle.input.v1.GetBindInfosResponse @@ -1417,6 +1457,12 @@ pinnacle.input.v1.Mousebind = {} pinnacle.input.v1.MousebindStreamRequest = {} pinnacle.input.v1.MousebindStreamResponse = {} pinnacle.input.v1.MousebindOnPressRequest = {} +pinnacle.input.v1.Gesturebind = {} +pinnacle.input.v1.GesturebindStreamRequest = {} +pinnacle.input.v1.GesturebindStreamResponse = {} +pinnacle.input.v1.GesturebindRequest = {} +pinnacle.input.v1.GesturebindOnBeginRequest = {} +pinnacle.input.v1.GesturebindOnFinishRequest = {} pinnacle.input.v1.GetBindInfosRequest = {} pinnacle.input.v1.GetBindInfosResponse = {} pinnacle.input.v1.BindInfo = {} @@ -1624,6 +1670,8 @@ pinnacle.util.v1.AbsOrRel = pinnacle_util_v1_AbsOrRel pinnacle.util.v1.Dir = pinnacle_util_v1_Dir pinnacle.input.v1.Modifier = pinnacle_input_v1_Modifier pinnacle.input.v1.Edge = pinnacle_input_v1_Edge +pinnacle.input.v1.GestureDirection = pinnacle_input_v1_GestureDirection +pinnacle.input.v1.GestureType = pinnacle_input_v1_GestureType pinnacle.input.v1.ClickMethod = pinnacle_input_v1_ClickMethod pinnacle.input.v1.AccelProfile = pinnacle_input_v1_AccelProfile pinnacle.input.v1.ScrollMethod = pinnacle_input_v1_ScrollMethod @@ -1832,6 +1880,25 @@ pinnacle.input.v1.InputService.MousebindStream.response = ".pinnacle.input.v1.Mo function Client:pinnacle_input_v1_InputService_MousebindStream(data, callback) return self:server_streaming_request(pinnacle.input.v1.InputService.MousebindStream, data, callback) end +pinnacle.input.v1.InputService.GesturebindStream = {} +pinnacle.input.v1.InputService.GesturebindStream.service = "pinnacle.input.v1.InputService" +pinnacle.input.v1.InputService.GesturebindStream.method = "GesturebindStream" +pinnacle.input.v1.InputService.GesturebindStream.request = ".pinnacle.input.v1.GesturebindStreamRequest" +pinnacle.input.v1.InputService.GesturebindStream.response = ".pinnacle.input.v1.GesturebindStreamResponse" + +---Performs a server-streaming request. +--- +---`callback` will be called with every streamed response. +--- +---@nodiscard +--- +---@param data pinnacle.input.v1.GesturebindStreamRequest +---@param callback fun(response: pinnacle.input.v1.GesturebindStreamResponse) +--- +---@return string | nil An error string, if any +function Client:pinnacle_input_v1_InputService_GesturebindStream(data, callback) + return self:server_streaming_request(pinnacle.input.v1.InputService.GesturebindStream, data, callback) +end pinnacle.input.v1.InputService.KeybindOnPress = {} pinnacle.input.v1.InputService.KeybindOnPress.service = "pinnacle.input.v1.InputService" pinnacle.input.v1.InputService.KeybindOnPress.method = "KeybindOnPress" @@ -1866,6 +1933,57 @@ pinnacle.input.v1.InputService.MousebindOnPress.response = ".google.protobuf.Emp function Client:pinnacle_input_v1_InputService_MousebindOnPress(data) return self:unary_request(pinnacle.input.v1.InputService.MousebindOnPress, data) end +pinnacle.input.v1.InputService.Gesturebind = {} +pinnacle.input.v1.InputService.Gesturebind.service = "pinnacle.input.v1.InputService" +pinnacle.input.v1.InputService.Gesturebind.method = "Gesturebind" +pinnacle.input.v1.InputService.Gesturebind.request = ".pinnacle.input.v1.GesturebindRequest" +pinnacle.input.v1.InputService.Gesturebind.response = ".google.protobuf.Empty" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.input.v1.GesturebindRequest +--- +---@return google.protobuf.Empty | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_input_v1_InputService_Gesturebind(data) + return self:unary_request(pinnacle.input.v1.InputService.Gesturebind, data) +end +pinnacle.input.v1.InputService.GesturebindOnBegin = {} +pinnacle.input.v1.InputService.GesturebindOnBegin.service = "pinnacle.input.v1.InputService" +pinnacle.input.v1.InputService.GesturebindOnBegin.method = "GesturebindOnBegin" +pinnacle.input.v1.InputService.GesturebindOnBegin.request = ".pinnacle.input.v1.GesturebindOnBeginRequest" +pinnacle.input.v1.InputService.GesturebindOnBegin.response = ".google.protobuf.Empty" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.input.v1.GesturebindOnBeginRequest +--- +---@return google.protobuf.Empty | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_input_v1_InputService_GesturebindOnBegin(data) + return self:unary_request(pinnacle.input.v1.InputService.GesturebindOnBegin, data) +end +pinnacle.input.v1.InputService.GesturebindOnFinish = {} +pinnacle.input.v1.InputService.GesturebindOnFinish.service = "pinnacle.input.v1.InputService" +pinnacle.input.v1.InputService.GesturebindOnFinish.method = "GesturebindOnFinish" +pinnacle.input.v1.InputService.GesturebindOnFinish.request = ".pinnacle.input.v1.GesturebindOnFinishRequest" +pinnacle.input.v1.InputService.GesturebindOnFinish.response = ".google.protobuf.Empty" + +---Performs a unary request. +--- +---@nodiscard +--- +---@param data pinnacle.input.v1.GesturebindOnFinishRequest +--- +---@return google.protobuf.Empty | nil response +---@return string | nil error An error string, if any +function Client:pinnacle_input_v1_InputService_GesturebindOnFinish(data) + return self:unary_request(pinnacle.input.v1.InputService.GesturebindOnFinish, data) +end pinnacle.input.v1.InputService.SetXkbConfig = {} pinnacle.input.v1.InputService.SetXkbConfig.service = "pinnacle.input.v1.InputService" pinnacle.input.v1.InputService.SetXkbConfig.method = "SetXkbConfig" diff --git a/api/lua/pinnacle/input.lua b/api/lua/pinnacle/input.lua index 30e929a94..72804a9d2 100644 --- a/api/lua/pinnacle/input.lua +++ b/api/lua/pinnacle/input.lua @@ -424,6 +424,142 @@ function input.mousebind(mods, button, on_press, bind_info) mousebind_inner(mb) end +---A gesturebind. +---@class pinnacle.input.gesturebind : pinnacle.input.Bind +---The gesture button that will trigger this bind. +---@field button pinnacle.input.gestureButton +---An action that will be run when the gesturebind is started. +---@field on_begin fun()? +---An action that will be run when the gesturebind is finished. +---@field on_finish fun()? + +---@param mb pinnacle.input.gesturebind +local function gesturebind_inner(mb) + local modifs = {} + local ignore_modifs = {} + for _, mod in ipairs(mb.mods) do + if string.match(mod, "ignore") then + table.insert(ignore_modifs, mods_with_ignore_values[mod]) + else + table.insert(modifs, mods_with_ignore_values[mod]) + end + end + + local response, err = client:pinnacle_input_v1_InputService_Bind({ + bind = { + mods = modifs, + ignore_mods = ignore_modifs, + layer_name = mb.bind_layer, + properties = { + group = mb.group, + description = mb.description, + quit = mb.quit, + reload_config = mb.reload_config, + allow_when_locked = mb.allow_when_locked, + }, + gesture = { + button = gesture_button_values[mb.button], + }, + }, + }) + + if err then + log.error(err) + return + end + + assert(response) + + local bind_id = response.bind_id or 0 + + local err = client:pinnacle_input_v1_InputService_gesturebindStream({ + bind_id = bind_id, + }, function(response) + if response.edge == edge_values.press then + if mb.on_press then + local success, error = pcall(mb.on_press) + if not success then + log.error("While handling `gesturebind:on_press`: " .. tostring(error)) + end + end + elseif response.edge == edge_values.release then + if mb.on_release then + local success, error = pcall(mb.on_release) + if not success then + log.error("While handling `gesturebind:on_release`: " .. tostring(error)) + end + end + end + end) + + if mb.on_press then + local _, err = client:pinnacle_input_v1_InputService_gesturebindOnPress({ + bind_id = bind_id, + }) + end + + if err then + log.error(err) + return + end +end + +---Sets a gesturebind. +--- +---This function can be called in two ways: +---1. As `Input.gesturebind(mods, button, on_press, bind_info?)` +---2. As `Input.gesturebind()` +--- +---Calling this with a `gesturebind` table gives you more options, including the ability to assign a bind layer +---to the keybind or set it to happen on release instead of press. +--- +---When calling using the first way, you must provide three arguments: +--- +--- - `mods`: An array of `Modifier`s. If you don't want any, provide an empty table. +--- - `button`: The gesture button. +--- - `on_press`: The function that will be run when the button is pressed. +--- +---#### Ignoring Modifiers +---Normally, modifiers that are not specified will require the bind to not have them held down. +---You can ignore this by adding the corresponding `"ignore_*"` modifier. +--- +---#### Descriptions +---You can specify a group and description for the bind. +---This will be used to categorize the bind in the bind overlay and provide a description. +--- +---#### Example +---```lua +--- -- Set `super + left gesture button` to move a window on press +---Input.gesturebind({ "super" }, "btn_left", "press", function() +--- Window.begin_move("btn_left") +---end) +---``` +--- +---@param mods pinnacle.input.Mod[] The modifiers that need to be held down for the bind to trigger +---@param button pinnacle.input.gestureButton The gesture button used to trigger the bind +---@param on_press fun() The function to run when the bind is triggered +---@param bind_info { group: string?, description: string? }? An optional group and description that will be displayed in the bind overlay. +--- +---@overload fun(gesturebind: pinnacle.input.gesturebind) +function input.gesturebind(mods, button, on_press, bind_info) + ---@type pinnacle.input.gesturebind + local mb + + if mods.button then + mb = mods + else + mb = { + mods = mods, + button = button, + on_press = on_press, + group = bind_info and bind_info.group, + description = bind_info and bind_info.description, + } + end + + gesturebind_inner(mb) +end + ---Enters the bind layer `layer`, or the default layer if `layer` is nil. --- ---@param layer string? The bind layer. diff --git a/api/protobuf/pinnacle/input/v1/input.proto b/api/protobuf/pinnacle/input/v1/input.proto index 42788673b..5bb0e0de9 100644 --- a/api/protobuf/pinnacle/input/v1/input.proto +++ b/api/protobuf/pinnacle/input/v1/input.proto @@ -97,20 +97,20 @@ message MousebindOnPressRequest { // Gesturebinds enum GestureDirection { - DOWN = 0; - LEFT = 1; - RIGHT = 2; - UP = 3; - DOWN_AND_LEFT = 4; - DOWN_AND_RIGHT = 5; - UP_AND_LEFT = 6; - UP_AND_RIGHT = 7; + GESTURE_DIRECTION_DOWN = 0; + GESTURE_DIRECTION_LEFT = 1; + GESTURE_DIRECTION_RIGHT = 2; + GESTURE_DIRECTION_UP = 3; + GESTURE_DIRECTION_DOWN_LEFT = 4; + GESTURE_DIRECTION_DOWN_RIGHT = 5; + GESTURE_DIRECTION_UP_LEFT = 6; + GESTURE_DIRECTION_UP_RIGHT = 7; } enum GestureType { - HOLD = 0; - PINCH = 1; - SWIPE = 2; + GESTURE_TYPE_HOLD = 0; + GESTURE_TYPE_PINCH = 1; + GESTURE_TYPE_SWIPE = 2; } message Gesturebind { diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index e73c14df3..9e09edb45 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -297,10 +297,10 @@ impl Program for BindOverlay { GestureDirection::Left => "Left", GestureDirection::Right => "Right", GestureDirection::Up => "Up", - GestureDirection::DownAndLeft => "Down and Left", - GestureDirection::DownAndRight => "Down and Right", - GestureDirection::UpAndLeft => "Up and Left", - GestureDirection::UpAndRight => "Up and Right", + GestureDirection::DownLeft => "Down-Left", + GestureDirection::DownRight => "Down-Right", + GestureDirection::UpLeft => "Up-Left", + GestureDirection::UpRight => "Up-Right", } .to_string(), fingers: format!("{fingers:?} fingers"), diff --git a/src/input.rs b/src/input.rs index a4419aa7c..3b58c9d9d 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1430,29 +1430,27 @@ fn constrain_point_inside_rects( } fn delta_to_direction(delta: (f64, f64)) -> GestureDirection { - use std::f64::consts::PI; - let (x, y) = delta; let angle = y.atan2(x); - let angle_deg = (angle * 180.0 / PI + 360.0) % 360.0; + let angle_deg = (angle.to_degrees() + 360.0) % 360.0; if !(22.5..337.5).contains(&angle_deg) { GestureDirection::Right } else if (22.5..67.5).contains(&angle_deg) { - GestureDirection::DownAndRight + GestureDirection::DownRight } else if (67.5..112.5).contains(&angle_deg) { GestureDirection::Down } else if (112.5..157.5).contains(&angle_deg) { - GestureDirection::DownAndLeft + GestureDirection::DownLeft } else if (157.5..202.5).contains(&angle_deg) { GestureDirection::Left } else if (202.5..247.5).contains(&angle_deg) { - GestureDirection::UpAndLeft + GestureDirection::UpLeft } else if (247.5..292.5).contains(&angle_deg) { GestureDirection::Up } else { - GestureDirection::UpAndRight + GestureDirection::UpRight } } diff --git a/src/input/bind.rs b/src/input/bind.rs index a04b64b18..47f73a742 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -28,6 +28,8 @@ impl BindState { self.keybinds.keysym_map.clear(); self.mousebinds.id_map.clear(); self.mousebinds.button_map.clear(); + self.gesturebinds.id_map.clear(); + self.gesturebinds.gesture_map.clear(); } pub fn enter_layer(&mut self, layer: Option) { From f8af4e6b8ca6c75be92f4f3a87fa7ce2eca2d793 Mon Sep 17 00:00:00 2001 From: rustysec Date: Mon, 2 Feb 2026 12:49:00 -0800 Subject: [PATCH 10/13] fixing naming and error handling --- api/protobuf/pinnacle/input/v1/input.proto | 21 ++-- api/rust/src/input.rs | 120 +++++++++++++++---- api/rust/src/snowcap.rs | 18 +-- src/api/input/v1.rs | 131 ++++++++++++++++++--- src/input.rs | 29 ++--- src/input/bind.rs | 19 ++- 6 files changed, 249 insertions(+), 89 deletions(-) diff --git a/api/protobuf/pinnacle/input/v1/input.proto b/api/protobuf/pinnacle/input/v1/input.proto index 5bb0e0de9..bb707fbdd 100644 --- a/api/protobuf/pinnacle/input/v1/input.proto +++ b/api/protobuf/pinnacle/input/v1/input.proto @@ -96,15 +96,16 @@ message MousebindOnPressRequest { // Gesturebinds -enum GestureDirection { - GESTURE_DIRECTION_DOWN = 0; - GESTURE_DIRECTION_LEFT = 1; - GESTURE_DIRECTION_RIGHT = 2; - GESTURE_DIRECTION_UP = 3; - GESTURE_DIRECTION_DOWN_LEFT = 4; - GESTURE_DIRECTION_DOWN_RIGHT = 5; - GESTURE_DIRECTION_UP_LEFT = 6; - GESTURE_DIRECTION_UP_RIGHT = 7; +enum SwipeDirection { + SWIPE_DIRECTION_DOWN = 0; + SWIPE_DIRECTION_LEFT = 1; + SWIPE_DIRECTION_RIGHT = 2; + SWIPE_DIRECTION_UP = 3; + SWIPE_DIRECTION_DOWN_LEFT = 4; + SWIPE_DIRECTION_DOWN_RIGHT = 5; + SWIPE_DIRECTION_UP_LEFT = 6; + SWIPE_DIRECTION_UP_RIGHT = 7; + SWIPE_DIRECTION_NONE = 8; } enum GestureType { @@ -114,7 +115,7 @@ enum GestureType { } message Gesturebind { - GestureDirection direction = 1; + SwipeDirection direction = 1; uint32 fingers = 2; GestureType gesture_type = 3; } diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index b547afd3c..6cf0a71a9 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -31,8 +31,6 @@ pub mod libinput; pub use xkbcommon::xkb::Keysym; -pub use pinnacle_api_defs::pinnacle::input::v1::{GestureDirection, GestureType}; - /// A mouse button. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)] #[repr(u32)] @@ -180,14 +178,8 @@ impl BindLayer { } /// Creates a gesturebind on this layer. - pub fn gesturebind( - &self, - mods: Mod, - direction: GestureDirection, - fingers: u32, - gesture_type: GestureType, - ) -> Gesturebind { - new_gesturebind(mods, direction, fingers, gesture_type, self).block_on_tokio() + pub fn gesturebind(&self, mods: Mod, gesture_type: GestureType, fingers: u32) -> Gesturebind { + new_gesturebind(mods, gesture_type, fingers, self).block_on_tokio() } /// Enters this layer, causing only its binds to be in effect. @@ -540,6 +532,78 @@ async fn new_mousebind_stream( // Gesturebinds +/// The direction of a swipe gesture +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromPrimitive, IntoPrimitive)] +#[repr(i32)] +pub enum SwipeDirection { + /// Moving Down + Down = 0, + /// Moving Left + Left = 1, + /// Moving Right + Right = 2, + /// Moving Up + Up = 3, + /// Moving diagonally down to the left + DownLeft = 4, + /// Moving diagonally down to the right + DownRight = 5, + /// Moving diagonally up to the left + UpLeft = 6, + /// Moving diagonally up to the right + UpRight = 7, + /// unknown direction + #[num_enum(catch_all)] + Unknown(i32), +} + +impl From for pinnacle_api_defs::pinnacle::input::v1::SwipeDirection { + fn from(other: SwipeDirection) -> pinnacle_api_defs::pinnacle::input::v1::SwipeDirection { + match other { + SwipeDirection::Down => pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::Down, + SwipeDirection::Left => pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::Left, + SwipeDirection::Right => pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::Right, + SwipeDirection::Up => pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::Up, + SwipeDirection::DownLeft => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::DownLeft + } + SwipeDirection::DownRight => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::DownRight + } + SwipeDirection::UpLeft => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::UpLeft + } + SwipeDirection::UpRight => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::UpRight + } + SwipeDirection::Unknown(_) => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::None + } + } + } +} + +/// The type of gesture +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum GestureType { + /// Hold gesture + Hold, + /// Pinch gesture + Pinch, + /// Swipe gesture with direction + Swipe(SwipeDirection), +} + +impl From for pinnacle_api_defs::pinnacle::input::v1::GestureType { + fn from(value: GestureType) -> Self { + match value { + GestureType::Hold => Self::Hold, + GestureType::Pinch => Self::Pinch, + GestureType::Swipe(_) => Self::Swipe, + } + } +} + type GesturebindCallback = (Box, Edge); /// A Gesturebind. @@ -551,13 +615,8 @@ pub struct Gesturebind { bind_impl!(Gesturebind); /// Creates a gesturebind on the [`DEFAULT`][BindLayer::DEFAULT] bind layer. -pub fn gesturebind( - mods: Mod, - direction: GestureDirection, - fingers: u32, - gesture_type: GestureType, -) -> Gesturebind { - BindLayer::DEFAULT.gesturebind(mods, direction, fingers, gesture_type) +pub fn gesturebind(mods: Mod, gesture_type: GestureType, fingers: u32) -> Gesturebind { + BindLayer::DEFAULT.gesturebind(mods, gesture_type, fingers) } impl Gesturebind { @@ -598,9 +657,8 @@ impl Gesturebind { async fn new_gesturebind( mods: Mod, - direction: GestureDirection, - fingers: u32, gesture_type: GestureType, + fingers: u32, layer: &BindLayer, ) -> Gesturebind { let ignore_mods = mods.api_ignore_mods(); @@ -614,9 +672,24 @@ async fn new_gesturebind( layer_name: layer.name.clone(), properties: Some(BindProperties::default()), bind: Some(input::v1::bind::Bind::Gesture(input::v1::Gesturebind { - direction: direction.into(), + direction: match gesture_type { + GestureType::Hold | GestureType::Pinch => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::None.into() + } + GestureType::Swipe(direction) => direction.into(), + }, fingers, - gesture_type: gesture_type.into(), + gesture_type: match gesture_type { + GestureType::Hold => { + pinnacle_api_defs::pinnacle::input::v1::GestureType::Hold.into() + } + GestureType::Pinch => { + pinnacle_api_defs::pinnacle::input::v1::GestureType::Pinch.into() + } + GestureType::Swipe(_) => { + pinnacle_api_defs::pinnacle::input::v1::GestureType::Swipe.into() + } + }, })), }), }) @@ -855,7 +928,7 @@ pub enum BindInfoKind { /// This is a gesturebind. Gesture { /// Direction of the gesture. - direction: GestureDirection, + direction: SwipeDirection, /// Fingers used in the gesture. fingers: u32, }, @@ -1006,8 +1079,7 @@ pub fn bind_infos() -> impl Iterator { button: MouseButton::from(mousebind.button), }, input::v1::bind::Bind::Gesture(gesturebind) => BindInfoKind::Gesture { - direction: GestureDirection::try_from(gesturebind.direction) - .expect("invalid gesture direction value"), + direction: SwipeDirection::from(gesturebind.direction), fingers: gesturebind.fingers, }, }; diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index 9e09edb45..b563f7b5c 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -7,7 +7,6 @@ use std::sync::{Arc, OnceLock}; use indexmap::IndexMap; -use pinnacle_api_defs::pinnacle::input::v1::GestureDirection; use snowcap_api::{ decoration::{DecorationHandle, NewDecorationError}, layer::{ExclusiveZone, KeyboardInteractivity, ZLayer}, @@ -293,14 +292,15 @@ impl Program for BindOverlay { let repr = GesturebindRepr { mods, direction: match direction { - GestureDirection::Down => "Down", - GestureDirection::Left => "Left", - GestureDirection::Right => "Right", - GestureDirection::Up => "Up", - GestureDirection::DownLeft => "Down-Left", - GestureDirection::DownRight => "Down-Right", - GestureDirection::UpLeft => "Up-Left", - GestureDirection::UpRight => "Up-Right", + crate::input::SwipeDirection::Down => "Swipe Down", + crate::input::SwipeDirection::Left => "Swipe Left", + crate::input::SwipeDirection::Right => "Swipe Right", + crate::input::SwipeDirection::Up => "Swipe Up", + crate::input::SwipeDirection::DownLeft => "Swipe Down-Left", + crate::input::SwipeDirection::DownRight => "Swipe Down-Right", + crate::input::SwipeDirection::UpLeft => "Swipe Up-Left", + crate::input::SwipeDirection::UpRight => "Swipe Up-Right", + crate::input::SwipeDirection::Unknown(_) => "Swipe Unknown", } .to_string(), fingers: format!("{fingers:?} fingers"), diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index 11d619ab4..a48b9765c 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -3,17 +3,17 @@ use pinnacle_api_defs::pinnacle::input::{ self, v1::{ AccelProfile, BindInfo, BindRequest, BindResponse, ClickMethod, EnterBindLayerRequest, - GestureDirection, GesturebindOnBeginRequest, GesturebindOnFinishRequest, - GesturebindRequest, GesturebindStreamRequest, GesturebindStreamResponse, - GetBindInfosRequest, GetBindInfosResponse, GetBindLayerStackRequest, - GetBindLayerStackResponse, GetDeviceCapabilitiesRequest, GetDeviceCapabilitiesResponse, - GetDeviceInfoRequest, GetDeviceInfoResponse, GetDeviceTypeRequest, GetDeviceTypeResponse, - GetDevicesRequest, GetDevicesResponse, KeybindOnPressRequest, KeybindStreamRequest, - KeybindStreamResponse, MousebindOnPressRequest, MousebindStreamRequest, - MousebindStreamResponse, ScrollMethod, SendEventsMode, SetBindPropertiesRequest, - SetDeviceLibinputSettingRequest, SetDeviceMapTargetRequest, SetRepeatRateRequest, - SetXcursorRequest, SetXkbConfigRequest, SetXkbKeymapRequest, SwitchXkbLayoutRequest, - TapButtonMap, set_device_map_target_request::Target, switch_xkb_layout_request::Action, + GesturebindOnBeginRequest, GesturebindOnFinishRequest, GesturebindRequest, + GesturebindStreamRequest, GesturebindStreamResponse, GetBindInfosRequest, + GetBindInfosResponse, GetBindLayerStackRequest, GetBindLayerStackResponse, + GetDeviceCapabilitiesRequest, GetDeviceCapabilitiesResponse, GetDeviceInfoRequest, + GetDeviceInfoResponse, GetDeviceTypeRequest, GetDeviceTypeResponse, GetDevicesRequest, + GetDevicesResponse, KeybindOnPressRequest, KeybindStreamRequest, KeybindStreamResponse, + MousebindOnPressRequest, MousebindStreamRequest, MousebindStreamResponse, ScrollMethod, + SendEventsMode, SetBindPropertiesRequest, SetDeviceLibinputSettingRequest, + SetDeviceMapTargetRequest, SetRepeatRateRequest, SetXcursorRequest, SetXkbConfigRequest, + SetXkbKeymapRequest, SwipeDirection, SwitchXkbLayoutRequest, TapButtonMap, + set_device_map_target_request::Target, switch_xkb_layout_request::Action, }, }; use smithay::reexports::input as libinput; @@ -165,20 +165,27 @@ impl input::v1::input_service_server::InputService for InputService { bind_id } input::v1::bind::Bind::Gesture(gesturebind) => { - let direction = GestureDirection::try_from(gesturebind.direction) - .expect("invalid gesture direction value"); let fingers = gesturebind.fingers; - let gesture_type = GestureType::try_from(gesturebind.gesture_type) - .expect("invalid gesture type"); + + let gesture_type = match gesturebind.gesture_type() { + pinnacle_api_defs::pinnacle::input::v1::GestureType::Hold => { + GestureType::Hold + } + pinnacle_api_defs::pinnacle::input::v1::GestureType::Pinch => { + GestureType::Pinch + } + pinnacle_api_defs::pinnacle::input::v1::GestureType::Swipe => { + GestureType::Swipe(gesturebind.direction.into()) + } + }; let bind_id = state .pinnacle .input_state .bind_state .gesturebinds .add_gesturebind( - direction, - fingers, gesture_type, + fingers, mods, layer, group, @@ -420,8 +427,96 @@ impl input::v1::input_service_server::InputService for InputService { } }); + let gesturebind_infos = state + .pinnacle + .input_state + .bind_state + .gesturebinds + .id_map + .values() + .map(|mousebind| { + let gesturebind = mousebind.borrow(); + + let mut mods = Vec::new(); + let mut ignore_mods = Vec::new(); + + push_mods( + &mut mods, + &mut ignore_mods, + gesturebind.bind_data.mods.shift, + input::v1::Modifier::Shift, + ); + push_mods( + &mut mods, + &mut ignore_mods, + gesturebind.bind_data.mods.ctrl, + input::v1::Modifier::Ctrl, + ); + push_mods( + &mut mods, + &mut ignore_mods, + gesturebind.bind_data.mods.alt, + input::v1::Modifier::Alt, + ); + push_mods( + &mut mods, + &mut ignore_mods, + gesturebind.bind_data.mods.super_, + input::v1::Modifier::Super, + ); + push_mods( + &mut mods, + &mut ignore_mods, + gesturebind.bind_data.mods.iso_level3_shift, + input::v1::Modifier::IsoLevel3Shift, + ); + push_mods( + &mut mods, + &mut ignore_mods, + gesturebind.bind_data.mods.iso_level5_shift, + input::v1::Modifier::IsoLevel5Shift, + ); + + let direction = match gesturebind.gesture_type { + GestureType::Hold => SwipeDirection::None, + GestureType::Pinch => SwipeDirection::None, + GestureType::Swipe(_) => SwipeDirection::None, + }; + + let gesture_type: pinnacle_api_defs::pinnacle::input::v1::GestureType = + gesturebind.gesture_type.into(); + + BindInfo { + bind_id: gesturebind.bind_data.id, + bind: Some(input::v1::Bind { + mods: mods.into_iter().map(|m| m.into()).collect(), + ignore_mods: ignore_mods.into_iter().map(|m| m.into()).collect(), + layer_name: gesturebind.bind_data.layer.clone(), + properties: Some(input::v1::BindProperties { + group: Some(gesturebind.bind_data.group.clone()), + description: Some(gesturebind.bind_data.desc.clone()), + quit: Some(gesturebind.bind_data.is_quit_bind), + reload_config: Some(gesturebind.bind_data.is_reload_config_bind), + allow_when_locked: Some(gesturebind.bind_data.allow_when_locked), + }), + #[allow( + clippy::useless_conversion, + clippy::unnecessary_fallible_conversions + )] + bind: Some(input::v1::bind::Bind::Gesture(input::v1::Gesturebind { + fingers: gesturebind.fingers, + direction: direction.into(), + gesture_type: gesture_type.into(), + })), + }), + } + }); + Ok(GetBindInfosResponse { - bind_infos: keybind_infos.chain(mousebind_infos).collect(), + bind_infos: keybind_infos + .chain(mousebind_infos) + .chain(gesturebind_infos) + .collect(), }) }) .await diff --git a/src/input.rs b/src/input.rs index 3b58c9d9d..4d65aa124 100644 --- a/src/input.rs +++ b/src/input.rs @@ -14,7 +14,7 @@ use crate::{ }; use bind::BindState; use libinput::LibinputState; -use pinnacle_api::input::{GestureDirection, GestureType}; +use pinnacle_api::input::{GestureType, SwipeDirection}; use smithay::{ backend::{ input::{ @@ -971,9 +971,8 @@ impl State { let fingers = self.pinnacle.input_state.gesture_state.fingers; let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - direction, + GestureType::Swipe(direction), fingers, - GestureType::Swipe, mods, Edge::Release, current_layer, @@ -1051,9 +1050,8 @@ impl State { let fingers = self.pinnacle.input_state.gesture_state.fingers; let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - GestureDirection::Up, // Direction doesn't matter for pinches - fingers, GestureType::Pinch, + fingers, mods, Edge::Release, current_layer, @@ -1109,9 +1107,8 @@ impl State { let fingers = self.pinnacle.input_state.gesture_state.fingers; let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - GestureDirection::Up, // Direction doesn't matter for pinches - fingers, GestureType::Hold, + fingers, mods, Edge::Release, current_layer, @@ -1429,28 +1426,28 @@ fn constrain_point_inside_rects( .unwrap_or(pos) } -fn delta_to_direction(delta: (f64, f64)) -> GestureDirection { +fn delta_to_direction(delta: (f64, f64)) -> SwipeDirection { let (x, y) = delta; let angle = y.atan2(x); let angle_deg = (angle.to_degrees() + 360.0) % 360.0; if !(22.5..337.5).contains(&angle_deg) { - GestureDirection::Right + SwipeDirection::Right } else if (22.5..67.5).contains(&angle_deg) { - GestureDirection::DownRight + SwipeDirection::DownRight } else if (67.5..112.5).contains(&angle_deg) { - GestureDirection::Down + SwipeDirection::Down } else if (112.5..157.5).contains(&angle_deg) { - GestureDirection::DownLeft + SwipeDirection::DownLeft } else if (157.5..202.5).contains(&angle_deg) { - GestureDirection::Left + SwipeDirection::Left } else if (202.5..247.5).contains(&angle_deg) { - GestureDirection::UpLeft + SwipeDirection::UpLeft } else if (247.5..292.5).contains(&angle_deg) { - GestureDirection::Up + SwipeDirection::Up } else { - GestureDirection::UpRight + SwipeDirection::UpRight } } diff --git a/src/input/bind.rs b/src/input/bind.rs index 47f73a742..3cf503dfe 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -7,7 +7,6 @@ use std::{ use indexmap::{IndexMap, map::Entry}; use pinnacle_api::input::GestureType; -use pinnacle_api_defs::pinnacle::input::v1::GestureDirection; use smithay::input::keyboard::ModifiersState; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use xkbcommon::xkb::Keysym; @@ -536,7 +535,6 @@ impl Mousebinds { #[derive(Debug)] pub struct Gesturebind { pub bind_data: BindData, - pub direction: GestureDirection, pub fingers: u32, pub gesture_type: GestureType, sender: UnboundedSender, @@ -547,9 +545,9 @@ pub struct Gesturebind { #[derive(Debug, Default)] pub struct Gesturebinds { pub id_map: IndexMap>>, - gesture_map: IndexMap<(GestureDirection, u32), Vec>>>, + gesture_map: IndexMap<(GestureType, u32), Vec>>>, - pub last_pressed_triggered_binds: HashMap<(GestureDirection, u32), Vec>, + pub last_pressed_triggered_binds: HashMap<(GestureType, u32), Vec>, } // TODO: may be able to dedup with Keybinds above @@ -559,15 +557,14 @@ impl Gesturebinds { /// Returns whether the gesture should be suppressed (not sent to the client). pub fn gesture( &mut self, - direction: GestureDirection, - fingers: u32, gesture_type: GestureType, + fingers: u32, mods: ModifiersState, edge: Edge, current_layer: Option, is_locked: bool, ) -> BindAction { - let Some(gesturebinds) = self.gesture_map.get_mut(&(direction, fingers)) else { + let Some(gesturebinds) = self.gesture_map.get_mut(&(gesture_type, fingers)) else { return BindAction::Forward; }; @@ -614,9 +611,8 @@ impl Gesturebinds { pub fn add_gesturebind( &mut self, - direction: GestureDirection, - fingers: u32, gesture_type: GestureType, + fingers: u32, mods: ModMask, layer: Option, group: String, @@ -640,9 +636,8 @@ impl Gesturebinds { is_reload_config_bind, allow_when_locked, }, - direction, - fingers, gesture_type, + fingers, sender, recv: Some(recv), has_on_begin: false, @@ -654,7 +649,7 @@ impl Gesturebinds { ); self.gesture_map - .entry((direction, fingers)) + .entry((gesture_type, fingers)) .or_default() .push(Rc::downgrade(&gesturebind)); From 8842b73b3a874c1a57842c6e2ee4db0e8be9cd87 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 3 Feb 2026 16:04:04 -0800 Subject: [PATCH 11/13] cleaning up overlay and rpc --- api/protobuf/pinnacle/input/v1/input.proto | 1 - api/rust/src/snowcap.rs | 39 ++++++++++++++++++++++ src/api/input/v1.rs | 26 ++++----------- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/api/protobuf/pinnacle/input/v1/input.proto b/api/protobuf/pinnacle/input/v1/input.proto index bb707fbdd..eeb9dcb1b 100644 --- a/api/protobuf/pinnacle/input/v1/input.proto +++ b/api/protobuf/pinnacle/input/v1/input.proto @@ -363,7 +363,6 @@ service InputService { rpc KeybindOnPress(KeybindOnPressRequest) returns (google.protobuf.Empty); rpc MousebindOnPress(MousebindOnPressRequest) returns (google.protobuf.Empty); - rpc Gesturebind(GesturebindRequest) returns (google.protobuf.Empty); rpc GesturebindOnBegin(GesturebindOnBeginRequest) returns (google.protobuf.Empty); rpc GesturebindOnFinish(GesturebindOnFinishRequest) returns (google.protobuf.Empty); diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index b563f7b5c..1e271ba64 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -404,10 +404,49 @@ impl Program for BindOverlay { } }); + let gesturebinds = data.gesturebinds.into_iter().map(|(gesture, descs)| { + if descs.is_empty() { + WidgetDef::from( + Text::new(gesture.to_string()) + .style(text::Style::new().font(self.font.clone())), + ) + } else if descs.len() == 1 { + Row::new_with_children([ + Text::new(gesture.to_string()) + .width(Length::FillPortion(1)) + .style(text::Style::new().font(self.font.clone())) + .into(), + Text::new(descs[0].clone()) + .width(Length::FillPortion(2)) + .style(text::Style::new().font(self.font.clone())) + .into(), + ]) + .into() + } else { + let mut children = Vec::>::new(); + children.push( + Text::new(gesture.to_string() + ":") + .style(text::Style::new().font(self.font.clone())) + .into(), + ); + + for desc in descs { + children.push( + Text::new(format!("\t{desc}")) + .style(text::Style::new().font(self.font.clone())) + .into(), + ); + } + + Column::new_with_children(children).into() + } + }); + let mut children = Vec::>::new(); children.push(group_title.into()); children.extend(keybinds); children.extend(mousebinds); + children.extend(gesturebinds); children.push(Text::new("").style(text::Style::new().pixels(8.0)).into()); // Spacing because I haven't impl'd that yet children diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index a48b9765c..b5f5b8568 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -3,12 +3,12 @@ use pinnacle_api_defs::pinnacle::input::{ self, v1::{ AccelProfile, BindInfo, BindRequest, BindResponse, ClickMethod, EnterBindLayerRequest, - GesturebindOnBeginRequest, GesturebindOnFinishRequest, GesturebindRequest, - GesturebindStreamRequest, GesturebindStreamResponse, GetBindInfosRequest, - GetBindInfosResponse, GetBindLayerStackRequest, GetBindLayerStackResponse, - GetDeviceCapabilitiesRequest, GetDeviceCapabilitiesResponse, GetDeviceInfoRequest, - GetDeviceInfoResponse, GetDeviceTypeRequest, GetDeviceTypeResponse, GetDevicesRequest, - GetDevicesResponse, KeybindOnPressRequest, KeybindStreamRequest, KeybindStreamResponse, + GesturebindOnBeginRequest, GesturebindOnFinishRequest, GesturebindStreamRequest, + GesturebindStreamResponse, GetBindInfosRequest, GetBindInfosResponse, + GetBindLayerStackRequest, GetBindLayerStackResponse, GetDeviceCapabilitiesRequest, + GetDeviceCapabilitiesResponse, GetDeviceInfoRequest, GetDeviceInfoResponse, + GetDeviceTypeRequest, GetDeviceTypeResponse, GetDevicesRequest, GetDevicesResponse, + KeybindOnPressRequest, KeybindStreamRequest, KeybindStreamResponse, MousebindOnPressRequest, MousebindStreamRequest, MousebindStreamResponse, ScrollMethod, SendEventsMode, SetBindPropertiesRequest, SetDeviceLibinputSettingRequest, SetDeviceMapTargetRequest, SetRepeatRateRequest, SetXcursorRequest, SetXkbConfigRequest, @@ -755,20 +755,6 @@ impl input::v1::input_service_server::InputService for InputService { .await } - async fn gesturebind(&self, request: Request) -> TonicResult<()> { - let bind_id = request.into_inner().bind_id; - - run_unary_no_response(&self.sender, move |state| { - state - .pinnacle - .input_state - .bind_state - .gesturebinds - .set_gesturebind_has_on_begin(bind_id); - }) - .await - } - async fn set_xkb_config(&self, request: Request) -> TonicResult<()> { let request = request.into_inner(); From 5f0ac717c9d04ee89e614d2d91e0d28f2e970d47 Mon Sep 17 00:00:00 2001 From: rustysec Date: Tue, 3 Feb 2026 21:27:36 -0800 Subject: [PATCH 12/13] fix swipe direction conversion --- api/lua/pinnacle/input.lua | 28 ++++++++++++++-------------- api/rust/src/input.rs | 4 +++- api/rust/src/snowcap.rs | 1 + src/api/input/v1.rs | 6 +++--- src/input/bind.rs | 10 ++++++++++ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/api/lua/pinnacle/input.lua b/api/lua/pinnacle/input.lua index 72804a9d2..a17919fb7 100644 --- a/api/lua/pinnacle/input.lua +++ b/api/lua/pinnacle/input.lua @@ -434,10 +434,10 @@ end ---@field on_finish fun()? ---@param mb pinnacle.input.gesturebind -local function gesturebind_inner(mb) +local function gesturebind_inner(gb) local modifs = {} local ignore_modifs = {} - for _, mod in ipairs(mb.mods) do + for _, mod in ipairs(gb.mods) do if string.match(mod, "ignore") then table.insert(ignore_modifs, mods_with_ignore_values[mod]) else @@ -449,16 +449,16 @@ local function gesturebind_inner(mb) bind = { mods = modifs, ignore_mods = ignore_modifs, - layer_name = mb.bind_layer, + layer_name = gb.bind_layer, properties = { - group = mb.group, - description = mb.description, - quit = mb.quit, - reload_config = mb.reload_config, - allow_when_locked = mb.allow_when_locked, + group = gb.group, + description = gb.description, + quit = gb.quit, + reload_config = gb.reload_config, + allow_when_locked = gb.allow_when_locked, }, gesture = { - button = gesture_button_values[mb.button], + button = gesture_button_values[gb.button], }, }, }) @@ -476,15 +476,15 @@ local function gesturebind_inner(mb) bind_id = bind_id, }, function(response) if response.edge == edge_values.press then - if mb.on_press then - local success, error = pcall(mb.on_press) + if gb.on_press then + local success, error = pcall(gb.on_press) if not success then log.error("While handling `gesturebind:on_press`: " .. tostring(error)) end end elseif response.edge == edge_values.release then - if mb.on_release then - local success, error = pcall(mb.on_release) + if gb.on_release then + local success, error = pcall(gb.on_release) if not success then log.error("While handling `gesturebind:on_release`: " .. tostring(error)) end @@ -492,7 +492,7 @@ local function gesturebind_inner(mb) end end) - if mb.on_press then + if gb.on_press then local _, err = client:pinnacle_input_v1_InputService_gesturebindOnPress({ bind_id = bind_id, }) diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index 6cf0a71a9..8c9644317 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -552,6 +552,8 @@ pub enum SwipeDirection { UpLeft = 6, /// Moving diagonally up to the right UpRight = 7, + /// No direction + None = 8, /// unknown direction #[num_enum(catch_all)] Unknown(i32), @@ -576,7 +578,7 @@ impl From for pinnacle_api_defs::pinnacle::input::v1::SwipeDirec SwipeDirection::UpRight => { pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::UpRight } - SwipeDirection::Unknown(_) => { + SwipeDirection::None | SwipeDirection::Unknown(_) => { pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::None } } diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index 1e271ba64..fded814e4 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -300,6 +300,7 @@ impl Program for BindOverlay { crate::input::SwipeDirection::DownRight => "Swipe Down-Right", crate::input::SwipeDirection::UpLeft => "Swipe Up-Left", crate::input::SwipeDirection::UpRight => "Swipe Up-Right", + crate::input::SwipeDirection::None => "Swipe None", crate::input::SwipeDirection::Unknown(_) => "Swipe Unknown", } .to_string(), diff --git a/src/api/input/v1.rs b/src/api/input/v1.rs index b5f5b8568..f04b93a12 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -434,8 +434,8 @@ impl input::v1::input_service_server::InputService for InputService { .gesturebinds .id_map .values() - .map(|mousebind| { - let gesturebind = mousebind.borrow(); + .map(|gesturebind| { + let gesturebind = gesturebind.borrow(); let mut mods = Vec::new(); let mut ignore_mods = Vec::new(); @@ -480,7 +480,7 @@ impl input::v1::input_service_server::InputService for InputService { let direction = match gesturebind.gesture_type { GestureType::Hold => SwipeDirection::None, GestureType::Pinch => SwipeDirection::None, - GestureType::Swipe(_) => SwipeDirection::None, + GestureType::Swipe(swipe_direction) => swipe_direction.into(), }; let gesture_type: pinnacle_api_defs::pinnacle::input::v1::GestureType = diff --git a/src/input/bind.rs b/src/input/bind.rs index 3cf503dfe..fb4689305 100644 --- a/src/input/bind.rs +++ b/src/input/bind.rs @@ -54,6 +54,8 @@ impl BindState { bind.borrow_mut().bind_data.group = group; } else if let Some(bind) = self.mousebinds.id_map.get(&bind_id) { bind.borrow_mut().bind_data.group = group; + } else if let Some(bind) = self.gesturebinds.id_map.get(&bind_id) { + bind.borrow_mut().bind_data.group = group; } } @@ -62,6 +64,8 @@ impl BindState { bind.borrow_mut().bind_data.desc = desc; } else if let Some(bind) = self.mousebinds.id_map.get(&bind_id) { bind.borrow_mut().bind_data.desc = desc; + } else if let Some(bind) = self.gesturebinds.id_map.get(&bind_id) { + bind.borrow_mut().bind_data.desc = desc; } } @@ -70,6 +74,8 @@ impl BindState { bind.borrow_mut().bind_data.is_quit_bind = quit; } else if let Some(bind) = self.mousebinds.id_map.get(&bind_id) { bind.borrow_mut().bind_data.is_quit_bind = quit; + } else if let Some(bind) = self.gesturebinds.id_map.get(&bind_id) { + bind.borrow_mut().bind_data.is_quit_bind = quit; } } @@ -78,6 +84,8 @@ impl BindState { bind.borrow_mut().bind_data.is_reload_config_bind = reload_config; } else if let Some(bind) = self.mousebinds.id_map.get(&bind_id) { bind.borrow_mut().bind_data.is_reload_config_bind = reload_config; + } else if let Some(bind) = self.gesturebinds.id_map.get(&bind_id) { + bind.borrow_mut().bind_data.is_reload_config_bind = reload_config; } } @@ -86,6 +94,8 @@ impl BindState { bind.borrow_mut().bind_data.allow_when_locked = allow_when_locked; } else if let Some(bind) = self.mousebinds.id_map.get(&bind_id) { bind.borrow_mut().bind_data.allow_when_locked = allow_when_locked; + } else if let Some(bind) = self.gesturebinds.id_map.get(&bind_id) { + bind.borrow_mut().bind_data.allow_when_locked = allow_when_locked; } } } From 383c7be2cc9636bb2e02d49e128f85c728aec70b Mon Sep 17 00:00:00 2001 From: rustysec Date: Fri, 6 Feb 2026 17:09:50 -0800 Subject: [PATCH 13/13] borrow some niri logic for gestures, a bit of clean up --- Cargo.lock | 2 + Cargo.toml | 1 + src/input.rs | 230 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 167 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79662f089..4479b49c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2286,6 +2286,7 @@ dependencies = [ "bitflags 2.10.0", "input-sys", "libc", + "log", "udev", ] @@ -3464,6 +3465,7 @@ dependencies = [ "drm-sys", "gag", "indexmap", + "input", "itertools", "libdisplay-info", "mlua", diff --git a/Cargo.toml b/Cargo.toml index 75c321628..8dfdc7d96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ cliclack = "0.3.7" drm-sys = "0.8.0" gag = "1.0.0" indexmap = { workspace = true } +input = { version = "0.9.1", features = ["libinput_1_21"] } itertools = "0.14.0" libdisplay-info = "0.3.0" passfd = { workspace = true } diff --git a/src/input.rs b/src/input.rs index 4d65aa124..88732a598 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,11 +8,12 @@ use std::{any::Any, time::Duration}; use crate::{ api::signal::Signal as _, focus::pointer::{PointerContents, PointerFocusTarget}, - input::bind::Edge, + input::bind::{BindAction, Edge}, state::{Pinnacle, WithState}, window::WindowElement, }; use bind::BindState; +use input::event::gesture::GestureEventCoordinates as _; use libinput::LibinputState; use pinnacle_api::input::{GestureType, SwipeDirection}; use smithay::{ @@ -56,6 +57,7 @@ use crate::state::State; pub struct GestureState { pub delta: Option<(f64, f64)>, pub fingers: u32, + pub handled: bool, } #[derive(Default, Debug)] @@ -286,7 +288,7 @@ impl Pinnacle { } impl State { - pub fn process_input_event(&mut self, event: InputEvent) + pub fn process_input_event(&mut self, event: InputEvent) where B::Device: 'static, { @@ -921,6 +923,7 @@ impl State { self.pinnacle.input_state.gesture_state = GestureState { delta: Some((0., 0.)), fingers: event.fingers(), + handled: false, }; pointer.gesture_swipe_begin( @@ -933,16 +936,71 @@ impl State { ); } - fn on_gesture_swipe_update(&mut self, event: I::GestureSwipeUpdateEvent) { + fn on_gesture_swipe_update( + &mut self, + event: I::GestureSwipeUpdateEvent, + ) { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; - use smithay::backend::input::GestureSwipeUpdateEvent as _; + let mods = self + .pinnacle + .seat + .get_keyboard() + .map(|keyboard| keyboard.modifier_state()) + .unwrap_or_default(); + + let mut delta_x = event.delta_x(); + let mut delta_y = event.delta_y(); - let delta = event.delta(); + if let Some(libinput_event) = + (&event as &dyn Any).downcast_ref::() + { + delta_x = libinput_event.dx_unaccelerated(); + delta_y = libinput_event.dy_unaccelerated(); + } + + let device = event.device(); + if let Some(device) = (&device as &dyn Any).downcast_ref::() + && device.config_scroll_natural_scroll_enabled() + { + delta_x = -delta_x; + delta_y = -delta_y; + } + + if let Some((cx, cy)) = &mut self.pinnacle.input_state.gesture_state.delta { + *cx += delta_x; + *cy += delta_y; + + // Check if the gesture moved far enough to decide. Threshold copied from GNOME Shell. + let (cx, cy) = (*cx, *cy); + if cx * cx + cy * cy >= 16. * 16. { + self.pinnacle.input_state.gesture_state.delta = None; + + let direction = delta_to_direction((cx, cy)); + + let current_layer = self.pinnacle.input_state.bind_state.current_layer(); + + let fingers = self.pinnacle.input_state.gesture_state.fingers; + + let bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + GestureType::Swipe(direction), + fingers, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); + + if bind_action != BindAction::Forward { + self.pinnacle.input_state.gesture_state.handled = true; + return; + } + } + } - self.pinnacle.input_state.gesture_state.delta = Some((delta.x, delta.y)); + use smithay::backend::input::GestureSwipeUpdateEvent as _; pointer.gesture_swipe_update( self, @@ -957,31 +1015,14 @@ impl State { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; - let Some(keyboard) = self.pinnacle.seat.get_keyboard() else { - return; - }; - - let mods = keyboard.modifier_state(); - - let current_layer = self.pinnacle.input_state.bind_state.current_layer(); - - if let Some(delta) = self.pinnacle.input_state.gesture_state.delta { - let direction = delta_to_direction(delta); - - let fingers = self.pinnacle.input_state.gesture_state.fingers; - let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - GestureType::Swipe(direction), - fingers, - mods, - Edge::Release, - current_layer, - !self.pinnacle.lock_state.is_unlocked(), - ); + if self.pinnacle.input_state.gesture_state.handled { + self.pinnacle.input_state.gesture_state.delta = None; + self.pinnacle.input_state.gesture_state.fingers = 0; + self.pinnacle.input_state.gesture_state.handled = false; + return; } - self.pinnacle.input_state.gesture_state.delta = None; - pointer.gesture_swipe_end( self, &GestureSwipeEndEvent { @@ -1000,6 +1041,7 @@ impl State { self.pinnacle.input_state.gesture_state = GestureState { delta: Some((0., 0.)), fingers: event.fingers(), + handled: false, }; pointer.gesture_pinch_begin( @@ -1012,16 +1054,69 @@ impl State { ); } - fn on_gesture_pinch_update(&mut self, event: I::GesturePinchUpdateEvent) { + fn on_gesture_pinch_update( + &mut self, + event: I::GesturePinchUpdateEvent, + ) { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; - use smithay::backend::input::GesturePinchUpdateEvent as _; + let mods = self + .pinnacle + .seat + .get_keyboard() + .map(|keyboard| keyboard.modifier_state()) + .unwrap_or_default(); - let delta = event.delta(); + let mut delta_x = event.delta_x(); + let mut delta_y = event.delta_y(); - self.pinnacle.input_state.gesture_state.delta = Some((delta.x, delta.y)); + if let Some(libinput_event) = + (&event as &dyn Any).downcast_ref::() + { + delta_x = libinput_event.dx_unaccelerated(); + delta_y = libinput_event.dy_unaccelerated(); + } + + let device = event.device(); + if let Some(device) = (&device as &dyn Any).downcast_ref::() + && device.config_scroll_natural_scroll_enabled() + { + delta_x = -delta_x; + delta_y = -delta_y; + } + + if let Some((cx, cy)) = &mut self.pinnacle.input_state.gesture_state.delta { + *cx += delta_x; + *cy += delta_y; + + // Check if the gesture moved far enough to decide. Threshold copied from GNOME Shell. + let (cx, cy) = (*cx, *cy); + if cx * cx + cy * cy >= 16. * 16. { + self.pinnacle.input_state.gesture_state.delta = None; + + let current_layer = self.pinnacle.input_state.bind_state.current_layer(); + + let fingers = self.pinnacle.input_state.gesture_state.fingers; + + let bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + GestureType::Pinch, + fingers, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); + + if bind_action != BindAction::Forward { + self.pinnacle.input_state.gesture_state.handled = true; + return; + } + } + } + + use smithay::backend::input::GesturePinchUpdateEvent as _; pointer.gesture_pinch_update( self, @@ -1038,29 +1133,14 @@ impl State { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; - let Some(keyboard) = self.pinnacle.seat.get_keyboard() else { - return; - }; - let mods = keyboard.modifier_state(); - - let current_layer = self.pinnacle.input_state.bind_state.current_layer(); - - if self.pinnacle.input_state.gesture_state.delta.is_some() { - let fingers = self.pinnacle.input_state.gesture_state.fingers; - - let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - GestureType::Pinch, - fingers, - mods, - Edge::Release, - current_layer, - !self.pinnacle.lock_state.is_unlocked(), - ); + if self.pinnacle.input_state.gesture_state.handled { + self.pinnacle.input_state.gesture_state.delta = None; + self.pinnacle.input_state.gesture_state.fingers = 0; + self.pinnacle.input_state.gesture_state.handled = false; + return; } - self.pinnacle.input_state.gesture_state.delta = None; - pointer.gesture_pinch_end( self, &GesturePinchEndEvent { @@ -1079,6 +1159,7 @@ impl State { self.pinnacle.input_state.gesture_state = GestureState { delta: Some((0., 0.)), fingers: event.fingers(), + handled: false, }; pointer.gesture_hold_begin( @@ -1091,32 +1172,49 @@ impl State { ); } - fn on_gesture_hold_end(&mut self, event: I::GestureHoldEndEvent) { + fn on_gesture_hold_end(&mut self, event: I::GestureHoldEndEvent) { let Some(pointer) = self.pinnacle.seat.get_pointer() else { return; }; - let Some(keyboard) = self.pinnacle.seat.get_keyboard() else { - return; - }; - let mods = keyboard.modifier_state(); + let mods = self + .pinnacle + .seat + .get_keyboard() + .map(|keyboard| keyboard.modifier_state()) + .unwrap_or_default(); let current_layer = self.pinnacle.input_state.bind_state.current_layer(); - if self.pinnacle.input_state.gesture_state.delta.is_some() { + let mut handled = false; + + if let Some(libinput_event) = + (&event as &dyn Any).downcast_ref::() + && self.pinnacle.input_state.gesture_state.delta.is_some() + { let fingers = self.pinnacle.input_state.gesture_state.fingers; - let _bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( - GestureType::Hold, - fingers, - mods, - Edge::Release, - current_layer, - !self.pinnacle.lock_state.is_unlocked(), - ); + if fingers == libinput_event.fingers() { + let bind_action = self.pinnacle.input_state.bind_state.gesturebinds.gesture( + GestureType::Hold, + fingers, + mods, + Edge::Release, + current_layer, + !self.pinnacle.lock_state.is_unlocked(), + ); + + handled = bind_action != BindAction::Forward; + } } self.pinnacle.input_state.gesture_state.delta = None; + self.pinnacle.input_state.gesture_state.fingers = 0; + self.pinnacle.input_state.gesture_state.handled = false; + + if handled { + return; + } pointer.gesture_hold_end( self,