make the tab's main surface an ordinary member of its window stack

This commit is contained in:
Andrew Hamilton 2026-08-22 10:47:47 +10:00
parent 753b37ee7e
commit aab41f286e

280
kick.c
View file

@ -214,18 +214,18 @@ struct kick_app {
bool closing;
struct wl_event_source *close_timer;
/* Stacked sub-windows (--sub-windows stack): same-process toplevels
rendered above this tab's surface instead of getting their own slot.
The list is ordered oldest first, so the tail is the topmost; newest
arrivals go to the tail. focus_sub is the entry that currently holds
the keyboard while this tab is active (NULL when the main surface
does). */
/* The tab's window stack: the wrapped main surface first (see
stack_wrap_main), then any stacked sub-windows (--sub-windows
stack) oldest first, newest arrivals at the tail. focus_sub is
the member that currently holds the keyboard while this tab is
active -- the rotation's cursor; it always points at a member
once the tab has one to show. */
struct wl_list sub_windows;
struct kick_sub_window *focus_sub;
/* Last overlay row title / icon app_id drawn for this tab. The row
shows whatever sits on top of the tab (a stacked window can swap it),
so these caches let the overlay log one line per actual change and
shows whatever member the tab is currently displaying, so these
caches let the overlay log one line per actual change and
re-resolve the icon when the effective app_id moves underneath. */
char row_title[128];
char icon_app_id[128];
@ -237,6 +237,12 @@ struct kick_app {
hides with the tab; show_current() keeps only the current window's node
enabled, so the sub paints alone while it is on top. */
struct kick_sub_window {
/* True for the wrapper that represents a tab's own main surface in
its window stack: every rotation/visibility/title path then walks
one plain list of windows with no privileged entry. The wrapper
carries no listeners -- the tab-level map/commit/destroy handlers
stay authoritative -- and its liveness mirrors app_has_window(). */
bool is_main;
struct wlr_xdg_toplevel *toplevel;
#if HAVE_XWAYLAND
bool is_x11;
@ -1835,16 +1841,20 @@ static void fit_text(const char *src, int max_px, char *out, int out_sz)
out[n] = '\0';
}
/* Row title for an overlay slot: whatever sits on top of the tab -- a mapped
stacked sub-window (--sub-windows stack), else the tab's own toplevel --
or the command line for a still-black (pending) tab. Logs one INFO line
whenever a row's resolved title changes, so a stacked window swapping the
title underneath an unchanged tab is observable in logs and tests. */
/* Row title for an overlay slot: the window the tab is actually showing --
on the active tab that is exactly the focused stack member (a stacked
sub-window or the wrapped main surface); a background tab previews its
topmost mapped member, what switching over would show -- or the command
line for a still-black (pending) tab. Logs one INFO line whenever a
row's resolved title changes, so a stacked window swapping the title
underneath an unchanged tab is observable in logs and tests. */
static const char *app_row_title(struct kick_app *app) {
const char *t = NULL;
struct kick_sub_window *top = top_mapped_sub(app);
const char *st = top ? sub_title(top) : NULL;
if (top && st && st[0])
struct kick_sub_window *pick =
(app == server.active_app && app->focus_sub)
? app->focus_sub : top_mapped_sub(app);
const char *st = pick ? sub_title(pick) : NULL;
if (pick && st && st[0])
t = st;
else {
const char *mt = app_title(app);
@ -1862,12 +1872,14 @@ static const char *app_row_title(struct kick_app *app) {
return t;
}
/* Effective overlay app_id for a tab: the topmost mapped stacked sub-window
wins over the tab's own toplevel, mirroring app_row_title. */
/* Effective overlay app_id for a tab: same member pick as app_row_title,
mirroring it so icons track the shown window. */
static const char *app_row_app_id(struct kick_app *app) {
struct kick_sub_window *top = top_mapped_sub(app);
const char *sid = top ? sub_app_id(top) : NULL;
if (top && sid && sid[0])
struct kick_sub_window *pick =
(app == server.active_app && app->focus_sub)
? app->focus_sub : top_mapped_sub(app);
const char *sid = pick ? sub_app_id(pick) : NULL;
if (pick && sid && sid[0])
return sid;
const char *mid = app_app_id(app);
if (mid && mid[0])
@ -2427,15 +2439,20 @@ static void remove_app(int idx) {
wlr_texture_destroy(dead->icon_wlr);
dead->icon_wlr = NULL;
}
/* Stacked sub-windows (--sub-windows stack): same-process toplevels
rendered above this tab's surface instead of getting their own slot.
The list is ordered oldest first, so the tail is the topmost; newest
arrivals go to the tail. focus_sub is the entry that currently holds
the keyboard while this tab is active (NULL when the main surface
does). The X11 branch of remove_app() mirrors this cleanup. */
/* The window stack: the wrapped main surface plus stacked sub-windows
(--sub-windows stack), oldest first, newest arrivals at the tail.
Members with listeners get them removed here; the listener-free
main wrapper just drops out of the list. */
while (!wl_list_empty(&dead->sub_windows)) {
struct kick_sub_window *sub =
wl_container_of(dead->sub_windows.next, sub, link);
if (sub->is_main) {
/* The wrapped main surface carries no listeners; its scene
nodes died with the toplevel above. */
wl_list_remove(&sub->link);
free(sub);
continue;
}
#if HAVE_XWAYLAND
if (sub->is_x11) {
wl_list_remove(&sub->destroy.link);
@ -2838,29 +2855,35 @@ static struct kick_app *sub_window_owner(pid_t cpid) {
/* ===================== Stacked sub-windows (--sub-windows stack) ===== */
/* The topmost mapped stacked sub-window of a tab, or NULL. */
/* Whether a stack member currently has a live, mapped surface: stacked
sub-windows track it through their map/unmap listeners; the wrapped
main surface exists exactly as long as its surface does. */
static bool stack_mapped(struct kick_sub_window *m) {
return m->is_main ? app_has_window(m->owner) : m->mapped;
}
/* The topmost mapped member of the tab's window stack -- newest stacked
sub-window if any, else the wrapped main surface, else NULL. */
static struct kick_sub_window *top_mapped_sub(struct kick_app *app) {
struct kick_sub_window *sub;
wl_list_for_each_reverse(sub, &app->sub_windows, link)
if (sub->mapped)
if (stack_mapped(sub))
return sub;
return NULL;
}
/* Show exactly one window of the tab: the focused stacked sub-window while
the tab is active and holds one, else the tab's main surface. Only the
shown window's scene node stays enabled -- hidden layers are neither
drawn nor hit-tested, and a sub's popups (parented on its own tree) hide
with it. */
/* Show exactly one window of the tab: the focused stack member (the
wrapped main surface and stacked sub-windows are all plain members, so
no kind gets special treatment). Only the shown window's scene node
stays enabled -- hidden layers are neither drawn nor hit-tested, and a
member's popups (parented on its own tree) hide with it. */
static void show_current(struct kick_app *app) {
if (!app || !app->stack) return;
struct kick_sub_window *cur =
(server.active_app == app) ? app->focus_sub : NULL;
struct kick_sub_window *sub;
wl_list_for_each(sub, &app->sub_windows, link)
wlr_scene_node_set_enabled(&sub->tree->node, sub == cur);
if (app->tree)
wlr_scene_node_set_enabled(&app->tree->node, cur == NULL);
struct kick_sub_window *m;
wl_list_for_each(m, &app->sub_windows, link)
wlr_scene_node_set_enabled(&m->tree->node, m == cur);
}
/* Give the keyboard to a stacked sub-window of the active tab, demoting
@ -2882,77 +2905,81 @@ static void sub_enter(struct kick_sub_window *sub) {
kick_request_redraw();
}
/* After the focused sub-window unmaps or dies, hand the keyboard back to
the owner's next topmost mapped sub-window or its main surface. No-op
unless the owner is the tab being shown. */
/* Give the keyboard to a window-stack member of the active tab -- a
stacked sub-window via sub_enter(), or the wrapped main surface, which
demotes whatever held the keyboard before and takes it itself. The
caller still calls show_current() to make the entered window the only
drawn one. */
static void stack_enter(struct kick_sub_window *m) {
struct kick_app *app = m->owner;
if (!m->is_main) {
sub_enter(m);
return;
}
if (app->focus_sub)
sub_set_activated(app->focus_sub, false);
/* The wrapped main is the cursor even when its surface is not live
yet (a still-black tab): focus_sub == the main member IS "the main
surface holds the keyboard". */
app->focus_sub = m;
if (!app_has_window(app))
return;
app_set_activated(app, true);
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
if (!kbd) kbd = &server.keyboard;
wlr_seat_keyboard_notify_enter(server.seat,
app_surface(app),
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
}
/* After the focused member unmaps or dies, hand the keyboard back to the
owner's next topmost mapped member -- newest stacked sub-window, else
the wrapped main surface. No-op unless the owner is the tab being
shown. */
static void sub_refocus_owner(struct kick_app *app) {
if (server.active_app != app) return;
struct kick_sub_window *top = top_mapped_sub(app);
if (top) {
sub_enter(top);
/* Historic log shape: "[sub] refocus" marks the keyboard landing
back on the main surface after the last stacked window went
away; member-to-member handoffs stay silent. */
if (top->is_main)
kick_log(KICK_LOG_INFO, "[sub] refocus app=%d\n", app_index(app));
stack_enter(top);
show_current(app);
kick_request_redraw();
return;
}
/* No live window anywhere in the stack: drop the keyboard entirely. */
app->focus_sub = NULL;
if (app_has_window(app)) {
kick_log(KICK_LOG_INFO, "[sub] refocus app=%d\n", app_index(app));
app_set_activated(app, true);
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
if (!kbd) kbd = &server.keyboard;
wlr_seat_keyboard_notify_enter(server.seat,
app_surface(app),
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
}
show_current(app);
kick_request_redraw();
}
/* Rotate the active tab's window stack one step in `dir` (-1 = backward):
the rotation visits every mapped stacked sub-window in creation order,
then the main surface, then wraps. Backward from a window means the
one stacked just before it; backward from the oldest is the main
surface; backward from the main surface wraps to the newest. The list
order itself is never mutated -- focus_sub alone tracks where the
rotation sits, so repeated steps walk the windows deterministically.
Returns false when the tab has no mapped sub-windows (nothing to
rotate), leaving the key free to pass through to the client. */
the plain list of members -- the wrapped main surface plus every mapped
stacked sub-window, oldest first -- steps to the neighbor of whatever
is showing, wrapping at both ends. The list itself is never mutated;
focus_sub alone tracks where the rotation sits, so repeated steps walk
the windows deterministically. Returns false when nothing is mapped
(nothing to rotate), leaving the key free to pass through to the
client. */
static bool cycle_stack(struct kick_app *app, int dir) {
struct kick_sub_window *subs[16];
struct kick_sub_window *sub;
struct kick_sub_window *members[16];
struct kick_sub_window *m;
int n = 0;
wl_list_for_each(sub, &app->sub_windows, link)
if (sub->mapped && n < 16) subs[n++] = sub;
wl_list_for_each(m, &app->sub_windows, link)
if (stack_mapped(m) && n < 16) members[n++] = m;
if (n == 0)
return false;
/* Position of the shown window in the rotation [w0..w_{n-1}, MAIN]:
-1 when the main surface is showing (or nothing is yet). */
int cur = -1;
for (int i = 0; i < n; i++)
if (subs[i] == app->focus_sub) { cur = i; break; }
int next = (cur + dir) % (n + 1);
if (next < 0) next += n + 1;
if (next == n) {
/* MAIN's turn: drop keyboard focus from the stacked sub;
show_current() below reveals the main surface and hides every
sub, making it drawn and reachable again. */
if (app->focus_sub) {
sub_set_activated(app->focus_sub, false);
app->focus_sub = NULL;
}
if (app_has_window(app)) {
app_set_activated(app, true);
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
if (!kbd) kbd = &server.keyboard;
wlr_seat_keyboard_notify_enter(server.seat,
app_surface(app),
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
}
} else {
sub_enter(subs[next]);
}
int cur = 0;
while (cur < n && members[cur] != app->focus_sub) cur++;
/* An unfocused tab position counts as the main surface (member 0),
so backward wraps to the newest window and forward to the oldest --
exactly where cycling picks up from a freshly shown tab. */
stack_enter(members[(cur + dir + n) % n]);
show_current(app);
kick_log(KICK_LOG_INFO, "[sub] cycle app=%d\n", app_index(app));
@ -3047,6 +3074,30 @@ static void handle_sub_destroy(struct wl_listener *listener, void *data) {
sub_destroy_common(sub);
}
/* Wrap the tab's own main surface as an ordinary member of its window
stack ([main, oldest..newest stacked subs]): rotation, visibility and
overlay titles then read one plain list with no privileged entry. The
wrapper carries no listeners -- the tab-level map/commit/destroy
handlers stay authoritative -- and remove_app() frees it with the rest. */
static void stack_wrap_main(struct kick_app *app) {
struct kick_sub_window *m = calloc(1, sizeof(*m));
if (!m || !app->tree) {
free(m);
return;
}
m->is_main = true;
m->owner = app;
m->tree = app->tree;
#if HAVE_XWAYLAND
if (app->is_x11) {
m->is_x11 = true;
m->xsurface = app->xsurface;
} else
#endif
m->toplevel = app->toplevel;
wl_list_insert(app->sub_windows.prev, &m->link);
}
/* Attach `toplevel` as a stacked sub-window of `owner`: sibling of the
owner's main surface under its stack container (shows/hides with the
tab; show_current() keeps only the current window visible), same
@ -3209,6 +3260,7 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
/* hidden until it becomes the active tab */
if (app->stack)
wlr_scene_node_set_enabled(&app->stack->node, false);
stack_wrap_main(app);
/* [claim] ties the app slot to the *connected client's* PID, so tests
can kill the tab that is actually active. The slot was chosen above by
@ -3766,6 +3818,7 @@ static void handle_xwayland_new_surface(struct wl_listener *listener,
}
/* hidden until it becomes the active tab */
wlr_scene_node_set_enabled(&app->stack->node, false);
stack_wrap_main(app);
app->client_pid = xpid;
pid_t cpg = xpid > 0 ? getpgid(xpid) : -1;
@ -3939,19 +3992,17 @@ static void create_popup(struct wlr_xdg_popup *popup,
xdg_surface->data = tp;
}
/* The scene tree a toplevel's popups must hang off: a stacked sub-window's
popups ride on the sub's own tree (so they show only while the sub is
the current window and position within it), everything else on the
tab's main tree. */
/* The scene tree a toplevel's popups must hang off: each stack member's
popups ride on that member's own tree (so they show only while it is
the current window and position within it) -- the wrapped main surface
is just the first member. */
static struct wlr_scene_tree *popup_tree_for_toplevel(
struct kick_app *app, struct wlr_xdg_toplevel *toplevel) {
if (!app) return NULL;
if (toplevel != app->toplevel) {
struct kick_sub_window *sub;
wl_list_for_each(sub, &app->sub_windows, link)
if (sub->toplevel == toplevel)
return sub->tree;
}
if (!app || !toplevel) return NULL;
struct kick_sub_window *m;
wl_list_for_each(m, &app->sub_windows, link)
if (m->toplevel && m->toplevel == toplevel)
return m->tree;
return app->tree;
}
@ -4059,24 +4110,15 @@ static void set_active_app(struct kick_app *app) {
the standalone handlers); fall back to the virtual keyboard otherwise
so the nested/ctl path keeps using it. A black active tab has no
surface to enter, so clear the focus instead of leaving a stale one.
A tab with mapped stacked sub-windows hands the keyboard to its
topmost one instead of its main surface. */
The tab's window stack hands the keyboard to its topmost mapped
member -- newest stacked sub-window, else the wrapped main surface. */
struct kick_sub_window *top = top_mapped_sub(app);
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
if (!kbd) kbd = &server.keyboard;
if (top) {
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", app_index(app));
app_set_activated(app, false);
sub_set_activated(top, true);
wlr_seat_keyboard_notify_enter(server.seat,
sub_surface(top),
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
app->focus_sub = top;
} else if (app_has_window(app)) {
wlr_seat_keyboard_notify_enter(server.seat,
app_surface(app),
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
app->focus_sub = NULL;
/* Only genuine sub-window entries announce themselves; entering
the wrapped main surface is every plain tab's quiet default. */
if (!top->is_main)
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", app_index(app));
stack_enter(top);
} else {
app->focus_sub = NULL;
wlr_seat_keyboard_notify_clear_focus(server.seat);