Skip to content
Open
10 changes: 10 additions & 0 deletions rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ source_devices:
name: AT Translated Set 2 keyboard
phys_path: isa0060/serio0/input0
handler: event*
- group: touchscreen
udev:
properties:
- name: ID_INPUT_TOUCHSCREEN
value: "1"
sys_name: "event*"
subsystem: input
config:
touchscreen:
grab: true
- group: imu
iio:
name: "{i2c-BMI0160:00,bmi260}"
Expand Down
11 changes: 11 additions & 0 deletions rootfs/usr/share/inputplumber/devices/50-onexplayer_x1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ source_devices:
- group: imu
iio:
name: "{i2c-BMI0260:00,bmi260}"
- group: touchscreen
udev:
properties:
- name: ID_INPUT_TOUCHSCREEN
value: "1"
sys_name: "event*"
subsystem: input
config:
touchscreen:
grab: true
orientation: "left"

# The target input device(s) that the virtual device profile can use
target_devices:
Expand Down
14 changes: 14 additions & 0 deletions rootfs/usr/share/inputplumber/profiles/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,17 @@ mapping:
direction: clockwise
target_events:
- keyboard: KeyBrightnessUp
- name: Swipe Right from Bottom-Left
source_event:
touchscreen:
gesture: SwipeRight:Bottom
target_events:
- gamepad:
button: Guide
- name: Swipe Left from Bottom-Right
source_event:
touchscreen:
gesture: SwipeLeft:Bottom
target_events:
- gamepad:
button: QuickAccess
4 changes: 4 additions & 0 deletions rootfs/usr/share/inputplumber/schema/composite_device_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@
"type": "object",
"additionalProperties": false,
"properties": {
"grab": {
"description": "If true, the touchscreen will be exclusively grabbed so raw touch events are not passed to other consumers. Defaults to false.",
"type": "boolean"
},
"orientation": {
"description": "Orientation of the touchscreen device. Defaults to normal.",
"type": "string",
Expand Down
14 changes: 14 additions & 0 deletions rootfs/usr/share/inputplumber/schema/device_profile_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,20 @@
"Touch",
"Press"
]
},
"gesture": {
"type": "string",
"description": "Edge-swipe gesture type. Left/right swipes support an optional area suffix (:Top, :Bottom, :Any).",
"examples": [
"SwipeRight:Top",
"SwipeRight:Bottom",
"SwipeRight:Any",
"SwipeLeft:Top",
"SwipeLeft:Bottom",
"SwipeLeft:Any",
"SwipeUp",
"SwipeDown"
]
}
},
"required": []
Expand Down
3 changes: 3 additions & 0 deletions src/config/capability_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ pub struct TouchCapability {
pub button: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub motion: Option<TouchMotionCapability>,
/// Edge-swipe gesture type
#[serde(skip_serializing_if = "Option::is_none")]
pub gesture: Option<String>,
}

#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema, PartialEq)]
Expand Down
6 changes: 6 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ pub struct TouchscreenConfig {
/// instead of the size advertised by the device itself.
#[serde(skip_serializing_if = "Option::is_none")]
pub override_source_size: Option<bool>,
/// If true, the device will be grabbed exclusively. Touch events will be
/// fully managed by InputPlumber; a virtual touchscreen target device will
/// be created and edge-swipe gestures will suppress raw touch delivery.
/// Defaults to false (pass-through mode).
#[serde(skip_serializing_if = "Option::is_none")]
pub grab: Option<bool>,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
Expand Down
116 changes: 116 additions & 0 deletions src/input/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,30 +69,50 @@ impl Capability {
Touch::Button(touch_button) => {
format!("Touchpad:LeftPad:Touch:Button:{touch_button}")
}
Touch::Gesture(g) => format!("Touchpad:LeftPad:Touch:Gesture:{g}"),
},
Touchpad::RightPad(touch) => match touch {
Touch::Motion => "Touchpad:RightPad:Touch:Motion".to_string(),
Touch::Button(touch_button) => {
format!("Touchpad:RightPad:Touch:Button:{touch_button}")
}
Touch::Gesture(g) => format!("Touchpad:RightPad:Touch:Gesture:{g}"),
},
Touchpad::CenterPad(touch) => match touch {
Touch::Motion => "Touchpad:CenterPad:Touch:Motion".to_string(),
Touch::Button(touch_button) => {
format!("Touchpad:CenterPad:Touch:Button:{touch_button}")
}
Touch::Gesture(g) => format!("Touchpad:CenterPad:Touch:Gesture:{g}"),
},
},
Capability::Touchscreen(touch) => match touch {
Touch::Motion => "Touchscreen:Touch:Motion".to_string(),
Touch::Button(touch_button) => {
format!("Touchscreen:Touch:Button:{touch_button}")
}
Touch::Gesture(g) => format!("Touchscreen:Touch:Gesture:{g}"),
},
Capability::Gyroscope(source) => format!("Gyroscope:{source}"),
Capability::Accelerometer(source) => format!("Accelerometer:{source}"),
}
}

