Compare commits

...
3 Commits
Author SHA1 Message Date
Codex 1fdb1fd3e5 Clear reported drag state on controller release 2026-10-01 22:40:24 -05:00
Codex 9da8bd5500 Prevent small desktop overlay pointer movements from starting a drag 2026-10-01 22:40:24 -05:00
DeeJanuzandClaude Opus 5.5 7816633353 README: link the Frametop Discord
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:18:09 -06:00
7 changed files with 170 additions and 2 deletions

No files matched your search

+3 -1
View File
@@ -13,6 +13,8 @@ Two settings apps come with it: Frametop Display Settings for the screens, profi
Frametop is an independent project, not made by or affiliated with Valve. Frametop is an independent project, not made by or affiliated with Valve.
Join the [Frametop Discord](https://discord.gg/W3X9f7z3Bc) for questions, ideas, and help with your setup.
## Install on the headset ## Install on the headset
You need a Steam Frame with an internet connection, a keyboard (Bluetooth, or the on-screen one), and about 3 GB of free space. You need a Steam Frame with an internet connection, a keyboard (Bluetooth, or the on-screen one), and about 3 GB of free space.
@@ -138,7 +140,7 @@ In a terminal on the headset, run:
cd ~/frametop && scripts/report.sh cd ~/frametop && scripts/report.sh
``` ```
This writes `frametop-report-<date>.txt` with version numbers, service states, settings, and recent logs. Bluetooth addresses and the headset's serial number are masked. Then [open an issue](https://github.com/DeeJanuz/frametop/issues), describe what you did, what you expected, and what happened, and attach the file. This writes `frametop-report-<date>.txt` with version numbers, service states, settings, and recent logs. Bluetooth addresses and the headset's serial number are masked. Then [open an issue](https://github.com/DeeJanuz/frametop/issues), describe what you did, what you expected, and what happened, and attach the file. Quick questions can go to [Discord](https://discord.gg/W3X9f7z3Bc) instead.
## Update ## Update
+32
View File
@@ -0,0 +1,32 @@
# Controller desktop click stability
Trigger presses reach KDE immediately, but controller motion within 8 logical
pixels of the press stays at that position until release. Releasing without a motion outside this
zone delivers the click at the original position, even if the hand moved during
release. Moving outside the zone begins a normal drag immediately; returning to
the zone does not turn it back into a click. There is no hold-duration timer.
This filters overlay pointer content events on desktop monitors only. Native mouse
input, SteamVR UI, separate screen grab bars and floating-app title-bar carrying
are unaffected. Multi-button gestures keep their existing behavior. A motion
onto another desktop monitor starts a drag; cross-monitor motion is not stabilized.
CLI (runtime preferences, reset to 8 on desktop restart):
```sh
input/ft-clickctl status
input/ft-clickctl threshold 8
input/ft-clickctl threshold 0 # disable without a restart
```
Thresholds are 0–64 logical pixels, normalized to each panel's KDE scale.
Status reports held state, suppressed motions, stabilized clicks and drags.
Changing the threshold while a controller button is held is refused.
This is a separate contribution from desktop mouse/controller ownership. Its
hardware validation must check small controls, intentional text selection,
long presses, cross-monitor dragging and simultaneous mouse use. The existing
renderer laser remains tracked; this change stabilizes desktop input rather
than smoothing the visual laser. Default threshold is a starting point to test.
Run `scripts/test-controller-click.sh` for the isolated gesture-state tests.
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Controller desktop click stability over local IPC; no SteamVR client."""
import argparse
import json
import math
import socket
def request(command):
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as client:
client.bind(''); client.settimeout(2)
client.sendto(command.encode(), '\0ft_screens')
response=client.recv(8192).decode()
if response.startswith('error'): raise RuntimeError(response)
return response
def main():
parser=argparse.ArgumentParser(description=__doc__)
sub=parser.add_subparsers(dest='command', required=True)
sub.add_parser('status')
sub.add_parser('threshold').add_argument('pixels', type=float)
args=parser.parse_args()
try:
if args.command == 'threshold':
if not math.isfinite(args.pixels) or not 0 <= args.pixels <= 64:
raise ValueError('Threshold must be 0–64 logical pixels; 0 disables stabilization')
if request(f'controller-click {args.pixels:g}') != 'ok': raise RuntimeError('Threshold rejected')
print(json.dumps(json.loads(request('controller-click?')), indent=2))
except (OSError, ValueError, RuntimeError) as error: parser.exit(1, str(error)+'\n')
if __name__ == '__main__': main()
+20 -1
View File
@@ -58,6 +58,7 @@
#include <wlr/util/log.h> #include <wlr/util/log.h>
#include "vr.h" #include "vr.h"
#include "controller-click.h"
#define MAX_SCREENS 24 // screens and spare outputs #define MAX_SCREENS 24 // screens and spare outputs
@@ -104,6 +105,7 @@ struct server {
struct wl_list buffers; // tracked_buffer struct wl_list buffers; // tracked_buffer
struct wl_event_source *tick; struct wl_event_source *tick;
struct screen *pointer_focus; struct screen *pointer_focus;
struct ft_controller_click controller_click;
pid_t child; pid_t child;
// Where typing goes: the screens after a click on one, Steam after a click on another // Where typing goes: the screens after a click on one, Steam after a click on another
// panel. The input relay grabs the keyboards while it's the screens (see keys_update). // panel. The input relay grabs the keyboards while it's the screens (see keys_update).
@@ -292,6 +294,11 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
return; return;
} }
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return; if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
struct ft_event filtered = *e;
if (e->screen < s->n_config &&
!ft_controller_click_filter(&s->controller_click, &filtered, s->scale[e->screen])) return;
e = &filtered;
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
struct screen *sc = s->screens[e->screen]; struct screen *sc = s->screens[e->screen];
struct wlr_surface *surface = sc->toplevel->base->surface; struct wlr_surface *surface = sc->toplevel->base->surface;
const uint32_t t = now_ms(); const uint32_t t = now_ms();
@@ -514,7 +521,18 @@ static int control_readable(int fd, uint32_t mask, void *data) {
unsigned code; unsigned code;
int value, index, w, h; int value, index, w, h;
double scale; double scale;
if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) { char tail;
if (strcmp(buf, "controller-click?") == 0) {
snprintf(reply, sizeof reply,
"{\"supported\":true,\"threshold\":%.3f,\"held\":%s,\"dragging\":%s,\"suppressedMotions\":%lu,\"clicks\":%lu,\"drags\":%lu}",
s->controller_click.threshold, s->controller_click.held ? "true" : "false",
s->controller_click.dragging ? "true" : "false", s->controller_click.suppressed,
s->controller_click.clicks, s->controller_click.drags);
} else if (sscanf(buf, "controller-click %lf %c", &scale, &tail) == 1) {
if (!isfinite(scale) || scale < 0 || scale > 64 || s->controller_click.buttons)
snprintf(reply, sizeof reply, "error threshold or held controller button");
else { s->controller_click.threshold = scale; snprintf(reply, sizeof reply, "ok"); }
} else if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
// A new resolution for a screen, live: KWin resizes the screen to match. (KWin makes // A new resolution for a screen, live: KWin resizes the screen to match. (KWin makes
// it this size times its scale; ft-floatd sends spares' sizes divided by theirs.) // it this size times its scale; ft-floatd sends spares' sizes divided by theirs.)
const int min_w = index - 1 < s->n_config ? 320 : 64, min_h = index - 1 < s->n_config ? 200 : 64; const int min_w = index - 1 < s->n_config ? 320 : 64, min_h = index - 1 < s->n_config ? 200 : 64;
@@ -641,6 +659,7 @@ static bool setup_dmabuf(struct server *s) {
int main(int argc, char **argv) { int main(int argc, char **argv) {
struct server s = {0}; struct server s = {0};
s.controller_click.threshold = 8;
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1; for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
s.kb_screen = -1; s.kb_screen = -1;
const char *socket_name = "ft-screens-0", *control_name = "ft_screens"; const char *socket_name = "ft-screens-0", *control_name = "ft_screens";
+45
View File
@@ -0,0 +1,45 @@
#ifndef FT_CONTROLLER_CLICK_H
#define FT_CONTROLLER_CLICK_H
#include <math.h>
#include <linux/input-event-codes.h>
#include "vr.h"
struct ft_controller_click {
bool held, dragging;
int screen;
uint32_t buttons;
double x, y, threshold, radius;
unsigned long suppressed, clicks, drags;
};
// Hold the desktop position at press time until movement exceeds a logical-pixel
// radius. No timer, delayed button-down, or change to native mouse input.
static inline bool ft_controller_click_filter(struct ft_controller_click *c,
struct ft_event *e, double scale) {
if (e->type == FT_BUTTON && e->button >= BTN_LEFT && e->button < BTN_LEFT+8) {
uint32_t bit = 1u << (e->button-BTN_LEFT);
if (e->pressed) c->buttons |= bit; else c->buttons &= ~bit;
}
if (e->type == FT_BUTTON && e->pressed) {
if (e->button == BTN_LEFT && !c->held && c->threshold > 0) {
c->held = true; c->dragging = false; c->screen = e->screen;
c->x = e->x; c->y = e->y; c->radius = c->threshold * scale;
} else if (e->button != BTN_LEFT) {
c->held = false; // Multi-button gestures retain their usual semantics.
}
} else if (e->type == FT_MOTION && c->held && !c->dragging) {
if (e->screen == c->screen && hypot(e->x-c->x, e->y-c->y) <= c->radius) {
++c->suppressed;
return false;
}
c->dragging = true; ++c->drags;
} else if (e->type == FT_BUTTON && e->button == BTN_LEFT && !e->pressed && c->held) {
if (!c->dragging) {
e->screen = c->screen; e->x = c->x; e->y = c->y;
++c->clicks;
}
c->held = false; c->dragging = false;
} else if (e->type == FT_LEAVE && c->held) {
return false; // Preserve the implicit grab through tiny edge excursions.
}
return true;
}
#endif
+37
View File
@@ -0,0 +1,37 @@
#include <assert.h>
#include "../controller-click.h"
static struct ft_event event(enum ft_event_type t, bool down, double x, double y) {
return (struct ft_event){.type=t,.screen=0,.button=BTN_LEFT,.pressed=down,.x=x,.y=y};
}
int main(void) {
struct ft_controller_click c={.threshold=8};
struct ft_event e=event(FT_BUTTON,true,100,100);
assert(ft_controller_click_filter(&c,&e,2));
e=event(FT_MOTION,false,112,108);
assert(!ft_controller_click_filter(&c,&e,2));
assert(c.held && !c.dragging && c.suppressed==1);
e=event(FT_BUTTON,false,116,111);
assert(ft_controller_click_filter(&c,&e,2));
assert(e.x==100 && e.y==100 && c.clicks==1 && !c.held);
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_MOTION,false,109,100);assert(ft_controller_click_filter(&c,&e,1));
assert(c.dragging && c.drags==1);
e=event(FT_MOTION,false,101,100);assert(ft_controller_click_filter(&c,&e,1));
e=event(FT_BUTTON,false,103,100);assert(ft_controller_click_filter(&c,&e,1));
assert(e.x==103 && c.clicks==1 && !c.held && !c.dragging);
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_LEAVE,false,0,0);assert(!ft_controller_click_filter(&c,&e,1));
e=event(FT_BUTTON,false,800,900);e.screen=1;ft_controller_click_filter(&c,&e,1);
assert(e.screen==0 && e.x==100 && c.clicks==2);
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_MOTION,false,101,100);e.screen=1;
assert(ft_controller_click_filter(&c,&e,1) && c.dragging);
e=event(FT_BUTTON,false,101,100);ft_controller_click_filter(&c,&e,1);
c.threshold=0;
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_MOTION,false,101,100);assert(ft_controller_click_filter(&c,&e,1) && !c.held);
c.threshold=8;
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_BUTTON,true,100,100);e.button=BTN_RIGHT;ft_controller_click_filter(&c,&e,1);
assert(!c.held);
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
"$root/scripts/frame.sh" -C screens 'mkdir -p build; gcc -std=c11 -Wall -Wextra -Werror tests/controller-click-test.c -lm -o build/controller-click-test && build/controller-click-test'