Skip to content

Commit fdf1f1b

Browse files
committed
feat: Gesture configuration in shortcuts config
This commit adds a Gesture abstraction, which can define gestures by the number of fingers used, and the direction the gesture is performed in. The abstraction is modeled after the Binding type, and the resulting Gestures type is similar to the Shortcuts type. This is the first step in implementing configurable touchpad gestures (pop-os/cosmic-comp#396) Signed-off-by: Ryan Brue <[email protected]>
1 parent ed1bc9e commit fdf1f1b

File tree

3 files changed

+265
-1
lines changed

3 files changed

+265
-1
lines changed

config/src/shortcuts/action.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: MPL-2.0
22
#![allow(deprecated)] // Derives on deprecated variants produce warnings...
33

4+
use std::str::FromStr;
5+
46
use serde::{Deserialize, Serialize};
57

68
/// An operation which may be bound to a keyboard shortcut.
@@ -198,8 +200,39 @@ pub enum System {
198200
WorkspaceOverview,
199201
}
200202

203+
/// Defines the number of fingers used for a gesture
204+
#[derive(Copy, Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, Hash)]
205+
pub enum FingerCount {
206+
Three,
207+
Four,
208+
Five,
209+
}
210+
211+
impl From<FingerCount> for &'static str {
212+
fn from(count: FingerCount) -> Self {
213+
match count {
214+
FingerCount::Three => "3",
215+
FingerCount::Four => "4",
216+
FingerCount::Five => "5",
217+
}
218+
}
219+
}
220+
221+
impl FromStr for FingerCount {
222+
type Err = String;
223+
224+
fn from_str(s: &str) -> Result<Self, Self::Err> {
225+
match s {
226+
"3" => Ok(Self::Three),
227+
"4" => Ok(Self::Four),
228+
"5" => Ok(Self::Five),
229+
_ => return Err(format!("String {} cannot be converted to finger count.", s)),
230+
}
231+
}
232+
}
233+
201234
/// Defines the direction of an operation
202-
#[derive(Copy, Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
235+
#[derive(Copy, Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, Hash)]
203236
pub enum Direction {
204237
Left,
205238
Right,
@@ -219,6 +252,31 @@ impl std::ops::Not for Direction {
219252
}
220253
}
221254

255+
impl From<Direction> for &'static str {
256+
fn from(direction: Direction) -> Self {
257+
match direction {
258+
Direction::Left => "Left",
259+
Direction::Right => "Right",
260+
Direction::Up => "Up",
261+
Direction::Down => "Down",
262+
}
263+
}
264+
}
265+
266+
impl FromStr for Direction {
267+
type Err = String;
268+
269+
fn from_str(s: &str) -> Result<Self, Self::Err> {
270+
match s {
271+
"Down" => Ok(Self::Down),
272+
"Up" => Ok(Self::Up),
273+
"Left" => Ok(Self::Left),
274+
"Right" => Ok(Self::Right),
275+
_ => return Err(format!("String {} cannot be converted to direction.", s)),
276+
}
277+
}
278+
}
279+
222280
/// Defines the direction to focus towards
223281
#[derive(Copy, Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
224282
pub enum FocusDirection {

config/src/shortcuts/gesture.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
use std::{fmt::Display, str::FromStr};
3+
4+
use serde::{Deserialize, Serialize};
5+
use thiserror::Error;
6+
7+
use super::action::{Direction, FingerCount};
8+
9+
/// Description of a gesture that can be handled by the compositor
10+
#[serde_with::serde_as]
11+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash, Ord, PartialOrd)]
12+
#[serde(deny_unknown_fields)]
13+
pub struct Gesture {
14+
/// How many fingers are held down
15+
pub fingers: FingerCount,
16+
pub direction: Direction,
17+
// A custom description for a custom binding
18+
#[serde(default, skip_serializing_if = "Option::is_none")]
19+
pub description: Option<String>,
20+
}
21+
22+
impl Gesture {
23+
/// Creates a new gesture from a number of fingers and a direction
24+
pub fn new(fingers: FingerCount, direction: Direction) -> Gesture {
25+
Gesture {
26+
fingers,
27+
direction,
28+
description: None,
29+
}
30+
}
31+
}
32+
33+
impl Display for Gesture {
34+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35+
write!(
36+
f,
37+
"{} Finger {}",
38+
<&'static str>::from(self.fingers),
39+
<&'static str>::from(self.direction)
40+
)
41+
}
42+
}
43+
44+
#[derive(Error, Debug, PartialEq)]
45+
pub enum GestureParseError {
46+
#[error("Expected value for the number of fingers")]
47+
NoFingerValue,
48+
#[error("Invalid finger value `{0}`")]
49+
InvalidFingerValue(String),
50+
#[error("Expected value for the direction")]
51+
NoDirectionValue,
52+
#[error("Invalid direction value `{0}`")]
53+
InvalidDirectionValue(String),
54+
#[error("Received unknown extra data `{0}`")]
55+
ExtraData(String),
56+
}
57+
58+
impl FromStr for Gesture {
59+
type Err = GestureParseError;
60+
61+
fn from_str(value: &str) -> Result<Self, Self::Err> {
62+
let mut value_iter = value.split("+");
63+
64+
let Some(n) = value_iter.next() else {
65+
return Err(GestureParseError::NoFingerValue);
66+
};
67+
let Ok(fingers) = FingerCount::from_str(n) else {
68+
return Err(GestureParseError::InvalidFingerValue(n.to_string()));
69+
};
70+
71+
let Some(n2) = value_iter.next() else {
72+
return Err(GestureParseError::NoDirectionValue);
73+
};
74+
75+
let Ok(direction) = Direction::from_str(n2) else {
76+
return Err(GestureParseError::InvalidDirectionValue(n2.to_string()));
77+
};
78+
79+
if let Some(n3) = value_iter.next() {
80+
return Err(GestureParseError::ExtraData(n3.to_string()));
81+
}
82+
83+
return Ok(Self {
84+
fingers,
85+
direction,
86+
description: None,
87+
});
88+
}
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
94+
use crate::shortcuts::action::{Direction, FingerCount};
95+
96+
use super::Gesture;
97+
use std::str::FromStr;
98+
99+
#[test]
100+
fn binding_from_str() {
101+
assert_eq!(
102+
Gesture::from_str("3+Left"),
103+
Ok(Gesture::new(FingerCount::Three, Direction::Left))
104+
);
105+
106+
assert_eq!(
107+
Gesture::from_str("5+Up"),
108+
Ok(Gesture::new(FingerCount::Five, Direction::Up))
109+
);
110+
111+
assert_ne!(
112+
Gesture::from_str("4+Left+More+Info"),
113+
Ok(Gesture::new(FingerCount::Four, Direction::Left))
114+
);
115+
}
116+
}

