diff --git a/Cargo.lock b/Cargo.lock index 798f75466..b363d123f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,6 +2292,7 @@ dependencies = [ "bitflags 2.10.0", "input-sys", "libc", + "log", "udev", ] @@ -3470,6 +3471,7 @@ dependencies = [ "drm-sys", "gag", "indexmap", + "input", "itertools", "libdisplay-info", "mlua", diff --git a/Cargo.toml b/Cargo.toml index 457f2d31c..ff9f6edef 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/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index c8e3c505b..ac4c0b150 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 @@ -1431,6 +1471,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 = {} @@ -1642,6 +1688,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 @@ -1850,6 +1898,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" @@ -1884,6 +1951,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..a17919fb7 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(gb) + local modifs = {} + local ignore_modifs = {} + for _, mod in ipairs(gb.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 = gb.bind_layer, + properties = { + 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[gb.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 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 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 + end + end + end) + + if gb.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 98249b244..eeb9dcb1b 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,51 @@ message MousebindOnPressRequest { uint32 bind_id = 1; } +// Gesturebinds + +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 { + GESTURE_TYPE_HOLD = 0; + GESTURE_TYPE_PINCH = 1; + GESTURE_TYPE_SWIPE = 2; +} + +message Gesturebind { + SwipeDirection direction = 1; + uint32 fingers = 2; + GestureType gesture_type = 3; +} + +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 +359,12 @@ 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 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..8c9644317 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, @@ -176,6 +177,11 @@ impl BindLayer { new_mousebind(mods, button, self).block_on_tokio() } + /// Creates a gesturebind on this layer. + 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. pub fn enter(&self) { Client::input() @@ -524,6 +530,228 @@ async fn new_mousebind_stream( send } +// 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, + /// No direction + None = 8, + /// 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::None | 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. +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, gesture_type: GestureType, fingers: u32) -> Gesturebind { + BindLayer::DEFAULT.gesturebind(mods, gesture_type, 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_gesturebind_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, + gesture_type: GestureType, + fingers: u32, + 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: match gesture_type { + GestureType::Hold | GestureType::Pinch => { + pinnacle_api_defs::pinnacle::input::v1::SwipeDirection::None.into() + } + GestureType::Swipe(direction) => direction.into(), + }, + fingers, + 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() + } + }, + })), + }), + }) + .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 +927,13 @@ pub enum BindInfoKind { /// Which mouse button this bind uses. button: MouseButton, }, + /// This is a gesturebind. + Gesture { + /// Direction of the gesture. + direction: SwipeDirection, + /// Fingers used in the gesture. + fingers: u32, + }, } /// Sets the keyboard's repeat rate. @@ -845,6 +1080,10 @@ 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: SwipeDirection::from(gesturebind.direction), + fingers: gesturebind.fingers, + }, }; let layer = BindLayer { diff --git a/api/rust/src/snowcap.rs b/api/rust/src/snowcap.rs index 99064287c..fded814e4 100644 --- a/api/rust/src/snowcap.rs +++ b/api/rust/src/snowcap.rs @@ -202,12 +202,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 +288,31 @@ impl Program for BindOverlay { descs.push(desc); } } + BindInfoKind::Gesture { direction, fingers } => { + let repr = GesturebindRepr { + mods, + direction: match direction { + 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::None => "Swipe None", + crate::input::SwipeDirection::Unknown(_) => "Swipe Unknown", + } + .to_string(), + fingers: format!("{fingers:?} fingers"), + layer, + }; + + let descs = group.gesturebinds.entry(repr).or_default(); + if !desc.is_empty() { + descs.push(desc); + } + } } } @@ -350,10 +405,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 241debc5b..f04b93a12 100644 --- a/src/api/input/v1.rs +++ b/src/api/input/v1.rs @@ -1,16 +1,19 @@ +use pinnacle_api::input::GestureType; use pinnacle_api_defs::pinnacle::input::{ self, v1::{ AccelProfile, BindInfo, BindRequest, BindResponse, ClickMethod, EnterBindLayerRequest, - 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, 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; @@ -37,6 +40,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 +164,39 @@ impl input::v1::input_service_server::InputService for InputService { bind_id } + input::v1::bind::Bind::Gesture(gesturebind) => { + let fingers = gesturebind.fingers; + + 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( + gesture_type, + fingers, + mods, + layer, + group, + desc, + quit, + reload_config, + allow_when_locked, + ); + + bind_id + } }; Ok(BindResponse { bind_id }) @@ -235,6 +272,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, @@ -389,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(|gesturebind| { + let gesturebind = gesturebind.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(swipe_direction) => swipe_direction.into(), + }; + + 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 @@ -517,6 +643,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 +721,40 @@ 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_finish(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..88732a598 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,11 +8,14 @@ use std::{any::Any, time::Duration}; use crate::{ api::signal::Signal as _, focus::pointer::{PointerContents, PointerFocusTarget}, + 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::{ backend::{ input::{ @@ -50,10 +53,18 @@ use tracing::{error, info}; use crate::state::State; +#[derive(Default, Debug)] +pub struct GestureState { + pub delta: Option<(f64, f64)>, + pub fingers: u32, + pub handled: bool, +} + #[derive(Default, Debug)] pub struct InputState { pub bind_state: BindState, pub libinput_state: LibinputState, + pub gesture_state: GestureState, } impl InputState { @@ -277,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, { @@ -909,6 +920,12 @@ impl State { return; }; + self.pinnacle.input_state.gesture_state = GestureState { + delta: Some((0., 0.)), + fingers: event.fingers(), + handled: false, + }; + pointer.gesture_swipe_begin( self, &GestureSwipeBeginEvent { @@ -919,11 +936,70 @@ 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; }; + 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(); + + 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; + } + } + } + use smithay::backend::input::GestureSwipeUpdateEvent as _; pointer.gesture_swipe_update( @@ -940,6 +1016,13 @@ impl State { return; }; + 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; + } + pointer.gesture_swipe_end( self, &GestureSwipeEndEvent { @@ -955,6 +1038,12 @@ impl State { return; }; + self.pinnacle.input_state.gesture_state = GestureState { + delta: Some((0., 0.)), + fingers: event.fingers(), + handled: false, + }; + pointer.gesture_pinch_begin( self, &GesturePinchBeginEvent { @@ -965,11 +1054,68 @@ 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; }; + 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(); + + 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( @@ -988,6 +1134,13 @@ impl State { return; }; + 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; + } + pointer.gesture_pinch_end( self, &GesturePinchEndEvent { @@ -1003,6 +1156,12 @@ impl State { return; }; + self.pinnacle.input_state.gesture_state = GestureState { + delta: Some((0., 0.)), + fingers: event.fingers(), + handled: false, + }; + pointer.gesture_hold_begin( self, &GestureHoldBeginEvent { @@ -1013,11 +1172,50 @@ 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 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(); + + 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; + + 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, &GestureHoldEndEvent { @@ -1326,6 +1524,31 @@ fn constrain_point_inside_rects( .unwrap_or(pos) } +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) { + SwipeDirection::Right + } else if (22.5..67.5).contains(&angle_deg) { + SwipeDirection::DownRight + } else if (67.5..112.5).contains(&angle_deg) { + SwipeDirection::Down + } else if (112.5..157.5).contains(&angle_deg) { + SwipeDirection::DownLeft + } else if (157.5..202.5).contains(&angle_deg) { + SwipeDirection::Left + } else if (202.5..247.5).contains(&angle_deg) { + SwipeDirection::UpLeft + } else if (247.5..292.5).contains(&angle_deg) { + SwipeDirection::Up + } else { + SwipeDirection::UpRight + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/input/bind.rs b/src/input/bind.rs index 1b038b582..fb4689305 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 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 { @@ -25,6 +27,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) { @@ -50,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; } } @@ -58,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; } } @@ -66,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; } } @@ -74,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; } } @@ -82,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; } } } @@ -525,3 +539,148 @@ impl Mousebinds { mousebind.borrow_mut().has_on_press = true; } } + +// Gesturebinds + +#[derive(Debug)] +pub struct Gesturebind { + pub bind_data: BindData, + pub fingers: u32, + pub gesture_type: GestureType, + sender: UnboundedSender, + pub recv: Option>, + pub has_on_begin: bool, +} + +#[derive(Debug, Default)] +pub struct Gesturebinds { + pub id_map: IndexMap>>, + gesture_map: IndexMap<(GestureType, u32), Vec>>>, + + pub last_pressed_triggered_binds: HashMap<(GestureType, u32), 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, + gesture_type: GestureType, + fingers: u32, + mods: ModifiersState, + edge: Edge, + current_layer: Option, + is_locked: bool, + ) -> BindAction { + let Some(gesturebinds) = self.gesture_map.get_mut(&(gesture_type, fingers)) else { + return BindAction::Forward; + }; + + if edge == Edge::Release { + let mut bind_action = BindAction::Forward; + + for gesturebind in gesturebinds { + let Some(gesturebind) = gesturebind.upgrade() else { + continue; + }; + + if !gesturebind.borrow().bind_data.mods.matches(mods) { + continue; + } + + if gesturebind.borrow().gesture_type != gesture_type { + continue; + } + + if gesturebind.borrow().bind_data.layer != current_layer { + continue; + } + + if gesturebind.borrow().bind_data.is_quit_bind { + return BindAction::Quit; + } + 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; + } + let _sent = gesturebind.borrow().sender.send(Edge::Release).is_ok(); + } + + return bind_action; + } + + BindAction::Forward + } + + pub fn add_gesturebind( + &mut self, + gesture_type: GestureType, + fingers: u32, + 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, + }, + gesture_type, + 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((gesture_type, 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; + } + + 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; + } +}