Fix wlr-data-control clipboard: handle primary_selection and echo ordering

labwc sends the primary_selection event (opcode 3) on bind; leaving the
listener unset made libwayland abort. Acknowledge and destroy the offer
(primary selection is not bridged).

Echo handling used one shared pending_echo flag: when the host selection
is set via data-control, the compositor echoes the seat change over both
the wl_data_device and data-control channels, so the first echo consumed
the shared flag and the second was misread as a real host selection,
clobbering the nested source via mirror_to_nested. Track the pending
echo per backend and clear both on any real selection.

Verified: clipboard tests pass on labwc (data-control) and GNOME/Mutter
(wl_data_device); full 7-test suite green on both hosts.
This commit is contained in:
Andrew Hamilton 2026-08-10 19:10:58 +10:00
parent fec8266154
commit ad46508962

View file

@ -224,7 +224,8 @@ struct clipboard_bridge {
/* MIME list currently being collected from a host offer */
char **mimes;
size_t mimes_len, mimes_cap;
bool pending_echo; /* set before host set_selection to catch its echo */
bool dc_pending_echo; /* our own set via data-control: echo not yet consumed */
bool dd_pending_echo; /* our own set via wl_data_device: echo not yet consumed */
bool host_set_pending; /* nested selection not yet on the host; retry on focus */
/* Eager text prefetch: when a real host selection is mirrored, its primary
@ -1371,7 +1372,8 @@ static void bridge_dc_source_cancelled(void *data,
(void)data;
zwlr_data_control_source_v1_destroy(src);
bridge.dc_source = NULL;
bridge.pending_echo = false;
bridge.dc_pending_echo = false;
bridge.dd_pending_echo = false;
}
static const struct zwlr_data_control_source_v1_listener bridge_dc_source_listener = {
@ -1412,8 +1414,8 @@ static void bridge_dc_device_data_offer(void *data,
struct zwlr_data_control_device_v1 *dev,
struct zwlr_data_control_offer_v1 *offer) {
(void)data; (void)dev;
if (bridge.pending_echo) {
bridge.pending_echo = false;
if (bridge.dc_pending_echo) {
bridge.dc_pending_echo = false;
bridge.dc_echo_offer = offer;
}
bridge_mimes_reset();
@ -1432,6 +1434,8 @@ static void bridge_dc_device_selection(void *data,
zwlr_data_control_offer_v1_destroy(offer);
return;
}
bridge.dc_pending_echo = false;
bridge.dd_pending_echo = false;
if (bridge.dc_offer)
zwlr_data_control_offer_v1_destroy(bridge.dc_offer);
bridge.dc_offer = offer;
@ -1444,11 +1448,22 @@ static void bridge_dc_device_selection(void *data,
bridge_mirror_to_nested();
}
/* Primary selection is intentionally not bridged; the offer must still be
acknowledged (NULL handler would make libwayland abort on the event) and
destroyed so it does not leak. */
static void bridge_dc_device_primary_selection(void *data,
struct zwlr_data_control_device_v1 *dev,
struct zwlr_data_control_offer_v1 *offer) {
(void)data; (void)dev;
if (offer)
zwlr_data_control_offer_v1_destroy(offer);
}
static const struct zwlr_data_control_device_v1_listener bridge_dc_listener = {
.data_offer = bridge_dc_device_data_offer,
.selection = bridge_dc_device_selection,
.primary_selection = bridge_dc_device_primary_selection,
.finished = bridge_dc_device_finished,
/* primary_selection left unset: not bridged */
};
/* ---------------- host backend: wl_data_device (focus-based) ---------------- */
@ -1466,7 +1481,8 @@ static void bridge_dd_source_cancelled(void *data, struct wl_data_source *src) {
(void)data;
wl_data_source_destroy(src);
bridge.dd_source = NULL;
bridge.pending_echo = false;
bridge.dc_pending_echo = false;
bridge.dd_pending_echo = false;
bridge.host_set_pending = true;
}
@ -1489,8 +1505,8 @@ static const struct wl_data_offer_listener bridge_dd_offer_listener = {
static void bridge_dd_data_offer(void *data, struct wl_data_device *dev,
struct wl_data_offer *offer) {
(void)data; (void)dev;
if (bridge.pending_echo) {
bridge.pending_echo = false;
if (bridge.dd_pending_echo) {
bridge.dd_pending_echo = false;
bridge.dd_echo_offer = offer;
}
bridge_mimes_reset();
@ -1508,6 +1524,8 @@ static void bridge_dd_selection(void *data, struct wl_data_device *dev,
wl_data_offer_destroy(offer);
return;
}
bridge.dc_pending_echo = false;
bridge.dd_pending_echo = false;
if (bridge.dd_offer)
wl_data_offer_destroy(bridge.dd_offer);
bridge.dd_offer = offer;
@ -1551,7 +1569,8 @@ static void bridge_host_set_selection(struct wlr_data_source *src) {
zwlr_data_control_source_v1_offer(s, *p);
zwlr_data_control_source_v1_add_listener(s, &bridge_dc_source_listener, NULL);
bridge.dc_source = s;
bridge.pending_echo = true;
bridge.dc_pending_echo = true;
bridge.dd_pending_echo = true;
zwlr_data_control_device_v1_set_selection(bridge.dc_device, s);
bridge.host_set_pending = false;
return;
@ -1575,7 +1594,8 @@ static void bridge_host_set_selection(struct wlr_data_source *src) {
wl_data_source_offer(s, *p);
wl_data_source_add_listener(s, &bridge_dd_source_listener, NULL);
bridge.dd_source = s;
bridge.pending_echo = true;
bridge.dc_pending_echo = true;
bridge.dd_pending_echo = true;
wl_data_device_set_selection(bridge.dd, s, bridge.kbd_serial);
bridge.host_set_pending = false;
return;
@ -1872,7 +1892,8 @@ static void host_kbd_enter(void *data, struct wl_keyboard *kbd, uint32_t serial,
/* Remember the enter serial for host clipboard set_selection, and drop any
stale echo guard from a set_selection made while unfocused. */
bridge.kbd_serial = serial;
bridge.pending_echo = false;
bridge.dc_pending_echo = false;
bridge.dd_pending_echo = false;
if (bridge.host_set_pending && server.seat->selection_source &&
server.seat->selection_source->impl != &bridge_inner_source_impl) {
bridge.host_set_pending = false;