From 781748d25d2d559c06974cf9cc9a80867adfc73f Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sat, 28 Mar 2026 15:41:48 +0100 Subject: [PATCH] wlr_xdg_session_management_v1: new protocol implementation See https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/18 --- .../wlr/types/wlr_xdg_session_management_v1.h | 145 ++++++ protocol/meson.build | 3 +- types/meson.build | 1 + types/wlr_xdg_session_management_v1.c | 432 ++++++++++++++++++ 4 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 include/wlr/types/wlr_xdg_session_management_v1.h create mode 100644 types/wlr_xdg_session_management_v1.c diff --git a/include/wlr/types/wlr_xdg_session_management_v1.h b/include/wlr/types/wlr_xdg_session_management_v1.h new file mode 100644 index 000000000..6de8e1887 --- /dev/null +++ b/include/wlr/types/wlr_xdg_session_management_v1.h @@ -0,0 +1,145 @@ +/* + * This an unstable interface of wlroots. No guarantees are made regarding the + * future consistency of this API. + */ +#ifndef WLR_USE_UNSTABLE +#error "Add -DWLR_USE_UNSTABLE to enable unstable wlroots features" +#endif + +#ifndef WLR_TYPES_WLR_XDG_SESSION_MANAGEMENT_V1_H +#define WLR_TYPES_WLR_XDG_SESSION_MANAGEMENT_V1_H + +#include +#include + +/** + * A toplevel part of a session. + */ +struct wlr_xdg_toplevel_session_v1 { + struct wl_resource *resource; + struct wlr_xdg_session_v1 *session; + struct wlr_xdg_toplevel *toplevel; + struct wl_list link; // wlr_xdg_session_v1.toplevels + + // Client-provided identifier, unique per session. + char *name; + + struct { + struct wl_signal destroy; + + // Toplevel session name has changed. + struct wl_signal rename; + } events; + + struct { + bool restorable; + struct wl_listener toplevel_destroy; + } WLR_PRIVATE; +}; + +struct wlr_xdg_session_v1_remove_toplevel_event { + const char *name; +}; + +/** + * An application session. + * + * When a new session is created, compositors should load any saved state based + * on wlr_xdg_session_v1.id (if non-NULL), then call + * wlr_xdg_session_v1_notify_created() or wlr_xdg_session_v1_notify_restored(). + * + * Compositors should save toplevel state for all toplevels added to the + * session. + */ +struct wlr_xdg_session_v1 { + struct wl_resource *resource; + struct wl_list link; // wlr_xdg_session_manager_v1.sessions + + enum xdg_session_manager_v1_reason reason; + char *id; + struct wl_list toplevels; // wlr_xdg_toplevel_session_v1.link + + struct { + struct wl_signal destroy; + + // All session saved state should be erased. + struct wl_signal remove; + + // Toplevel state should be tracked as part of the session. + struct wl_signal add_toplevel; // struct wlr_xdg_toplevel_session_v1 * + + // If the toplevel state was saved, it should be restored and + // wlr_xdg_toplevel_session_v1_notify_restored() should be called. Then + // toplevel state should be tracked as part of the session. + struct wl_signal restore_toplevel; // struct wlr_xdg_toplevel_session_v1 * + + // Toplevel state should no longer be tracked as part of the session. + struct wl_signal remove_toplevel; // struct wlr_xdg_session_v1_remove_toplevel_event * + } events; + + struct { + bool initialized; // added or restored + } WLR_PRIVATE; +}; + +/** + * Session manager global. + */ +struct wlr_xdg_session_manager_v1 { + struct wl_global *global; + + struct wl_list sessions; // wlr_xdg_session_v1.link + + struct { + struct wl_signal destroy; + struct wl_signal new_session; // struct wlr_xdg_session_v1 * + } events; + + struct { + struct wl_listener display_destroy; + } WLR_PRIVATE; +}; + +/** + * Create the session manager global. + */ +struct wlr_xdg_session_manager_v1 *wlr_xdg_session_manager_v1_create( + struct wl_display *display, uint32_t version); + +/** + * Notify that a new session state has been created. + * + * This function should be called in response to + * wlr_xdg_session_manager_v1.events.new_session if no state could be restored. + */ +void wlr_xdg_session_v1_notify_created(struct wlr_xdg_session_v1 *session, const char *session_id); + +/** + * Notify that the session has been restored. + * + * This function should be called in response to + * wlr_xdg_session_manager_v1.events.new_session if at least some state could + * be restored. + */ +void wlr_xdg_session_v1_notify_restored(struct wlr_xdg_session_v1 *session); + +/** + * Notify that the session has been taken over by another client. + * + * This function destroys the session. + */ +void wlr_xdg_session_v1_notify_replaced_and_destroy(struct wlr_xdg_session_v1 *session); + +/** + * Notify that the toplevel state has been restored. + * + * This event is part of a toplevel configure sequence. + * + * This function should be called in response to + * wlr_xdg_session_v1.events.restore_toplevel if the toplevel was successfully + * restored. + */ +uint32_t wlr_xdg_toplevel_session_v1_notify_restored( + struct wlr_xdg_toplevel_session_v1 *toplevel_session); + +#endif diff --git a/protocol/meson.build b/protocol/meson.build index 4d667d94a..4a1308177 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -1,5 +1,5 @@ wayland_protos = dependency('wayland-protocols', - version: '>=1.47', + version: '>=1.48', fallback: 'wayland-protocols', default_options: ['tests=false'], ) @@ -44,6 +44,7 @@ protocols = { 'single-pixel-buffer-v1': wl_protocol_dir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', 'xdg-activation-v1': wl_protocol_dir / 'staging/xdg-activation/xdg-activation-v1.xml', 'xdg-dialog-v1': wl_protocol_dir / 'staging/xdg-dialog/xdg-dialog-v1.xml', + 'xdg-session-management-v1': wl_protocol_dir / 'staging/xdg-session-management/xdg-session-management-v1.xml', 'xdg-system-bell-v1': wl_protocol_dir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', 'xdg-toplevel-icon-v1': wl_protocol_dir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', 'xdg-toplevel-tag-v1': wl_protocol_dir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml', diff --git a/types/meson.build b/types/meson.build index bc3d32cd9..f1165b0f6 100644 --- a/types/meson.build +++ b/types/meson.build @@ -105,6 +105,7 @@ wlr_files += files( 'wlr_xdg_foreign_v2.c', 'wlr_xdg_foreign_registry.c', 'wlr_xdg_output_v1.c', + 'wlr_xdg_session_management_v1.c', 'wlr_xdg_system_bell_v1.c', 'wlr_xdg_toplevel_icon_v1.c', 'wlr_xdg_toplevel_tag_v1.c', diff --git a/types/wlr_xdg_session_management_v1.c b/types/wlr_xdg_session_management_v1.c new file mode 100644 index 000000000..9db6387a7 --- /dev/null +++ b/types/wlr_xdg_session_management_v1.c @@ -0,0 +1,432 @@ +#include +#include +#include +#include +#include + +#include "xdg-session-management-v1-protocol.h" + +#define MANAGER_VERSION 1 + +static const struct xdg_toplevel_session_v1_interface toplevel_session_impl; +static const struct xdg_session_v1_interface session_impl; +static const struct xdg_session_manager_v1_interface manager_impl; + +static struct wlr_xdg_toplevel_session_v1 *toplevel_session_from_resource(struct wl_resource *resource) { + assert(wl_resource_instance_of(resource, &xdg_toplevel_session_v1_interface, &toplevel_session_impl)); + return wl_resource_get_user_data(resource); +} + +static struct wlr_xdg_session_v1 *session_from_resource(struct wl_resource *resource) { + assert(wl_resource_instance_of(resource, &xdg_session_v1_interface, &session_impl)); + return wl_resource_get_user_data(resource); +} + +static struct wlr_xdg_session_manager_v1 *manager_from_resource(struct wl_resource *resource) { + assert(wl_resource_instance_of(resource, &xdg_session_manager_v1_interface, &manager_impl)); + return wl_resource_get_user_data(resource); +} + +static struct wlr_xdg_toplevel_session_v1 *session_find_toplevel(struct wlr_xdg_session_v1 *session, + const char *name); + +static void toplevel_session_handle_rename(struct wl_client *client, + struct wl_resource *toplevel_session_resource, const char *name) { + struct wlr_xdg_toplevel_session_v1 *toplevel_session = + toplevel_session_from_resource(toplevel_session_resource); + if (toplevel_session == NULL) { + return; + } + + if (strcmp(toplevel_session->name, name) == 0) { + return; + } + if (session_find_toplevel(toplevel_session->session, name) != NULL) { + wl_resource_post_error(toplevel_session_resource, + XDG_SESSION_V1_ERROR_NAME_IN_USE, + "Name already in use by another toplevel in the same session"); + return; + } + + char *name_copy = strdup(name); + if (name_copy == NULL) { + wl_resource_post_no_memory(toplevel_session_resource); + return; + } + + free(toplevel_session->name); + toplevel_session->name = name_copy; + + wl_signal_emit_mutable(&toplevel_session->events.rename, NULL); +} + +static void toplevel_session_handle_destroy(struct wl_client *client, + struct wl_resource *toplevel_session_resource) { + wl_resource_destroy(toplevel_session_resource); +} + +static const struct xdg_toplevel_session_v1_interface toplevel_session_impl = { + .destroy = toplevel_session_handle_destroy, + .rename = toplevel_session_handle_rename, +}; + +uint32_t wlr_xdg_toplevel_session_v1_notify_restored( + struct wlr_xdg_toplevel_session_v1 *toplevel_session) { + assert(toplevel_session->restorable); + toplevel_session->restorable = false; + + xdg_toplevel_session_v1_send_restored(toplevel_session->resource); + return wlr_xdg_surface_schedule_configure(toplevel_session->toplevel->base); +} + +static void toplevel_session_destroy(struct wlr_xdg_toplevel_session_v1 *toplevel_session) { + if (toplevel_session == NULL) { + return; + } + + wl_list_remove(&toplevel_session->toplevel_destroy.link); + + wl_signal_emit_mutable(&toplevel_session->events.destroy, NULL); + + assert(wl_list_empty(&toplevel_session->events.destroy.listener_list)); + assert(wl_list_empty(&toplevel_session->events.rename.listener_list)); + + wl_resource_set_user_data(toplevel_session->resource, NULL); // make inert + wl_list_remove(&toplevel_session->link); + free(toplevel_session->name); + free(toplevel_session); +} + +static void toplevel_session_handle_resource_destroy(struct wl_resource *resource) { + struct wlr_xdg_toplevel_session_v1 *toplevel_session = toplevel_session_from_resource(resource); + toplevel_session_destroy(toplevel_session); +} + +static void toplevel_session_handle_toplevel_destroy(struct wl_listener *listener, void *data) { + struct wlr_xdg_toplevel_session_v1 *toplevel_session = + wl_container_of(listener, toplevel_session, toplevel_destroy); + toplevel_session_destroy(toplevel_session); +} + +static void session_handle_remove(struct wl_client *client, struct wl_resource *session_resource) { + struct wlr_xdg_session_v1 *session = session_from_resource(session_resource); + wl_signal_emit_mutable(&session->events.remove, NULL); +} + +static struct wlr_xdg_toplevel_session_v1 *session_find_toplevel(struct wlr_xdg_session_v1 *session, + const char *name) { + struct wlr_xdg_toplevel_session_v1 *toplevel_session; + wl_list_for_each(toplevel_session, &session->toplevels, link) { + if (strcmp(toplevel_session->name, name) == 0) { + return toplevel_session; + } + } + return NULL; +} + +static struct wlr_xdg_toplevel_session_v1 *session_create_toplevel(struct wl_resource *session_resource, + uint32_t id, struct wl_resource *toplevel_resource, const char *name) { + struct wlr_xdg_session_v1 *session = session_from_resource(session_resource); + struct wlr_xdg_toplevel *toplevel = wlr_xdg_toplevel_from_resource(toplevel_resource); + + struct wl_client *client = wl_resource_get_client(session_resource); + uint32_t version = wl_resource_get_version(session_resource); + struct wl_resource *resource = wl_resource_create(client, + &xdg_toplevel_session_v1_interface, version, id); + if (resource == NULL) { + wl_resource_post_no_memory(session_resource); + return NULL; + } + wl_resource_set_implementation(resource, &toplevel_session_impl, NULL, + toplevel_session_handle_resource_destroy); + + if (session == NULL || toplevel == NULL) { + return NULL; + } + + if (session_find_toplevel(session, name) != NULL) { + wl_resource_post_error(session->resource, XDG_SESSION_V1_ERROR_NAME_IN_USE, + "Name already in use by a toplevel in the same session"); + return NULL; + } + + // TODO: post already_added error if toplevel was already added to *any* session + struct wlr_xdg_toplevel_session_v1 *toplevel_session; + wl_list_for_each(toplevel_session, &session->toplevels, link) { + if (toplevel_session->toplevel == toplevel) { + wl_resource_post_error(session->resource, XDG_SESSION_V1_ERROR_ALREADY_ADDED, + "Toplevel already added to session"); + return NULL; + } + } + + toplevel_session = calloc(1, sizeof(*toplevel_session)); + if (toplevel_session == NULL) { + wl_resource_post_no_memory(session_resource); + return NULL; + } + + toplevel_session->resource = resource; + toplevel_session->session = session; + toplevel_session->toplevel = toplevel; + + toplevel_session->name = strdup(name); + if (toplevel_session->name == NULL) { + wl_resource_post_no_memory(session_resource); + free(toplevel_session); + return NULL; + } + + toplevel_session->toplevel_destroy.notify = toplevel_session_handle_toplevel_destroy; + wl_signal_add(&toplevel->events.destroy, &toplevel_session->toplevel_destroy); + + wl_resource_set_user_data(resource, toplevel_session); + wl_list_insert(&session->toplevels, &toplevel_session->link); + + return toplevel_session; +} + +static void session_handle_add_toplevel(struct wl_client *client, struct wl_resource *session_resource, + uint32_t id, struct wl_resource *toplevel_resource, const char *name) { + struct wlr_xdg_session_v1 *session = session_from_resource(session_resource); + struct wlr_xdg_toplevel_session_v1 *toplevel_session = + session_create_toplevel(session_resource, id, toplevel_resource, name); + if (toplevel_session == NULL) { + return; + } + + wl_signal_emit_mutable(&session->events.add_toplevel, toplevel_session); +} + +static void session_handle_restore_toplevel(struct wl_client *client, struct wl_resource *session_resource, + uint32_t id, struct wl_resource *toplevel_resource, const char *name) { + struct wlr_xdg_session_v1 *session = session_from_resource(session_resource); + struct wlr_xdg_toplevel *toplevel = wlr_xdg_toplevel_from_resource(toplevel_resource); + if (toplevel && toplevel->base->surface->mapped) { + wl_resource_post_error(session_resource, XDG_SESSION_V1_ERROR_ALREADY_MAPPED, + "Restored toplevel is already mapped"); + return; + } + + struct wlr_xdg_toplevel_session_v1 *toplevel_session = + session_create_toplevel(session_resource, id, toplevel_resource, name); + if (toplevel_session == NULL) { + return; + } + + toplevel_session->restorable = true; + + wl_signal_emit_mutable(&session->events.restore_toplevel, toplevel_session); +} + +static void session_handle_remove_toplevel(struct wl_client *client, struct wl_resource *session_resource, + const char *name) { + struct wlr_xdg_session_v1 *session = session_from_resource(session_resource); + if (session == NULL) { + return; + } + + struct wlr_xdg_session_v1_remove_toplevel_event event = { + .name = name, + }; + wl_signal_emit_mutable(&session->events.remove_toplevel, &event); + + struct wlr_xdg_toplevel_session_v1 *toplevel_session = session_find_toplevel(session, name); + toplevel_session_destroy(toplevel_session); +} + +static void session_handle_destroy(struct wl_client *client, struct wl_resource *session_resource) { + wl_resource_destroy(session_resource); +} + +static const struct xdg_session_v1_interface session_impl = { + .destroy = session_handle_destroy, + .remove = session_handle_remove, + .add_toplevel = session_handle_add_toplevel, + .restore_toplevel = session_handle_restore_toplevel, + .remove_toplevel = session_handle_remove_toplevel, +}; + +static void session_destroy(struct wlr_xdg_session_v1 *session) { + if (session == NULL) { + return; + } + + wl_signal_emit_mutable(&session->events.destroy, NULL); + + assert(wl_list_empty(&session->events.destroy.listener_list)); + assert(wl_list_empty(&session->events.remove.listener_list)); + assert(wl_list_empty(&session->events.add_toplevel.listener_list)); + assert(wl_list_empty(&session->events.restore_toplevel.listener_list)); + assert(wl_list_empty(&session->events.remove_toplevel.listener_list)); + + struct wlr_xdg_toplevel_session_v1 *toplevel_session, *toplevel_session_tmp; + wl_list_for_each_safe(toplevel_session, toplevel_session_tmp, &session->toplevels, link) { + toplevel_session_destroy(toplevel_session); + } + + wl_resource_set_user_data(session->resource, NULL); // make inert + wl_list_remove(&session->link); + free(session->id); + free(session); +} + +static void session_handle_resource_destroy(struct wl_resource *resource) { + struct wlr_xdg_session_v1 *session = session_from_resource(resource); + session_destroy(session); +} + +void wlr_xdg_session_v1_notify_created(struct wlr_xdg_session_v1 *session, const char *session_id) { + assert(!session->initialized); + + char *session_id_copy = strdup(session_id); + if (session_id_copy == NULL) { + wl_resource_post_no_memory(session->resource); + return; + } + free(session->id); + session->id = session_id_copy; + + xdg_session_v1_send_created(session->resource, session_id); + + session->initialized = true; +} + +void wlr_xdg_session_v1_notify_restored(struct wlr_xdg_session_v1 *session) { + assert(!session->initialized); + assert(session->id != NULL); + + xdg_session_v1_send_restored(session->resource); + + session->initialized = true; +} + +void wlr_xdg_session_v1_notify_replaced_and_destroy(struct wlr_xdg_session_v1 *session) { + xdg_session_v1_send_replaced(session->resource); + session_destroy(session); +} + +static void manager_handle_get_session(struct wl_client *client, + struct wl_resource *manager_resource, uint32_t id, uint32_t reason, + const char *session_id) { + struct wlr_xdg_session_manager_v1 *manager = manager_from_resource(manager_resource); + uint32_t version = wl_resource_get_version(manager_resource); + + if (!xdg_session_manager_v1_reason_is_valid(reason, version)) { + wl_resource_post_error(manager_resource, XDG_SESSION_MANAGER_V1_ERROR_INVALID_REASON, + "Invalid reason"); + return; + } + + struct wlr_xdg_session_v1 *sess; + wl_list_for_each(sess, &manager->sessions, link) { + if (wl_resource_get_client(sess->resource) == client && sess->id != NULL && + strcmp(sess->id, session_id) == 0) { + wl_resource_post_error(manager_resource, + XDG_SESSION_MANAGER_V1_ERROR_IN_USE, + "Session ID already in use by another session object"); + return; + } + } + + struct wlr_xdg_session_v1 *session = calloc(1, sizeof(*session)); + if (session == NULL) { + wl_client_post_no_memory(client); + return; + } + + char *session_id_copy = NULL; + if (session_id != NULL) { + session_id_copy = strdup(session_id); + if (session_id_copy == NULL) { + wl_client_post_no_memory(client); + free(session); + return; + } + } + + session->resource = wl_resource_create(client, + &xdg_session_v1_interface, version, id); + if (session->resource == NULL) { + wl_resource_post_no_memory(manager_resource); + free(session); + return; + } + wl_resource_set_implementation(session->resource, &session_impl, session, + session_handle_resource_destroy); + + wl_signal_init(&session->events.destroy); + wl_signal_init(&session->events.remove); + wl_signal_init(&session->events.add_toplevel); + wl_signal_init(&session->events.restore_toplevel); + wl_signal_init(&session->events.remove_toplevel); + + session->reason = reason; + session->id = session_id_copy; + wl_list_init(&session->toplevels); + + wl_list_insert(&manager->sessions, &session->link); + + wl_signal_emit_mutable(&manager->events.new_session, session); +} + +static void manager_handle_destroy(struct wl_client *client, struct wl_resource *manager_resource) { + wl_resource_destroy(manager_resource); +} + +static const struct xdg_session_manager_v1_interface manager_impl = { + .destroy = manager_handle_destroy, + .get_session = manager_handle_get_session, +}; + +static void manager_bind(struct wl_client *client, void *data, uint32_t version, uint32_t id) { + struct wlr_xdg_session_manager_v1 *manager = data; + + struct wl_resource *resource = wl_resource_create(client, + &xdg_session_manager_v1_interface, version, id); + if (resource == NULL) { + wl_client_post_no_memory(client); + return; + } + wl_resource_set_implementation(resource, &manager_impl, manager, NULL); +} + +static void manager_handle_display_destroy(struct wl_listener *listener, void *data) { + struct wlr_xdg_session_manager_v1 *manager = wl_container_of(listener, manager, display_destroy); + + wl_signal_emit_mutable(&manager->events.destroy, NULL); + + assert(wl_list_empty(&manager->events.destroy.listener_list)); + assert(wl_list_empty(&manager->events.new_session.listener_list)); + + wl_list_remove(&manager->display_destroy.link); + wl_global_destroy(manager->global); + free(manager); +} + +struct wlr_xdg_session_manager_v1 *wlr_xdg_session_manager_v1_create( + struct wl_display *display, uint32_t version) { + assert(version <= MANAGER_VERSION); + + struct wlr_xdg_session_manager_v1 *manager = calloc(1, sizeof(*manager)); + if (manager == NULL) { + return NULL; + } + + manager->global = wl_global_create(display, &xdg_session_manager_v1_interface, + version, manager, manager_bind); + if (manager->global == NULL) { + free(manager); + return NULL; + } + + wl_signal_init(&manager->events.destroy); + wl_signal_init(&manager->events.new_session); + + wl_list_init(&manager->sessions); + + manager->display_destroy.notify = manager_handle_display_destroy; + wl_display_add_destroy_listener(display, &manager->display_destroy); + + return manager; +}