make the tab's main surface an ordinary member of its window stack
This commit is contained in:
parent
753b37ee7e
commit
aab41f286e
1 changed files with 161 additions and 119 deletions
280
kick.c
280
kick.c
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue