wayland: Phase 46 — client_close vtable; title updates; protocol guards

Add client_close vtable slot to WMBackend:
  - x11: XKillClient + XFlush
  - wl: wlr_xdg_toplevel_send_close (politely asks client to close)
  - wClientKill: use wm_backend->client_close() for Wayland path

Guard wClientSendProtocol with if (!dpy) return so it is safe to call
on Wayland windows (protocols.DELETE_WINDOW is always 0 for Wayland).

Wire XDG toplevel set_title signal: add title_listener to
wl_toplevel_view; handle_toplevel_title updates wwin->wm_class /
wm_instance when the client changes its title, and calls
wWindowUpdateName (which no-ops on frameless windows but posts
WMNChangedName when a frame is present).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-28 06:46:46 +01:00
parent 69cee6fb15
commit fc0e233865
4 changed files with 70 additions and 3 deletions

View File

@ -811,6 +811,12 @@ typedef struct WMBackend {
*/
void (*client_remove_from_save_set)(WNativeWindow win);
/*
* Request a client window to close itself.
* On X11: XKillClient (forceful); on Wayland: xdg_toplevel_send_close.
*/
void (*client_close)(WNativeWindow win);
/* ------------------------------------------------------------------ */
/* Fake group-leader window (Phase 13) */
/* ------------------------------------------------------------------ */

View File

@ -27,6 +27,9 @@
* so clients receive size changes from the WM.
* Phase 45: pointer_query returns 1 (always on-screen); wWindowFocus/Unfocus
* guarded on frame!=NULL so wSetFocusTo works without a frame window.
* Phase 46: client_close vtable slot (wlr_xdg_toplevel_send_close);
* title_listener updates wwin->wm_instance/wm_class on set_title signal;
* wClientSendProtocol/wClientKill guarded for Wayland.
*/
#ifdef HAVE_CONFIG_H
@ -75,6 +78,7 @@ struct wl_toplevel_view {
struct wl_listener map_listener;
struct wl_listener unmap_listener;
struct wl_listener destroy_listener;
struct wl_listener title_listener; /* Phase 46: xdg_toplevel set_title */
WNativeWindow id; /* synthetic handle, >= 0x10000000 */
struct wl_list link; /* wl_state.toplevel_list */
};
@ -159,6 +163,34 @@ wl_find_view_by_id(WNativeWindow id)
return NULL;
}
/*
* Phase 46: XDG toplevel title changed — update wwin->wm_instance.
*/
static void
handle_toplevel_title(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, title_listener);
(void)data;
const char *new_title = view->xdg_surface->toplevel->title;
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (!desc || desc->parent_type != WCLASS_WINDOW)
return;
WWindow *wwin = (WWindow *)desc->parent;
if (wwin->wm_instance)
wfree(wwin->wm_instance);
wwin->wm_instance = new_title ? strdup(new_title) : NULL;
if (wwin->wm_class)
wfree(wwin->wm_class);
const char *app_id = (view->xdg_surface->toplevel->app_id)
? view->xdg_surface->toplevel->app_id : "wayland-client";
wwin->wm_class = strdup(app_id);
wWindowUpdateName(wwin, new_title);
}
/*
* Phase 38: toplevel map — surface is ready to be managed.
*/
@ -226,6 +258,7 @@ handle_toplevel_destroy(struct wl_listener *listener, void *data)
wl_list_remove(&view->map_listener.link);
wl_list_remove(&view->unmap_listener.link);
wl_list_remove(&view->destroy_listener.link);
wl_list_remove(&view->title_listener.link);
wl_list_remove(&view->link);
wl_state.managed_surface_count--;
wfree(view);
@ -254,10 +287,12 @@ handle_xdg_new_surface(struct wl_listener *listener, void *data)
view->map_listener.notify = handle_toplevel_map;
view->unmap_listener.notify = handle_toplevel_unmap;
view->destroy_listener.notify = handle_toplevel_destroy;
view->title_listener.notify = handle_toplevel_title;
wl_signal_add(&xdg_surface->events.map, &view->map_listener);
wl_signal_add(&xdg_surface->events.unmap, &view->unmap_listener);
wl_signal_add(&xdg_surface->events.destroy, &view->destroy_listener);
wl_signal_add(&xdg_surface->events.map, &view->map_listener);
wl_signal_add(&xdg_surface->events.unmap, &view->unmap_listener);
wl_signal_add(&xdg_surface->events.destroy, &view->destroy_listener);
wl_signal_add(&xdg_surface->toplevel->events.set_title, &view->title_listener);
wl_list_insert(&wl_state.toplevel_list, &view->link);
wl_state.managed_surface_count++;
@ -864,6 +899,15 @@ wl_client_synth_configure(WNativeWindow client_win,
static void wl_client_add_to_save_set(WNativeWindow win) { (void)win; }
static void wl_client_remove_from_save_set(WNativeWindow win) { (void)win; }
static void
wl_client_close(WNativeWindow win)
{
/* Phase 46: ask the XDG toplevel to close itself. */
struct wl_toplevel_view *v = wl_find_view_by_id(win);
if (v && v->xdg_surface)
wlr_xdg_toplevel_send_close(v->xdg_surface);
}
static void wl_client_query_shaped(WNativeWindow win, int *shaped_out)
{ (void)win; *shaped_out = 0; }
@ -1101,6 +1145,7 @@ const WMBackend wl_backend = {
.client_synth_configure = wl_client_synth_configure,
.client_add_to_save_set = wl_client_add_to_save_set,
.client_remove_from_save_set = wl_client_remove_from_save_set,
.client_close = wl_client_close,
.client_query_shaped = wl_client_query_shaped,
.client_get_gnustep_attrs = wl_client_get_gnustep_attrs,
.client_get_pid = wl_client_get_pid,

View File

@ -963,6 +963,13 @@ x11_client_remove_from_save_set(WNativeWindow win)
XRemoveFromSaveSet(dpy, WNW_TO_XID(win));
}
static void
x11_client_close(WNativeWindow win)
{
XKillClient(dpy, WNW_TO_XID(win));
XFlush(dpy);
}
/* -------------------------------------------------------------------- */
/* Fake group-leader window (Phase 13) */
/* -------------------------------------------------------------------- */
@ -2458,6 +2465,7 @@ const WMBackend x11_backend = {
.client_reparent = x11_client_reparent,
.client_add_to_save_set = x11_client_add_to_save_set,
.client_remove_from_save_set = x11_client_remove_from_save_set,
.client_close = x11_client_close,
.fake_leader_create = x11_fake_leader_create,
.fake_leader_destroy = x11_fake_leader_destroy,
.icccm_set_class_hint = x11_icccm_set_class_hint,

View File

@ -250,6 +250,9 @@ void wClientSendProtocol(WWindow * wwin, Atom protocol, Time time)
{
XEvent event;
if (!dpy)
return; /* Wayland: no X11 ClientMessage */
event.xclient.type = ClientMessage;
event.xclient.message_type = w_global.atom.wm.protocols;
event.xclient.format = 32;
@ -265,6 +268,11 @@ void wClientSendProtocol(WWindow * wwin, Atom protocol, Time time)
void wClientKill(WWindow * wwin)
{
if (!dpy) {
/* Wayland: request close via vtable */
wm_backend->client_close(wwin->client_win);
return;
}
XKillClient(dpy, wwin->client_win);
XFlush(dpy);