/// If this capability is a touchscreen gesture with a specific area (Top or
/// Bottom), return the same gesture with `GestureArea::Any`. Returns `None`
/// for gestures that have no area concept (SwipeUp/SwipeDown) or for
/// non-gesture capabilities.
pub fn with_gesture_area_any(&self) -> Option<Capability> {
let Capability::Touchscreen(Touch::Gesture(gesture)) = self else {
return None;
};
let any_gesture = match gesture {
GestureType::Right(_) => GestureType::Right(GestureArea::Any),
GestureType::Left(_) => GestureType::Left(GestureArea::Any),
GestureType::Up | GestureType::Down => return None,
};
Some(Capability::Touchscreen(Touch::Gesture(any_gesture)))
}
}

impl fmt::Display for Capability {
Expand Down Expand Up @@ -332,6 +352,17 @@ impl From<CapabilityConfig> for Capability {
let button = button.unwrap();
return Capability::Touchscreen(Touch::Button(button));
}

// Gesture
if let Some(gesture_string) = touch.gesture.as_ref() {
let gesture = GestureType::from_str(gesture_string);
if gesture.is_err() {
log::error!("Invalid or unimplemented gesture: {gesture_string}");
return Capability::NotImplemented;
}
let gesture = gesture.unwrap();
return Capability::Touchscreen(Touch::Gesture(gesture));
}
}

// Gyroscope
Expand Down Expand Up @@ -1350,17 +1381,99 @@ impl FromStr for Touchpad {
}
}

/// Area on the screen where a gesture starts, used to differentiate regions
/// for left/right swipes. `Any` is used only in profile configurations as a
/// wildcard that matches both Top and Bottom.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum GestureArea {
/// Upper portion of the screen
Top,
/// Lower portion of the screen
Bottom,
/// Wildcard used in profile mappings; matches both Top and Bottom
Any,
}

impl fmt::Display for GestureArea {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GestureArea::Top => write!(f, "Top"),
GestureArea::Bottom => write!(f, "Bottom"),
GestureArea::Any => write!(f, "Any"),
}
}
}

impl FromStr for GestureArea {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Top" => Ok(GestureArea::Top),
"Bottom" => Ok(GestureArea::Bottom),
"Any" => Ok(GestureArea::Any),
_ => Err(()),
}
}
}

/// Touchscreen edge swipe gestures detected in userspace from evdev MT events
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum GestureType {
/// Swipe inward from the left edge
Right(GestureArea),
/// Swipe inward from the right edge
Left(GestureArea),
/// Swipe inward from the bottom edge
Up,
/// Swipe inward from the top edge
Down,
}

impl fmt::Display for GestureType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GestureType::Right(area) => write!(f, "SwipeRight:{area}"),
GestureType::Left(area) => write!(f, "SwipeLeft:{area}"),
GestureType::Up => write!(f, "SwipeUp"),
GestureType::Down => write!(f, "SwipeDown"),
}
}
}

impl FromStr for GestureType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(':').collect();
let Some((part, rest)) = parts.split_first() else {
return Err(());
};
match *part {
"SwipeRight" => Ok(GestureType::Right(GestureArea::from_str(
rest.join(":").as_str(),
)?)),
"SwipeLeft" => Ok(GestureType::Left(GestureArea::from_str(
rest.join(":").as_str(),
)?)),
"SwipeUp" => Ok(GestureType::Up),
"SwipeDown" => Ok(GestureType::Down),
_ => Err(()),
}
}
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Touch {
Motion,
Button(TouchButton),
Gesture(GestureType),
}

