implement primary selection v1
Advertises zwp_primary_selection_v1 so text editors can publish and middle-click-paste without a full clipboard roundtrip. The device-manager global hooks the seat; the request_set_primary_selection handler installs the source on the seat (wlroots only emits the request), and the seat's focus-change/set fan-out delivers the offer to the keyboard-focused tab's device, mirroring the clipboard path. No host bridging: the selection stays within hosted tabs for now. wltest gains --primary-set <text> and --primary-demand <file>. The setter defers its set_selection until a seat-granted serial arrives (wlroots rejects serials it never handed out), the demander pulls the payload over a pipe polling both the pipe and the display socket (the compositor forwards the fd to the owning tab, whose reply arrives with no display event of ours). The smoke test cycles between the two tabs so the setter gets a serial and the demander is refocused to flush the offer, then verifies the payload lands in the file.
This commit is contained in:
parent
3eda7016e6
commit
f1d92afe19
4 changed files with 273 additions and 4 deletions
209
tests/wltest.c
209
tests/wltest.c
|
|
@ -3,6 +3,7 @@
|
|||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
#include "xdg-shell-client.h"
|
||||
#include "cursor-shape-v1-client.h"
|
||||
#include "pointer-constraints-unstable-v1-client.h"
|
||||
#include "primary-selection-unstable-v1-client.h"
|
||||
#include "relative-pointer-unstable-v1-client.h"
|
||||
|
||||
/* Minimal deterministic xdg toplevel test client: shows a solid-color shm
|
||||
|
|
@ -57,6 +59,36 @@ static uint32_t cursor_shape; /* --cursor-shape value, 0 = none requested */
|
|||
static bool shape_sent;
|
||||
static bool fullscreen;
|
||||
|
||||
/* Primary selection: --primary-set <text> installs a source on the seat;
|
||||
--primary-demand [<outfile>] pulls that selection's "text/plain" content
|
||||
into the outfile and exits. Together they exercise the
|
||||
zwp_primary_selection_device_manager_v1 round trip between two tabs. */
|
||||
static const char *primary_text;
|
||||
static const char *primary_out;
|
||||
static struct zwp_primary_selection_device_manager_v1 *ps_mgr;
|
||||
static struct zwp_primary_selection_device_v1 *ps_dev;
|
||||
static struct zwp_primary_selection_offer_v1 *ps_offer;
|
||||
static bool ps_demanded;
|
||||
static bool ps_set_done;
|
||||
static const struct zwp_primary_selection_source_v1_listener ps_source_listener;
|
||||
|
||||
/* A primary-selection set_selection serial must be one the seat actually
|
||||
gave THIS client (wlr_seat_client_validate_event_serial), so the setter
|
||||
cannot hammer a byzantine constant. Defer the request until the first
|
||||
keyboard/pointer event serial arrives, then use it. */
|
||||
static void maybe_set_primary(uint32_t serial) {
|
||||
if (!primary_text || ps_set_done || !ps_dev)
|
||||
return;
|
||||
ps_set_done = true;
|
||||
struct zwp_primary_selection_source_v1 *s =
|
||||
zwp_primary_selection_device_manager_v1_create_source(ps_mgr);
|
||||
zwp_primary_selection_source_v1_offer(s, "text/plain;charset=utf-8");
|
||||
zwp_primary_selection_source_v1_offer(s, "text/plain");
|
||||
zwp_primary_selection_source_v1_add_listener(s, &ps_source_listener, NULL);
|
||||
zwp_primary_selection_device_v1_set_selection(ps_dev, s, serial);
|
||||
wl_display_flush(display);
|
||||
}
|
||||
|
||||
static int W = 1280, H = 800;
|
||||
static uint8_t PIXEL[4] = { 0xff, 0x00, 0x00, 0xff }; /* RGBA */
|
||||
|
||||
|
|
@ -134,7 +166,7 @@ static void keyboard_keymap(void *data, struct wl_keyboard *kb,
|
|||
|
||||
static void keyboard_key(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
uint32_t time, uint32_t key, uint32_t state) {
|
||||
(void)data; (void)kb; (void)serial; (void)time;
|
||||
(void)data; (void)kb; (void)time;
|
||||
/* 'F' (evdev 33) toggles fullscreen, giving the smoke tests a real
|
||||
client-initiated fullscreen request to feed the compositor. */
|
||||
if (key == 33 && state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||
|
|
@ -144,11 +176,14 @@ static void keyboard_key(void *data, struct wl_keyboard *kb, uint32_t serial,
|
|||
else
|
||||
xdg_toplevel_unset_fullscreen(toplevel);
|
||||
}
|
||||
if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
|
||||
maybe_set_primary(serial);
|
||||
}
|
||||
|
||||
static void keyboard_enter(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
struct wl_surface *srf, struct wl_array *keys) {
|
||||
(void)data; (void)kb; (void)serial; (void)srf; (void)keys;
|
||||
(void)data; (void)kb; (void)srf; (void)keys;
|
||||
maybe_set_primary(serial);
|
||||
}
|
||||
|
||||
static void keyboard_leave(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
|
|
@ -184,6 +219,7 @@ static const struct wl_keyboard_listener keyboard_listener = {
|
|||
static void pointer_enter(void *data, struct wl_pointer *p, uint32_t serial,
|
||||
struct wl_surface *srf, wl_fixed_t sx, wl_fixed_t sy) {
|
||||
(void)data; (void)p; (void)srf; (void)sx; (void)sy;
|
||||
maybe_set_primary(serial);
|
||||
/* wl_display_dispatch does not flush queued requests, so push the shape
|
||||
out immediately; otherwise the compositor only sees it after the next
|
||||
roundtrip the client happens to do. */
|
||||
|
|
@ -309,6 +345,11 @@ static void registry_global(void *data, struct wl_registry *reg, uint32_t name,
|
|||
rel_mgr = wl_registry_bind(reg, name,
|
||||
&zwp_relative_pointer_manager_v1_interface,
|
||||
version < 1 ? version : 1);
|
||||
} else if (strcmp(interface,
|
||||
zwp_primary_selection_device_manager_v1_interface.name) == 0) {
|
||||
ps_mgr = wl_registry_bind(reg, name,
|
||||
&zwp_primary_selection_device_manager_v1_interface,
|
||||
version < 1 ? version : 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,10 +484,139 @@ static const struct { uint32_t shape; const char *name; } SHAPES[] = {
|
|||
{ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR, "crosshair" },
|
||||
};
|
||||
|
||||
static void ps_source_send(void *data, struct zwp_primary_selection_source_v1 *s,
|
||||
const char *mime, int32_t fd) {
|
||||
(void)data; (void)s; (void)mime;
|
||||
size_t n = primary_text ? strlen(primary_text) : 0;
|
||||
if (n > 0 && write(fd, primary_text, n) < 0) {
|
||||
/* best effort; the reader will see a short/empty transfer */
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void ps_source_cancelled(void *data,
|
||||
struct zwp_primary_selection_source_v1 *s) {
|
||||
(void)data; (void)s;
|
||||
}
|
||||
|
||||
static const struct zwp_primary_selection_source_v1_listener ps_source_listener = {
|
||||
.send = ps_source_send,
|
||||
.cancelled = ps_source_cancelled,
|
||||
};
|
||||
|
||||
static void demand_primary(void);
|
||||
|
||||
static void ps_offer_offer(void *data, struct zwp_primary_selection_offer_v1 *o,
|
||||
const char *mime) {
|
||||
(void)data; (void)o;
|
||||
if (ps_demanded || !primary_out)
|
||||
return;
|
||||
if (strcmp(mime, "text/plain") == 0) {
|
||||
/* Deferred to the main loop: demand_primary() dispatches the display
|
||||
to let the setter answer, which cannot nest inside this callback. */
|
||||
ps_demanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
static const struct zwp_primary_selection_offer_v1_listener ps_offer_listener = {
|
||||
.offer = ps_offer_offer,
|
||||
};
|
||||
|
||||
static void ps_device_data_offer(void *data,
|
||||
struct zwp_primary_selection_device_v1 *d,
|
||||
struct zwp_primary_selection_offer_v1 *offer) {
|
||||
(void)data; (void)d;
|
||||
ps_offer = offer;
|
||||
zwp_primary_selection_offer_v1_add_listener(offer, &ps_offer_listener, NULL);
|
||||
}
|
||||
|
||||
static void ps_device_selection(void *data,
|
||||
struct zwp_primary_selection_device_v1 *d,
|
||||
struct zwp_primary_selection_offer_v1 *offer) {
|
||||
(void)data; (void)d; (void)offer;
|
||||
}
|
||||
|
||||
static const struct zwp_primary_selection_device_v1_listener ps_device_listener = {
|
||||
.data_offer = ps_device_data_offer,
|
||||
.selection = ps_device_selection,
|
||||
};
|
||||
|
||||
/* Receive the selection over a pipe. Reading can't be driven by display
|
||||
events alone: the compositor forwards the fd to the owning *setter* tab
|
||||
via its `send` event, so the data arrives on the pipe with no display
|
||||
event of ours to wake a wl_display_dispatch(). Poll both descriptors and
|
||||
only dispatch when the display socket actually has events. */
|
||||
static void demand_primary(void) {
|
||||
int pfd[2];
|
||||
if (pipe(pfd) != 0) {
|
||||
fprintf(stderr, "wltest: primary pipe failed\n");
|
||||
exit(1);
|
||||
}
|
||||
zwp_primary_selection_offer_v1_receive(ps_offer, "text/plain", pfd[1]);
|
||||
close(pfd[1]);
|
||||
wl_display_flush(display);
|
||||
int disp_fd = wl_display_get_fd(display);
|
||||
char *acc = NULL;
|
||||
size_t acc_len = 0, acc_cap = 0;
|
||||
char buf[4096];
|
||||
for (;;) {
|
||||
struct pollfd pfds[2];
|
||||
pfds[0].fd = pfd[0];
|
||||
pfds[0].events = POLLIN;
|
||||
pfds[1].fd = disp_fd;
|
||||
pfds[1].events = POLLIN;
|
||||
int r = poll(pfds, 2, -1);
|
||||
if (r < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (pfds[0].revents & (POLLIN | POLLHUP)) {
|
||||
ssize_t n = read(pfd[0], buf, sizeof buf);
|
||||
if (n > 0) {
|
||||
if (acc_len + (size_t)n > acc_cap) {
|
||||
acc_cap = (acc_cap ? acc_cap * 2 : 1024) + (size_t)n;
|
||||
acc = realloc(acc, acc_cap);
|
||||
if (!acc) {
|
||||
fprintf(stderr, "wltest: primary alloc failed\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
memcpy(acc + acc_len, buf, (size_t)n);
|
||||
acc_len += (size_t)n;
|
||||
continue;
|
||||
}
|
||||
if (n == 0)
|
||||
break;
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (pfds[1].revents & (POLLIN | POLLHUP | POLLERR)) {
|
||||
if (wl_display_dispatch(display) < 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
int fd = open(primary_out, O_CREAT | O_TRUNC | O_WRONLY, 0600);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "wltest: cannot open %s\n", primary_out);
|
||||
exit(1);
|
||||
}
|
||||
ssize_t w = write(fd, acc ? acc : "", acc_len);
|
||||
(void)w;
|
||||
close(fd);
|
||||
close(pfd[0]);
|
||||
free(acc);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
static void print_usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--app-id <id>] [--extra #RRGGBB] "
|
||||
"[--cursor-shape name] [--globals-out <file>] "
|
||||
"[--constrain locked|confined] [--confine-rect X Y W H] "
|
||||
"[--primary-set <text>] [--primary-demand <file>] "
|
||||
"#RRGGBB [WIDTHxHEIGHT]\n"
|
||||
" %s shows a solid-color xdg_toplevel, e.g. #ff0000\n"
|
||||
" --extra also opens a second toplevel from the same client\n"
|
||||
|
|
@ -454,7 +624,9 @@ static void print_usage(const char *argv0) {
|
|||
"pointer enter (default|text|pointer|crosshair)\n"
|
||||
" --globals-out lists advertised globals to <file> and exits\n"
|
||||
" --constrain requests a pointer constraint (locked/confined)\n"
|
||||
" --confine-rect sets the confined pointer's region\n",
|
||||
" --confine-rect sets the confined pointer's region\n"
|
||||
" --primary-set installs <text> as the primary selection\n"
|
||||
" --primary-demand pulls the selection into <file> and exits\n",
|
||||
argv0, argv0);
|
||||
}
|
||||
|
||||
|
|
@ -525,6 +697,20 @@ int main(int argc, char **argv) {
|
|||
return 1;
|
||||
}
|
||||
i += 2;
|
||||
} else if (strcmp(argv[i], "--primary-set") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "wltest: --primary-set needs a value\n");
|
||||
return 1;
|
||||
}
|
||||
primary_text = argv[i + 1];
|
||||
i += 2;
|
||||
} else if (strcmp(argv[i], "--primary-demand") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "wltest: --primary-demand needs a path\n");
|
||||
return 1;
|
||||
}
|
||||
primary_out = argv[i + 1];
|
||||
i += 2;
|
||||
} else if (strcmp(argv[i], "--confine-rect") == 0) {
|
||||
if (i + 4 >= argc) {
|
||||
fprintf(stderr, "wltest: --confine-rect needs X Y W H\n");
|
||||
|
|
@ -615,13 +801,28 @@ int main(int argc, char **argv) {
|
|||
rel_ptr = zwp_relative_pointer_manager_v1_get_relative_pointer(rel_mgr, pointer);
|
||||
}
|
||||
|
||||
/* Primary selection: the setter installs a source; the demander just
|
||||
listens and pulls on offer. get_device is what hooks the device into
|
||||
the seat's set_primary_selection fan-out, so both sides need it. The
|
||||
setter's request itself is deferred to maybe_set_primary() so it can
|
||||
attach a seat-granted serial. */
|
||||
if (ps_mgr && seat) {
|
||||
ps_dev = zwp_primary_selection_device_manager_v1_get_device(ps_mgr, seat);
|
||||
zwp_primary_selection_device_v1_add_listener(ps_dev, &ps_device_listener, NULL);
|
||||
}
|
||||
|
||||
if (constrain_mode != NO_CONSTRAINT)
|
||||
request_constraint();
|
||||
|
||||
wl_surface_commit(surface);
|
||||
wl_display_roundtrip(display);
|
||||
|
||||
while (wl_display_dispatch(display) >= 0) {
|
||||
for (;;) {
|
||||
if (ps_demanded && primary_out) {
|
||||
demand_primary(); /* never returns: reads, writes, exits */
|
||||
}
|
||||
if (wl_display_dispatch(display) < 0)
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue