Add opt-in desktop mouse and controller ownership handoff

This commit is contained in:
Codex committed 2026-10-01 23:09:50 -05:00
1 parent 8afd42798f
commit 023287ef60
7 files changed
+220 -30

No files matched your search

+17
View File
@@ -102,3 +102,20 @@ with `input/ft-mousectl desktop --save`; no driver bindings or SteamVR defaults
are changed by installation. The new native mode suppresses virtual gaze, hand are changed by installation. The new native mode suppresses virtual gaze, hand
gesture and gaze-click input while selected. Switch back to spatial mode to gesture and gaze-click input while selected. Switch back to spatial mode to
use those features. Ordinary VR keyboard events remain available. use those features. Ordinary VR keyboard events remain available.
## Experimental desktop handoff
`input/ft-mousectl policy last-active` enables shared desktop input. Controller
clicks and nonzero scrolls inside desktop monitors take ownership; aiming alone
does not. Native mouse motion, clicks and scrolling reclaim ownership. Same
millisecond claims favor the mouse. Held buttons keep ownership until released;
other-device events are discarded rather than replayed after the drag. SteamVR
controls and separate monitor grab/placement bars do not participate.
`input/ft-mousectl policy mouse` restores the original mouse-first behavior
(including configured mouse-absent fallback). `policy pointer` reserves desktop
input for the controller. Changes refuse while either device holds a desktop
button. `status` reports policy, owner, held buttons and ignored mouse events.
These choices are runtime-only and reset to `mouse` when the desktop restarts.
The original PR checkpoint is retained on `checkpoint/desktop-input-before-handoff`;
this experiment is developed separately on `feat/desktop-input-handoff`.
+9 -1
View File
@@ -39,7 +39,8 @@ def query(address, command):
def status(): def status():
result = {'relay': query(RELAY, 'mouse-mode?'), 'desktop': query(SCREENS, 'mouse?'), result = {'relay': query(RELAY, 'mouse-mode?'), 'desktop': query(SCREENS, 'mouse?'),
'pointer': query(HELPER, 'pointerstatus'), 'pointer': query(HELPER, 'pointerstatus'),
'controllerFallback': query(SCREENS, 'mouse-presence?')} 'controllerFallback': query(SCREENS, 'mouse-presence?'),
'desktopPolicy': query(SCREENS, 'desktop-policy?')}
result['ready'] = bool(result['relay'].get('supported') and result['desktop'].get('supported') result['ready'] = bool(result['relay'].get('supported') and result['desktop'].get('supported')
and 'desktopInput' in result['pointer']) and 'desktopInput' in result['pointer'])
result['active'] = bool(result['ready'] and result['relay'].get('mode') == 'desktop' result['active'] = bool(result['ready'] and result['relay'].get('mode') == 'desktop'
@@ -124,6 +125,8 @@ def main():
sub = parser.add_subparsers(dest='command', required=True) sub = parser.add_subparsers(dest='command', required=True)
sub.add_parser('status') sub.add_parser('status')
sub.add_parser('layout') sub.add_parser('layout')
policy = sub.add_parser('policy', help='Choose desktop mouse/pointer ownership (runtime only)')
policy.add_argument('mode', choices=('mouse', 'pointer', 'last-active'))
for name in ('desktop', 'spatial'): for name in ('desktop', 'spatial'):
sub.add_parser(name).add_argument('--save', action='store_true') sub.add_parser(name).add_argument('--save', action='store_true')
pos = sub.add_parser('position') pos = sub.add_parser('position')
@@ -135,6 +138,11 @@ def main():
args = parser.parse_args() args = parser.parse_args()
try: try:
if args.command == 'status': result = status() if args.command == 'status': result = status()
elif args.command == 'policy':
require_ready()
if request(SCREENS, 'desktop-policy '+args.mode) != 'ok':
raise RuntimeError('Desktop policy rejected')
result = query(SCREENS, 'desktop-policy?')
elif args.command in ('desktop', 'spatial'): result = switch(args.command, args.save) elif args.command in ('desktop', 'spatial'): result = switch(args.command, args.save)
else: else:
require_ready() require_ready()
+92 -29
View File
@@ -26,6 +26,7 @@
// Runs in the dev container (wlroots 0.20); KWin connects from the host. // Runs in the dev container (wlroots 0.20); KWin connects from the host.
#define _GNU_SOURCE #define _GNU_SOURCE
#include "controller-fallback.h" #include "controller-fallback.h"
#include "desktop-handoff.h"
#include <drm_fourcc.h> #include <drm_fourcc.h>
#include <linux/input-event-codes.h> #include <linux/input-event-codes.h>
#include <math.h> #include <math.h>
@@ -112,6 +113,10 @@ struct server {
bool desktop_mouse, cursor_ready, cursor_frame_pending; bool desktop_mouse, cursor_ready, cursor_frame_pending;
struct ft_controller_fallback controller_fallback; struct ft_controller_fallback controller_fallback;
bool controller_desktop; bool controller_desktop;
struct ft_handoff handoff;
struct ft_event controller_position[MAX_SCREENS];
bool controller_position_known[MAX_SCREENS];
unsigned long mouse_events_ignored;
uint32_t controller_buttons; uint32_t controller_buttons;
struct ft_cursor_cache cursor_cache; struct ft_cursor_cache cursor_cache;
const char *cursor_error; const char *cursor_error;
@@ -374,11 +379,10 @@ static void deliver_pointer_event(const struct ft_event *e, void *data) {
wlr_seat_pointer_notify_frame(s->seat); wlr_seat_pointer_notify_frame(s->seat);
} }
static void update_controller_fallback(struct server *s) { static void set_desktop_owner(struct server *s, bool pointer) {
bool active = s->desktop_mouse && !s->mouse.buttons && if (pointer == s->controller_desktop) return;
ft_fallback_active(&s->controller_fallback, now_ms()); // Claims are refused during a held drag. Legacy disconnect recovery may
if (active == s->controller_desktop) return; // still revoke a stale controller, releasing its buttons safely.
// Release controller-owned presses before the native seat takes over.
for (unsigned bit = 0; bit < 8; ++bit) for (unsigned bit = 0; bit < 8; ++bit)
if (s->controller_buttons & (1u << bit)) if (s->controller_buttons & (1u << bit))
wlr_seat_pointer_notify_button(s->seat, now_ms(), BTN_LEFT+bit, wlr_seat_pointer_notify_button(s->seat, now_ms(), BTN_LEFT+bit,
@@ -387,34 +391,70 @@ static void update_controller_fallback(struct server *s) {
wlr_seat_pointer_notify_clear_focus(s->seat); wlr_seat_pointer_notify_clear_focus(s->seat);
wlr_seat_pointer_notify_frame(s->seat); wlr_seat_pointer_notify_frame(s->seat);
s->pointer_focus = NULL; s->pointer_focus = NULL;
s->controller_desktop = active; s->controller_desktop = pointer;
wlr_log(WLR_INFO, "desktop controller fallback %s (mouse %s, telemetry %s)", wlr_log(WLR_INFO, "desktop owner %s (policy %s)", pointer ? "pointer" : "mouse",
active ? "active" : "inactive", s->controller_fallback.present ? "present" : "absent", ft_handoff_name(s->handoff.policy));
s->controller_fallback.known && (uint32_t)(now_ms()-s->controller_fallback.received)<3000 ? "fresh" : "unknown/stale");
ft_vr_cursor_move(-1, 0, 0, 1, false); ft_vr_cursor_move(-1, 0, 0, 1, false);
} }
static void update_controller_fallback(struct server *s) {
bool active = s->desktop_mouse && !s->mouse.buttons &&
ft_fallback_active(&s->controller_fallback, now_ms());
if (s->desktop_mouse && s->handoff.policy != FT_PREFER_MOUSE) {
active = s->controller_desktop;
if (!s->mouse.buttons && !s->controller_buttons &&
(s->handoff.policy == FT_PREFER_POINTER ||
ft_fallback_active(&s->controller_fallback, now_ms()))) active = true;
}
set_desktop_owner(s, active);
}
static bool claim_desktop_mouse(struct server *s) {
if (!ft_handoff_claim(&s->handoff, false, now_ms(), s->mouse.buttons, s->controller_buttons)) {
++s->mouse_events_ignored;
return false;
}
ft_fallback_presence(&s->controller_fallback, true, s->controller_fallback.enabled, now_ms());
set_desktop_owner(s, false);
return true;
}
static void handle_vr_event(const struct ft_event *e, void *data) { static void handle_vr_event(const struct ft_event *e, void *data) {
struct server *s = data; struct server *s = data;
// Native mouse mode owns the desktop seat. Controller lasers still operate // Events here are panel content, not SteamVR or separate screen grab bars.
// SteamVR, games and FrameTop's separate screen placement controls. bool desktop = e->screen >= 0 && e->screen < s->n_config;
if (desktop && e->type == FT_MOTION) {
s->controller_position[e->screen] = *e;
s->controller_position_known[e->screen] = true;
}
update_controller_fallback(s); update_controller_fallback(s);
if (s->desktop_mouse && !s->controller_desktop && e->screen < s->n_config && if (s->desktop_mouse && desktop) {
(e->type == FT_MOTION || e->type == FT_BUTTON || e->type == FT_SCROLL || e->type == FT_LEAVE)) { ++s->vr_mouse_ignored; return; } bool pointer_event = e->type == FT_MOTION || e->type == FT_BUTTON ||
if (s->controller_desktop && e->screen < s->n_config && e->type == FT_BUTTON && e->button >= BTN_LEFT && e->button < BTN_LEFT+8) { e->type == FT_SCROLL || e->type == FT_LEAVE;
uint32_t bit = 1u << (e->button-BTN_LEFT); uint32_t bit = e->type == FT_BUTTON && e->button >= BTN_LEFT && e->button < BTN_LEFT+8
if (e->pressed) s->controller_buttons |= bit; ? 1u << (e->button-BTN_LEFT) : 0;
else s->controller_buttons &= ~bit; bool action = (e->type == FT_BUTTON && e->pressed && bit) ||
(e->type == FT_SCROLL && (e->dx != 0 || e->dy != 0));
if (action && s->handoff.policy != FT_PREFER_MOUSE &&
ft_handoff_claim(&s->handoff, true, now_ms(), s->mouse.buttons, s->controller_buttons)) {
set_desktop_owner(s, true);
if (e->type == FT_SCROLL && s->controller_position_known[e->screen])
deliver_pointer_event(&s->controller_position[e->screen], s);
}
if (pointer_event && (!s->controller_desktop || s->mouse.buttons ||
(e->type == FT_BUTTON && !e->pressed && !(s->controller_buttons & bit)) ||
(e->type == FT_LEAVE && s->controller_buttons))) {
++s->vr_mouse_ignored; return;
}
if (bit) {
if (e->pressed) s->controller_buttons |= bit;
else s->controller_buttons &= ~bit;
}
} }
deliver_pointer_event(e, data); deliver_pointer_event(e, data);
} }
static void desktop_motion(struct server *s) { static void desktop_motion(struct server *s) {
// A real mouse packet wins immediately, before the next hotplug heartbeat.
if (s->controller_desktop) {
ft_fallback_presence(&s->controller_fallback, true, s->controller_fallback.enabled, now_ms());
update_controller_fallback(s);
}
int index = ft_mouse_target(&s->mouse); int index = ft_mouse_target(&s->mouse);
if (index < 0 || !s->screens[index]) return; if (index < 0 || !s->screens[index]) return;
const struct ft_mouse_output *o = &s->mouse.outputs[index]; const struct ft_mouse_output *o = &s->mouse.outputs[index];
@@ -680,7 +720,23 @@ static int control_readable(int fd, uint32_t mask, void *data) {
double scale, wheel_x, wheel_y; double scale, wheel_x, wheel_y;
double mx, my, mw, mh; double mx, my, mw, mh;
char mode[16], tail; char mode[16], tail;
if (strcmp(buf, "mouse-presence?") == 0) { if (strcmp(buf, "desktop-policy?") == 0) {
snprintf(reply, sizeof reply,
"{\"supported\":true,\"policy\":\"%s\",\"owner\":\"%s\",\"mouseButtons\":%u,\"pointerButtons\":%u,\"mouseEventsIgnored\":%lu}",
ft_handoff_name(s->handoff.policy), s->controller_desktop ? "pointer" : "mouse",
s->mouse.buttons, s->controller_buttons, s->mouse_events_ignored);
} else if (sscanf(buf, "desktop-policy %15s %c", mode, &tail) == 1) {
if (s->mouse.buttons || s->controller_buttons ||
(strcmp(mode, "mouse") && strcmp(mode, "pointer") && strcmp(mode, "last-active")))
snprintf(reply, sizeof reply, "error policy or held button");
else {
s->handoff = (struct ft_handoff){.policy = !strcmp(mode, "last-active") ? FT_LAST_ACTIVE :
!strcmp(mode, "pointer") ? FT_PREFER_POINTER : FT_PREFER_MOUSE};
set_desktop_owner(s, s->handoff.policy == FT_PREFER_POINTER);
update_controller_fallback(s);
snprintf(reply, sizeof reply, "ok");
}
} else if (strcmp(buf, "mouse-presence?") == 0) {
snprintf(reply, sizeof reply, snprintf(reply, sizeof reply,
"{\"supported\":true,\"known\":%s,\"present\":%s,\"enabled\":%s,\"controllerDesktop\":%s,\"fresh\":%s}", "{\"supported\":true,\"known\":%s,\"present\":%s,\"enabled\":%s,\"controllerDesktop\":%s,\"fresh\":%s}",
s->controller_fallback.known ? "true" : "false", s->controller_fallback.known ? "true" : "false",
@@ -714,7 +770,7 @@ static int control_readable(int fd, uint32_t mask, void *data) {
snprintf(reply, sizeof reply, "error invalid mouse output or held button"); snprintf(reply, sizeof reply, "error invalid mouse output or held button");
else { s->scale[index-1] = scale; snprintf(reply, sizeof reply, "ok"); } else { s->scale[index-1] = scale; snprintf(reply, sizeof reply, "ok"); }
} else if (sscanf(buf, "mouse-mode %15s %c", mode, &tail) == 1) { } else if (sscanf(buf, "mouse-mode %15s %c", mode, &tail) == 1) {
if (s->mouse.buttons || (strcmp(mode, "desktop") && strcmp(mode, "spatial"))) if (s->mouse.buttons || s->controller_buttons || (strcmp(mode, "desktop") && strcmp(mode, "spatial")))
snprintf(reply, sizeof reply, "error mouse mode or held button"); snprintf(reply, sizeof reply, "error mouse mode or held button");
else if (!strcmp(mode, "desktop") && !ft_mouse_move(&s->mouse, 0, 0)) else if (!strcmp(mode, "desktop") && !ft_mouse_move(&s->mouse, 0, 0))
snprintf(reply, sizeof reply, "error configure mouse layout first"); snprintf(reply, sizeof reply, "error configure mouse layout first");
@@ -729,22 +785,25 @@ static int control_readable(int fd, uint32_t mask, void *data) {
snprintf(reply, sizeof reply, "ok"); snprintf(reply, sizeof reply, "ok");
} }
} else if (sscanf(buf, "mouse-move %lf %lf %c", &mx, &my, &tail) == 2) { } else if (sscanf(buf, "mouse-move %lf %lf %c", &mx, &my, &tail) == 2) {
if (!s->desktop_mouse || (s->vr && !ft_vr_screens_shown()) || !ft_mouse_move(&s->mouse, mx, my)) if (!s->desktop_mouse || (s->vr && !ft_vr_screens_shown()) || !isfinite(mx) || !isfinite(my) || fabs(mx)>16384 || fabs(my)>16384)
snprintf(reply, sizeof reply, "error desktop mouse unavailable or invalid motion"); snprintf(reply, sizeof reply, "error desktop mouse unavailable or invalid motion");
else { desktop_motion(s); ++s->mouse_motions; snprintf(reply, sizeof reply, "ok"); } else { if ((mx != 0 || my != 0) && claim_desktop_mouse(s) && ft_mouse_move(&s->mouse, mx, my)) { desktop_motion(s); ++s->mouse_motions; } snprintf(reply, sizeof reply, "ok"); }
} else if (sscanf(buf, "mouse-position %d %lf %lf %c", &index, &mx, &my, &tail) == 3) { } else if (sscanf(buf, "mouse-position %d %lf %lf %c", &index, &mx, &my, &tail) == 3) {
if (!s->desktop_mouse || s->mouse.buttons || index < 1 || index > MAX_SCREENS || if (!s->desktop_mouse || s->mouse.buttons || s->controller_buttons || index < 1 || index > MAX_SCREENS ||
!s->mouse.outputs[index-1].enabled || !isfinite(mx) || !isfinite(my) || !s->mouse.outputs[index-1].enabled || !isfinite(mx) || !isfinite(my) ||
mx < 0 || my < 0 || mx >= s->mouse.outputs[index-1].width || my >= s->mouse.outputs[index-1].height) mx < 0 || my < 0 || mx >= s->mouse.outputs[index-1].width || my >= s->mouse.outputs[index-1].height)
snprintf(reply, sizeof reply, "error mouse position or held button"); snprintf(reply, sizeof reply, "error mouse position or held button");
else { else {
ft_mouse_position(&s->mouse, s->mouse.outputs[index-1].x+mx, s->mouse.outputs[index-1].y+my); ft_mouse_position(&s->mouse, s->mouse.outputs[index-1].x+mx, s->mouse.outputs[index-1].y+my);
desktop_motion(s); snprintf(reply, sizeof reply, "ok"); if (claim_desktop_mouse(s)) desktop_motion(s);
snprintf(reply, sizeof reply, "ok");
} }
} else if (sscanf(buf, "mouse-button %u %d %c", &code, &value, &tail) == 2) { } else if (sscanf(buf, "mouse-button %u %d %c", &code, &value, &tail) == 2) {
if (!s->desktop_mouse || (value != 0 && value != 1) || if (!s->desktop_mouse || (value != 0 && value != 1) ||
(s->vr && !ft_vr_screens_shown() && value) || code < BTN_LEFT || code > BTN_TASK) (s->vr && !ft_vr_screens_shown() && value) || code < BTN_LEFT || code > BTN_TASK)
snprintf(reply, sizeof reply, "error mouse button or hidden desktop"); snprintf(reply, sizeof reply, "error mouse button or hidden desktop");
else if ((!value && !(s->mouse.buttons & (1u << (code-BTN_LEFT)))) || !claim_desktop_mouse(s))
snprintf(reply, sizeof reply, "ok");
else { else {
desktop_motion(s); desktop_motion(s);
int target = ft_mouse_target(&s->mouse); int target = ft_mouse_target(&s->mouse);
@@ -765,6 +824,8 @@ static int control_readable(int fd, uint32_t mask, void *data) {
!s->mouse.outputs[target].enabled || !s->mouse.outputs[target].enabled ||
!isfinite(wheel_x) || !isfinite(wheel_y) || fabs(wheel_x)>120 || fabs(wheel_y)>120) !isfinite(wheel_x) || !isfinite(wheel_y) || fabs(wheel_x)>120 || fabs(wheel_y)>120)
snprintf(reply, sizeof reply, "error mouse wheel or hidden desktop"); snprintf(reply, sizeof reply, "error mouse wheel or hidden desktop");
else if ((wheel_x == 0 && wheel_y == 0) || !claim_desktop_mouse(s))
snprintf(reply, sizeof reply, "ok");
else { else {
desktop_motion(s); desktop_motion(s);
struct ft_event event = {.type = FT_SCROLL, .screen = target, .dx = wheel_x, .dy = -wheel_y}; struct ft_event event = {.type = FT_SCROLL, .screen = target, .dx = wheel_x, .dy = -wheel_y};
@@ -809,11 +870,13 @@ static int control_readable(int fd, uint32_t mask, void *data) {
else if (strcmp(button, "middle") == 0) e.button = BTN_MIDDLE; else if (strcmp(button, "middle") == 0) e.button = BTN_MIDDLE;
if (got >= 2 && strcmp(what, "leave") == 0) e.type = FT_LEAVE; if (got >= 2 && strcmp(what, "leave") == 0) e.type = FT_LEAVE;
else if (got >= 4 && strcmp(what, "move") == 0) e.type = FT_MOTION; else if (got >= 4 && strcmp(what, "move") == 0) e.type = FT_MOTION;
else if (got >= 4 && strcmp(what, "scroll") == 0 && isfinite(x) && isfinite(y) && fabs(x)<=120 && fabs(y)<=120)
e.type = FT_SCROLL, e.dx = x, e.dy = y;
else if (got >= 4 && (strcmp(what, "down") == 0 || strcmp(what, "up") == 0)) else if (got >= 4 && (strcmp(what, "down") == 0 || strcmp(what, "up") == 0))
e.type = FT_BUTTON, e.pressed = what[0] == 'd'; e.type = FT_BUTTON, e.pressed = what[0] == 'd';
else index = 0; else index = 0;
if (index < 1 || index > MAX_SCREENS || !s->screens[index - 1]) { if (index < 1 || index > MAX_SCREENS || !s->screens[index - 1]) {
snprintf(reply, sizeof reply, "error input <screen> move|down|up|leave [x y [button]]"); snprintf(reply, sizeof reply, "error input <screen> move|down|up|scroll|leave [x y [button]]");
} else { } else {
handle_vr_event(&e, s); handle_vr_event(&e, s);
snprintf(reply, sizeof reply, "ok"); snprintf(reply, sizeof reply, "ok");
+24
View File
@@ -0,0 +1,24 @@
#ifndef FT_DESKTOP_HANDOFF_H
#define FT_DESKTOP_HANDOFF_H
#include <stdbool.h>
#include <stdint.h>
enum ft_handoff_policy { FT_PREFER_MOUSE, FT_LAST_ACTIVE, FT_PREFER_POINTER };
struct ft_handoff { enum ft_handoff_policy policy; bool mouse_seen; uint32_t mouse_at; };
// Only deliberate desktop actions call this. Tracking motion never claims a seat.
static inline bool ft_handoff_claim(struct ft_handoff *h, bool pointer,
uint32_t now, uint32_t mouse_buttons,
uint32_t pointer_buttons) {
if (pointer) {
if (mouse_buttons || h->policy == FT_PREFER_MOUSE) return false;
if (h->policy == FT_LAST_ACTIVE && !pointer_buttons && h->mouse_seen && now == h->mouse_at)
return false; // Same millisecond: native mouse wins.
} else {
if (pointer_buttons || h->policy == FT_PREFER_POINTER) return false;
h->mouse_seen = true; h->mouse_at = now;
}
return true;
}
static inline const char *ft_handoff_name(enum ft_handoff_policy p) {
return p == FT_LAST_ACTIVE ? "last-active" : p == FT_PREFER_POINTER ? "pointer" : "mouse";
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#include <assert.h>
#include "../desktop-handoff.h"
int main(void) {
struct ft_handoff h = {.policy=FT_LAST_ACTIVE};
assert(ft_handoff_claim(&h, false, 100, 0, 0));
assert(!ft_handoff_claim(&h, true, 100, 0, 0));
assert(ft_handoff_claim(&h, true, 101, 0, 0));
assert(!ft_handoff_claim(&h, true, 102, 1, 0));
assert(!ft_handoff_claim(&h, false, 102, 0, 1));
assert(ft_handoff_claim(&h, true, 102, 0, 1));
assert(ft_handoff_claim(&h, false, 102, 1, 0));
assert(ft_handoff_claim(&h, false, 103, 0, 0));
h.policy=FT_PREFER_MOUSE;
assert(!ft_handoff_claim(&h, true, 104, 0, 0));
assert(ft_handoff_claim(&h, false, 104, 0, 0));
h.policy=FT_PREFER_POINTER;
assert(!ft_handoff_claim(&h, false, 105, 0, 0));
assert(ft_handoff_claim(&h, true, 105, 0, 0));
h=(struct ft_handoff){.policy=FT_LAST_ACTIVE};
assert(ft_handoff_claim(&h, false, UINT32_MAX, 0, 0));
assert(ft_handoff_claim(&h, true, 0, 0, 0));
}
@@ -33,3 +33,57 @@ with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as client:
assert not json.loads(ask('mouse-presence?'))['controllerDesktop'] assert not json.loads(ask('mouse-presence?'))['controllerDesktop']
ask('mouse-button 272 1'); ask('mouse-button 272 0') ask('mouse-button 272 1'); ask('mouse-button 272 0')
print('Native motion/click/wheel, cross-output drag, output bounds and controller fallback passed') print('Native motion/click/wheel, cross-output drag, output bounds and controller fallback passed')
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as client:
client.bind(''); client.settimeout(2)
def ask(command):
client.sendto(command.encode(), '\0ft_screens_test')
return client.recv(8192).decode()
def policy(): return json.loads(ask('desktop-policy?'))
assert ask('desktop-policy last-active') == 'ok'
assert ask('mouse-presence 1 1') == 'ok'
assert ask('mouse-move 3 0') == 'ok'
assert policy()['owner'] == 'mouse'
for _ in range(3): ask('input 1 move 200 100')
assert policy()['owner'] == 'mouse', 'aiming must not claim ownership'
time.sleep(.01)
ask('input 1 down 200 100 left')
assert policy()['owner'] == 'pointer'
assert policy()['pointerButtons'] == 1
before = json.loads(ask('mouse?'))
ask('mouse-move 40 0'); ask('mouse-button 272 1'); ask('mouse-button 272 0'); ask('mouse-wheel 0 1')
after = json.loads(ask('mouse?'))
assert before['x'] == after['x'], 'mouse motion must not accumulate during pointer drag'
assert before['buttonEvents'] == after['buttonEvents']
assert before['wheelEvents'] == after['wheelEvents']
assert policy()['owner'] == 'pointer'
assert ask('desktop-policy mouse').startswith('error'), 'cannot change policy during drag'
ask('mouse-presence 1 1')
assert policy()['owner'] == 'pointer', 'presence heartbeat must not steal ownership'
ask('input 1 up 200 100 left')
assert policy()['pointerButtons'] == 0
ask('mouse-wheel 0 1')
assert policy()['owner'] == 'mouse'
ask('mouse-button 272 1'); time.sleep(.01)
ask('input 1 down 200 100 left'); ask('input 1 up 200 100 left')
assert policy()['owner'] == 'mouse'
assert policy()['mouseButtons'] == 1
ask('mouse-button 272 0')
time.sleep(.01); ask('input 1 down 200 100 left'); ask('input 1 up 200 100 left')
assert policy()['owner'] == 'pointer'
ask('mouse-move 0 0')
assert policy()['owner'] == 'pointer', 'zero movement must not claim'
ask('mouse-move 1 0')
assert policy()['owner'] == 'mouse'
time.sleep(.01)
ask('input 1 move 200 100'); ask('input 1 scroll 0 1')
assert policy()['owner'] == 'pointer', 'controller scroll must claim desktop'
ask('mouse-wheel 0 1')
assert policy()['owner'] == 'mouse'
assert ask('desktop-policy pointer') == 'ok'
ask('mouse-move 1 0')
assert policy()['owner'] == 'pointer'
assert ask('desktop-policy mouse') == 'ok'
ask('input 1 down 200 100 left'); ask('input 1 up 200 100 left')
assert policy()['owner'] == 'mouse'
print('Desktop handoff, aiming exclusion, drag locks, ignored releases, presence stability and policy controls passed')
+2
View File
@@ -10,6 +10,8 @@ gcc -std=c11 -Wall -Wextra -Werror tests/desktop-mouse-test.c -lm -o build/deskt
build/desktop-mouse-test build/desktop-mouse-test
gcc -std=c11 -Wall -Wextra -Werror tests/controller-fallback-test.c -o build/controller-fallback-test gcc -std=c11 -Wall -Wextra -Werror tests/controller-fallback-test.c -o build/controller-fallback-test
build/controller-fallback-test build/controller-fallback-test
gcc -std=c11 -Wall -Wextra -Werror tests/desktop-handoff-test.c -o build/desktop-handoff-test
build/desktop-handoff-test
gcc -std=c11 -Wall -Wextra -Werror $(pkg-config --cflags libdrm) tests/desktop-cursor-test.c -o build/desktop-cursor-test gcc -std=c11 -Wall -Wextra -Werror $(pkg-config --cflags libdrm) tests/desktop-cursor-test.c -o build/desktop-cursor-test
build/desktop-cursor-test build/desktop-cursor-test
gcc -std=c11 -Wall -Wextra -Werror tests/cursor-cache-test.c $(pkg-config --cflags --libs wlroots-0.20 wayland-server wayland-client libdrm pixman-1) -o build/cursor-cache-test gcc -std=c11 -Wall -Wextra -Werror tests/cursor-cache-test.c $(pkg-config --cflags --libs wlroots-0.20 wayland-server wayland-client libdrm pixman-1) -o build/cursor-cache-test