config/src/shortcuts/mod.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ pub use action::Action;
55

66
pub mod modifier;
77

8+
use action::{Direction, FingerCount};
89
pub use modifier::{Modifier, Modifiers, ModifiersDef};
910

1011
mod binding;
12+
mod gesture;
1113
pub use binding::Binding;
14+
pub use gesture::Gesture;
1215

1316
pub mod sym;
1417

@@ -49,6 +52,33 @@ pub fn shortcuts(context: &cosmic_config::Config) -> Shortcuts {
4952
shortcuts
5053
}
5154

55+
/// Get the current system gesture configuration
56+
///
57+
/// Merges user-defined custom gestures to the system default config
58+
pub fn gestures(context: &cosmic_config::Config) -> Gestures {
59+
// Load gestures defined by the system.
60+
let mut gestures = context
61+
.get::<Gestures>("default_gestures")
62+
.unwrap_or_else(|why| {
63+
if why.is_err() {
64+
tracing::error!("shortcuts defaults config error: {why:?}");
65+
}
66+
Gestures::default()
67+
});
68+
69+
// Load custom gestures defined by the user.
70+
let custom_gestures = context
71+
.get::<Gestures>("custom_gestures")
72+
.unwrap_or_else(|why| {
73+
tracing::error!("shortcuts custom config error: {why:?}");
74+
Gestures::default()
75+
});
76+
77+
// Combine while overriding system gestures.
78+
gestures.0.extend(custom_gestures.0);
79+
gestures
80+
}
81+
5282
/// Get a map of system actions and their configured commands
5383
pub fn system_actions(context: &cosmic_config::Config) -> SystemActions {
5484
let mut config = SystemActions::default();
@@ -80,6 +110,8 @@ pub fn system_actions(context: &cosmic_config::Config) -> SystemActions {
80110
pub struct Config {
81111
pub defaults: Shortcuts,
82112
pub custom: Shortcuts,
113+
pub default_gestures: Gestures,
114+
pub custom_gestures: Gestures,
83115
pub system_actions: SystemActions,
84116
}
85117

@@ -97,6 +129,18 @@ impl Config {
97129
.shortcut_for_action(action)
98130
.or_else(|| self.defaults.shortcut_for_action(action))
99131
}
132+
133+
pub fn gestures(&self) -> impl Iterator<Item = (&Gesture, &Action)> {
134+
self.custom_gestures
135+
.iter()
136+
.chain(self.default_gestures.iter())
137+
}
138+
139+
pub fn gesture_for_action(&self, action: &Action) -> Option<String> {
140+
self.custom_gestures
141+
.gesture_for_action(action)
142+
.or_else(|| self.default_gestures.gesture_for_action(action))
143+
}
100144
}
101145

102146
/// A map of defined key [Binding]s and their triggerable [Action]s
@@ -261,3 +305,49 @@ impl<'de> Deserialize<'de> for SystemActionsImpl {
261305
deserializer.deserialize_map(SystemActionsMapVisitor)
262306
}
263307
}
308+
309+
/// A map of defined [Gesture]s and their triggerable [Action]s
310+
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
311+
#[serde(transparent)]
312+
pub struct Gestures(pub BTreeMap<Gesture, Action>);
313+
314+
impl Gestures {
315+
pub fn insert_default_gesture(
316+
&mut self,
317+
fingers: FingerCount,
318+
direction: Direction,
319+
action: Action,
320+
) {
321+
if !self.0.values().any(|a| a == &action) {
322+
let pattern = Gesture {
323+
description: None,
324+
fingers,
325+
direction,
326+
};
327+
if !self.0.contains_key(&pattern) {
328+
self.0.insert(pattern, action.clone());
329+
}
330+
}
331+
}
332+
333+
pub fn iter(&self) -> impl Iterator<Item = (&Gesture, &Action)> {
334+
self.0.iter()
335+
}
336+
337+
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Gesture, &mut Action)> {
338+
self.0.iter_mut()
339+
}
340+
341+
pub fn gesture_for_action(&self, action: &Action) -> Option<String> {
342+
self.gestures(action)
343+
.next() // take the first one
344+
.map(|gesture| gesture.to_string())
345+
}
346+
347+
pub fn gestures<'a>(&'a self, action: &'a Action) -> impl Iterator<Item = &'a Gesture> {
348+
self.0
349+
.iter()
350+
.filter(move |(_, a)| *a == action)
351+
.map(|(b, _)| b)
352+
}
353+
}

0 commit comments

Comments
 (0)