impl fmt::Display for Touch {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Touch::Motion => write!(f, "Motion"),
Touch::Button(_) => write!(f, "Button"),
Touch::Gesture(g) => write!(f, "Gesture:{g}"),
}
}
}
Expand All @@ -1377,6 +1490,9 @@ impl FromStr for Touch {
"Button" => Ok(Touch::Button(TouchButton::from_str(
parts.join(":").as_str(),
)?)),
"Gesture" => Ok(Touch::Gesture(GestureType::from_str(
parts.join(":").as_str(),
)?)),
_ => Err(()),
}
}
Expand Down
89 changes: 88 additions & 1 deletion src/input/composite_device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ impl CompositeDevice {
let (tx, rx) = mpsc::channel(BUFFER_SIZE);
let name = config.name.clone();
let dbus = DBusInterfaceManager::new(conn.clone(), dbus_path.clone())?;
let targets = CompositeDeviceTargets::new(conn, dbus_path, tx.clone().into(), manager, &config);
let mut device = Self {
dbus,
config,
Expand Down Expand Up @@ -205,7 +206,7 @@ impl CompositeDevice {
source_device_tasks: JoinSet::new(),
source_device_persistent_ids: HashMap::new(),
source_devices_used: Vec::new(),
targets: CompositeDeviceTargets::new(conn, dbus_path, tx.into(), manager),
targets,
ff_enabled: true,
ff_effect_ids: (0..64).collect(),
ff_effect_id_source_map: HashMap::new(),
Expand Down Expand Up @@ -1563,6 +1564,92 @@ impl CompositeDevice {
return Ok(events);
}

// For gesture capabilities with a specific area (Top/Bottom), fall back
// to any GestureArea::Any mapping configured in the profile.
if let Some(any_cap) = source_cap.with_gesture_area_any() {
if let Some(mappings) = self.device_profile_config_map.get(&any_cap) {
let matched_mappings = mappings
.iter()
.filter(|mapping| mapping.source_matches_properties(event));

let mut events = Vec::new();
for mapping in matched_mappings {
log::trace!(
"Found Any-area translation for gesture {:?} via mapping: {}",
source_cap,
mapping.name
);

for target_event in mapping.target_events.iter() {
let target_cap: Capability = target_event.clone().into();
let result = event.get_value().translate(
&source_cap,
&mapping.source_event,
&target_cap,
target_event,
);
let value = match result {
Ok(v) => v,
Err(err) => {
match err {
TranslationError::NotImplemented => {
log::warn!(
"Translation not implemented for Any-area mapping '{}': {:?} -> {:?}",
mapping.name,
source_cap,
target_cap,
);
continue;
}
TranslationError::ImpossibleTranslation(msg) => {
log::warn!(
"Impossible translation for Any-area mapping '{}': {msg}",
mapping.name
);
continue;
}
TranslationError::InvalidSourceConfig(msg) => {
log::warn!("Invalid source event config in Any-area mapping '{}': {msg}", mapping.name);
continue;
}
TranslationError::InvalidTargetConfig(msg) => {
log::warn!("Invalid target event config in Any-area mapping '{}': {msg}", mapping.name);
continue;
}
}
}
};
if matches!(value, InputValue::None) {
continue;
}

if source_cap.is_momentary_translation(&target_cap) {
events.push(NativeEvent::new_translated(
source_cap.clone(),
target_cap.clone(),
InputValue::Bool(true),
));
events.push(NativeEvent::new_translated(
source_cap.clone(),
target_cap,
InputValue::Bool(false),
));
continue;
}

events.push(NativeEvent::new_translated(
source_cap.clone(),
target_cap,
value,
));
}
}
if !events.is_empty() {
return Ok(events);
}
}
}

log::trace!("No translation mapping found for event: {:?}", source_cap);
Ok(vec![event.clone()])
}
Expand Down
Loading
Loading