feat: show selected window title as header row in preview overlay

Preview menus open with a full-width header naming the active tab's current window (icon plus ellipsized title). Full width keeps menu sizing independent of the selection, so moving the highlight never resizes anything; thumbnail fill-share accounts for the header block within the existing margins. Taps on the header are swallowed.
This commit is contained in:
Andrew Hamilton 2026-09-29 07:12:04 +10:00
parent d47cb4ad03
commit 28d5df8d52
3 changed files with 152 additions and 14 deletions

View file

@ -262,7 +262,9 @@ Pass `--overlay-previews` to list window thumbnails instead of titles:
each row is the window's icon followed by a small view of the window as
it last appeared (no captions). Rows divide the content-sized menu, which
stays centered with a 50 pixel top and bottom margin; no thumbnail grows
past a quarter of the screen height. The
past a quarter of the screen height. A header row at the top of the menu
names the selected window (icon plus ellipsized title) without affecting
the menu size. The
shown window keeps its thumbnail live -- it commits continuously, so the
preview tracks it. Hidden windows freeze with no frame callbacks while
another tab is shown, so their thumbnails sit at the last-shown frame

87
kick.c
View file

@ -2673,8 +2673,9 @@ static void overlay_pass_icon_tex(struct wlr_render_pass *pass,
/* --overlay-previews geometry: thumbnail rows divide the menu, which is
content-sized and centered with a 50px top/bottom margin. Each row would
fill an equal share of the margined height, but no thumbnail grows past
a quarter of the screen height; the menu then wraps the capped rows.
fill an equal share of the margined height less the selected-window
header block, but no thumbnail grows past a quarter of the screen
height; the menu then wraps the header plus the capped rows.
Thumbnails keep a fixed 16:9 size; the preview row pads them like a text
row pads its font (+8). Pending (windowless) rows share the uniform row
height. */
@ -2689,10 +2690,12 @@ static int overlay_title_row_h(void) {
A slightly stale snapshot only changes the effective downscale filtering
at draw time, which stretches any snapshot to the live thumb box. */
static int preview_thumb_w_cached, preview_thumb_h_cached;
static int overlay_preview_head_h(void);
static void overlay_preview_metrics(int n_rows, int *row_h, int *thumb_w,
int *thumb_h) {
int boxpad = 6, row_gap = 8;
int avail_h = host.height - 2 * OVERLAY_MENU_MARGIN_V;
int avail_h = host.height - 2 * OVERLAY_MENU_MARGIN_V -
overlay_preview_head_h();
int fill = 0;
if (n_rows > 0)
fill = (avail_h - 2 * boxpad - (n_rows - 1) * row_gap) / n_rows - 8;
@ -3317,7 +3320,8 @@ static int overlay_tab_box(struct overlay_box *box) {
if (max_row_w > avail_w) max_row_w = avail_w;
int bw = max_row_w + 2 * boxpad;
int bh = n_rows * row_h + (n_rows - 1) * row_gap + 2 * boxpad;
int bh = overlay_preview_head_h() + n_rows * row_h +
(n_rows - 1) * row_gap + 2 * boxpad;
if (bw > host.width - 2 * margin) bw = host.width - 2 * margin;
/* Preview menus are content-sized by construction; the margin clamp is
only a safety net for degenerate (tiny) screens. */
@ -3402,8 +3406,62 @@ static void draw_confirm_quit_pass(struct wlr_render_pass *pass,
}
}
/* Selected-window header (preview mode): the active tab's current window
-- what the highlight sits on -- as a full-width header row at the top
of the menu: icon left-anchored, title centered in the remaining width
and ellipsized to it. Full width keeps the menu size independent of
the selection, so moving the highlight never resizes anything. Reuses
the row machinery: tab_row_title for the string (with its pending-tab
command fallback), the icon loader, and a budgeted text tile. */
static struct kick_window *overlay_selected_window(void) {
if (!server.active_tab) return NULL;
return tab_current_window(server.active_tab);
}
/* Vertical block the header reserves above the thumbnail rows: the title
row plus one standard row gap. Zero outside preview mode. */
static int overlay_preview_head_h(void) {
if (!opt_overlay_previews) return 0;
return overlay_title_row_h() + 8; /* row_gap */
}
static void overlay_pass_selected_header(struct wlr_render_pass *pass,
int box_x, int box_y, int boxpad,
int row_inner) {
struct kick_window *w = overlay_selected_window();
if (!w) return;
const char *t = tab_row_title(server.active_tab);
if (!t) t = "";
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8;
int row_h = overlay_title_row_h();
int hy = box_y + boxpad;
overlay_pass_rect(pass, box_x + boxpad - 2, hy - 1, row_inner + 4,
row_h, 0.22f, 0.22f, 0.25f, 1.0f);
if (overlay_window_icon_needs_load(w))
overlay_icon_load_window(w);
int ix = box_x + boxpad, iy = hy + (row_h - icon_sz) / 2;
if (w->icon_wlr) {
overlay_pass_icon_tex(pass, w->icon_wlr, ix, iy, icon_sz, 1.0f);
} else {
overlay_pass_rect(pass, ix, iy, icon_sz, icon_sz,
0.45f, 0.45f, 0.48f, 0.85f);
overlay_pass_rect(pass, ix + 3, iy + 3, icon_sz - 6,
icon_sz - 6, 0.07f, 0.07f, 0.09f, 1.0f);
}
int rx = ix + icon_sz + icon_gap;
int rw = row_inner - (icon_sz + icon_gap);
if (rw < 0) rw = 0;
struct overlay_text_entry *te = overlay_text_tile(t, rw);
int tw = te ? te->w : 0;
int tx = rx + (rw - tw) / 2;
if (tx < rx) tx = rx;
overlay_pass_text(pass, t, rw, tx,
hy + (row_h - host.overlay_font_h) / 2, 1.0f);
}
static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
int origin_x, int origin_y) {
int origin_x, int origin_y) {
if (!host.overlay_visible && !confirm_quit_pending) return;
if (confirm_quit_pending) {
draw_confirm_quit_pass(pass, origin_x, origin_y);
@ -3441,13 +3499,19 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
overlay_pass_rect(pass, box_x + box_w - bw, box_y, bw, box_h,
0.45f, 0.45f, 0.48f, 1.0f);
/* Selected-window header first (preview mode); thumbnail rows follow
below it with the standard gap. */
int head_h = overlay_preview_head_h();
int row_inner = box_w - 2 * boxpad;
if (opt_overlay_previews && head_h > 0)
overlay_pass_selected_header(pass, box_x, box_y, boxpad, row_inner);
int row = 0;
wl_list_for_each(tab, &server.tabs, link) {
if (!tab_overlay_row(tab)) continue;
(void)tab_row_title(tab);
int ry = box_y + boxpad + row * (row_h + row_gap);
int ry = box_y + boxpad + head_h + row * (row_h + row_gap);
bool active = tab == server.active_tab;
int row_inner = box_w - 2 * boxpad;
struct kick_window *wins[32];
char tbuf[32][256];
@ -3577,10 +3641,17 @@ static bool overlay_hit_test(double ox, double oy,
}
struct kick_tab *tab;
/* The selected-window header is display-only: taps on it change
nothing. */
int head_h = overlay_preview_head_h();
if (head_h > 0 && oy >= box_y + boxpad &&
oy < box_y + boxpad + head_h)
return false;
int row = 0;
wl_list_for_each(tab, &server.tabs, link) {
if (!tab_overlay_row(tab)) continue;
int ry = box_y + boxpad + row * (row_h + row_gap);
int ry = box_y + boxpad + head_h + row * (row_h + row_gap);
int row_inner = box_w - 2 * boxpad;
struct kick_window *wins[32];

View file

@ -6683,8 +6683,9 @@ def _backdrop_band(s, path):
def test_standalone_headless_overlay_previews_menu_height():
"""Preview menu is content-sized and centered: on the 800px headless
screen two rows cap at 200px thumbs (208px rows), so the menu is
2*208+8+2*6 = 436px tall starting at (800-436)/2 = 182."""
screen two rows cap at 200px thumbs (208px rows) below a 44px title
header + 8px gap, so the menu is 44+8+2*208+8+2*6 = 488px tall
starting at (800-488)/2 = 156."""
with _run_standalone(
{}, "--overlay-previews",
"%s '#ff0000'" % WL, "%s '#0000ff'" % WL,
@ -6698,9 +6699,73 @@ def test_standalone_headless_overlay_previews_menu_height():
finally:
s.mods(0)
assert first is not None, "no menu backdrop visible while Alt held"
assert abs(first - 182) <= 3, f"menu top {first}, want 182"
assert abs((last - first + 1) - 436) <= 3, \
f"menu height {last - first + 1}, want 436"
assert abs(first - 156) <= 3, f"menu top {first}, want 156"
assert abs((last - first + 1) - 488) <= 3, \
f"menu height {last - first + 1}, want 488"
_assert_alive(s)
def test_standalone_headless_overlay_previews_selected_title():
"""Preview menu opens with a full-width header naming the selected
window (icon + title), followed by the thumbnail rows: one contiguous
488px band at y=156 on the 800px screen. Dumping with the selection
on each of two very differently titled tabs gives identical menu
extents (no width jumping)."""
with _run_standalone(
{}, "--overlay-previews",
"%s --title R '#ff0000'" % WL,
"%s --title A_much_longer_blue_window_title '#0000ff'" % WL,
wait="any_frame") as s:
s.wait_surface_mapped(0)
s.wait_surface_mapped(1)
def is_backdrop(r, g, b):
return abs(r - 18) <= 6 and abs(g - 18) <= 6 and abs(b - 23) <= 6
def is_bright(r, g, b):
return r > 150 and g > 150 and b > 150
extents = []
for _ in range(2):
s.mods(ALT)
try:
time.sleep(1.0)
w, h, px = s.ppm_rgb(s.dump_host())
finally:
s.mods(0)
rows = [y for y in range(h)
if any(is_backdrop(px[(y * w + i) * 3],
px[(y * w + i) * 3 + 1],
px[(y * w + i) * 3 + 2])
for i in range(w))]
assert rows, "no menu backdrop visible while Alt held"
# One contiguous band: header lives inside the menu now.
assert rows == list(range(rows[0], rows[-1] + 1)), \
"menu backdrop not contiguous"
xs = [i for y in rows for i in range(w)
if is_backdrop(px[(y * w + i) * 3],
px[(y * w + i) * 3 + 1],
px[(y * w + i) * 3 + 2])]
extents.append((rows[0], rows[-1], min(xs), max(xs)))
# Bright title-text pixels sit in the header at the top of
# the menu (dumps are vertically flipped, so flip back).
hdr = [h - 1 - y for y in rows
if any(is_bright(px[(y * w + i) * 3],
px[(y * w + i) * 3 + 1],
px[(y * w + i) * 3 + 2])
for i in range(w))]
assert hdr, "no title text pixels in the menu"
top = h - 1 - rows[-1]
assert all(top <= y <= top + 60 for y in hdr), \
f"title text outside header: {hdr[:5]}"
s.cycle_ack(-1)
s.wait_active_frame()
(f0, l0, x0, x1), (f1, l1, x2, x3) = extents
assert abs((h - 1 - l0) - 156) <= 3, f"menu top {h - 1 - l0}, want 156"
assert abs((l0 - f0 + 1) - 488) <= 3, \
f"menu height {l0 - f0 + 1}, want 488"
assert (f0, l0, x0, x1) == (f1, l1, x2, x3), \
"menu extents moved with the selection"
_assert_alive(s)