feat(touch): cross-client touch-input modes on Linux + Windows
ci / web (push) Successful in 43s
ci / rust (push) Failing after 53s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 16s
ci / docs-site (push) Successful in 1m5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m53s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 52s
apple / swift (push) Successful in 4m41s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 54s
ci / bench (push) Successful in 6m30s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
ci / web (push) Successful in 43s
ci / rust (push) Failing after 53s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 16s
ci / docs-site (push) Successful in 1m5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m53s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 52s
apple / swift (push) Successful in 4m41s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 54s
ci / bench (push) Successful in 6m30s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
Bring the SDL presenter (Linux/Deck + Windows) to parity with the Android and
Apple clients: a persisted TouchMode selects how a touchscreen drives the host —
* Trackpad (default): relative cursor with pointer ballistics + the shared
gesture vocabulary (tap = left click, two-finger tap = right click,
two-finger drag = scroll, tap-then-drag = held left drag, three-finger tap =
cycle the stats overlay).
* Direct pointer: the cursor jumps to and follows the finger (absolute).
* Touch passthrough: every finger is a real host touchscreen contact.
Previously the presenter had no finger handling, so SDL synthesized mouse events
from touch and — under the stream's relative-mouse lock — walked the host cursor
into the corner (the reported Deck bug). SDL touch->mouse synthesis is now off;
DIRECT touchscreens route through a new incremental gesture engine (a port of
Android TouchInput.kt / Apple TouchMouse.swift), while INDIRECT trackpads keep
driving the mouse. Fingers map through the aspect-fit letterbox onto the content
rect.
TouchMode lives in the shared trust::Settings (default trackpad, so passthrough
is opt-in like the other clients); the GTK and WinUI settings screens both gained
a "Touch input" picker. Gesture engine, letterbox mapping, and settings
back-compat are unit-tested (28 tests green); clippy -D warnings clean; full
Linux client + session build verified on-host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,53 @@ impl StatsVerbosity {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a touchscreen's fingers drive the host — the cross-client touch-input model (Android
|
||||
/// `TouchMode`, Apple `TouchInputMode`). Stored stringly in [`Settings::touch_mode`] so the
|
||||
/// file stays readable; parsed with [`TouchMode::from_name`].
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum TouchMode {
|
||||
/// Relative cursor like a laptop touchpad: the cursor stays put on touch-down and moves
|
||||
/// by the finger's delta (with mild acceleration), tap to click. The default — a cursor
|
||||
/// is the universally workable model on a screen the host isn't sized for.
|
||||
Trackpad,
|
||||
/// Direct pointing: the cursor jumps to the finger and follows it (absolute).
|
||||
Pointer,
|
||||
/// Real multi-touch passthrough: every finger is a host touchscreen contact, no gesture
|
||||
/// interpretation — only helps hosts/apps that actually understand touch.
|
||||
Touch,
|
||||
}
|
||||
|
||||
impl TouchMode {
|
||||
/// Cycle/picker order (also the settings pickers' option order).
|
||||
pub const ALL: [TouchMode; 3] = [TouchMode::Trackpad, TouchMode::Pointer, TouchMode::Touch];
|
||||
|
||||
/// Parse the persisted name, defaulting to `Trackpad` for unset/unknown values.
|
||||
pub fn from_name(s: &str) -> TouchMode {
|
||||
match s {
|
||||
"pointer" => TouchMode::Pointer,
|
||||
"touch" => TouchMode::Touch,
|
||||
_ => TouchMode::Trackpad,
|
||||
}
|
||||
}
|
||||
|
||||
/// The persisted name (the inverse of [`from_name`](Self::from_name)).
|
||||
pub fn as_name(self) -> &'static str {
|
||||
match self {
|
||||
TouchMode::Trackpad => "trackpad",
|
||||
TouchMode::Pointer => "pointer",
|
||||
TouchMode::Touch => "touch",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
TouchMode::Trackpad => "Trackpad",
|
||||
TouchMode::Pointer => "Direct pointer",
|
||||
TouchMode::Touch => "Touch passthrough",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// App settings, persisted as JSON. Stringly-typed gamepad/compositor prefs so the file
|
||||
/// stays readable; parsed with `*Pref::from_name` at connect time.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -363,6 +410,12 @@ pub struct Settings {
|
||||
/// Which host compositor backend to request (advisory; the host falls back to
|
||||
/// auto-detect when unavailable).
|
||||
pub compositor: String,
|
||||
/// How a touchscreen's fingers drive the host (Deck/tablet): a [`TouchMode`] name —
|
||||
/// `"trackpad"` (default), `"pointer"`, or `"touch"`. Read at connect via
|
||||
/// [`Settings::touch_mode`]; irrelevant on a mouse-only client. `default` so pre-existing
|
||||
/// stores load as trackpad.
|
||||
#[serde(default = "default_touch_mode")]
|
||||
pub touch_mode: String,
|
||||
/// Grab compositor shortcuts (Alt+Tab, Super…) while input is captured.
|
||||
pub inhibit_shortcuts: bool,
|
||||
/// Stream the default microphone to the host's virtual mic source.
|
||||
@@ -425,6 +478,10 @@ fn default_codec() -> String {
|
||||
"auto".into()
|
||||
}
|
||||
|
||||
fn default_touch_mode() -> String {
|
||||
"trackpad".into()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -447,6 +504,11 @@ impl Settings {
|
||||
self.show_stats = v != StatsVerbosity::Off;
|
||||
}
|
||||
|
||||
/// The touch-input model for this session (parsed from the stored name).
|
||||
pub fn touch_mode(&self) -> TouchMode {
|
||||
TouchMode::from_name(&self.touch_mode)
|
||||
}
|
||||
|
||||
/// The `codec` setting as a `quic::CODEC_*` preference bit (`0` = auto).
|
||||
pub fn preferred_codec(&self) -> u8 {
|
||||
match self.codec.as_str() {
|
||||
@@ -468,6 +530,7 @@ impl Default for Settings {
|
||||
gamepad: "auto".into(),
|
||||
forward_pad: String::new(),
|
||||
compositor: "auto".into(),
|
||||
touch_mode: "trackpad".into(),
|
||||
inhibit_shortcuts: true,
|
||||
mic_enabled: false,
|
||||
audio_channels: 2,
|
||||
@@ -519,6 +582,23 @@ impl Settings {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A settings file predating the touch-input model loads as `trackpad` (the shipped
|
||||
/// default), and the name round-trips through the enum both ways.
|
||||
#[test]
|
||||
fn settings_touch_mode_defaults_trackpad() {
|
||||
let old = r#"{"width":1280,"height":720,"gamepad":"auto","compositor":"auto"}"#;
|
||||
let s: Settings = serde_json::from_str(old).unwrap();
|
||||
assert_eq!(s.touch_mode, "trackpad");
|
||||
assert_eq!(s.touch_mode(), TouchMode::Trackpad);
|
||||
// Explicit values parse; an unknown name falls back to trackpad.
|
||||
assert_eq!(TouchMode::from_name("pointer"), TouchMode::Pointer);
|
||||
assert_eq!(TouchMode::from_name("touch"), TouchMode::Touch);
|
||||
assert_eq!(TouchMode::from_name("bogus"), TouchMode::Trackpad);
|
||||
for m in TouchMode::ALL {
|
||||
assert_eq!(TouchMode::from_name(m.as_name()), m);
|
||||
}
|
||||
}
|
||||
|
||||
/// A pre-`forward_pad` settings file (≤ 0.5.0) loads with the pin on automatic.
|
||||
#[test]
|
||||
fn settings_forward_pad_defaults_empty() {
|
||||
|
||||
Reference in New Issue
Block a user