mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 07:00:14 +02:00
Compare commits
3
Commits
click-threshold
...
pr-11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fdb1fd3e5 | ||
|
|
9da8bd5500 | ||
|
|
7816633353 |
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Executable
+29
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Executable
+4
@@ -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'
|
||||||
Reference in new issue
Block a user