diff options
Diffstat (limited to 'xwayland')
-rw-r--r-- | xwayland/meson.build | 51 | ||||
-rw-r--r-- | xwayland/selection/dnd.c | 339 | ||||
-rw-r--r-- | xwayland/selection/incoming.c | 464 | ||||
-rw-r--r-- | xwayland/selection/outgoing.c | 419 | ||||
-rw-r--r-- | xwayland/selection/selection.c | 317 | ||||
-rw-r--r-- | xwayland/sockets.c | 168 | ||||
-rw-r--r-- | xwayland/sockets.h | 7 | ||||
-rw-r--r-- | xwayland/xwayland.c | 488 | ||||
-rw-r--r-- | xwayland/xwm.c | 1802 |
9 files changed, 4055 insertions, 0 deletions
diff --git a/xwayland/meson.build b/xwayland/meson.build new file mode 100644 index 00000000..e5f9b9be --- /dev/null +++ b/xwayland/meson.build @@ -0,0 +1,51 @@ +xwayland_libs = [] +xwayland_required = [ + 'xcb', + 'xcb-composite', + 'xcb-render', + 'xcb-xfixes', +] +xwayland_optional = [ + 'xcb-errors', + 'xcb-icccm', +] + +foreach lib : xwayland_required + dep = dependency(lib, required: get_option('xwayland')) + if not dep.found() + subdir_done() + endif + + xwayland_libs += dep +endforeach + +foreach lib : xwayland_optional + dep = dependency(lib, required: get_option(lib)) + if dep.found() + xwayland_libs += dep + conf_data.set10('WLR_HAS_' + lib.underscorify().to_upper(), true) + endif +endforeach + +lib_wlr_xwayland = static_library( + 'wlr_xwayland', + files( + 'selection/dnd.c', + 'selection/incoming.c', + 'selection/outgoing.c', + 'selection/selection.c', + 'sockets.c', + 'xwayland.c', + 'xwm.c', + ), + include_directories: wlr_inc, + dependencies: [ + wayland_server, + xwayland_libs, + xkbcommon, + pixman, + ], +) + +wlr_parts += lib_wlr_xwayland +conf_data.set10('WLR_HAS_XWAYLAND', true) diff --git a/xwayland/selection/dnd.c b/xwayland/selection/dnd.c new file mode 100644 index 00000000..ec5f16c7 --- /dev/null +++ b/xwayland/selection/dnd.c @@ -0,0 +1,339 @@ +#include <assert.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wlr/types/wlr_data_device.h> +#include <wlr/types/wlr_gtk_primary_selection.h> +#include <wlr/util/log.h> +#include <xcb/xfixes.h> +#include "xwayland/xwm.h" +#include "xwayland/selection.h" + +static xcb_atom_t data_device_manager_dnd_action_to_atom( + struct wlr_xwm *xwm, enum wl_data_device_manager_dnd_action action) { + if (action & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY) { + return xwm->atoms[DND_ACTION_COPY]; + } else if (action & WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE) { + return xwm->atoms[DND_ACTION_MOVE]; + } else if (action & WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK) { + return xwm->atoms[DND_ACTION_ASK]; + } + return XCB_ATOM_NONE; +} + +static enum wl_data_device_manager_dnd_action + data_device_manager_dnd_action_from_atom(struct wlr_xwm *xwm, + enum atom_name atom) { + if (atom == xwm->atoms[DND_ACTION_COPY] || + atom == xwm->atoms[DND_ACTION_PRIVATE]) { + return WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY; + } else if (atom == xwm->atoms[DND_ACTION_MOVE]) { + return WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE; + } else if (atom == xwm->atoms[DND_ACTION_ASK]) { + return WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK; + } + return WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; +} + +static void xwm_dnd_send_event(struct wlr_xwm *xwm, xcb_atom_t type, + xcb_client_message_data_t *data) { + struct wlr_xwayland_surface *dest = xwm->drag_focus; + assert(dest != NULL); + + xcb_client_message_event_t event = { + .response_type = XCB_CLIENT_MESSAGE, + .format = 32, + .sequence = 0, + .window = dest->window_id, + .type = type, + .data = *data, + }; + + xcb_send_event(xwm->xcb_conn, + 0, // propagate + dest->window_id, + XCB_EVENT_MASK_NO_EVENT, + (const char *)&event); + xcb_flush(xwm->xcb_conn); +} + +static void xwm_dnd_send_enter(struct wlr_xwm *xwm) { + struct wlr_drag *drag = xwm->drag; + assert(drag != NULL); + struct wl_array *mime_types = &drag->source->mime_types; + + xcb_client_message_data_t data = { 0 }; + data.data32[0] = xwm->dnd_window; + data.data32[1] = XDND_VERSION << 24; + + // If we have 3 MIME types or less, we can send them directly in the + // DND_ENTER message + size_t n = mime_types->size / sizeof(char *); + if (n <= 3) { + size_t i = 0; + char **mime_type_ptr; + wl_array_for_each(mime_type_ptr, mime_types) { + char *mime_type = *mime_type_ptr; + data.data32[2+i] = xwm_mime_type_to_atom(xwm, mime_type); + ++i; + } + } else { + // Let the client know that targets are not contained in the message + // data and must be retrieved with the DND_TYPE_LIST property + data.data32[1] |= 1; + + xcb_atom_t targets[n]; + size_t i = 0; + char **mime_type_ptr; + wl_array_for_each(mime_type_ptr, mime_types) { + char *mime_type = *mime_type_ptr; + targets[i] = xwm_mime_type_to_atom(xwm, mime_type); + ++i; + } + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xwm->dnd_window, + xwm->atoms[DND_TYPE_LIST], + XCB_ATOM_ATOM, + 32, // format + n, targets); + } + + xwm_dnd_send_event(xwm, xwm->atoms[DND_ENTER], &data); +} + +static void xwm_dnd_send_position(struct wlr_xwm *xwm, uint32_t time, int16_t x, + int16_t y) { + struct wlr_drag *drag = xwm->drag; + assert(drag != NULL); + + xcb_client_message_data_t data = { 0 }; + data.data32[0] = xwm->dnd_window; + data.data32[2] = (x << 16) | y; + data.data32[3] = time; + data.data32[4] = + data_device_manager_dnd_action_to_atom(xwm, drag->source->actions); + + xwm_dnd_send_event(xwm, xwm->atoms[DND_POSITION], &data); +} + +static void xwm_dnd_send_drop(struct wlr_xwm *xwm, uint32_t time) { + struct wlr_drag *drag = xwm->drag; + assert(drag != NULL); + struct wlr_xwayland_surface *dest = xwm->drag_focus; + assert(dest != NULL); + + xcb_client_message_data_t data = { 0 }; + data.data32[0] = xwm->dnd_window; + data.data32[2] = time; + + xwm_dnd_send_event(xwm, xwm->atoms[DND_DROP], &data); +} + +static void xwm_dnd_send_leave(struct wlr_xwm *xwm) { + struct wlr_drag *drag = xwm->drag; + assert(drag != NULL); + struct wlr_xwayland_surface *dest = xwm->drag_focus; + assert(dest != NULL); + + xcb_client_message_data_t data = { 0 }; + data.data32[0] = xwm->dnd_window; + + xwm_dnd_send_event(xwm, xwm->atoms[DND_LEAVE], &data); +} + +/*static void xwm_dnd_send_finished(struct wlr_xwm *xwm) { + struct wlr_drag *drag = xwm->drag; + assert(drag != NULL); + struct wlr_xwayland_surface *dest = xwm->drag_focus; + assert(dest != NULL); + + xcb_client_message_data_t data = { 0 }; + data.data32[0] = xwm->dnd_window; + data.data32[1] = drag->source->accepted; + + if (drag->source->accepted) { + data.data32[2] = data_device_manager_dnd_action_to_atom(xwm, + drag->source->current_dnd_action); + } + + xwm_dnd_send_event(xwm, xwm->atoms[DND_FINISHED], &data); +}*/ + +int xwm_handle_selection_client_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *ev) { + if (ev->type == xwm->atoms[DND_STATUS]) { + if (xwm->drag == NULL) { + wlr_log(WLR_DEBUG, "ignoring XdndStatus client message because " + "there's no drag"); + return 1; + } + + xcb_client_message_data_t *data = &ev->data; + xcb_window_t target_window = data->data32[0]; + bool accepted = data->data32[1] & 1; + xcb_atom_t action_atom = data->data32[4]; + + if (xwm->drag_focus == NULL || + target_window != xwm->drag_focus->window_id) { + wlr_log(WLR_DEBUG, "ignoring XdndStatus client message because " + "it doesn't match the current drag focus window ID"); + return 1; + } + + enum wl_data_device_manager_dnd_action action = + data_device_manager_dnd_action_from_atom(xwm, action_atom); + + struct wlr_drag *drag = xwm->drag; + assert(drag != NULL); + + drag->source->accepted = accepted; + wlr_data_source_dnd_action(drag->source, action); + + wlr_log(WLR_DEBUG, "DND_STATUS window=%d accepted=%d action=%d", + target_window, accepted, action); + return 1; + } else if (ev->type == xwm->atoms[DND_FINISHED]) { + // This should only happen after the drag has ended, but before the drag + // source is destroyed + if (xwm->seat == NULL || xwm->seat->drag_source == NULL || + xwm->drag != NULL) { + wlr_log(WLR_DEBUG, "ignoring XdndFinished client message because " + "there's no finished drag"); + return 1; + } + + struct wlr_data_source *source = xwm->seat->drag_source; + + xcb_client_message_data_t *data = &ev->data; + xcb_window_t target_window = data->data32[0]; + bool performed = data->data32[1] & 1; + xcb_atom_t action_atom = data->data32[2]; + + if (xwm->drag_focus == NULL || + target_window != xwm->drag_focus->window_id) { + wlr_log(WLR_DEBUG, "ignoring XdndFinished client message because " + "it doesn't match the finished drag focus window ID"); + return 1; + } + + enum wl_data_device_manager_dnd_action action = + data_device_manager_dnd_action_from_atom(xwm, action_atom); + + if (performed) { + wlr_data_source_dnd_finish(source); + } + + wlr_log(WLR_DEBUG, "DND_FINISH window=%d performed=%d action=%d", + target_window, performed, action); + return 1; + } else { + return 0; + } +} + +static void seat_handle_drag_focus(struct wl_listener *listener, void *data) { + struct wlr_drag *drag = data; + struct wlr_xwm *xwm = wl_container_of(listener, xwm, seat_drag_focus); + + struct wlr_xwayland_surface *focus = NULL; + if (drag->focus != NULL) { + // TODO: check for subsurfaces? + struct wlr_xwayland_surface *surface; + wl_list_for_each(surface, &xwm->surfaces, link) { + if (surface->surface == drag->focus) { + focus = surface; + break; + } + } + } + + if (focus == xwm->drag_focus) { + return; + } + + if (xwm->drag_focus != NULL) { + wlr_data_source_dnd_action(drag->source, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); + xwm_dnd_send_leave(xwm); + } + + xwm->drag_focus = focus; + + if (xwm->drag_focus != NULL) { + xwm_dnd_send_enter(xwm); + } +} + +static void seat_handle_drag_motion(struct wl_listener *listener, void *data) { + struct wlr_xwm *xwm = wl_container_of(listener, xwm, seat_drag_motion); + struct wlr_drag_motion_event *event = data; + struct wlr_xwayland_surface *surface = xwm->drag_focus; + + if (surface == NULL) { + return; // No xwayland surface focused + } + + xwm_dnd_send_position(xwm, event->time, surface->x + (int16_t)event->sx, + surface->y + (int16_t)event->sy); +} + +static void seat_handle_drag_drop(struct wl_listener *listener, void *data) { + struct wlr_xwm *xwm = wl_container_of(listener, xwm, seat_drag_drop); + struct wlr_drag_drop_event *event = data; + + if (xwm->drag_focus == NULL) { + return; // No xwayland surface focused + } + + wlr_log(WLR_DEBUG, "Wayland drag dropped over an Xwayland window"); + xwm_dnd_send_drop(xwm, event->time); +} + +static void seat_handle_drag_destroy(struct wl_listener *listener, void *data) { + struct wlr_xwm *xwm = wl_container_of(listener, xwm, seat_drag_destroy); + + // Don't reset drag focus yet because the target will read the drag source + // right after + if (xwm->drag_focus != NULL && !xwm->drag->source->accepted) { + wlr_log(WLR_DEBUG, "Wayland drag cancelled over an Xwayland window"); + xwm_dnd_send_leave(xwm); + } + + wl_list_remove(&xwm->seat_drag_focus.link); + wl_list_remove(&xwm->seat_drag_motion.link); + wl_list_remove(&xwm->seat_drag_drop.link); + wl_list_remove(&xwm->seat_drag_destroy.link); + xwm->drag = NULL; +} + +static void seat_handle_drag_source_destroy(struct wl_listener *listener, + void *data) { + struct wlr_xwm *xwm = + wl_container_of(listener, xwm, seat_drag_source_destroy); + + wl_list_remove(&xwm->seat_drag_source_destroy.link); + xwm->drag_focus = NULL; +} + +void xwm_seat_handle_start_drag(struct wlr_xwm *xwm, struct wlr_drag *drag) { + xwm->drag = drag; + xwm->drag_focus = NULL; + + if (drag != NULL) { + wl_signal_add(&drag->events.focus, &xwm->seat_drag_focus); + xwm->seat_drag_focus.notify = seat_handle_drag_focus; + wl_signal_add(&drag->events.motion, &xwm->seat_drag_motion); + xwm->seat_drag_motion.notify = seat_handle_drag_motion; + wl_signal_add(&drag->events.drop, &xwm->seat_drag_drop); + xwm->seat_drag_drop.notify = seat_handle_drag_drop; + wl_signal_add(&drag->events.destroy, &xwm->seat_drag_destroy); + xwm->seat_drag_destroy.notify = seat_handle_drag_destroy; + + wl_signal_add(&drag->source->events.destroy, + &xwm->seat_drag_source_destroy); + xwm->seat_drag_source_destroy.notify = seat_handle_drag_source_destroy; + } +} diff --git a/xwayland/selection/incoming.c b/xwayland/selection/incoming.c new file mode 100644 index 00000000..3e97ca04 --- /dev/null +++ b/xwayland/selection/incoming.c @@ -0,0 +1,464 @@ +#define _POSIX_C_SOURCE 200809L +#include <assert.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wlr/types/wlr_data_device.h> +#include <wlr/types/wlr_primary_selection.h> +#include <wlr/util/log.h> +#include <xcb/xfixes.h> +#include "xwayland/selection.h" +#include "xwayland/xwm.h" + +/** + * Write the X11 selection to a Wayland client. + */ +static int xwm_data_source_write(int fd, uint32_t mask, void *data) { + struct wlr_xwm_selection_transfer *transfer = data; + struct wlr_xwm *xwm = transfer->selection->xwm; + + char *property = xcb_get_property_value(transfer->property_reply); + int remainder = xcb_get_property_value_length(transfer->property_reply) - + transfer->property_start; + + ssize_t len = write(fd, property + transfer->property_start, remainder); + if (len == -1) { + xwm_selection_transfer_destroy_property_reply(transfer); + xwm_selection_transfer_remove_source(transfer); + xwm_selection_transfer_close_source_fd(transfer); + wlr_log(WLR_ERROR, "write error to target fd: %m"); + return 1; + } + + wlr_log(WLR_DEBUG, "wrote %zd (chunk size %zd) of %d bytes", + transfer->property_start + len, + len, xcb_get_property_value_length(transfer->property_reply)); + + transfer->property_start += len; + if (len == remainder) { + xwm_selection_transfer_destroy_property_reply(transfer); + xwm_selection_transfer_remove_source(transfer); + + if (transfer->incr) { + wlr_log(WLR_DEBUG, "deleting property"); + xcb_delete_property(xwm->xcb_conn, transfer->selection->window, + xwm->atoms[WL_SELECTION]); + xcb_flush(xwm->xcb_conn); + } else { + wlr_log(WLR_DEBUG, "transfer complete"); + xwm_selection_transfer_close_source_fd(transfer); + } + } + + return 1; +} + +static void xwm_write_property(struct wlr_xwm_selection_transfer *transfer, + xcb_get_property_reply_t *reply) { + struct wlr_xwm *xwm = transfer->selection->xwm; + + transfer->property_start = 0; + transfer->property_reply = reply; + + xwm_data_source_write(transfer->source_fd, WL_EVENT_WRITABLE, transfer); + + if (transfer->property_reply != NULL) { + struct wl_event_loop *loop = + wl_display_get_event_loop(xwm->xwayland->wl_display); + transfer->source = wl_event_loop_add_fd(loop, + transfer->source_fd, WL_EVENT_WRITABLE, xwm_data_source_write, + transfer); + } +} + +void xwm_get_incr_chunk(struct wlr_xwm_selection_transfer *transfer) { + struct wlr_xwm *xwm = transfer->selection->xwm; + wlr_log(WLR_DEBUG, "xwm_get_incr_chunk"); + + xcb_get_property_cookie_t cookie = xcb_get_property(xwm->xcb_conn, + 0, // delete + transfer->selection->window, + xwm->atoms[WL_SELECTION], + XCB_GET_PROPERTY_TYPE_ANY, + 0, // offset + 0x1fffffff // length + ); + + xcb_get_property_reply_t *reply = + xcb_get_property_reply(xwm->xcb_conn, cookie, NULL); + if (reply == NULL) { + wlr_log(WLR_ERROR, "cannot get selection property"); + return; + } + //dump_property(xwm, xwm->atoms[WL_SELECTION], reply); + + if (xcb_get_property_value_length(reply) > 0) { + /* Reply's ownership is transferred to xwm, which is responsible + * for freeing it */ + xwm_write_property(transfer, reply); + } else { + wlr_log(WLR_DEBUG, "transfer complete"); + xwm_selection_transfer_close_source_fd(transfer); + free(reply); + } +} + +static void xwm_selection_get_data(struct wlr_xwm_selection *selection) { + struct wlr_xwm *xwm = selection->xwm; + + xcb_get_property_cookie_t cookie = xcb_get_property(xwm->xcb_conn, + 1, // delete + selection->window, + xwm->atoms[WL_SELECTION], + XCB_GET_PROPERTY_TYPE_ANY, + 0, // offset + 0x1fffffff // length + ); + + xcb_get_property_reply_t *reply = + xcb_get_property_reply(xwm->xcb_conn, cookie, NULL); + if (reply == NULL) { + wlr_log(WLR_ERROR, "Cannot get selection property"); + return; + } + + struct wlr_xwm_selection_transfer *transfer = &selection->incoming; + if (reply->type == xwm->atoms[INCR]) { + transfer->incr = true; + free(reply); + } else { + transfer->incr = false; + // reply's ownership is transferred to wm, which is responsible + // for freeing it + xwm_write_property(transfer, reply); + } +} + +static void source_send(struct wlr_xwm_selection *selection, + struct wl_array *mime_types, struct wl_array *mime_types_atoms, + const char *requested_mime_type, int fd) { + struct wlr_xwm *xwm = selection->xwm; + struct wlr_xwm_selection_transfer *transfer = &selection->incoming; + + xcb_atom_t *atoms = mime_types_atoms->data; + bool found = false; + xcb_atom_t mime_type_atom; + char **mime_type_ptr; + size_t i = 0; + wl_array_for_each(mime_type_ptr, mime_types) { + char *mime_type = *mime_type_ptr; + if (strcmp(mime_type, requested_mime_type) == 0) { + found = true; + mime_type_atom = atoms[i]; + break; + } + ++i; + } + if (!found) { + wlr_log(WLR_DEBUG, "Cannot send X11 selection to Wayland: " + "unsupported MIME type"); + return; + } + + xcb_convert_selection(xwm->xcb_conn, + selection->window, + selection->atom, + mime_type_atom, + xwm->atoms[WL_SELECTION], + XCB_TIME_CURRENT_TIME); + + xcb_flush(xwm->xcb_conn); + + fcntl(fd, F_SETFL, O_WRONLY | O_NONBLOCK); + transfer->source_fd = fd; +} + +struct x11_data_source { + struct wlr_data_source base; + struct wlr_xwm_selection *selection; + struct wl_array mime_types_atoms; +}; + +static const struct wlr_data_source_impl data_source_impl; + +bool data_source_is_xwayland( + struct wlr_data_source *wlr_source) { + return wlr_source->impl == &data_source_impl; +} + +static struct x11_data_source *data_source_from_wlr_data_source( + struct wlr_data_source *wlr_source) { + assert(data_source_is_xwayland(wlr_source)); + return (struct x11_data_source *)wlr_source; +} + +static void data_source_send(struct wlr_data_source *wlr_source, + const char *mime_type, int32_t fd) { + struct x11_data_source *source = + data_source_from_wlr_data_source(wlr_source); + struct wlr_xwm_selection *selection = source->selection; + + source_send(selection, &wlr_source->mime_types, &source->mime_types_atoms, + mime_type, fd); +} + +static void data_source_cancel(struct wlr_data_source *wlr_source) { + struct x11_data_source *source = + data_source_from_wlr_data_source(wlr_source); + wlr_data_source_finish(&source->base); + wl_array_release(&source->mime_types_atoms); + free(source); +} + +static const struct wlr_data_source_impl data_source_impl = { + .send = data_source_send, + .cancel = data_source_cancel, +}; + +struct x11_primary_selection_source { + struct wlr_primary_selection_source base; + struct wlr_xwm_selection *selection; + struct wl_array mime_types_atoms; +}; + +static const struct wlr_primary_selection_source_impl + primary_selection_source_impl; + +bool primary_selection_source_is_xwayland( + struct wlr_primary_selection_source *wlr_source) { + return wlr_source->impl == &primary_selection_source_impl; +} + +static void primary_selection_source_send( + struct wlr_primary_selection_source *wlr_source, + const char *mime_type, int fd) { + struct x11_primary_selection_source *source = + (struct x11_primary_selection_source *)wlr_source; + struct wlr_xwm_selection *selection = source->selection; + + source_send(selection, &wlr_source->mime_types, &source->mime_types_atoms, + mime_type, fd); +} + +static void primary_selection_source_destroy( + struct wlr_primary_selection_source *wlr_source) { + struct x11_primary_selection_source *source = + (struct x11_primary_selection_source *)wlr_source; + wl_array_release(&source->mime_types_atoms); + free(source); +} + +static const struct wlr_primary_selection_source_impl + primary_selection_source_impl = { + .send = primary_selection_source_send, + .destroy = primary_selection_source_destroy, +}; + +static bool source_get_targets(struct wlr_xwm_selection *selection, + struct wl_array *mime_types, struct wl_array *mime_types_atoms) { + struct wlr_xwm *xwm = selection->xwm; + + xcb_get_property_cookie_t cookie = xcb_get_property(xwm->xcb_conn, + 1, // delete + selection->window, + xwm->atoms[WL_SELECTION], + XCB_GET_PROPERTY_TYPE_ANY, + 0, // offset + 4096 // length + ); + + xcb_get_property_reply_t *reply = + xcb_get_property_reply(xwm->xcb_conn, cookie, NULL); + if (reply == NULL) { + return false; + } + + if (reply->type != XCB_ATOM_ATOM) { + free(reply); + return false; + } + + xcb_atom_t *value = xcb_get_property_value(reply); + for (uint32_t i = 0; i < reply->value_len; i++) { + char *mime_type = NULL; + + if (value[i] == xwm->atoms[UTF8_STRING]) { + mime_type = strdup("text/plain;charset=utf-8"); + } else if (value[i] == xwm->atoms[TEXT]) { + mime_type = strdup("text/plain"); + } else if (value[i] != xwm->atoms[TARGETS] && + value[i] != xwm->atoms[TIMESTAMP]) { + xcb_get_atom_name_cookie_t name_cookie = + xcb_get_atom_name(xwm->xcb_conn, value[i]); + xcb_get_atom_name_reply_t *name_reply = + xcb_get_atom_name_reply(xwm->xcb_conn, name_cookie, NULL); + if (name_reply == NULL) { + continue; + } + size_t len = xcb_get_atom_name_name_length(name_reply); + char *name = xcb_get_atom_name_name(name_reply); // not a C string + if (memchr(name, '/', len) != NULL) { + mime_type = malloc((len + 1) * sizeof(char)); + if (mime_type == NULL) { + free(name_reply); + continue; + } + memcpy(mime_type, name, len); + mime_type[len] = '\0'; + } + free(name_reply); + } + + if (mime_type != NULL) { + char **mime_type_ptr = + wl_array_add(mime_types, sizeof(*mime_type_ptr)); + if (mime_type_ptr == NULL) { + free(mime_type); + break; + } + *mime_type_ptr = mime_type; + + xcb_atom_t *atom_ptr = + wl_array_add(mime_types_atoms, sizeof(*atom_ptr)); + if (atom_ptr == NULL) { + break; + } + *atom_ptr = value[i]; + } + } + + free(reply); + return true; +} + +static void xwm_selection_get_targets(struct wlr_xwm_selection *selection) { + // set the wayland selection to the X11 selection + struct wlr_xwm *xwm = selection->xwm; + + if (selection == &xwm->clipboard_selection) { + struct x11_data_source *source = + calloc(1, sizeof(struct x11_data_source)); + if (source == NULL) { + return; + } + wlr_data_source_init(&source->base, &data_source_impl); + + source->selection = selection; + wl_array_init(&source->mime_types_atoms); + + bool ok = source_get_targets(selection, &source->base.mime_types, + &source->mime_types_atoms); + if (ok) { + wlr_seat_set_selection(xwm->seat, &source->base, + wl_display_next_serial(xwm->xwayland->wl_display)); + } else { + wlr_data_source_cancel(&source->base); + } + } else if (selection == &xwm->primary_selection) { + struct x11_primary_selection_source *source = + calloc(1, sizeof(struct x11_primary_selection_source)); + if (source == NULL) { + return; + } + wlr_primary_selection_source_init(&source->base, + &primary_selection_source_impl); + + source->selection = selection; + wl_array_init(&source->mime_types_atoms); + + bool ok = source_get_targets(selection, &source->base.mime_types, + &source->mime_types_atoms); + if (ok) { + wlr_seat_set_primary_selection(xwm->seat, &source->base); + } else { + wlr_primary_selection_source_destroy(&source->base); + } + } else if (selection == &xwm->dnd_selection) { + // TODO + } +} + +void xwm_handle_selection_notify(struct wlr_xwm *xwm, + xcb_selection_notify_event_t *event) { + wlr_log(WLR_DEBUG, "XCB_SELECTION_NOTIFY (selection=%u, property=%u, target=%u)", + event->selection, event->property, + event->target); + + struct wlr_xwm_selection *selection = + xwm_get_selection(xwm, event->selection); + if (selection == NULL) { + return; + } + + if (event->property == XCB_ATOM_NONE) { + wlr_log(WLR_ERROR, "convert selection failed"); + } else if (event->target == xwm->atoms[TARGETS]) { + // No xwayland surface focused, deny access to clipboard + if (xwm->focus_surface == NULL) { + wlr_log(WLR_DEBUG, "denying write access to clipboard: " + "no xwayland surface focused"); + return; + } + + // This sets the Wayland clipboard (by calling wlr_seat_set_selection) + xwm_selection_get_targets(selection); + } else { + xwm_selection_get_data(selection); + } +} + +int xwm_handle_xfixes_selection_notify(struct wlr_xwm *xwm, + xcb_xfixes_selection_notify_event_t *event) { + wlr_log(WLR_DEBUG, "XCB_XFIXES_SELECTION_NOTIFY (selection=%u, owner=%u)", + event->selection, event->owner); + + struct wlr_xwm_selection *selection = + xwm_get_selection(xwm, event->selection); + if (selection == NULL) { + return 0; + } + + if (event->owner == XCB_WINDOW_NONE) { + if (selection->owner != selection->window) { + // A real X client selection went away, not our + // proxy selection + if (selection == &xwm->clipboard_selection) { + wlr_seat_set_selection(xwm->seat, NULL, + wl_display_next_serial(xwm->xwayland->wl_display)); + } else if (selection == &xwm->primary_selection) { + wlr_seat_set_primary_selection(xwm->seat, NULL); + } else if (selection == &xwm->dnd_selection) { + // TODO: DND + } else { + wlr_log(WLR_DEBUG, "X11 selection has been cleared, but cannot " + "clear Wayland selection"); + } + } + + selection->owner = XCB_WINDOW_NONE; + return 1; + } + + selection->owner = event->owner; + + // We have to use XCB_TIME_CURRENT_TIME when we claim the + // selection, so grab the actual timestamp here so we can + // answer TIMESTAMP conversion requests correctly. + if (event->owner == selection->window) { + selection->timestamp = event->timestamp; + return 1; + } + + struct wlr_xwm_selection_transfer *transfer = &selection->incoming; + transfer->incr = false; + // doing this will give a selection notify where we actually handle the sync + xcb_convert_selection(xwm->xcb_conn, selection->window, + selection->atom, + xwm->atoms[TARGETS], + xwm->atoms[WL_SELECTION], + event->timestamp); + xcb_flush(xwm->xcb_conn); + + return 1; +} diff --git a/xwayland/selection/outgoing.c b/xwayland/selection/outgoing.c new file mode 100644 index 00000000..fd5021d5 --- /dev/null +++ b/xwayland/selection/outgoing.c @@ -0,0 +1,419 @@ +#include <assert.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wlr/types/wlr_data_device.h> +#include <wlr/types/wlr_primary_selection.h> +#include <wlr/util/log.h> +#include <xcb/xfixes.h> +#include "xwayland/selection.h" +#include "xwayland/xwm.h" + +static void xwm_selection_send_notify(struct wlr_xwm *xwm, + xcb_selection_request_event_t *req, bool success) { + xcb_selection_notify_event_t selection_notify = { + .response_type = XCB_SELECTION_NOTIFY, + .sequence = 0, + .time = req->time, + .requestor = req->requestor, + .selection = req->selection, + .target = req->target, + .property = success ? req->property : XCB_ATOM_NONE, + }; + + wlr_log(WLR_DEBUG, "SendEvent destination=%d SelectionNotify(31) time=%d " + "requestor=%d selection=%d target=%d property=%d", req->requestor, + req->time, req->requestor, req->selection, req->target, + selection_notify.property); + xcb_send_event(xwm->xcb_conn, + 0, // propagate + req->requestor, + XCB_EVENT_MASK_NO_EVENT, + (const char *)&selection_notify); + xcb_flush(xwm->xcb_conn); +} + +static int xwm_selection_flush_source_data( + struct wlr_xwm_selection_transfer *transfer) { + xcb_change_property(transfer->selection->xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + transfer->request.requestor, + transfer->request.property, + transfer->request.target, + 8, // format + transfer->source_data.size, + transfer->source_data.data); + xcb_flush(transfer->selection->xwm->xcb_conn); + transfer->property_set = true; + size_t length = transfer->source_data.size; + transfer->source_data.size = 0; + return length; +} + +static void xwm_selection_transfer_start_outgoing( + struct wlr_xwm_selection_transfer *transfer); + +static void xwm_selection_transfer_destroy_outgoing( + struct wlr_xwm_selection_transfer *transfer) { + wl_list_remove(&transfer->outgoing_link); + + // Start next queued transfer + struct wlr_xwm_selection_transfer *first = NULL; + if (!wl_list_empty(&transfer->selection->outgoing)) { + first = wl_container_of(transfer->selection->outgoing.prev, first, + outgoing_link); + xwm_selection_transfer_start_outgoing(first); + } + + xwm_selection_transfer_remove_source(transfer); + xwm_selection_transfer_close_source_fd(transfer); + wl_array_release(&transfer->source_data); + free(transfer); +} + +static int xwm_data_source_read(int fd, uint32_t mask, void *data) { + struct wlr_xwm_selection_transfer *transfer = data; + struct wlr_xwm *xwm = transfer->selection->xwm; + + void *p; + size_t current = transfer->source_data.size; + if (transfer->source_data.size < INCR_CHUNK_SIZE) { + p = wl_array_add(&transfer->source_data, INCR_CHUNK_SIZE); + if (p == NULL) { + wlr_log(WLR_ERROR, "Could not allocate selection source_data"); + goto error_out; + } + } else { + p = (char *)transfer->source_data.data + transfer->source_data.size; + } + + size_t available = transfer->source_data.alloc - current; + ssize_t len = read(fd, p, available); + if (len == -1) { + wlr_log(WLR_ERROR, "read error from data source: %m"); + goto error_out; + } + + wlr_log(WLR_DEBUG, "read %zd bytes (available %zu, mask 0x%x)", len, + available, mask); + + transfer->source_data.size = current + len; + if (transfer->source_data.size >= INCR_CHUNK_SIZE) { + if (!transfer->incr) { + wlr_log(WLR_DEBUG, "got %zu bytes, starting incr", + transfer->source_data.size); + + size_t incr_chunk_size = INCR_CHUNK_SIZE; + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + transfer->request.requestor, + transfer->request.property, + xwm->atoms[INCR], + 32, /* format */ + 1, &incr_chunk_size); + transfer->incr = true; + transfer->property_set = true; + transfer->flush_property_on_delete = true; + xwm_selection_transfer_remove_source(transfer); + xwm_selection_send_notify(xwm, &transfer->request, true); + } else if (transfer->property_set) { + wlr_log(WLR_DEBUG, "got %zu bytes, waiting for property delete", + transfer->source_data.size); + + transfer->flush_property_on_delete = true; + xwm_selection_transfer_remove_source(transfer); + } else { + wlr_log(WLR_DEBUG, "got %zu bytes, property deleted, setting new " + "property", transfer->source_data.size); + xwm_selection_flush_source_data(transfer); + } + } else if (len == 0 && !transfer->incr) { + wlr_log(WLR_DEBUG, "non-incr transfer complete"); + xwm_selection_flush_source_data(transfer); + xwm_selection_send_notify(xwm, &transfer->request, true); + xwm_selection_transfer_destroy_outgoing(transfer); + } else if (len == 0 && transfer->incr) { + wlr_log(WLR_DEBUG, "incr transfer complete"); + + transfer->flush_property_on_delete = true; + if (transfer->property_set) { + wlr_log(WLR_DEBUG, "got %zu bytes, waiting for property delete", + transfer->source_data.size); + } else { + wlr_log(WLR_DEBUG, "got %zu bytes, property deleted, setting new " + "property", transfer->source_data.size); + xwm_selection_flush_source_data(transfer); + } + xwm_selection_transfer_remove_source(transfer); + xwm_selection_transfer_close_source_fd(transfer); + } else { + wlr_log(WLR_DEBUG, "nothing happened, buffered the bytes"); + } + + return 1; + +error_out: + xwm_selection_send_notify(xwm, &transfer->request, false); + xwm_selection_transfer_destroy_outgoing(transfer); + return 0; +} + +void xwm_send_incr_chunk(struct wlr_xwm_selection_transfer *transfer) { + wlr_log(WLR_DEBUG, "property deleted"); + + transfer->property_set = false; + if (transfer->flush_property_on_delete) { + wlr_log(WLR_DEBUG, "setting new property, %zu bytes", + transfer->source_data.size); + transfer->flush_property_on_delete = false; + int length = xwm_selection_flush_source_data(transfer); + + if (transfer->source_fd >= 0) { + xwm_selection_transfer_start_outgoing(transfer); + } else if (length > 0) { + /* Transfer is all done, but queue a flush for + * the delete of the last chunk so we can set + * the 0 sized property to signal the end of + * the transfer. */ + transfer->flush_property_on_delete = true; + wl_array_release(&transfer->source_data); + wl_array_init(&transfer->source_data); + } else { + xwm_selection_transfer_destroy_outgoing(transfer); + } + } +} + +static void xwm_selection_source_send(struct wlr_xwm_selection *selection, + const char *mime_type, int32_t fd) { + if (selection == &selection->xwm->clipboard_selection) { + struct wlr_data_source *source = + selection->xwm->seat->selection_source; + if (source != NULL) { + wlr_data_source_send(source, mime_type, fd); + return; + } + } else if (selection == &selection->xwm->primary_selection) { + struct wlr_primary_selection_source *source = + selection->xwm->seat->primary_selection_source; + if (source != NULL) { + wlr_primary_selection_source_send(source, mime_type, fd); + return; + } + } else if (selection == &selection->xwm->dnd_selection) { + struct wlr_data_source *source = + selection->xwm->seat->drag_source; + if (source != NULL) { + wlr_data_source_send(source, mime_type, fd); + return; + } + } + + wlr_log(WLR_DEBUG, "not sending selection: no selection source available"); +} + +static void xwm_selection_transfer_start_outgoing( + struct wlr_xwm_selection_transfer *transfer) { + struct wlr_xwm *xwm = transfer->selection->xwm; + struct wl_event_loop *loop = + wl_display_get_event_loop(xwm->xwayland->wl_display); + transfer->source = wl_event_loop_add_fd(loop, transfer->source_fd, + WL_EVENT_READABLE, xwm_data_source_read, transfer); +} + +static struct wl_array *xwm_selection_source_get_mime_types( + struct wlr_xwm_selection *selection) { + if (selection == &selection->xwm->clipboard_selection) { + struct wlr_data_source *source = + selection->xwm->seat->selection_source; + if (source != NULL) { + return &source->mime_types; + } + } else if (selection == &selection->xwm->primary_selection) { + struct wlr_primary_selection_source *source = + selection->xwm->seat->primary_selection_source; + if (source != NULL) { + return &source->mime_types; + } + } else if (selection == &selection->xwm->dnd_selection) { + struct wlr_data_source *source = + selection->xwm->seat->drag_source; + if (source != NULL) { + return &source->mime_types; + } + } + return NULL; +} + +/** + * Read the Wayland selection and send it to an Xwayland client. + */ +static void xwm_selection_send_data(struct wlr_xwm_selection *selection, + xcb_selection_request_event_t *req, const char *mime_type) { + // Check MIME type + struct wl_array *mime_types = + xwm_selection_source_get_mime_types(selection); + if (mime_types == NULL) { + wlr_log(WLR_ERROR, "not sending selection: no MIME type list available"); + xwm_selection_send_notify(selection->xwm, req, false); + return; + } + + bool found = false; + char **mime_type_ptr; + wl_array_for_each(mime_type_ptr, mime_types) { + char *t = *mime_type_ptr; + if (strcmp(t, mime_type) == 0) { + found = true; + break; + } + } + if (!found) { + wlr_log(WLR_ERROR, "not sending selection: " + "requested an unsupported MIME type %s", mime_type); + xwm_selection_send_notify(selection->xwm, req, false); + return; + } + + struct wlr_xwm_selection_transfer *transfer = + calloc(1, sizeof(struct wlr_xwm_selection_transfer)); + if (transfer == NULL) { + wlr_log(WLR_ERROR, "Allocation failed"); + return; + } + transfer->selection = selection; + transfer->request = *req; + wl_array_init(&transfer->source_data); + + int p[2]; + if (pipe(p) == -1) { + wlr_log(WLR_ERROR, "pipe() failed: %m"); + xwm_selection_send_notify(selection->xwm, req, false); + return; + } + fcntl(p[0], F_SETFD, FD_CLOEXEC); + fcntl(p[0], F_SETFL, O_NONBLOCK); + fcntl(p[1], F_SETFD, FD_CLOEXEC); + fcntl(p[1], F_SETFL, O_NONBLOCK); + + transfer->source_fd = p[0]; + + wlr_log(WLR_DEBUG, "Sending Wayland selection %u to Xwayland window with " + "MIME type %s, target %u", req->target, mime_type, req->target); + xwm_selection_source_send(selection, mime_type, p[1]); + + wl_list_insert(&selection->outgoing, &transfer->outgoing_link); + + // We can only handle one transfer at a time + if (wl_list_length(&selection->outgoing) == 1) { + xwm_selection_transfer_start_outgoing(transfer); + } +} + +static void xwm_selection_send_targets(struct wlr_xwm_selection *selection, + xcb_selection_request_event_t *req) { + struct wlr_xwm *xwm = selection->xwm; + + struct wl_array *mime_types = + xwm_selection_source_get_mime_types(selection); + if (mime_types == NULL) { + wlr_log(WLR_ERROR, "not sending selection targets: " + "no selection source available"); + xwm_selection_send_notify(selection->xwm, req, false); + return; + } + + size_t n = 2 + mime_types->size / sizeof(char *); + xcb_atom_t targets[n]; + targets[0] = xwm->atoms[TIMESTAMP]; + targets[1] = xwm->atoms[TARGETS]; + + size_t i = 0; + char **mime_type_ptr; + wl_array_for_each(mime_type_ptr, mime_types) { + char *mime_type = *mime_type_ptr; + targets[2+i] = xwm_mime_type_to_atom(xwm, mime_type); + ++i; + } + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + req->requestor, + req->property, + XCB_ATOM_ATOM, + 32, // format + n, targets); + + xwm_selection_send_notify(selection->xwm, req, true); +} + +static void xwm_selection_send_timestamp(struct wlr_xwm_selection *selection, + xcb_selection_request_event_t *req) { + xcb_change_property(selection->xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + req->requestor, + req->property, + XCB_ATOM_INTEGER, + 32, // format + 1, &selection->timestamp); + + xwm_selection_send_notify(selection->xwm, req, true); +} + +void xwm_handle_selection_request(struct wlr_xwm *xwm, + xcb_selection_request_event_t *req) { + wlr_log(WLR_DEBUG, "XCB_SELECTION_REQUEST (time=%u owner=%u, requestor=%u " + "selection=%u, target=%u, property=%u)", + req->time, req->owner, req->requestor, req->selection, req->target, + req->property); + + if (req->selection == xwm->atoms[CLIPBOARD_MANAGER]) { + // The wlroots clipboard should already have grabbed the first target, + // so just send selection notify now. This isn't synchronized with the + // clipboard finishing getting the data, so there's a race here. + xwm_selection_send_notify(xwm, req, true); + return; + } + + struct wlr_xwm_selection *selection = + xwm_get_selection(xwm, req->selection); + if (selection == NULL) { + wlr_log(WLR_DEBUG, "received selection request for unknown selection"); + return; + } + + if (selection->window != req->owner) { + wlr_log(WLR_DEBUG, "received selection request with invalid owner"); + return; + } + + // No xwayland surface focused, deny access to clipboard + if (xwm->focus_surface == NULL && xwm->drag_focus == NULL) { + char *selection_name = xwm_get_atom_name(xwm, selection->atom); + wlr_log(WLR_DEBUG, "denying read access to selection %u (%s): " + "no xwayland surface focused", selection->atom, selection_name); + free(selection_name); + xwm_selection_send_notify(xwm, req, false); + return; + } + + if (req->target == xwm->atoms[TARGETS]) { + xwm_selection_send_targets(selection, req); + } else if (req->target == xwm->atoms[TIMESTAMP]) { + xwm_selection_send_timestamp(selection, req); + } else if (req->target == xwm->atoms[DELETE]) { + xwm_selection_send_notify(selection->xwm, req, true); + } else { + // Send data + char *mime_type = xwm_mime_type_from_atom(xwm, req->target); + if (mime_type == NULL) { + wlr_log(WLR_ERROR, "ignoring selection request: unknown atom %u", + req->target); + xwm_selection_send_notify(xwm, req, false); + return; + } + xwm_selection_send_data(selection, req, mime_type); + free(mime_type); + } +} diff --git a/xwayland/selection/selection.c b/xwayland/selection/selection.c new file mode 100644 index 00000000..4d7732cb --- /dev/null +++ b/xwayland/selection/selection.c @@ -0,0 +1,317 @@ +#define _POSIX_C_SOURCE 200809L +#include <assert.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wlr/types/wlr_data_device.h> +#include <wlr/types/wlr_primary_selection.h> +#include <wlr/util/log.h> +#include <xcb/xfixes.h> +#include "xwayland/selection.h" +#include "xwayland/xwm.h" + +void xwm_selection_transfer_remove_source( + struct wlr_xwm_selection_transfer *transfer) { + if (transfer->source != NULL) { + wl_event_source_remove(transfer->source); + transfer->source = NULL; + } +} + +void xwm_selection_transfer_close_source_fd( + struct wlr_xwm_selection_transfer *transfer) { + if (transfer->source_fd >= 0) { + close(transfer->source_fd); + transfer->source_fd = -1; + } +} + +void xwm_selection_transfer_destroy_property_reply( + struct wlr_xwm_selection_transfer *transfer) { + free(transfer->property_reply); + transfer->property_reply = NULL; +} + +xcb_atom_t xwm_mime_type_to_atom(struct wlr_xwm *xwm, char *mime_type) { + if (strcmp(mime_type, "text/plain;charset=utf-8") == 0) { + return xwm->atoms[UTF8_STRING]; + } else if (strcmp(mime_type, "text/plain") == 0) { + return xwm->atoms[TEXT]; + } + + xcb_intern_atom_cookie_t cookie = + xcb_intern_atom(xwm->xcb_conn, 0, strlen(mime_type), mime_type); + xcb_intern_atom_reply_t *reply = + xcb_intern_atom_reply(xwm->xcb_conn, cookie, NULL); + if (reply == NULL) { + return XCB_ATOM_NONE; + } + xcb_atom_t atom = reply->atom; + free(reply); + return atom; +} + +char *xwm_mime_type_from_atom(struct wlr_xwm *xwm, xcb_atom_t atom) { + if (atom == xwm->atoms[UTF8_STRING]) { + return strdup("text/plain;charset=utf-8"); + } else if (atom == xwm->atoms[TEXT]) { + return strdup("text/plain"); + } else { + return xwm_get_atom_name(xwm, atom); + } +} + +struct wlr_xwm_selection *xwm_get_selection(struct wlr_xwm *xwm, + xcb_atom_t selection_atom) { + if (selection_atom == xwm->atoms[CLIPBOARD]) { + return &xwm->clipboard_selection; + } else if (selection_atom == xwm->atoms[PRIMARY]) { + return &xwm->primary_selection; + } else if (selection_atom == xwm->atoms[DND_SELECTION]) { + return &xwm->dnd_selection; + } else { + return NULL; + } +} + +static int xwm_handle_selection_property_notify(struct wlr_xwm *xwm, + xcb_property_notify_event_t *event) { + struct wlr_xwm_selection *selections[] = { + &xwm->clipboard_selection, + &xwm->primary_selection, + &xwm->dnd_selection, + }; + + for (size_t i = 0; i < sizeof(selections)/sizeof(selections[0]); ++i) { + struct wlr_xwm_selection *selection = selections[i]; + + if (event->window == selection->window) { + if (event->state == XCB_PROPERTY_NEW_VALUE && + event->atom == xwm->atoms[WL_SELECTION] && + selection->incoming.incr) { + xwm_get_incr_chunk(&selection->incoming); + } + return 1; + } + + struct wlr_xwm_selection_transfer *outgoing; + wl_list_for_each(outgoing, &selection->outgoing, outgoing_link) { + if (event->window == outgoing->request.requestor) { + if (event->state == XCB_PROPERTY_DELETE && + event->atom == outgoing->request.property && + outgoing->incr) { + xwm_send_incr_chunk(outgoing); + } + return 1; + } + } + } + + return 0; +} + +int xwm_handle_selection_event(struct wlr_xwm *xwm, + xcb_generic_event_t *event) { + if (xwm->seat == NULL) { + wlr_log(WLR_DEBUG, "not handling selection events: " + "no seat assigned to xwayland"); + return 0; + } + + switch (event->response_type & XCB_EVENT_RESPONSE_TYPE_MASK) { + case XCB_SELECTION_NOTIFY: + xwm_handle_selection_notify(xwm, (xcb_selection_notify_event_t *)event); + return 1; + case XCB_PROPERTY_NOTIFY: + return xwm_handle_selection_property_notify(xwm, + (xcb_property_notify_event_t *)event); + case XCB_SELECTION_REQUEST: + xwm_handle_selection_request(xwm, + (xcb_selection_request_event_t *)event); + return 1; + } + + switch (event->response_type - xwm->xfixes->first_event) { + case XCB_XFIXES_SELECTION_NOTIFY: + // an X11 window has copied something to the clipboard + return xwm_handle_xfixes_selection_notify(xwm, + (xcb_xfixes_selection_notify_event_t *)event); + } + + return 0; +} + +static void selection_init(struct wlr_xwm *xwm, + struct wlr_xwm_selection *selection, xcb_atom_t atom) { + selection->xwm = xwm; + selection->atom = atom; + selection->window = xwm->selection_window; + selection->incoming.selection = selection; + wl_list_init(&selection->outgoing); + + uint32_t mask = + XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER | + XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY | + XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE; + xcb_xfixes_select_selection_input(xwm->xcb_conn, selection->window, + selection->atom, mask); +} + +void xwm_selection_init(struct wlr_xwm *xwm) { + // Clipboard and primary selection + uint32_t selection_values[] = { + XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE + }; + xwm->selection_window = xcb_generate_id(xwm->xcb_conn); + xcb_create_window(xwm->xcb_conn, + XCB_COPY_FROM_PARENT, + xwm->selection_window, + xwm->screen->root, + 0, 0, + 10, 10, + 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + xwm->screen->root_visual, + XCB_CW_EVENT_MASK, selection_values); + + xcb_set_selection_owner(xwm->xcb_conn, + xwm->selection_window, + xwm->atoms[CLIPBOARD_MANAGER], + XCB_TIME_CURRENT_TIME); + + selection_init(xwm, &xwm->clipboard_selection, xwm->atoms[CLIPBOARD]); + selection_init(xwm, &xwm->primary_selection, xwm->atoms[PRIMARY]); + + // Drag'n'drop + uint32_t dnd_values[] = { + XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE + }; + xwm->dnd_window = xcb_generate_id(xwm->xcb_conn); + xcb_create_window(xwm->xcb_conn, + XCB_COPY_FROM_PARENT, + xwm->dnd_window, + xwm->screen->root, + 0, 0, + 8192, 8192, + 0, + XCB_WINDOW_CLASS_INPUT_ONLY, + xwm->screen->root_visual, + XCB_CW_EVENT_MASK, dnd_values); + + uint32_t version = XDND_VERSION; + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xwm->dnd_window, + xwm->atoms[DND_AWARE], + XCB_ATOM_ATOM, + 32, // format + 1, &version); + + selection_init(xwm, &xwm->dnd_selection, xwm->atoms[DND_SELECTION]); +} + +void xwm_selection_finish(struct wlr_xwm *xwm) { + if (!xwm) { + return; + } + if (xwm->selection_window) { + xcb_destroy_window(xwm->xcb_conn, xwm->selection_window); + } + if (xwm->dnd_window) { + xcb_destroy_window(xwm->xcb_conn, xwm->dnd_window); + } + if (xwm->seat) { + if (xwm->seat->selection_source && + data_source_is_xwayland( + xwm->seat->selection_source)) { + wlr_seat_set_selection(xwm->seat, NULL, + wl_display_next_serial(xwm->xwayland->wl_display)); + } + if (xwm->seat->primary_selection_source && + primary_selection_source_is_xwayland( + xwm->seat->primary_selection_source)) { + wlr_seat_set_primary_selection(xwm->seat, NULL); + } + wlr_xwayland_set_seat(xwm->xwayland, NULL); + } +} + +static void xwm_selection_set_owner(struct wlr_xwm_selection *selection, + bool set) { + if (set) { + xcb_set_selection_owner(selection->xwm->xcb_conn, + selection->window, + selection->atom, + XCB_TIME_CURRENT_TIME); + } else { + if (selection->owner == selection->window) { + xcb_set_selection_owner(selection->xwm->xcb_conn, + XCB_WINDOW_NONE, + selection->atom, + selection->timestamp); + } + } +} + +static void seat_handle_selection(struct wl_listener *listener, + void *data) { + struct wlr_seat *seat = data; + struct wlr_xwm *xwm = + wl_container_of(listener, xwm, seat_selection); + struct wlr_data_source *source = seat->selection_source; + + if (source != NULL && data_source_is_xwayland(source)) { + return; + } + + xwm_selection_set_owner(&xwm->clipboard_selection, source != NULL); +} + +static void seat_handle_primary_selection(struct wl_listener *listener, + void *data) { + struct wlr_seat *seat = data; + struct wlr_xwm *xwm = + wl_container_of(listener, xwm, seat_primary_selection); + struct wlr_primary_selection_source *source = + seat->primary_selection_source; + + if (source != NULL && primary_selection_source_is_xwayland(source)) { + return; + } + + xwm_selection_set_owner(&xwm->primary_selection, source != NULL); +} + +static void seat_handle_start_drag(struct wl_listener *listener, void *data) { + struct wlr_xwm *xwm = wl_container_of(listener, xwm, seat_start_drag); + struct wlr_drag *drag = data; + + xwm_selection_set_owner(&xwm->dnd_selection, drag != NULL); + xwm_seat_handle_start_drag(xwm, drag); +} + +void xwm_set_seat(struct wlr_xwm *xwm, struct wlr_seat *seat) { + if (xwm->seat != NULL) { + wl_list_remove(&xwm->seat_selection.link); + wl_list_remove(&xwm->seat_primary_selection.link); + wl_list_remove(&xwm->seat_start_drag.link); + xwm->seat = NULL; + } + + if (seat == NULL) { + return; + } + + xwm->seat = seat; + + wl_signal_add(&seat->events.selection, &xwm->seat_selection); + xwm->seat_selection.notify = seat_handle_selection; + wl_signal_add(&seat->events.primary_selection, &xwm->seat_primary_selection); + xwm->seat_primary_selection.notify = seat_handle_primary_selection; + wl_signal_add(&seat->events.start_drag, &xwm->seat_start_drag); + xwm->seat_start_drag.notify = seat_handle_start_drag; + + seat_handle_selection(&xwm->seat_selection, seat); + seat_handle_primary_selection(&xwm->seat_primary_selection, seat); +} diff --git a/xwayland/sockets.c b/xwayland/sockets.c new file mode 100644 index 00000000..112a8bb0 --- /dev/null +++ b/xwayland/sockets.c @@ -0,0 +1,168 @@ +#define _POSIX_C_SOURCE 200809L +#ifdef __FreeBSD__ +// for SOCK_CLOEXEC +#define __BSD_VISIBLE 1 +#endif +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> +#include <wlr/util/log.h> +#include "sockets.h" + +static const char *lock_fmt = "/tmp/.X%d-lock"; +static const char *socket_dir = "/tmp/.X11-unix"; +static const char *socket_fmt = "/tmp/.X11-unix/X%d"; +#ifndef __linux__ +static const char *socket_fmt2 = "/tmp/.X11-unix/X%d_"; +#endif + +static int open_socket(struct sockaddr_un *addr, size_t path_size) { + int fd, rc; + socklen_t size = offsetof(struct sockaddr_un, sun_path) + path_size + 1; + + fd = socket(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) { + wlr_log_errno(WLR_DEBUG, "Failed to create socket %c%s", + addr->sun_path[0] ? addr->sun_path[0] : '@', + addr->sun_path + 1); + return -1; + } + + if (addr->sun_path[0]) { + unlink(addr->sun_path); + } + if (bind(fd, (struct sockaddr*)addr, size) < 0) { + rc = errno; + wlr_log_errno(WLR_DEBUG, "Failed to bind socket %c%s", + addr->sun_path[0] ? addr->sun_path[0] : '@', + addr->sun_path + 1); + goto cleanup; + } + if (listen(fd, 1) < 0) { + rc = errno; + wlr_log_errno(WLR_DEBUG, "Failed to listen to socket %c%s", + addr->sun_path[0] ? addr->sun_path[0] : '@', + addr->sun_path + 1); + goto cleanup; + } + + return fd; + +cleanup: + close(fd); + if (addr->sun_path[0]) { + unlink(addr->sun_path); + } + errno = rc; + return -1; +} + +static bool open_sockets(int socks[2], int display) { + struct sockaddr_un addr = { .sun_family = AF_LOCAL }; + size_t path_size; + + mkdir(socket_dir, 0777); + +#ifdef __linux__ + addr.sun_path[0] = 0; + path_size = snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, socket_fmt, display); +#else + path_size = snprintf(addr.sun_path, sizeof(addr.sun_path), socket_fmt2, display); +#endif + socks[0] = open_socket(&addr, path_size); + if (socks[0] < 0) { + return false; + } + + path_size = snprintf(addr.sun_path, sizeof(addr.sun_path), socket_fmt, display); + socks[1] = open_socket(&addr, path_size); + if (socks[1] < 0) { + close(socks[0]); + socks[0] = -1; + return false; + } + + return true; +} + +void unlink_display_sockets(int display) { + char sun_path[64]; + + snprintf(sun_path, sizeof(sun_path), socket_fmt, display); + unlink(sun_path); + +#ifndef __linux__ + snprintf(sun_path, sizeof(sun_path), socket_fmt2, display); + unlink(sun_path); +#endif + + snprintf(sun_path, sizeof(sun_path), lock_fmt, display); + unlink(sun_path); +} + +int open_display_sockets(int socks[2]) { + int lock_fd, display; + char lock_name[64]; + + for (display = 0; display <= 32; display++) { + snprintf(lock_name, sizeof(lock_name), lock_fmt, display); + if ((lock_fd = open(lock_name, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0444)) >= 0) { + if (!open_sockets(socks, display)) { + unlink(lock_name); + close(lock_fd); + continue; + } + char pid[12]; + snprintf(pid, sizeof(pid), "%10d", getpid()); + if (write(lock_fd, pid, sizeof(pid) - 1) != sizeof(pid) - 1) { + unlink(lock_name); + close(lock_fd); + continue; + } + close(lock_fd); + break; + } + + if ((lock_fd = open(lock_name, O_RDONLY | O_CLOEXEC)) < 0) { + continue; + } + + char pid[12] = { 0 }, *end_pid; + ssize_t bytes = read(lock_fd, pid, sizeof(pid) - 1); + close(lock_fd); + + if (bytes != sizeof(pid) - 1) { + continue; + } + long int read_pid; + read_pid = strtol(pid, &end_pid, 10); + if (read_pid < 0 || read_pid > INT32_MAX || end_pid != pid + sizeof(pid) - 2) { + continue; + } + errno = 0; + if (kill((pid_t)read_pid, 0) != 0 && errno == ESRCH) { + if (unlink(lock_name) != 0) { + continue; + } + // retry + display--; + continue; + } + } + + if (display > 32) { + wlr_log(WLR_ERROR, "No display available in the first 33"); + return -1; + } + + return display; +} diff --git a/xwayland/sockets.h b/xwayland/sockets.h new file mode 100644 index 00000000..73eb36e0 --- /dev/null +++ b/xwayland/sockets.h @@ -0,0 +1,7 @@ +#ifndef XWAYLAND_SOCKETS_H +#define XWAYLAND_SOCKETS_H + +void unlink_display_sockets(int display); +int open_display_sockets(int socks[2]); + +#endif diff --git a/xwayland/xwayland.c b/xwayland/xwayland.c new file mode 100644 index 00000000..e6f15735 --- /dev/null +++ b/xwayland/xwayland.c @@ -0,0 +1,488 @@ +#define _POSIX_C_SOURCE 200112L +#ifdef __FreeBSD__ +// for SOCK_CLOEXEC +#define __BSD_VISIBLE 1 +#endif +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> +#include <wayland-server.h> +#include <wlr/util/log.h> +#include <wlr/xwayland.h> +#include "sockets.h" +#include "util/signal.h" +#include "xwayland/xwm.h" + +struct wlr_xwayland_cursor { + uint8_t *pixels; + uint32_t stride; + uint32_t width; + uint32_t height; + int32_t hotspot_x; + int32_t hotspot_y; +}; + +static void safe_close(int fd) { + if (fd >= 0) { + close(fd); + } +} + +static int unset_cloexec(int fd) { + if (fcntl(fd, F_SETFD, 0) != 0) { + wlr_log_errno(WLR_ERROR, "fcntl() failed on fd %d", fd); + return -1; + } + return 0; +} + +static int fill_arg(char ***argv, const char *fmt, ...) { + int len; + char **cur_arg = *argv; + va_list args; + va_start(args, fmt); + len = vsnprintf(NULL, 0, fmt, args) + 1; + va_end(args); + while (*cur_arg) { + cur_arg++; + } + *cur_arg = malloc(len); + if (!*cur_arg) { + return -1; + } + *argv = cur_arg; + va_start(args, fmt); + len = vsnprintf(*cur_arg, len, fmt, args); + va_end(args); + return len; +} + +_Noreturn +static void exec_xwayland(struct wlr_xwayland *wlr_xwayland) { + if (unset_cloexec(wlr_xwayland->x_fd[0]) || + unset_cloexec(wlr_xwayland->x_fd[1]) || + unset_cloexec(wlr_xwayland->wm_fd[1]) || + unset_cloexec(wlr_xwayland->wl_fd[1])) { + _exit(EXIT_FAILURE); + } + + /* Make Xwayland signal us when it's ready */ + signal(SIGUSR1, SIG_IGN); + + char *argv[] = { + "Xwayland", NULL /* display, e.g. :1 */, + "-rootless", "-terminate", + "-listen", NULL /* x_fd[0] */, + "-listen", NULL /* x_fd[1] */, + "-wm", NULL /* wm_fd[1] */, + NULL }; + char **cur_arg = argv; + + if (fill_arg(&cur_arg, ":%d", wlr_xwayland->display) < 0 || + fill_arg(&cur_arg, "%d", wlr_xwayland->x_fd[0]) < 0 || + fill_arg(&cur_arg, "%d", wlr_xwayland->x_fd[1]) < 0 || + fill_arg(&cur_arg, "%d", wlr_xwayland->wm_fd[1]) < 0) { + wlr_log_errno(WLR_ERROR, "alloc/print failure"); + _exit(EXIT_FAILURE); + } + + char wayland_socket_str[16]; + snprintf(wayland_socket_str, sizeof(wayland_socket_str), "%d", wlr_xwayland->wl_fd[1]); + setenv("WAYLAND_SOCKET", wayland_socket_str, true); + + wlr_log(WLR_INFO, "WAYLAND_SOCKET=%d Xwayland :%d -rootless -terminate -listen %d -listen %d -wm %d", + wlr_xwayland->wl_fd[1], wlr_xwayland->display, wlr_xwayland->x_fd[0], + wlr_xwayland->x_fd[1], wlr_xwayland->wm_fd[1]); + + // Closes stdout/stderr depending on log verbosity + enum wlr_log_importance verbosity = wlr_log_get_verbosity(); + int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); + if (devnull < 0) { + wlr_log_errno(WLR_ERROR, "XWayland: failed to open /dev/null"); + _exit(EXIT_FAILURE); + } + if (verbosity < WLR_INFO) { + dup2(devnull, STDOUT_FILENO); + } + if (verbosity < WLR_ERROR) { + dup2(devnull, STDERR_FILENO); + } + + // This returns if and only if the call fails + execvp("Xwayland", argv); + + wlr_log_errno(WLR_ERROR, "failed to exec Xwayland"); + close(devnull); + _exit(EXIT_FAILURE); +} + +static void xwayland_finish_server(struct wlr_xwayland *wlr_xwayland) { + if (!wlr_xwayland || wlr_xwayland->display == -1) { + return; + } + + if (wlr_xwayland->x_fd_read_event[0]) { + wl_event_source_remove(wlr_xwayland->x_fd_read_event[0]); + wl_event_source_remove(wlr_xwayland->x_fd_read_event[1]); + + wlr_xwayland->x_fd_read_event[0] = wlr_xwayland->x_fd_read_event[1] = NULL; + } + + if (wlr_xwayland->cursor != NULL) { + free(wlr_xwayland->cursor); + } + + xwm_destroy(wlr_xwayland->xwm); + + if (wlr_xwayland->client) { + wl_list_remove(&wlr_xwayland->client_destroy.link); + wl_client_destroy(wlr_xwayland->client); + } + if (wlr_xwayland->sigusr1_source) { + wl_event_source_remove(wlr_xwayland->sigusr1_source); + } + + safe_close(wlr_xwayland->wl_fd[0]); + safe_close(wlr_xwayland->wl_fd[1]); + safe_close(wlr_xwayland->wm_fd[0]); + safe_close(wlr_xwayland->wm_fd[1]); + memset(wlr_xwayland, 0, offsetof(struct wlr_xwayland, display)); + wlr_xwayland->wl_fd[0] = wlr_xwayland->wl_fd[1] = -1; + wlr_xwayland->wm_fd[0] = wlr_xwayland->wm_fd[1] = -1; + + /* We do not kill the Xwayland process, it dies to broken pipe + * after we close our side of the wm/wl fds. This is more reliable + * than trying to kill something that might no longer be Xwayland. + */ +} + +static void xwayland_finish_display(struct wlr_xwayland *wlr_xwayland) { + if (!wlr_xwayland || wlr_xwayland->display == -1) { + return; + } + + safe_close(wlr_xwayland->x_fd[0]); + safe_close(wlr_xwayland->x_fd[1]); + wlr_xwayland->x_fd[0] = wlr_xwayland->x_fd[1] = -1; + + wl_list_remove(&wlr_xwayland->display_destroy.link); + + unlink_display_sockets(wlr_xwayland->display); + wlr_xwayland->display = -1; + unsetenv("DISPLAY"); +} + +static bool xwayland_start_display(struct wlr_xwayland *wlr_xwayland, + struct wl_display *wl_display); + +static bool xwayland_start_server(struct wlr_xwayland *wlr_xwayland); +static bool xwayland_start_server_lazy(struct wlr_xwayland *wlr_xwayland); + +static void handle_client_destroy(struct wl_listener *listener, void *data) { + struct wlr_xwayland *wlr_xwayland = + wl_container_of(listener, wlr_xwayland, client_destroy); + + if (wlr_xwayland->sigusr1_source) { + // Xwayland failed to start, let the sigusr1 handler deal with it + return; + } + + // Don't call client destroy: it's being destroyed already + wlr_xwayland->client = NULL; + wl_list_remove(&wlr_xwayland->client_destroy.link); + + xwayland_finish_server(wlr_xwayland); + + if (time(NULL) - wlr_xwayland->server_start > 5) { + if (wlr_xwayland->lazy) { + wlr_log(WLR_INFO, "Restarting Xwayland (lazy)"); + xwayland_start_server_lazy(wlr_xwayland); + } else { + wlr_log(WLR_INFO, "Restarting Xwayland"); + xwayland_start_server(wlr_xwayland); + } + } +} + +static void handle_display_destroy(struct wl_listener *listener, void *data) { + struct wlr_xwayland *wlr_xwayland = + wl_container_of(listener, wlr_xwayland, display_destroy); + + // Don't call client destroy: the display is being destroyed, it's too late + if (wlr_xwayland->client) { + wlr_xwayland->client = NULL; + wl_list_remove(&wlr_xwayland->client_destroy.link); + } + + wlr_xwayland_destroy(wlr_xwayland); +} + +static int xserver_handle_ready(int signal_number, void *data) { + struct wlr_xwayland *wlr_xwayland = data; + + int stat_val = -1; + while (waitpid(wlr_xwayland->pid, &stat_val, 0) < 0) { + if (errno == EINTR) { + continue; + } + wlr_log_errno(WLR_ERROR, "waitpid for Xwayland fork failed"); + return 1; + } + if (stat_val) { + wlr_log(WLR_ERROR, "Xwayland startup failed, not setting up xwm"); + return 1; + } + wlr_log(WLR_DEBUG, "Xserver is ready"); + + wlr_xwayland->xwm = xwm_create(wlr_xwayland); + if (!wlr_xwayland->xwm) { + xwayland_finish_server(wlr_xwayland); + return 1; + } + + if (wlr_xwayland->seat) { + xwm_set_seat(wlr_xwayland->xwm, wlr_xwayland->seat); + } + + wl_event_source_remove(wlr_xwayland->sigusr1_source); + wlr_xwayland->sigusr1_source = NULL; + + if (wlr_xwayland->cursor != NULL) { + struct wlr_xwayland_cursor *cur = wlr_xwayland->cursor; + xwm_set_cursor(wlr_xwayland->xwm, cur->pixels, cur->stride, cur->width, + cur->height, cur->hotspot_x, cur->hotspot_y); + free(cur); + wlr_xwayland->cursor = NULL; + } + + + wlr_signal_emit_safe(&wlr_xwayland->events.ready, wlr_xwayland); + /* ready is a one-shot signal, fire and forget */ + wl_signal_init(&wlr_xwayland->events.ready); + + return 1; /* wayland event loop dispatcher's count */ +} + +static int xwayland_socket_connected(int fd, uint32_t mask, void* data){ + struct wlr_xwayland *wlr_xwayland = data; + + wl_event_source_remove(wlr_xwayland->x_fd_read_event[0]); + wl_event_source_remove(wlr_xwayland->x_fd_read_event[1]); + + wlr_xwayland->x_fd_read_event[0] = wlr_xwayland->x_fd_read_event[1] = NULL; + + xwayland_start_server(wlr_xwayland); + + return 0; +} + +static bool xwayland_start_display(struct wlr_xwayland *wlr_xwayland, + struct wl_display *wl_display) { + + wlr_xwayland->display_destroy.notify = handle_display_destroy; + wl_display_add_destroy_listener(wl_display, &wlr_xwayland->display_destroy); + + wlr_xwayland->display = open_display_sockets(wlr_xwayland->x_fd); + if (wlr_xwayland->display < 0) { + xwayland_finish_display(wlr_xwayland); + return false; + } + + char display_name[16]; + snprintf(display_name, sizeof(display_name), ":%d", wlr_xwayland->display); + setenv("DISPLAY", display_name, true); + + return true; +} + +static bool xwayland_start_server(struct wlr_xwayland *wlr_xwayland) { + + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, wlr_xwayland->wl_fd) != 0 || + socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, wlr_xwayland->wm_fd) != 0) { + wlr_log_errno(WLR_ERROR, "failed to create socketpair"); + xwayland_finish_server(wlr_xwayland); + return false; + } + wlr_xwayland->server_start = time(NULL); + + if (!(wlr_xwayland->client = wl_client_create(wlr_xwayland->wl_display, wlr_xwayland->wl_fd[0]))) { + wlr_log_errno(WLR_ERROR, "wl_client_create failed"); + xwayland_finish_server(wlr_xwayland); + return false; + } + + wlr_xwayland->wl_fd[0] = -1; /* not ours anymore */ + + wlr_xwayland->client_destroy.notify = handle_client_destroy; + wl_client_add_destroy_listener(wlr_xwayland->client, + &wlr_xwayland->client_destroy); + + struct wl_event_loop *loop = wl_display_get_event_loop(wlr_xwayland->wl_display); + wlr_xwayland->sigusr1_source = wl_event_loop_add_signal(loop, SIGUSR1, + xserver_handle_ready, wlr_xwayland); + + if ((wlr_xwayland->pid = fork()) == 0) { + /* Double-fork, but we need to forward SIGUSR1 once Xserver(1) + * is ready, or error if there was one. */ + pid_t pid, ppid; + sigset_t sigset; + int sig; + ppid = getppid(); + sigemptyset(&sigset); + sigaddset(&sigset, SIGUSR1); + sigaddset(&sigset, SIGCHLD); + sigprocmask(SIG_BLOCK, &sigset, NULL); + if ((pid = fork()) == 0) { + exec_xwayland(wlr_xwayland); + } + if (pid < 0) { + wlr_log_errno(WLR_ERROR, "second fork failed"); + _exit(EXIT_FAILURE); + } + sigwait(&sigset, &sig); + kill(ppid, SIGUSR1); + wlr_log(WLR_DEBUG, "sent SIGUSR1 to process %d", ppid); + if (sig == SIGCHLD) { + waitpid(pid, NULL, 0); + _exit(EXIT_FAILURE); + } + _exit(EXIT_SUCCESS); + } + if (wlr_xwayland->pid < 0) { + wlr_log_errno(WLR_ERROR, "fork failed"); + xwayland_finish_server(wlr_xwayland); + return false; + } + + /* close child fds */ + /* remain managing x sockets for lazy start */ + close(wlr_xwayland->wl_fd[1]); + close(wlr_xwayland->wm_fd[1]); + wlr_xwayland->wl_fd[1] = wlr_xwayland->wm_fd[1] = -1; + + return true; +} + +static bool xwayland_start_server_lazy(struct wlr_xwayland *wlr_xwayland) { + struct wl_event_loop *loop = wl_display_get_event_loop(wlr_xwayland->wl_display); + wlr_xwayland->x_fd_read_event[0] = + wl_event_loop_add_fd(loop, wlr_xwayland->x_fd[0], WL_EVENT_READABLE, + xwayland_socket_connected, wlr_xwayland); + wlr_xwayland->x_fd_read_event[1] = + wl_event_loop_add_fd(loop, wlr_xwayland->x_fd[1], WL_EVENT_READABLE, + xwayland_socket_connected, wlr_xwayland); + + return true; +} + +void wlr_xwayland_destroy(struct wlr_xwayland *wlr_xwayland) { + if (!wlr_xwayland) { + return; + } + + wlr_xwayland_set_seat(wlr_xwayland, NULL); + xwayland_finish_server(wlr_xwayland); + xwayland_finish_display(wlr_xwayland); + free(wlr_xwayland); +} + +struct wlr_xwayland *wlr_xwayland_create(struct wl_display *wl_display, + struct wlr_compositor *compositor, bool lazy) { + struct wlr_xwayland *wlr_xwayland = calloc(1, sizeof(struct wlr_xwayland)); + if (!wlr_xwayland) { + return NULL; + } + + wlr_xwayland->wl_display = wl_display; + wlr_xwayland->compositor = compositor; + wlr_xwayland->lazy = lazy; + + wlr_xwayland->x_fd[0] = wlr_xwayland->x_fd[1] = -1; + wlr_xwayland->wl_fd[0] = wlr_xwayland->wl_fd[1] = -1; + wlr_xwayland->wm_fd[0] = wlr_xwayland->wm_fd[1] = -1; + + wl_signal_init(&wlr_xwayland->events.new_surface); + wl_signal_init(&wlr_xwayland->events.ready); + + if (!xwayland_start_display(wlr_xwayland, wl_display)) { + goto error_alloc; + } + + if (wlr_xwayland->lazy) { + if (!xwayland_start_server_lazy(wlr_xwayland)) { + goto error_display; + } + } else { + if (!xwayland_start_server(wlr_xwayland)) { + goto error_display; + } + } + + return wlr_xwayland; + +error_display: + xwayland_finish_display(wlr_xwayland); + +error_alloc: + free(wlr_xwayland); + return NULL; +} + +void wlr_xwayland_set_cursor(struct wlr_xwayland *wlr_xwayland, + uint8_t *pixels, uint32_t stride, uint32_t width, uint32_t height, + int32_t hotspot_x, int32_t hotspot_y) { + if (wlr_xwayland->xwm != NULL) { + xwm_set_cursor(wlr_xwayland->xwm, pixels, stride, width, height, + hotspot_x, hotspot_y); + return; + } + + free(wlr_xwayland->cursor); + + wlr_xwayland->cursor = calloc(1, sizeof(struct wlr_xwayland_cursor)); + if (wlr_xwayland->cursor == NULL) { + return; + } + wlr_xwayland->cursor->pixels = pixels; + wlr_xwayland->cursor->stride = stride; + wlr_xwayland->cursor->width = width; + wlr_xwayland->cursor->height = height; + wlr_xwayland->cursor->hotspot_x = hotspot_x; + wlr_xwayland->cursor->hotspot_y = hotspot_y; +} + +static void xwayland_handle_seat_destroy(struct wl_listener *listener, + void *data) { + struct wlr_xwayland *xwayland = + wl_container_of(listener, xwayland, seat_destroy); + + wlr_xwayland_set_seat(xwayland, NULL); +} + +void wlr_xwayland_set_seat(struct wlr_xwayland *xwayland, + struct wlr_seat *seat) { + if (xwayland->seat) { + wl_list_remove(&xwayland->seat_destroy.link); + } + + xwayland->seat = seat; + + if (xwayland->xwm) { + xwm_set_seat(xwayland->xwm, seat); + } + + if (seat == NULL) { + return; + } + + xwayland->seat_destroy.notify = xwayland_handle_seat_destroy; + wl_signal_add(&seat->events.destroy, &xwayland->seat_destroy); +} diff --git a/xwayland/xwm.c b/xwayland/xwm.c new file mode 100644 index 00000000..91fa7e3d --- /dev/null +++ b/xwayland/xwm.c @@ -0,0 +1,1802 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#include <assert.h> +#include <stdlib.h> +#include <unistd.h> +#include <wlr/config.h> +#include <wlr/types/wlr_surface.h> +#include <wlr/util/edges.h> +#include <wlr/util/log.h> +#include <wlr/xcursor.h> +#include <wlr/xwayland.h> +#include <xcb/composite.h> +#include <xcb/render.h> +#include <xcb/xfixes.h> +#include "util/signal.h" +#include "xwayland/xwm.h" + +const char *atom_map[ATOM_LAST] = { + "WL_SURFACE_ID", + "WM_DELETE_WINDOW", + "WM_PROTOCOLS", + "WM_HINTS", + "WM_NORMAL_HINTS", + "WM_SIZE_HINTS", + "WM_WINDOW_ROLE", + "_MOTIF_WM_HINTS", + "UTF8_STRING", + "WM_S0", + "_NET_SUPPORTED", + "_NET_WM_CM_S0", + "_NET_WM_PID", + "_NET_WM_NAME", + "_NET_WM_STATE", + "_NET_WM_WINDOW_TYPE", + "WM_TAKE_FOCUS", + "WINDOW", + "_NET_ACTIVE_WINDOW", + "_NET_WM_MOVERESIZE", + "_NET_WM_NAME", + "_NET_SUPPORTING_WM_CHECK", + "_NET_WM_STATE_MODAL", + "_NET_WM_STATE_FULLSCREEN", + "_NET_WM_STATE_MAXIMIZED_VERT", + "_NET_WM_STATE_MAXIMIZED_HORZ", + "_NET_WM_PING", + "WM_STATE", + "CLIPBOARD", + "PRIMARY", + "_WL_SELECTION", + "TARGETS", + "CLIPBOARD_MANAGER", + "INCR", + "TEXT", + "TIMESTAMP", + "DELETE", + "_NET_WM_WINDOW_TYPE_NORMAL", + "_NET_WM_WINDOW_TYPE_UTILITY", + "_NET_WM_WINDOW_TYPE_TOOLTIP", + "_NET_WM_WINDOW_TYPE_DND", + "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", + "_NET_WM_WINDOW_TYPE_POPUP_MENU", + "_NET_WM_WINDOW_TYPE_COMBO", + "_NET_WM_WINDOW_TYPE_MENU", + "_NET_WM_WINDOW_TYPE_NOTIFICATION", + "_NET_WM_WINDOW_TYPE_SPLASH", + "XdndSelection", + "XdndAware", + "XdndStatus", + "XdndPosition", + "XdndEnter", + "XdndLeave", + "XdndDrop", + "XdndFinished", + "XdndProxy", + "XdndTypeList", + "XdndActionMove", + "XdndActionCopy", + "XdndActionAsk", + "XdndActionPrivate", +}; + +static const struct wlr_surface_role xwayland_surface_role; + +bool wlr_surface_is_xwayland_surface(struct wlr_surface *surface) { + return surface->role == &xwayland_surface_role; +} + +struct wlr_xwayland_surface *wlr_xwayland_surface_from_wlr_surface( + struct wlr_surface *surface) { + assert(wlr_surface_is_xwayland_surface(surface)); + return (struct wlr_xwayland_surface *)surface->role_data; +} + +// TODO: replace this with hash table? +static struct wlr_xwayland_surface *lookup_surface(struct wlr_xwm *xwm, + xcb_window_t window_id) { + struct wlr_xwayland_surface *surface; + wl_list_for_each(surface, &xwm->surfaces, link) { + if (surface->window_id == window_id) { + return surface; + } + } + return NULL; +} + +static int xwayland_surface_handle_ping_timeout(void *data) { + struct wlr_xwayland_surface *surface = data; + + wlr_signal_emit_safe(&surface->events.ping_timeout, surface); + surface->pinging = false; + return 1; +} + +static struct wlr_xwayland_surface *xwayland_surface_create( + struct wlr_xwm *xwm, xcb_window_t window_id, int16_t x, int16_t y, + uint16_t width, uint16_t height, bool override_redirect) { + struct wlr_xwayland_surface *surface = + calloc(1, sizeof(struct wlr_xwayland_surface)); + if (!surface) { + wlr_log(WLR_ERROR, "Could not allocate wlr xwayland surface"); + return NULL; + } + + xcb_get_geometry_cookie_t geometry_cookie = + xcb_get_geometry(xwm->xcb_conn, window_id); + + uint32_t values[1]; + values[0] = + XCB_EVENT_MASK_FOCUS_CHANGE | + XCB_EVENT_MASK_PROPERTY_CHANGE; + xcb_change_window_attributes(xwm->xcb_conn, window_id, + XCB_CW_EVENT_MASK, values); + + surface->xwm = xwm; + surface->window_id = window_id; + surface->x = x; + surface->y = y; + surface->width = width; + surface->height = height; + surface->override_redirect = override_redirect; + wl_list_insert(&xwm->surfaces, &surface->link); + wl_list_init(&surface->children); + wl_list_init(&surface->parent_link); + wl_signal_init(&surface->events.destroy); + wl_signal_init(&surface->events.request_configure); + wl_signal_init(&surface->events.request_move); + wl_signal_init(&surface->events.request_resize); + wl_signal_init(&surface->events.request_maximize); + wl_signal_init(&surface->events.request_fullscreen); + wl_signal_init(&surface->events.request_activate); + wl_signal_init(&surface->events.map); + wl_signal_init(&surface->events.unmap); + wl_signal_init(&surface->events.set_class); + wl_signal_init(&surface->events.set_role); + wl_signal_init(&surface->events.set_title); + wl_signal_init(&surface->events.set_parent); + wl_signal_init(&surface->events.set_pid); + wl_signal_init(&surface->events.set_window_type); + wl_signal_init(&surface->events.set_hints); + wl_signal_init(&surface->events.set_decorations); + wl_signal_init(&surface->events.set_override_redirect); + wl_signal_init(&surface->events.ping_timeout); + + xcb_get_geometry_reply_t *geometry_reply = + xcb_get_geometry_reply(xwm->xcb_conn, geometry_cookie, NULL); + if (geometry_reply != NULL) { + surface->has_alpha = geometry_reply->depth == 32; + } + free(geometry_reply); + + struct wl_display *display = xwm->xwayland->wl_display; + struct wl_event_loop *loop = wl_display_get_event_loop(display); + surface->ping_timer = wl_event_loop_add_timer(loop, + xwayland_surface_handle_ping_timeout, surface); + if (surface->ping_timer == NULL) { + free(surface); + wlr_log(WLR_ERROR, "Could not add timer to event loop"); + return NULL; + } + + wlr_signal_emit_safe(&xwm->xwayland->events.new_surface, surface); + + return surface; +} + +static void xwm_set_net_active_window(struct wlr_xwm *xwm, + xcb_window_t window) { + xcb_change_property(xwm->xcb_conn, XCB_PROP_MODE_REPLACE, + xwm->screen->root, xwm->atoms[_NET_ACTIVE_WINDOW], + xwm->atoms[WINDOW], 32, 1, &window); +} + +static void xwm_send_wm_message(struct wlr_xwayland_surface *surface, + xcb_client_message_data_t *data, uint32_t event_mask) { + struct wlr_xwm *xwm = surface->xwm; + + xcb_client_message_event_t event = { + .response_type = XCB_CLIENT_MESSAGE, + .format = 32, + .sequence = 0, + .window = surface->window_id, + .type = xwm->atoms[WM_PROTOCOLS], + .data = *data, + }; + + xcb_send_event(xwm->xcb_conn, + 0, // propagate + surface->window_id, + event_mask, + (const char *)&event); + xcb_flush(xwm->xcb_conn); +} + +static void xwm_send_focus_window(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface) { + if (!xsurface) { + xcb_set_input_focus_checked(xwm->xcb_conn, + XCB_INPUT_FOCUS_POINTER_ROOT, + XCB_NONE, XCB_CURRENT_TIME); + return; + } + + if (xsurface->override_redirect) { + return; + } + + xcb_client_message_data_t message_data = { 0 }; + message_data.data32[0] = xwm->atoms[WM_TAKE_FOCUS]; + message_data.data32[1] = XCB_TIME_CURRENT_TIME; + + if (xsurface->hints && !xsurface->hints->input) { + // if the surface doesn't allow the focus request, we will send him + // only the take focus event. It will get the focus by itself. + xwm_send_wm_message(xsurface, &message_data, XCB_EVENT_MASK_NO_EVENT); + } else { + xwm_send_wm_message(xsurface, &message_data, XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT); + + xcb_set_input_focus(xwm->xcb_conn, XCB_INPUT_FOCUS_POINTER_ROOT, + xsurface->window_id, XCB_CURRENT_TIME); + } + + uint32_t values[1]; + values[0] = XCB_STACK_MODE_ABOVE; + xcb_configure_window(xwm->xcb_conn, xsurface->window_id, + XCB_CONFIG_WINDOW_STACK_MODE, values); +} + +static void xwm_surface_activate(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface) { + if (xwm->focus_surface == xsurface || + (xsurface && xsurface->override_redirect)) { + return; + } + + if (xsurface) { + xwm_set_net_active_window(xwm, xsurface->window_id); + } else { + xwm_set_net_active_window(xwm, XCB_WINDOW_NONE); + } + + xwm_send_focus_window(xwm, xsurface); + + xwm->focus_surface = xsurface; + + xcb_flush(xwm->xcb_conn); +} + +static void xsurface_set_net_wm_state(struct wlr_xwayland_surface *xsurface) { + struct wlr_xwm *xwm = xsurface->xwm; + uint32_t property[3]; + int i; + + i = 0; + if (xsurface->modal) { + property[i++] = xwm->atoms[_NET_WM_STATE_MODAL]; + } + if (xsurface->fullscreen) { + property[i++] = xwm->atoms[_NET_WM_STATE_FULLSCREEN]; + } + if (xsurface->maximized_vert) { + property[i++] = xwm->atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + } + if (xsurface->maximized_horz) { + property[i++] = xwm->atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + } + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xsurface->window_id, + xwm->atoms[NET_WM_STATE], + XCB_ATOM_ATOM, + 32, // format + i, property); +} + +static void xsurface_unmap(struct wlr_xwayland_surface *surface); + +static void xwayland_surface_destroy( + struct wlr_xwayland_surface *xsurface) { + xsurface_unmap(xsurface); + + wlr_signal_emit_safe(&xsurface->events.destroy, xsurface); + + if (xsurface == xsurface->xwm->focus_surface) { + xwm_surface_activate(xsurface->xwm, NULL); + } + + wl_list_remove(&xsurface->link); + wl_list_remove(&xsurface->parent_link); + + struct wlr_xwayland_surface *child, *next; + wl_list_for_each_safe(child, next, &xsurface->children, parent_link) { + wl_list_remove(&child->parent_link); + wl_list_init(&child->parent_link); + child->parent = NULL; + } + + if (xsurface->surface_id) { + wl_list_remove(&xsurface->unpaired_link); + } + + if (xsurface->surface) { + wl_list_remove(&xsurface->surface_destroy.link); + xsurface->surface->role_data = NULL; + } + + wl_event_source_remove(xsurface->ping_timer); + + free(xsurface->title); + free(xsurface->class); + free(xsurface->instance); + free(xsurface->role); + free(xsurface->window_type); + free(xsurface->protocols); + free(xsurface->hints); + free(xsurface->size_hints); + free(xsurface); +} + +static void read_surface_class(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *surface, xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_STRING && + reply->type != xwm->atoms[UTF8_STRING]) { + return; + } + + size_t len = xcb_get_property_value_length(reply); + char *class = xcb_get_property_value(reply); + + // Unpack two sequentially stored strings: instance, class + size_t instance_len = strnlen(class, len); + free(surface->instance); + if (len > 0 && instance_len < len) { + surface->instance = strndup(class, instance_len); + class += instance_len + 1; + } else { + surface->instance = NULL; + } + free(surface->class); + if (len > 0) { + surface->class = strndup(class, len); + } else { + surface->class = NULL; + } + + wlr_log(WLR_DEBUG, "XCB_ATOM_WM_CLASS: %s %s", surface->instance, + surface->class); + wlr_signal_emit_safe(&surface->events.set_class, surface); +} + +static void read_surface_role(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_STRING && + reply->type != xwm->atoms[UTF8_STRING]) { + return; + } + + size_t len = xcb_get_property_value_length(reply); + char *role = xcb_get_property_value(reply); + + free(xsurface->role); + if (len > 0) { + xsurface->role = strndup(role, len); + } else { + xsurface->role = NULL; + } + + wlr_log(WLR_DEBUG, "XCB_ATOM_WM_WINDOW_ROLE: %s", xsurface->role); + wlr_signal_emit_safe(&xsurface->events.set_role, xsurface); +} + +static void read_surface_title(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_STRING && + reply->type != xwm->atoms[UTF8_STRING]) { + return; + } + + bool is_utf8 = reply->type == xwm->atoms[UTF8_STRING]; + if (!is_utf8 && xsurface->has_utf8_title) { + return; + } + + size_t len = xcb_get_property_value_length(reply); + char *title = xcb_get_property_value(reply); + + free(xsurface->title); + if (len > 0) { + xsurface->title = strndup(title, len); + } else { + xsurface->title = NULL; + } + xsurface->has_utf8_title = is_utf8; + + wlr_log(WLR_DEBUG, "XCB_ATOM_WM_NAME: %s", xsurface->title); + wlr_signal_emit_safe(&xsurface->events.set_title, xsurface); +} + +static void read_surface_parent(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_WINDOW) { + return; + } + + xcb_window_t *xid = xcb_get_property_value(reply); + if (xid != NULL) { + xsurface->parent = lookup_surface(xwm, *xid); + } else { + xsurface->parent = NULL; + } + + wl_list_remove(&xsurface->parent_link); + if (xsurface->parent != NULL) { + wl_list_insert(&xsurface->parent->children, &xsurface->parent_link); + } else { + wl_list_init(&xsurface->parent_link); + } + + wlr_log(WLR_DEBUG, "XCB_ATOM_WM_TRANSIENT_FOR: %p", xsurface->parent); + wlr_signal_emit_safe(&xsurface->events.set_parent, xsurface); +} + +static void read_surface_pid(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_CARDINAL) { + return; + } + + pid_t *pid = xcb_get_property_value(reply); + xsurface->pid = *pid; + wlr_log(WLR_DEBUG, "NET_WM_PID %d", xsurface->pid); + wlr_signal_emit_safe(&xsurface->events.set_pid, xsurface); +} + +static void read_surface_window_type(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_ATOM) { + return; + } + + xcb_atom_t *atoms = xcb_get_property_value(reply); + size_t atoms_len = reply->value_len; + size_t atoms_size = sizeof(xcb_atom_t) * atoms_len; + + free(xsurface->window_type); + xsurface->window_type = malloc(atoms_size); + if (xsurface->window_type == NULL) { + return; + } + memcpy(xsurface->window_type, atoms, atoms_size); + xsurface->window_type_len = atoms_len; + + wlr_log(WLR_DEBUG, "NET_WM_WINDOW_TYPE (%zu)", atoms_len); + wlr_signal_emit_safe(&xsurface->events.set_window_type, xsurface); +} + +static void read_surface_protocols(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != XCB_ATOM_ATOM) { + return; + } + + xcb_atom_t *atoms = xcb_get_property_value(reply); + size_t atoms_len = reply->value_len; + size_t atoms_size = sizeof(xcb_atom_t) * atoms_len; + + free(xsurface->protocols); + xsurface->protocols = malloc(atoms_size); + if (xsurface->protocols == NULL) { + return; + } + memcpy(xsurface->protocols, atoms, atoms_size); + xsurface->protocols_len = atoms_len; + + wlr_log(WLR_DEBUG, "WM_PROTOCOLS (%zu)", atoms_len); +} + +#if WLR_HAS_XCB_ICCCM +static void read_surface_hints(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + // According to the docs, reply->type == xwm->atoms[WM_HINTS] + // In practice, reply->type == XCB_ATOM_ATOM + if (reply->value_len == 0) { + return; + } + + xcb_icccm_wm_hints_t hints; + xcb_icccm_get_wm_hints_from_reply(&hints, reply); + + free(xsurface->hints); + xsurface->hints = calloc(1, sizeof(struct wlr_xwayland_surface_hints)); + if (xsurface->hints == NULL) { + return; + } + memcpy(xsurface->hints, &hints, sizeof(struct wlr_xwayland_surface_hints)); + xsurface->hints_urgency = xcb_icccm_wm_hints_get_urgency(&hints); + + if (!(xsurface->hints->flags & XCB_ICCCM_WM_HINT_INPUT)) { + // The client didn't specify whether it wants input. + // Assume it does. + xsurface->hints->input = true; + } + + wlr_log(WLR_DEBUG, "WM_HINTS (%d)", reply->value_len); + wlr_signal_emit_safe(&xsurface->events.set_hints, xsurface); +} +#else +static void read_surface_hints(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + // Do nothing +} +#endif + +#if WLR_HAS_XCB_ICCCM +static void read_surface_normal_hints(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->type != xwm->atoms[WM_SIZE_HINTS] || reply->value_len == 0) { + return; + } + + xcb_size_hints_t size_hints; + xcb_icccm_get_wm_size_hints_from_reply(&size_hints, reply); + + free(xsurface->size_hints); + xsurface->size_hints = + calloc(1, sizeof(struct wlr_xwayland_surface_size_hints)); + if (xsurface->size_hints == NULL) { + return; + } + memcpy(xsurface->size_hints, &size_hints, + sizeof(struct wlr_xwayland_surface_size_hints)); + + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) == 0) { + xsurface->size_hints->min_width = -1; + xsurface->size_hints->min_height = -1; + } + if ((size_hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE) == 0) { + xsurface->size_hints->max_width = -1; + xsurface->size_hints->max_height = -1; + } + + wlr_log(WLR_DEBUG, "WM_NORMAL_HINTS (%d)", reply->value_len); +} +#else +static void read_surface_normal_hints(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + // Do nothing +} +#endif + + +#define MWM_HINTS_FLAGS_FIELD 0 +#define MWM_HINTS_DECORATIONS_FIELD 2 + +#define MWM_HINTS_DECORATIONS (1 << 1) + +#define MWM_DECOR_ALL (1 << 0) +#define MWM_DECOR_BORDER (1 << 1) +#define MWM_DECOR_TITLE (1 << 3) + +static void read_surface_motif_hints(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + if (reply->value_len < 5) { + return; + } + + uint32_t *motif_hints = xcb_get_property_value(reply); + if (motif_hints[MWM_HINTS_FLAGS_FIELD] & MWM_HINTS_DECORATIONS) { + xsurface->decorations = WLR_XWAYLAND_SURFACE_DECORATIONS_ALL; + uint32_t decorations = motif_hints[MWM_HINTS_DECORATIONS_FIELD]; + if ((decorations & MWM_DECOR_ALL) == 0) { + if ((decorations & MWM_DECOR_BORDER) == 0) { + xsurface->decorations |= + WLR_XWAYLAND_SURFACE_DECORATIONS_NO_BORDER; + } + if ((decorations & MWM_DECOR_TITLE) == 0) { + xsurface->decorations |= + WLR_XWAYLAND_SURFACE_DECORATIONS_NO_TITLE; + } + } + wlr_signal_emit_safe(&xsurface->events.set_decorations, xsurface); + } + + wlr_log(WLR_DEBUG, "MOTIF_WM_HINTS (%d)", reply->value_len); +} + +static void read_surface_net_wm_state(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, + xcb_get_property_reply_t *reply) { + xsurface->fullscreen = 0; + xcb_atom_t *atom = xcb_get_property_value(reply); + for (uint32_t i = 0; i < reply->value_len; i++) { + if (atom[i] == xwm->atoms[_NET_WM_STATE_MODAL]) { + xsurface->modal = true; + } else if (atom[i] == xwm->atoms[_NET_WM_STATE_FULLSCREEN]) { + xsurface->fullscreen = true; + } else if (atom[i] == xwm->atoms[_NET_WM_STATE_MAXIMIZED_VERT]) { + xsurface->maximized_vert = true; + } else if (atom[i] == xwm->atoms[_NET_WM_STATE_MAXIMIZED_HORZ]) { + xsurface->maximized_horz = true; + } + } +} + +char *xwm_get_atom_name(struct wlr_xwm *xwm, xcb_atom_t atom) { + xcb_get_atom_name_cookie_t name_cookie = + xcb_get_atom_name(xwm->xcb_conn, atom); + xcb_get_atom_name_reply_t *name_reply = + xcb_get_atom_name_reply(xwm->xcb_conn, name_cookie, NULL); + if (name_reply == NULL) { + return NULL; + } + size_t len = xcb_get_atom_name_name_length(name_reply); + char *buf = xcb_get_atom_name_name(name_reply); // not a C string + char *name = strndup(buf, len); + free(name_reply); + return name; +} + +static void read_surface_property(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, xcb_atom_t property) { + xcb_get_property_cookie_t cookie = xcb_get_property(xwm->xcb_conn, 0, + xsurface->window_id, property, XCB_ATOM_ANY, 0, 2048); + xcb_get_property_reply_t *reply = xcb_get_property_reply(xwm->xcb_conn, + cookie, NULL); + if (reply == NULL) { + return; + } + + if (property == XCB_ATOM_WM_CLASS) { + read_surface_class(xwm, xsurface, reply); + } else if (property == XCB_ATOM_WM_NAME || + property == xwm->atoms[NET_WM_NAME]) { + read_surface_title(xwm, xsurface, reply); + } else if (property == XCB_ATOM_WM_TRANSIENT_FOR) { + read_surface_parent(xwm, xsurface, reply); + } else if (property == xwm->atoms[NET_WM_PID]) { + read_surface_pid(xwm, xsurface, reply); + } else if (property == xwm->atoms[NET_WM_WINDOW_TYPE]) { + read_surface_window_type(xwm, xsurface, reply); + } else if (property == xwm->atoms[WM_PROTOCOLS]) { + read_surface_protocols(xwm, xsurface, reply); + } else if (property == xwm->atoms[NET_WM_STATE]) { + read_surface_net_wm_state(xwm, xsurface, reply); + } else if (property == xwm->atoms[WM_HINTS]) { + read_surface_hints(xwm, xsurface, reply); + } else if (property == xwm->atoms[WM_NORMAL_HINTS]) { + read_surface_normal_hints(xwm, xsurface, reply); + } else if (property == xwm->atoms[MOTIF_WM_HINTS]) { + read_surface_motif_hints(xwm, xsurface, reply); + } else if (property == xwm->atoms[WM_WINDOW_ROLE]) { + read_surface_role(xwm, xsurface, reply); + } else { + char *prop_name = xwm_get_atom_name(xwm, property); + wlr_log(WLR_DEBUG, "unhandled X11 property %u (%s) for window %u", + property, prop_name, xsurface->window_id); + free(prop_name); + } + + free(reply); +} + +static void xwayland_surface_role_commit(struct wlr_surface *wlr_surface) { + assert(wlr_surface->role == &xwayland_surface_role); + struct wlr_xwayland_surface *surface = wlr_surface->role_data; + if (surface == NULL) { + return; + } + + if (!surface->mapped && wlr_surface_has_buffer(surface->surface)) { + wlr_signal_emit_safe(&surface->events.map, surface); + surface->mapped = true; + } +} + +static void xwayland_surface_role_precommit(struct wlr_surface *wlr_surface) { + assert(wlr_surface->role == &xwayland_surface_role); + struct wlr_xwayland_surface *surface = wlr_surface->role_data; + if (surface == NULL) { + return; + } + + if (wlr_surface->pending.committed & WLR_SURFACE_STATE_BUFFER && + wlr_surface->pending.buffer_resource == NULL) { + // This is a NULL commit + if (surface->mapped) { + wlr_signal_emit_safe(&surface->events.unmap, surface); + surface->mapped = false; + } + } +} + +static const struct wlr_surface_role xwayland_surface_role = { + .name = "wlr_xwayland_surface", + .commit = xwayland_surface_role_commit, + .precommit = xwayland_surface_role_precommit, +}; + +static void handle_surface_destroy(struct wl_listener *listener, void *data) { + struct wlr_xwayland_surface *surface = + wl_container_of(listener, surface, surface_destroy); + xsurface_unmap(surface); +} + +static void xwm_map_shell_surface(struct wlr_xwm *xwm, + struct wlr_xwayland_surface *xsurface, struct wlr_surface *surface) { + if (!wlr_surface_set_role(surface, &xwayland_surface_role, xsurface, + NULL, 0)) { + wlr_log(WLR_ERROR, "Failed to set xwayland surface role"); + return; + } + + xsurface->surface = surface; + + // read all surface properties + const xcb_atom_t props[] = { + XCB_ATOM_WM_CLASS, + XCB_ATOM_WM_NAME, + XCB_ATOM_WM_TRANSIENT_FOR, + xwm->atoms[WM_PROTOCOLS], + xwm->atoms[WM_HINTS], + xwm->atoms[WM_NORMAL_HINTS], + xwm->atoms[MOTIF_WM_HINTS], + xwm->atoms[NET_WM_STATE], + xwm->atoms[NET_WM_WINDOW_TYPE], + xwm->atoms[NET_WM_NAME], + xwm->atoms[NET_WM_PID], + }; + for (size_t i = 0; i < sizeof(props)/sizeof(xcb_atom_t); i++) { + read_surface_property(xwm, xsurface, props[i]); + } + + xsurface->surface_destroy.notify = handle_surface_destroy; + wl_signal_add(&surface->events.destroy, &xsurface->surface_destroy); +} + +static void xsurface_unmap(struct wlr_xwayland_surface *surface) { + if (surface->mapped) { + surface->mapped = false; + wlr_signal_emit_safe(&surface->events.unmap, surface); + } + + if (surface->surface_id) { + // Make sure we're not on the unpaired surface list or we + // could be assigned a surface during surface creation that + // was mapped before this unmap request. + wl_list_remove(&surface->unpaired_link); + surface->surface_id = 0; + } + + if (surface->surface) { + wl_list_remove(&surface->surface_destroy.link); + surface->surface->role_data = NULL; + surface->surface = NULL; + } +} + +static void xwm_handle_create_notify(struct wlr_xwm *xwm, + xcb_create_notify_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_CREATE_NOTIFY (%u)", ev->window); + + if (ev->window == xwm->window || + ev->window == xwm->selection_window || + ev->window == xwm->dnd_window) { + return; + } + + xwayland_surface_create(xwm, ev->window, ev->x, ev->y, + ev->width, ev->height, ev->override_redirect); +} + +static void xwm_handle_destroy_notify(struct wlr_xwm *xwm, + xcb_destroy_notify_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_DESTROY_NOTIFY (%u)", ev->window); + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (xsurface == NULL) { + return; + } + xwayland_surface_destroy(xsurface); +} + +static void xwm_handle_configure_request(struct wlr_xwm *xwm, + xcb_configure_request_event_t *ev) { + struct wlr_xwayland_surface *surface = lookup_surface(xwm, ev->window); + if (surface == NULL) { + return; + } + + // TODO: handle ev->{parent,sibling}? + + uint16_t mask = ev->value_mask; + uint16_t geo_mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; + if ((mask & geo_mask) == 0) { + return; + } + + struct wlr_xwayland_surface_configure_event wlr_event = { + .surface = surface, + .x = mask & XCB_CONFIG_WINDOW_X ? ev->x : surface->x, + .y = mask & XCB_CONFIG_WINDOW_Y ? ev->y : surface->y, + .width = mask & XCB_CONFIG_WINDOW_WIDTH ? ev->width : surface->width, + .height = mask & XCB_CONFIG_WINDOW_HEIGHT ? ev->height : surface->height, + }; + wlr_log(WLR_DEBUG, "XCB_CONFIGURE_REQUEST (%u) [%ux%u+%d,%d]", ev->window, + wlr_event.width, wlr_event.height, wlr_event.x, wlr_event.y); + + wlr_signal_emit_safe(&surface->events.request_configure, &wlr_event); +} + +static void xwm_handle_configure_notify(struct wlr_xwm *xwm, + xcb_configure_notify_event_t *ev) { + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (!xsurface) { + return; + } + + xsurface->x = ev->x; + xsurface->y = ev->y; + xsurface->width = ev->width; + xsurface->height = ev->height; + + if (xsurface->override_redirect != ev->override_redirect) { + xsurface->override_redirect = ev->override_redirect; + wlr_signal_emit_safe(&xsurface->events.set_override_redirect, xsurface); + } +} + +#define ICCCM_WITHDRAWN_STATE 0 +#define ICCCM_NORMAL_STATE 1 +#define ICCCM_ICONIC_STATE 3 + +static void xsurface_set_wm_state(struct wlr_xwayland_surface *xsurface, + int32_t state) { + struct wlr_xwm *xwm = xsurface->xwm; + uint32_t property[2]; + + property[0] = state; + property[1] = XCB_WINDOW_NONE; + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xsurface->window_id, + xwm->atoms[WM_STATE], + xwm->atoms[WM_STATE], + 32, // format + 2, property); +} + +static void xwm_handle_map_request(struct wlr_xwm *xwm, + xcb_map_request_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_MAP_REQUEST (%u)", ev->window); + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (!xsurface) { + return; + } + + xsurface_set_wm_state(xsurface, ICCCM_NORMAL_STATE); + xsurface_set_net_wm_state(xsurface); + xcb_map_window(xwm->xcb_conn, ev->window); +} + +static void xwm_handle_map_notify(struct wlr_xwm *xwm, + xcb_map_notify_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_MAP_NOTIFY (%u)", ev->window); + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (!xsurface) { + return; + } + + if (xsurface->override_redirect != ev->override_redirect) { + xsurface->override_redirect = ev->override_redirect; + wlr_signal_emit_safe(&xsurface->events.set_override_redirect, xsurface); + } +} + +static void xwm_handle_unmap_notify(struct wlr_xwm *xwm, + xcb_unmap_notify_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_UNMAP_NOTIFY (%u)", ev->window); + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (xsurface == NULL) { + return; + } + + xsurface_unmap(xsurface); + xsurface_set_wm_state(xsurface, ICCCM_WITHDRAWN_STATE); +} + +static void xwm_handle_property_notify(struct wlr_xwm *xwm, + xcb_property_notify_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_PROPERTY_NOTIFY (%u)", ev->window); + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (xsurface == NULL) { + return; + } + + read_surface_property(xwm, xsurface, ev->atom); +} + +static void xwm_handle_surface_id_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *ev) { + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (xsurface == NULL) { + wlr_log(WLR_DEBUG, + "client message WL_SURFACE_ID but no new window %u ?", + ev->window); + return; + } + /* Check if we got notified after wayland surface create event */ + uint32_t id = ev->data.data32[0]; + struct wl_resource *resource = + wl_client_get_object(xwm->xwayland->client, id); + if (resource) { + struct wlr_surface *surface = wlr_surface_from_resource(resource); + xsurface->surface_id = 0; + xwm_map_shell_surface(xwm, xsurface, surface); + } else { + xsurface->surface_id = id; + wl_list_insert(&xwm->unpaired_surfaces, &xsurface->unpaired_link); + } +} + +#define _NET_WM_MOVERESIZE_SIZE_TOPLEFT 0 +#define _NET_WM_MOVERESIZE_SIZE_TOP 1 +#define _NET_WM_MOVERESIZE_SIZE_TOPRIGHT 2 +#define _NET_WM_MOVERESIZE_SIZE_RIGHT 3 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT 4 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOM 5 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT 6 +#define _NET_WM_MOVERESIZE_SIZE_LEFT 7 +#define _NET_WM_MOVERESIZE_MOVE 8 // movement only +#define _NET_WM_MOVERESIZE_SIZE_KEYBOARD 9 // size via keyboard +#define _NET_WM_MOVERESIZE_MOVE_KEYBOARD 10 // move via keyboard +#define _NET_WM_MOVERESIZE_CANCEL 11 // cancel operation + +static enum wlr_edges net_wm_edges_to_wlr(uint32_t net_wm_edges) { + enum wlr_edges edges = WLR_EDGE_NONE; + + switch(net_wm_edges) { + case _NET_WM_MOVERESIZE_SIZE_TOPLEFT: + edges = WLR_EDGE_TOP | WLR_EDGE_LEFT; + break; + case _NET_WM_MOVERESIZE_SIZE_TOP: + edges = WLR_EDGE_TOP; + break; + case _NET_WM_MOVERESIZE_SIZE_TOPRIGHT: + edges = WLR_EDGE_TOP | WLR_EDGE_RIGHT; + break; + case _NET_WM_MOVERESIZE_SIZE_RIGHT: + edges = WLR_EDGE_RIGHT; + break; + case _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT: + edges = WLR_EDGE_BOTTOM | WLR_EDGE_RIGHT; + break; + case _NET_WM_MOVERESIZE_SIZE_BOTTOM: + edges = WLR_EDGE_BOTTOM; + break; + case _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT: + edges = WLR_EDGE_BOTTOM | WLR_EDGE_LEFT; + break; + case _NET_WM_MOVERESIZE_SIZE_LEFT: + edges = WLR_EDGE_LEFT; + break; + default: + break; + } + + return edges; +} + +static void xwm_handle_net_wm_moveresize_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *ev) { + struct wlr_xwayland_surface *xsurface = lookup_surface(xwm, ev->window); + if (!xsurface) { + return; + } + + // TODO: we should probably add input or seat info to this but we would just + // be guessing + struct wlr_xwayland_resize_event resize_event; + struct wlr_xwayland_move_event move_event; + + int detail = ev->data.data32[2]; + switch (detail) { + case _NET_WM_MOVERESIZE_MOVE: + move_event.surface = xsurface; + wlr_signal_emit_safe(&xsurface->events.request_move, &move_event); + break; + case _NET_WM_MOVERESIZE_SIZE_TOPLEFT: + case _NET_WM_MOVERESIZE_SIZE_TOP: + case _NET_WM_MOVERESIZE_SIZE_TOPRIGHT: + case _NET_WM_MOVERESIZE_SIZE_RIGHT: + case _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT: + case _NET_WM_MOVERESIZE_SIZE_BOTTOM: + case _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT: + case _NET_WM_MOVERESIZE_SIZE_LEFT: + resize_event.surface = xsurface; + resize_event.edges = net_wm_edges_to_wlr(detail); + wlr_signal_emit_safe(&xsurface->events.request_resize, &resize_event); + break; + case _NET_WM_MOVERESIZE_CANCEL: + // handled by the compositor + break; + } +} + +#define _NET_WM_STATE_REMOVE 0 +#define _NET_WM_STATE_ADD 1 +#define _NET_WM_STATE_TOGGLE 2 + +static bool update_state(int action, bool *state) { + int new_state, changed; + + switch (action) { + case _NET_WM_STATE_REMOVE: + new_state = false; + break; + case _NET_WM_STATE_ADD: + new_state = true; + break; + case _NET_WM_STATE_TOGGLE: + new_state = !*state; + break; + default: + return false; + } + + changed = (*state != new_state); + *state = new_state; + + return changed; +} + +static inline bool xsurface_is_maximized( + struct wlr_xwayland_surface *xsurface) { + return xsurface->maximized_horz && xsurface->maximized_vert; +} + +static void xwm_handle_net_wm_state_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *client_message) { + struct wlr_xwayland_surface *xsurface = + lookup_surface(xwm, client_message->window); + if (!xsurface) { + return; + } + if (client_message->format != 32) { + return; + } + + bool fullscreen = xsurface->fullscreen; + bool maximized = xsurface_is_maximized(xsurface); + + uint32_t action = client_message->data.data32[0]; + for (size_t i = 0; i < 2; ++i) { + uint32_t property = client_message->data.data32[1 + i]; + + if (property == xwm->atoms[_NET_WM_STATE_MODAL] && + update_state(action, &xsurface->modal)) { + xsurface_set_net_wm_state(xsurface); + } else if (property == xwm->atoms[_NET_WM_STATE_FULLSCREEN] && + update_state(action, &xsurface->fullscreen)) { + xsurface_set_net_wm_state(xsurface); + } else if (property == xwm->atoms[_NET_WM_STATE_MAXIMIZED_VERT] && + update_state(action, &xsurface->maximized_vert)) { + xsurface_set_net_wm_state(xsurface); + } else if (property == xwm->atoms[_NET_WM_STATE_MAXIMIZED_HORZ] && + update_state(action, &xsurface->maximized_horz)) { + xsurface_set_net_wm_state(xsurface); + } + } + // client_message->data.data32[3] is the source indication + // all other values are set to 0 + + if (fullscreen != xsurface->fullscreen) { + if (xsurface->fullscreen) { + xsurface->saved_width = xsurface->width; + xsurface->saved_height = xsurface->height; + } + + wlr_signal_emit_safe(&xsurface->events.request_fullscreen, xsurface); + } + + if (maximized != xsurface_is_maximized(xsurface)) { + if (xsurface_is_maximized(xsurface)) { + xsurface->saved_width = xsurface->width; + xsurface->saved_height = xsurface->height; + } + + wlr_signal_emit_safe(&xsurface->events.request_maximize, xsurface); + } +} + +static void xwm_handle_wm_protocols_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *ev) { + xcb_atom_t type = ev->data.data32[0]; + + if (type == xwm->atoms[_NET_WM_PING]) { + xcb_window_t window_id = ev->data.data32[2]; + + struct wlr_xwayland_surface *surface = lookup_surface(xwm, window_id); + if (surface == NULL) { + return; + } + + if (!surface->pinging) { + return; + } + + wl_event_source_timer_update(surface->ping_timer, 0); + surface->pinging = false; + } else { + char *type_name = xwm_get_atom_name(xwm, type); + wlr_log(WLR_DEBUG, "unhandled WM_PROTOCOLS client message %u (%s)", + type, type_name); + free(type_name); + } +} + +static void xwm_handle_net_active_window_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *ev) { + struct wlr_xwayland_surface *surface = lookup_surface(xwm, ev->window); + if (surface == NULL) { + return; + } + wlr_signal_emit_safe(&surface->events.request_activate, surface); +} + +static void xwm_handle_client_message(struct wlr_xwm *xwm, + xcb_client_message_event_t *ev) { + wlr_log(WLR_DEBUG, "XCB_CLIENT_MESSAGE (%u)", ev->window); + + if (ev->type == xwm->atoms[WL_SURFACE_ID]) { + xwm_handle_surface_id_message(xwm, ev); + } else if (ev->type == xwm->atoms[NET_WM_STATE]) { + xwm_handle_net_wm_state_message(xwm, ev); + } else if (ev->type == xwm->atoms[_NET_WM_MOVERESIZE]) { + xwm_handle_net_wm_moveresize_message(xwm, ev); + } else if (ev->type == xwm->atoms[WM_PROTOCOLS]) { + xwm_handle_wm_protocols_message(xwm, ev); + } else if (ev->type == xwm->atoms[_NET_ACTIVE_WINDOW]) { + xwm_handle_net_active_window_message(xwm, ev); + } else if (!xwm_handle_selection_client_message(xwm, ev)) { + char *type_name = xwm_get_atom_name(xwm, ev->type); + wlr_log(WLR_DEBUG, "unhandled x11 client message %u (%s)", ev->type, + type_name); + free(type_name); + } +} + +static void xwm_handle_focus_in(struct wlr_xwm *xwm, + xcb_focus_in_event_t *ev) { + // Do not interfere with grabs + if (ev->mode == XCB_NOTIFY_MODE_GRAB || + ev->mode == XCB_NOTIFY_MODE_UNGRAB) { + return; + } + + // Do not let X clients change the focus behind the compositor's + // back. Reset the focus to the old one if it changed. + if (!xwm->focus_surface || ev->event != xwm->focus_surface->window_id) { + xwm_send_focus_window(xwm, xwm->focus_surface); + } +} + +static void xwm_handle_xcb_error(struct wlr_xwm *xwm, xcb_value_error_t *ev) { +#if WLR_HAS_XCB_ERRORS + const char *major_name = + xcb_errors_get_name_for_major_code(xwm->errors_context, + ev->major_opcode); + if (!major_name) { + wlr_log(WLR_DEBUG, "xcb error happened, but could not get major name"); + goto log_raw; + } + + const char *minor_name = + xcb_errors_get_name_for_minor_code(xwm->errors_context, + ev->major_opcode, ev->minor_opcode); + + const char *extension; + const char *error_name = + xcb_errors_get_name_for_error(xwm->errors_context, + ev->error_code, &extension); + if (!error_name) { + wlr_log(WLR_DEBUG, "xcb error happened, but could not get error name"); + goto log_raw; + } + + wlr_log(WLR_ERROR, "xcb error: op %s (%s), code %s (%s), sequence %"PRIu16", value %"PRIu32, + major_name, minor_name ? minor_name : "no minor", + error_name, extension ? extension : "no extension", + ev->sequence, ev->bad_value); + + return; +log_raw: +#endif + wlr_log(WLR_ERROR, + "xcb error: op %"PRIu8":%"PRIu16", code %"PRIu8", sequence %"PRIu16", value %"PRIu32, + ev->major_opcode, ev->minor_opcode, ev->error_code, + ev->sequence, ev->bad_value); + +} + +static void xwm_handle_unhandled_event(struct wlr_xwm *xwm, xcb_generic_event_t *ev) { +#if WLR_HAS_XCB_ERRORS + const char *extension; + const char *event_name = + xcb_errors_get_name_for_xcb_event(xwm->errors_context, + ev, &extension); + if (!event_name) { + wlr_log(WLR_DEBUG, "no name for unhandled event: %u", + ev->response_type); + return; + } + + wlr_log(WLR_DEBUG, "unhandled X11 event: %s (%u)", event_name, ev->response_type); +#else + wlr_log(WLR_DEBUG, "unhandled X11 event: %u", ev->response_type); +#endif +} + +static int x11_event_handler(int fd, uint32_t mask, void *data) { + int count = 0; + xcb_generic_event_t *event; + struct wlr_xwm *xwm = data; + + while ((event = xcb_poll_for_event(xwm->xcb_conn))) { + count++; + + if (xwm->xwayland->user_event_handler && + xwm->xwayland->user_event_handler(xwm, event)) { + break; + } + + if (xwm_handle_selection_event(xwm, event)) { + free(event); + continue; + } + + switch (event->response_type & XCB_EVENT_RESPONSE_TYPE_MASK) { + case XCB_CREATE_NOTIFY: + xwm_handle_create_notify(xwm, (xcb_create_notify_event_t *)event); + break; + case XCB_DESTROY_NOTIFY: + xwm_handle_destroy_notify(xwm, (xcb_destroy_notify_event_t *)event); + break; + case XCB_CONFIGURE_REQUEST: + xwm_handle_configure_request(xwm, + (xcb_configure_request_event_t *)event); + break; + case XCB_CONFIGURE_NOTIFY: + xwm_handle_configure_notify(xwm, + (xcb_configure_notify_event_t *)event); + break; + case XCB_MAP_REQUEST: + xwm_handle_map_request(xwm, (xcb_map_request_event_t *)event); + break; + case XCB_MAP_NOTIFY: + xwm_handle_map_notify(xwm, (xcb_map_notify_event_t *)event); + break; + case XCB_UNMAP_NOTIFY: + xwm_handle_unmap_notify(xwm, (xcb_unmap_notify_event_t *)event); + break; + case XCB_PROPERTY_NOTIFY: + xwm_handle_property_notify(xwm, + (xcb_property_notify_event_t *)event); + break; + case XCB_CLIENT_MESSAGE: + xwm_handle_client_message(xwm, (xcb_client_message_event_t *)event); + break; + case XCB_FOCUS_IN: + xwm_handle_focus_in(xwm, (xcb_focus_in_event_t *)event); + break; + case 0: + xwm_handle_xcb_error(xwm, (xcb_value_error_t *)event); + break; + default: + xwm_handle_unhandled_event(xwm, event); + break; + } + free(event); + } + + if (count) { + xcb_flush(xwm->xcb_conn); + } + + return count; +} + +static void handle_compositor_new_surface(struct wl_listener *listener, + void *data) { + struct wlr_xwm *xwm = + wl_container_of(listener, xwm, compositor_new_surface); + struct wlr_surface *surface = data; + if (wl_resource_get_client(surface->resource) != xwm->xwayland->client) { + return; + } + + wlr_log(WLR_DEBUG, "New xwayland surface: %p", surface); + + uint32_t surface_id = wl_resource_get_id(surface->resource); + struct wlr_xwayland_surface *xsurface; + wl_list_for_each(xsurface, &xwm->unpaired_surfaces, unpaired_link) { + if (xsurface->surface_id == surface_id) { + xwm_map_shell_surface(xwm, xsurface, surface); + xsurface->surface_id = 0; + wl_list_remove(&xsurface->unpaired_link); + xcb_flush(xwm->xcb_conn); + return; + } + } +} + +static void handle_compositor_destroy(struct wl_listener *listener, + void *data) { + struct wlr_xwm *xwm = + wl_container_of(listener, xwm, compositor_destroy); + wl_list_remove(&xwm->compositor_new_surface.link); + wl_list_remove(&xwm->compositor_destroy.link); + wl_list_init(&xwm->compositor_new_surface.link); + wl_list_init(&xwm->compositor_destroy.link); +} + +void wlr_xwayland_surface_activate(struct wlr_xwayland_surface *xsurface, + bool activated) { + struct wlr_xwayland_surface *focused = xsurface->xwm->focus_surface; + if (activated) { + xwm_surface_activate(xsurface->xwm, xsurface); + } else if (focused == xsurface) { + xwm_surface_activate(xsurface->xwm, NULL); + } +} + +void wlr_xwayland_surface_configure(struct wlr_xwayland_surface *xsurface, + int16_t x, int16_t y, uint16_t width, uint16_t height) { + xsurface->x = x; + xsurface->y = y; + xsurface->width = width; + xsurface->height = height; + + struct wlr_xwm *xwm = xsurface->xwm; + uint32_t mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT | + XCB_CONFIG_WINDOW_BORDER_WIDTH; + uint32_t values[] = {x, y, width, height, 0}; + xcb_configure_window(xwm->xcb_conn, xsurface->window_id, mask, values); + xcb_flush(xwm->xcb_conn); +} + +void wlr_xwayland_surface_close(struct wlr_xwayland_surface *xsurface) { + struct wlr_xwm *xwm = xsurface->xwm; + + bool supports_delete = false; + for (size_t i = 0; i < xsurface->protocols_len; i++) { + if (xsurface->protocols[i] == xwm->atoms[WM_DELETE_WINDOW]) { + supports_delete = true; + break; + } + } + + if (supports_delete) { + xcb_client_message_data_t message_data = {0}; + message_data.data32[0] = xwm->atoms[WM_DELETE_WINDOW]; + message_data.data32[1] = XCB_CURRENT_TIME; + xwm_send_wm_message(xsurface, &message_data, XCB_EVENT_MASK_NO_EVENT); + } else { + xcb_kill_client(xwm->xcb_conn, xsurface->window_id); + xcb_flush(xwm->xcb_conn); + } +} + +void xwm_destroy(struct wlr_xwm *xwm) { + if (!xwm) { + return; + } + xwm_selection_finish(xwm); + if (xwm->cursor) { + xcb_free_cursor(xwm->xcb_conn, xwm->cursor); + } + if (xwm->colormap) { + xcb_free_colormap(xwm->xcb_conn, xwm->colormap); + } + if (xwm->window) { + xcb_destroy_window(xwm->xcb_conn, xwm->window); + } + if (xwm->event_source) { + wl_event_source_remove(xwm->event_source); + } +#if WLR_HAS_XCB_ERRORS + if (xwm->errors_context) { + xcb_errors_context_free(xwm->errors_context); + } +#endif + struct wlr_xwayland_surface *xsurface, *tmp; + wl_list_for_each_safe(xsurface, tmp, &xwm->surfaces, link) { + xwayland_surface_destroy(xsurface); + } + wl_list_for_each_safe(xsurface, tmp, &xwm->unpaired_surfaces, link) { + xwayland_surface_destroy(xsurface); + } + wl_list_remove(&xwm->compositor_new_surface.link); + wl_list_remove(&xwm->compositor_destroy.link); + xcb_disconnect(xwm->xcb_conn); + + free(xwm); +} + +static void xwm_get_resources(struct wlr_xwm *xwm) { + xcb_prefetch_extension_data(xwm->xcb_conn, &xcb_xfixes_id); + xcb_prefetch_extension_data(xwm->xcb_conn, &xcb_composite_id); + + size_t i; + xcb_intern_atom_cookie_t cookies[ATOM_LAST]; + + for (i = 0; i < ATOM_LAST; i++) { + cookies[i] = + xcb_intern_atom(xwm->xcb_conn, 0, strlen(atom_map[i]), atom_map[i]); + } + for (i = 0; i < ATOM_LAST; i++) { + xcb_generic_error_t *error; + xcb_intern_atom_reply_t *reply = + xcb_intern_atom_reply(xwm->xcb_conn, cookies[i], &error); + if (reply && !error) { + xwm->atoms[i] = reply->atom; + } + free(reply); + + if (error) { + wlr_log(WLR_ERROR, "could not resolve atom %s, x11 error code %d", + atom_map[i], error->error_code); + free(error); + return; + } + } + + xwm->xfixes = xcb_get_extension_data(xwm->xcb_conn, &xcb_xfixes_id); + + if (!xwm->xfixes || !xwm->xfixes->present) { + wlr_log(WLR_DEBUG, "xfixes not available"); + } + + xcb_xfixes_query_version_cookie_t xfixes_cookie; + xcb_xfixes_query_version_reply_t *xfixes_reply; + xfixes_cookie = + xcb_xfixes_query_version(xwm->xcb_conn, XCB_XFIXES_MAJOR_VERSION, + XCB_XFIXES_MINOR_VERSION); + xfixes_reply = + xcb_xfixes_query_version_reply(xwm->xcb_conn, xfixes_cookie, NULL); + + wlr_log(WLR_DEBUG, "xfixes version: %d.%d", + xfixes_reply->major_version, xfixes_reply->minor_version); + + free(xfixes_reply); +} + +static void xwm_create_wm_window(struct wlr_xwm *xwm) { + static const char name[] = "wlroots wm"; + + xwm->window = xcb_generate_id(xwm->xcb_conn); + + xcb_create_window(xwm->xcb_conn, + XCB_COPY_FROM_PARENT, + xwm->window, + xwm->screen->root, + 0, 0, + 10, 10, + 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + xwm->screen->root_visual, + 0, NULL); + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xwm->window, + xwm->atoms[_NET_WM_NAME], + xwm->atoms[UTF8_STRING], + 8, // format + strlen(name), name); + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xwm->screen->root, + xwm->atoms[_NET_SUPPORTING_WM_CHECK], + XCB_ATOM_WINDOW, + 32, // format + 1, &xwm->window); + + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xwm->window, + xwm->atoms[_NET_SUPPORTING_WM_CHECK], + XCB_ATOM_WINDOW, + 32, // format + 1, &xwm->window); + + xcb_set_selection_owner(xwm->xcb_conn, + xwm->window, + xwm->atoms[WM_S0], + XCB_CURRENT_TIME); + + xcb_set_selection_owner(xwm->xcb_conn, + xwm->window, + xwm->atoms[NET_WM_CM_S0], + XCB_CURRENT_TIME); +} + +// TODO use me to support 32 bit color somehow +static void xwm_get_visual_and_colormap(struct wlr_xwm *xwm) { + xcb_depth_iterator_t d_iter; + xcb_visualtype_iterator_t vt_iter; + xcb_visualtype_t *visualtype; + + d_iter = xcb_screen_allowed_depths_iterator(xwm->screen); + visualtype = NULL; + while (d_iter.rem > 0) { + if (d_iter.data->depth == 32) { + vt_iter = xcb_depth_visuals_iterator(d_iter.data); + visualtype = vt_iter.data; + break; + } + + xcb_depth_next(&d_iter); + } + + if (visualtype == NULL) { + wlr_log(WLR_DEBUG, "No 32 bit visualtype\n"); + return; + } + + xwm->visual_id = visualtype->visual_id; + xwm->colormap = xcb_generate_id(xwm->xcb_conn); + xcb_create_colormap(xwm->xcb_conn, + XCB_COLORMAP_ALLOC_NONE, + xwm->colormap, + xwm->screen->root, + xwm->visual_id); +} + +static void xwm_get_render_format(struct wlr_xwm *xwm) { + xcb_render_query_pict_formats_cookie_t cookie = + xcb_render_query_pict_formats(xwm->xcb_conn); + xcb_render_query_pict_formats_reply_t *reply = + xcb_render_query_pict_formats_reply(xwm->xcb_conn, cookie, NULL); + if (!reply) { + wlr_log(WLR_ERROR, "Did not get any reply from xcb_render_query_pict_formats"); + return; + } + xcb_render_pictforminfo_iterator_t iter = + xcb_render_query_pict_formats_formats_iterator(reply); + xcb_render_pictforminfo_t *format = NULL; + while (iter.rem > 0) { + if (iter.data->depth == 32) { + format = iter.data; + break; + } + + xcb_render_pictforminfo_next(&iter); + } + + if (format == NULL) { + wlr_log(WLR_DEBUG, "No 32 bit render format"); + free(reply); + return; + } + + xwm->render_format_id = format->id; + free(reply); +} + +void xwm_set_cursor(struct wlr_xwm *xwm, const uint8_t *pixels, uint32_t stride, + uint32_t width, uint32_t height, int32_t hotspot_x, int32_t hotspot_y) { + if (!xwm->render_format_id) { + wlr_log(WLR_ERROR, "Cannot set xwm cursor: no render format available"); + return; + } + if (xwm->cursor) { + xcb_free_cursor(xwm->xcb_conn, xwm->cursor); + } + + int depth = 32; + + xcb_pixmap_t pix = xcb_generate_id(xwm->xcb_conn); + xcb_create_pixmap(xwm->xcb_conn, depth, pix, xwm->screen->root, width, + height); + + xcb_render_picture_t pic = xcb_generate_id(xwm->xcb_conn); + xcb_render_create_picture(xwm->xcb_conn, pic, pix, xwm->render_format_id, + 0, 0); + + xcb_gcontext_t gc = xcb_generate_id(xwm->xcb_conn); + xcb_create_gc(xwm->xcb_conn, gc, pix, 0, NULL); + + xcb_put_image(xwm->xcb_conn, XCB_IMAGE_FORMAT_Z_PIXMAP, pix, gc, + width, height, 0, 0, 0, depth, stride * height * sizeof(uint8_t), + pixels); + xcb_free_gc(xwm->xcb_conn, gc); + + xwm->cursor = xcb_generate_id(xwm->xcb_conn); + xcb_render_create_cursor(xwm->xcb_conn, xwm->cursor, pic, hotspot_x, + hotspot_y); + xcb_free_pixmap(xwm->xcb_conn, pix); + + uint32_t values[] = {xwm->cursor}; + xcb_change_window_attributes(xwm->xcb_conn, xwm->screen->root, + XCB_CW_CURSOR, values); + xcb_flush(xwm->xcb_conn); +} + +struct wlr_xwm *xwm_create(struct wlr_xwayland *wlr_xwayland) { + struct wlr_xwm *xwm = calloc(1, sizeof(struct wlr_xwm)); + if (xwm == NULL) { + return NULL; + } + + xwm->xwayland = wlr_xwayland; + wl_list_init(&xwm->surfaces); + wl_list_init(&xwm->unpaired_surfaces); + xwm->ping_timeout = 10000; + + xwm->xcb_conn = xcb_connect_to_fd(wlr_xwayland->wm_fd[0], NULL); + + int rc = xcb_connection_has_error(xwm->xcb_conn); + if (rc) { + wlr_log(WLR_ERROR, "xcb connect failed: %d", rc); + close(wlr_xwayland->wm_fd[0]); + free(xwm); + return NULL; + } + +#if WLR_HAS_XCB_ERRORS + if (xcb_errors_context_new(xwm->xcb_conn, &xwm->errors_context)) { + wlr_log(WLR_ERROR, "Could not allocate error context"); + xwm_destroy(xwm); + return NULL; + } +#endif + xcb_screen_iterator_t screen_iterator = + xcb_setup_roots_iterator(xcb_get_setup(xwm->xcb_conn)); + xwm->screen = screen_iterator.data; + + struct wl_event_loop *event_loop = wl_display_get_event_loop( + wlr_xwayland->wl_display); + xwm->event_source = + wl_event_loop_add_fd(event_loop, + wlr_xwayland->wm_fd[0], + WL_EVENT_READABLE, + x11_event_handler, + xwm); + wl_event_source_check(xwm->event_source); + + xwm_get_resources(xwm); + xwm_get_visual_and_colormap(xwm); + xwm_get_render_format(xwm); + + uint32_t values[] = { + XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | + XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | + XCB_EVENT_MASK_PROPERTY_CHANGE, + }; + xcb_change_window_attributes(xwm->xcb_conn, + xwm->screen->root, + XCB_CW_EVENT_MASK, + values); + + xcb_composite_redirect_subwindows(xwm->xcb_conn, + xwm->screen->root, + XCB_COMPOSITE_REDIRECT_MANUAL); + + xcb_atom_t supported[] = { + xwm->atoms[NET_WM_STATE], + xwm->atoms[_NET_ACTIVE_WINDOW], + xwm->atoms[_NET_WM_MOVERESIZE], + xwm->atoms[_NET_WM_STATE_MODAL], + xwm->atoms[_NET_WM_STATE_FULLSCREEN], + xwm->atoms[_NET_WM_STATE_MAXIMIZED_VERT], + xwm->atoms[_NET_WM_STATE_MAXIMIZED_HORZ], + }; + xcb_change_property(xwm->xcb_conn, + XCB_PROP_MODE_REPLACE, + xwm->screen->root, + xwm->atoms[NET_SUPPORTED], + XCB_ATOM_ATOM, + 32, + sizeof(supported)/sizeof(*supported), + supported); + + xcb_flush(xwm->xcb_conn); + + xwm_set_net_active_window(xwm, XCB_WINDOW_NONE); + + xwm_selection_init(xwm); + + xwm->compositor_new_surface.notify = handle_compositor_new_surface; + wl_signal_add(&wlr_xwayland->compositor->events.new_surface, + &xwm->compositor_new_surface); + xwm->compositor_destroy.notify = handle_compositor_destroy; + wl_signal_add(&wlr_xwayland->compositor->events.destroy, + &xwm->compositor_destroy); + + xwm_create_wm_window(xwm); + + xcb_flush(xwm->xcb_conn); + + return xwm; +} + +void wlr_xwayland_surface_set_maximized(struct wlr_xwayland_surface *surface, + bool maximized) { + surface->maximized_horz = maximized; + surface->maximized_vert = maximized; + xsurface_set_net_wm_state(surface); + xcb_flush(surface->xwm->xcb_conn); +} + +void wlr_xwayland_surface_set_fullscreen(struct wlr_xwayland_surface *surface, + bool fullscreen) { + surface->fullscreen = fullscreen; + xsurface_set_net_wm_state(surface); + xcb_flush(surface->xwm->xcb_conn); +} + +bool xwm_atoms_contains(struct wlr_xwm *xwm, xcb_atom_t *atoms, + size_t num_atoms, enum atom_name needle) { + xcb_atom_t atom = xwm->atoms[needle]; + + for (size_t i = 0; i < num_atoms; ++i) { + if (atom == atoms[i]) { + return true; + } + } + + return false; +} + +void wlr_xwayland_surface_ping(struct wlr_xwayland_surface *surface) { + xcb_client_message_data_t data = { 0 }; + data.data32[0] = surface->xwm->atoms[_NET_WM_PING]; + data.data32[1] = XCB_CURRENT_TIME; + data.data32[2] = surface->window_id; + + xwm_send_wm_message(surface, &data, XCB_EVENT_MASK_NO_EVENT); + + wl_event_source_timer_update(surface->ping_timer, + surface->xwm->ping_timeout); + surface->pinging = true; +} + +bool wlr_xwayland_or_surface_wants_focus( + const struct wlr_xwayland_surface *surface) { + bool ret = true; + static enum atom_name needles[] = { + NET_WM_WINDOW_TYPE_COMBO, + NET_WM_WINDOW_TYPE_DND, + NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + NET_WM_WINDOW_TYPE_MENU, + NET_WM_WINDOW_TYPE_NOTIFICATION, + NET_WM_WINDOW_TYPE_POPUP_MENU, + NET_WM_WINDOW_TYPE_SPLASH, + NET_WM_WINDOW_TYPE_TOOLTIP, + NET_WM_WINDOW_TYPE_UTILITY, + }; + for (size_t i = 0; i < sizeof(needles) / sizeof(needles[0]); ++i) { + if (xwm_atoms_contains(surface->xwm, surface->window_type, + surface->window_type_len, needles[i])) { + ret = false; + } + } + + return ret; +} |