Skip to content

feat(Touchscreen): edge-swipe gesture detection for handheld#571

Open
honjow wants to merge 8 commits into
ShadowBlip:mainfrom
honjow:pr/touchscreen-gestures
Open

feat(Touchscreen): edge-swipe gesture detection for handheld#571
honjow wants to merge 8 commits into
ShadowBlip:mainfrom
honjow:pr/touchscreen-gestures

Conversation

@honjow
Copy link
Copy Markdown
Contributor

@honjow honjow commented Mar 29, 2026

Summary

Adds single-finger edge-swipe gesture detection to the touchscreen source device, allowing touchscreen edges to be mapped to gamepad buttons via device profiles — enabling gestures like swiping from the left edge to open the Steam menu, or from the right edge to open the Quick Access Menu, without requiring changes to the compositor.

Tested and verified on GPD Win5 and ONEXPLAYER X1 Mini.


How gesture detection works

Touch coordinates are normalized to [0.0, 1.0]. A gesture is recognized when the touch starts within 3% of a screen edge, travels inward by at least 12%, and completes within 400 ms.

+-----------------------------------+
|###################################|
|#                                 #|
|#   +--------------------------+  #|
|#   |                          |  #|
|#   |     normal touch area    |  #|
|#   |                          |  #|
|#   +--------------------------+  #|
|#                                 #|
|###################################|
+-----------------------------------+
  ^                               ^
  # = edge zone (3% of each side)

Gesture names describe the direction of finger movement (e.g. swiping inward from the left edge moves rightward → SwipeRight). Each direction carries a Top/Bottom zone qualifier split at 33% along the perpendicular axis.

Default profile mappings:

Gesture Action
SwipeRight:Bottom Guide
SwipeLeft:Bottom QuickAccess

Two operating modes: passthrough vs. grab

Passthrough (grab: false, default): Raw touch events are forwarded normally alongside any gesture events. Suitable for desktop use; avoid in game compositors as gestures will also register as taps.

Grab (grab: true): The touchscreen is exclusively captured via EVIOCGRAB. Touches starting within 1% of an edge are held back until the gesture outcome is known — confirmed gestures suppress the touch, unconfirmed ones replay it as a normal tap. Multi-finger touch immediately cancels gesture detection and passes all events through. If EVIOCGRAB fails, the source falls back to passthrough with a warning.

Grab mode is recommended for devices running a game compositor such as Gamescope.


Why MIN_FRAME_TIME was moved to TargetDriver

Gesture detection emits press and release nearly simultaneously. Without a minimum press duration, both events fall within a single HID poll interval and the compositor sees a report where the button was never pressed.

steam_deck already handled this with an 80 ms minimum, but only for itself. This PR moves the enforcement into TargetDriver so every target benefits automatically, and removes the now-redundant steam_deck implementation.

honjow added 8 commits March 30, 2026 04:49
Add Touchscreen as a new top-level Capability variant with Touch and
Gesture sub-types. Gesture covers single-finger edge-swipe directions
(SwipeLeft/Right/Up/Down) with optional zone qualifiers (Top/Bottom).

Update InputValue translation, evdev/dbus event enums, and capability
map config to handle the new types.
… replay

Implement single-finger edge-swipe gesture detection in the touchscreen
source device:
- Detect swipes starting from left/right/top/bottom screen edges
- Optional exclusive grab mode: suppresses raw touch events while a
  gesture is being evaluated, replaying them as a tap if no gesture
  is confirmed
- Orientation config to rotate coordinates for rotated displays
- TouchscreenConfig extended with grab and orientation fields
…le mapping

- Pass Touchscreen capability events through translate_event so they
  can be mapped to gamepad buttons via device profiles
- Bypass intercept-mode checks for touchscreen events (gesture handling
  is always active)
- Add Touchscreen capability to the output capability lists of
  DualSense, SteamDeckUhid and UnifiedGamepad target devices
Map SwipeRight:Bottom to Guide and SwipeLeft:Bottom to QuickAccess in
the default profile, matching hhd-style Steam/QAM gestures.

Update composite_device_v1.json to add grab field to TouchscreenConfig.
Update device_profile_v1.json to include Touchscreen gesture capability
definitions.
Button press+release pairs arriving within 80ms now have their release
delayed at the TargetDriver layer, so all target devices benefit
automatically. Previously this logic only existed in steam_deck.

Add ScheduledNativeEvent::event() accessor to support filtering
scheduled events during ClearState.

Also clear pending button events when ClearState is called to avoid
ghost releases after intercept mode resets device state.
…NEXPLAYER X1

GPD Win5: enable touchscreen source with grab mode.
ONEXPLAYER X1 Mini: enable touchscreen source with grab mode and
left orientation (display is rotated 90 degrees).
@honjow honjow changed the title feat(Touchscreen): edge-swipe gesture detection for handheld game controllers feat(Touchscreen): edge-swipe gesture detection for handheld Mar 30, 2026
Copy link
Copy Markdown
Contributor

@ShadowApex ShadowApex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature is definitely something we would like to include, but we may need to consider exactly how we want to implement this.

Comment on lines +246 to +249
"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"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary as this is already managed using the passthrough config option.

Comment thread src/config/mod.rs
Comment on lines +306 to +311
/// 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>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, this is already implemented by passthrough.

Comment on lines +57 to +59
/// True if this composite device has a grabbed touchscreen source.
/// When true, the touchscreen target will always be kept in the target list.
has_touchscreen_source: bool,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not always keep a touchscreen target in the target list. This should be controllable by the user. There may be cases where the user does not want to emulate a touchscreen (e.g. by converting the touchscreen inputs into touchpad inputs).

Comment on lines +148 to +156
// If the composite device has a grabbed touchscreen source, always keep
// touchscreen in the target list regardless of what the caller requested.
if self.has_touchscreen_source {
let touchscreen_id: TargetDeviceTypeId = TOUCHSCREEN_TARGET_ID.try_into().unwrap();
if !device_types.contains(&touchscreen_id) {
log::debug!("[{dbus_path}] Auto-adding touchscreen target (grabbed touchscreen source detected)");
device_types.push(touchscreen_id);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, we should not assume to always emulate a virtual touchscreen. Users may not want to use their touchscreen as a touchscreen.

Comment on lines +64 to +78
/// Lifecycle of a single-finger edge-swipe gesture within one touch sequence
#[derive(Debug, Default)]
enum GesturePhase {
/// No touch in progress.
#[default]
Idle,
/// Touch started; watching for a gesture.
/// In grab mode, `suppressing` indicates whether slot-0 events are being
/// held back because the touch started in an edge zone.
Tracking { suppressing: bool },
/// A gesture was recognized; suppress remaining touch events until lift.
Triggered,
/// A second finger arrived; gesture detection disabled for this touch.
Invalidated,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we actually want gesture detection to take place in the source device implementation. We may want to instead handle this in the composite device.

@pastaq thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but I'm having difficulty imagining how that would work exactly. They don't cleanly map to native events, and we don't want to lose the rest of the touchscreen.

Perhaps we should allow for a config file or dbus command to assign a capability_map to specific regions by start/end for x and y by % of the screen height/width, then the driver can translate those regions to a specific native event. This would be more flexible than just using gestures as it would allow for a fully virtual gamepad or touchpad on the screen itself. An overlay program could then assign those regions to match the location for its glyphs over the top of a game.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants