feat: add --overlay-previews live window thumbnails
Adds a --overlay-previews flag that switches the hold-Alt tab overlay from icon+title rows to icon plus a small live view of each window (three times as tall as a title row, no captions). Tabs with no mapped window keep their command-line text row. Thumbnails stay live while the overlay is held: hidden windows get no scene frame callbacks, so a ~15fps tick paces them with frame-done (disarmed on release; the plain title list never wakes background clients). Each commit is routed to its fresh source -- a CPU snapshot for SHM attaches, a locked dma-buf import for GPU clients, or the surface's own texture for in-place SHM updates -- and hidden commits force a repaint. Also adds a wltest --pulse MS mode (alternating color/black on frame callbacks) as a deterministic live source, three smoke tests (appear on open, map mid-hold, pulse flip), and README docs.
This commit is contained in:
parent
2b8560dd7c
commit
a9e5f3dcad
4 changed files with 522 additions and 8 deletions
12
README.md
12
README.md
|
|
@ -258,6 +258,18 @@ highlighted like a cursor: `Alt+Up`/`Alt+Down` switch tabs and
|
|||
`Alt+Left`/`Alt+Right` move between windows within the active tab, so the
|
||||
highlight walks through every window in the session.
|
||||
|
||||
Pass `--overlay-previews` to list live thumbnails instead of titles: each
|
||||
row is the window's icon followed by a small live view of the window itself
|
||||
(three times as tall as a title row, no captions). The preview tracks the
|
||||
window's commits while the overlay is held, so a window that repaints
|
||||
mid-hold visibly updates; a tab with no mapped window keeps its
|
||||
command-line text row. Both software (SHM) and GPU (DMA-BUF) clients are
|
||||
covered. Hidden windows get no frame callbacks from the scene while
|
||||
another tab is shown, so while the preview overlay is up kick paces them
|
||||
with frame-done at ~15fps instead -- just enough to keep the thumbnails
|
||||
live, frozen again on release. The plain title list never wakes background
|
||||
clients this way.
|
||||
|
||||
`Alt+Tab` (and its synonym `Alt+1`, the chord that survives hosts like
|
||||
GNOME which grab `Alt+Tab`, `Alt+Esc` *and* `Alt+Backtick` -- the last from
|
||||
the metacity-era `switch_group` default) walk the session's
|
||||
|
|
|
|||
321
kick.c
321
kick.c
|
|
@ -316,6 +316,19 @@ struct kick_window {
|
|||
desktop-file guesser (or placeholder) is in charge. */
|
||||
struct wlr_xdg_toplevel_icon_v1 *proto_icon;
|
||||
bool icon_protocol;
|
||||
/* Live overlay-preview thumbnail (--overlay-previews): the window's
|
||||
latest frame, routed per commit to whichever source is fresh --
|
||||
a CPU snapshot (fresh SHM attaches), a locked dma-buf (GPU
|
||||
attaches), or the surface's own texture (in-place SHM updates,
|
||||
where current.buffer is already NULL at commit time). Released
|
||||
with the window. */
|
||||
struct wlr_texture *thumb_tex;
|
||||
struct wlr_buffer *thumb_buf;
|
||||
uint32_t *thumb_px;
|
||||
int thumb_pw, thumb_ph;
|
||||
bool thumb_dirty;
|
||||
enum { THUMB_SRC_NONE, THUMB_SRC_SNAP, THUMB_SRC_DMABUF,
|
||||
THUMB_SRC_LIVE } thumb_src;
|
||||
/* Client-supplied tag + description (xdg-toplevel-tag-v1): e.g. "main
|
||||
window" plus its translation. Shown in the overlay row ("title
|
||||
[tag]") and used for --tab-groups grouping; NULL until the client
|
||||
|
|
@ -648,6 +661,11 @@ struct kick_server {
|
|||
navigated session commits the then-current window to the MRU front. */
|
||||
struct kick_window *mru_alt_anchor;
|
||||
bool mru_alt_navigated;
|
||||
/* --overlay-previews live tick: while the preview overlay is held,
|
||||
hidden windows get no scene frame callbacks and would freeze, so a
|
||||
timer paces them with frame-done at preview rate instead. Armed in
|
||||
update_overlay_visible, disarmed on release. */
|
||||
struct wl_event_source *preview_timer;
|
||||
|
||||
/* wlr-layer-shell (waybar / panels). Layer surfaces render into their
|
||||
own always-on-top scene tree (layer_tree) instead of being tabs, so a
|
||||
|
|
@ -894,6 +912,13 @@ static bool tab_groups = true;
|
|||
competes. */
|
||||
static bool opt_focus_priority = false;
|
||||
|
||||
/* --overlay-previews: show live window thumbnails in the tab overlay instead
|
||||
of icon+title rows. Each row is the window's icon followed by a small live
|
||||
view of the window itself (aspect-fit into a fixed box three times as tall
|
||||
as a title row); tabs with no mapped window keep their command-line text
|
||||
row. Off by default. */
|
||||
static bool opt_overlay_previews = false;
|
||||
|
||||
/* ===================== Clipboard bridge ===================== */
|
||||
|
||||
enum bridge_backend {
|
||||
|
|
@ -2629,6 +2654,183 @@ static void overlay_pass_icon_tex(struct wlr_render_pass *pass,
|
|||
wlr_render_pass_add_texture(pass, &o);
|
||||
}
|
||||
|
||||
/* --overlay-previews geometry: thumbnail boxes are three times as tall as a
|
||||
title row, at a fixed 16:9 size; the preview row pads them like a text row
|
||||
pads its font (+8). Pending (windowless) rows stay short in both modes. */
|
||||
#define OVERLAY_PREVIEW_TALL 3
|
||||
static int overlay_title_row_h(void) {
|
||||
int rh = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||
if (rh < OVERLAY_ICON_SZ + 4) rh = OVERLAY_ICON_SZ + 4;
|
||||
return rh;
|
||||
}
|
||||
static int overlay_preview_thumb_h(void) {
|
||||
return overlay_title_row_h() * OVERLAY_PREVIEW_TALL;
|
||||
}
|
||||
static int overlay_preview_thumb_w(void) {
|
||||
return overlay_preview_thumb_h() * 16 / 9;
|
||||
}
|
||||
static int overlay_preview_row_h(void) {
|
||||
return overlay_preview_thumb_h() + 8;
|
||||
}
|
||||
|
||||
/* Live overlay-preview thumbnail (--overlay-previews). Capture runs in the
|
||||
commit handler and routes each commit to its fresh source: a fresh attach
|
||||
snapshots (SHM) or locks (dma-buf) the committed buffer; an in-place SHM
|
||||
update (current.buffer already NULL) leaves the pixels in the surface's
|
||||
own texture, which wlroots refreshed from the dropped buffer. Draw
|
||||
serves whichever source the latest commit took. */
|
||||
static void thumb_release(struct kick_window *w) {
|
||||
if (!w) return;
|
||||
if (w->thumb_tex) {
|
||||
wlr_texture_destroy(w->thumb_tex);
|
||||
w->thumb_tex = NULL;
|
||||
}
|
||||
if (w->thumb_buf) {
|
||||
wlr_buffer_unlock(w->thumb_buf);
|
||||
w->thumb_buf = NULL;
|
||||
}
|
||||
free(w->thumb_px);
|
||||
w->thumb_px = NULL;
|
||||
w->thumb_pw = w->thumb_ph = 0;
|
||||
w->thumb_dirty = false;
|
||||
w->thumb_src = THUMB_SRC_NONE;
|
||||
}
|
||||
|
||||
static void thumb_capture(struct kick_window *w) {
|
||||
if (!w || !opt_overlay_previews) return;
|
||||
struct wlr_surface *s = window_surface(w);
|
||||
struct wlr_buffer *b = (s && s->current.buffer) ? s->current.buffer : NULL;
|
||||
if (b && b->width > 0 && b->height > 0) {
|
||||
void *data = NULL;
|
||||
uint32_t fmt = 0;
|
||||
size_t stride = 0;
|
||||
bool begun = wlr_buffer_begin_data_ptr_access(
|
||||
b, WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &fmt, &stride);
|
||||
bool cpu = begun &&
|
||||
(fmt == DRM_FORMAT_ARGB8888 || fmt == DRM_FORMAT_XRGB8888);
|
||||
if (cpu) {
|
||||
int bw = b->width, bh = b->height;
|
||||
int tw = overlay_preview_thumb_w(), th = overlay_preview_thumb_h();
|
||||
if (!w->thumb_px || w->thumb_pw != tw || w->thumb_ph != th) {
|
||||
free(w->thumb_px);
|
||||
w->thumb_px = malloc((size_t)tw * th * 4);
|
||||
w->thumb_pw = tw;
|
||||
w->thumb_ph = th;
|
||||
}
|
||||
if (w->thumb_px) {
|
||||
for (int y = 0; y < th; y++) {
|
||||
const uint32_t *row = (const uint32_t *)
|
||||
((const char *)data + (size_t)y * bh / th * stride);
|
||||
for (int x = 0; x < tw; x++) {
|
||||
uint32_t p = row[(size_t)x * bw / tw];
|
||||
unsigned a = (p >> 24) & 0xff;
|
||||
if (fmt == DRM_FORMAT_XRGB8888 || a == 0)
|
||||
a = 0xff;
|
||||
unsigned r = (p >> 16) & 0xff, g = (p >> 8) & 0xff,
|
||||
bl = p & 0xff;
|
||||
r = r * a / 255;
|
||||
g = g * a / 255;
|
||||
bl = bl * a / 255;
|
||||
w->thumb_px[(size_t)y * tw + x] =
|
||||
(a << 24) | (r << 16) | (g << 8) | bl;
|
||||
}
|
||||
}
|
||||
w->thumb_src = THUMB_SRC_SNAP;
|
||||
w->thumb_dirty = true;
|
||||
}
|
||||
wlr_buffer_end_data_ptr_access(b);
|
||||
if (w->thumb_buf) {
|
||||
/* Source switched back to SHM: drop the GPU buffer lock. */
|
||||
wlr_buffer_unlock(w->thumb_buf);
|
||||
w->thumb_buf = NULL;
|
||||
}
|
||||
} else {
|
||||
/* Not CPU-readable (dma-buf): hold a lock and import at draw. */
|
||||
if (begun)
|
||||
wlr_buffer_end_data_ptr_access(b);
|
||||
if (b != w->thumb_buf) {
|
||||
if (w->thumb_buf)
|
||||
wlr_buffer_unlock(w->thumb_buf);
|
||||
w->thumb_buf = wlr_buffer_lock(b);
|
||||
}
|
||||
w->thumb_src = THUMB_SRC_DMABUF;
|
||||
w->thumb_dirty = true;
|
||||
}
|
||||
} else if (s && wlr_surface_get_texture(s)) {
|
||||
/* In-place SHM update: the pixels went straight into the surface
|
||||
texture (current.buffer was already consumed), which stays live
|
||||
through every applied commit. */
|
||||
if (w->thumb_buf) {
|
||||
wlr_buffer_unlock(w->thumb_buf);
|
||||
w->thumb_buf = NULL;
|
||||
}
|
||||
w->thumb_src = THUMB_SRC_LIVE;
|
||||
w->thumb_dirty = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
/* A hidden window's commit damages no output, so repaint explicitly for
|
||||
the preview to track the window live. */
|
||||
if (host.overlay_visible)
|
||||
kick_request_redraw();
|
||||
}
|
||||
|
||||
static struct wlr_texture *overlay_thumb_tex(struct kick_window *w) {
|
||||
if (!w || !server.renderer) return NULL;
|
||||
if (w->thumb_src == THUMB_SRC_LIVE) {
|
||||
struct wlr_surface *s = window_surface(w);
|
||||
struct wlr_texture *live = s ? wlr_surface_get_texture(s) : NULL;
|
||||
if (live) return live;
|
||||
}
|
||||
if (w->thumb_tex && !w->thumb_dirty)
|
||||
return w->thumb_tex;
|
||||
struct wlr_texture *tex = NULL;
|
||||
if (w->thumb_src == THUMB_SRC_DMABUF && w->thumb_buf) {
|
||||
tex = wlr_texture_from_buffer(server.renderer, w->thumb_buf);
|
||||
} else if (w->thumb_src == THUMB_SRC_SNAP && w->thumb_px) {
|
||||
tex = wlr_texture_from_pixels(
|
||||
server.renderer, DRM_FORMAT_ARGB8888,
|
||||
(uint32_t)w->thumb_pw * 4,
|
||||
(uint32_t)w->thumb_pw, (uint32_t)w->thumb_ph, w->thumb_px);
|
||||
} else {
|
||||
return w->thumb_tex;
|
||||
}
|
||||
if (!tex) return w->thumb_tex; /* keep the previous frame; retry later */
|
||||
if (w->thumb_tex)
|
||||
wlr_texture_destroy(w->thumb_tex);
|
||||
w->thumb_tex = tex;
|
||||
w->thumb_dirty = false;
|
||||
return w->thumb_tex;
|
||||
}
|
||||
|
||||
/* Icon + aspect-fit live thumbnail. The thumb box is letterboxed (black)
|
||||
and the window's current frame is centered inside, downscaled with
|
||||
bilinear filtering; a window with no frame yet keeps the letterbox. */
|
||||
static void overlay_pass_thumb(struct wlr_render_pass *pass,
|
||||
struct kick_window *w,
|
||||
int tx, int ty, int tw, int th, float alpha) {
|
||||
overlay_pass_rect(pass, tx, ty, tw, th, 0.0f, 0.0f, 0.0f, 1.0f);
|
||||
struct wlr_texture *tex = overlay_thumb_tex(w);
|
||||
if (!tex || tex->width == 0 || tex->height == 0) return;
|
||||
int dw = tw, dh = (int)((int64_t)tw * tex->height / tex->width);
|
||||
if (dh > th) {
|
||||
dh = th;
|
||||
dw = (int)((int64_t)th * tex->width / tex->height);
|
||||
}
|
||||
if (dw <= 0 || dh <= 0) return;
|
||||
struct wlr_render_texture_options o = {
|
||||
.texture = tex,
|
||||
.src_box = { .x = 0, .y = 0,
|
||||
.width = (float)tex->width, .height = (float)tex->height },
|
||||
.dst_box = { .x = tx + (tw - dw) / 2, .y = ty + (th - dh) / 2,
|
||||
.width = dw, .height = dh },
|
||||
.alpha = &alpha,
|
||||
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
||||
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
||||
};
|
||||
wlr_render_pass_add_texture(pass, &o);
|
||||
}
|
||||
|
||||
/* Cursor inside the overlay plane (Option 1): while the overlay is held on
|
||||
standalone DRM the hardware cursor plane is parked, so the cursor image
|
||||
is painted into the plane buffer after the menu -- cursor above overlay
|
||||
|
|
@ -2754,6 +2956,11 @@ static int overlay_tab_box(struct overlay_box *box) {
|
|||
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||
int row_gap = 8;
|
||||
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
||||
int thumb_w = 0;
|
||||
if (opt_overlay_previews) {
|
||||
row_h = overlay_preview_row_h();
|
||||
thumb_w = overlay_preview_thumb_w();
|
||||
}
|
||||
int max_row_w = 0, n_rows = 0;
|
||||
struct kick_tab *tab;
|
||||
wl_list_for_each(tab, &server.tabs, link) {
|
||||
|
|
@ -2762,10 +2969,14 @@ static int overlay_tab_box(struct overlay_box *box) {
|
|||
struct kick_window *w;
|
||||
wl_list_for_each(w, &tab->windows, link) {
|
||||
if (!w->mapped) continue;
|
||||
if (opt_overlay_previews) {
|
||||
row_w += icon_sz + icon_gap + thumb_w;
|
||||
} else {
|
||||
char tb[256];
|
||||
const char *t = overlay_window_title(w, tb, sizeof(tb));
|
||||
overlay_utf8_cut(tb);
|
||||
row_w += icon_sz + icon_gap + overlay_text_width(t);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
if (n > 0) {
|
||||
|
|
@ -2884,6 +3095,12 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
|
|||
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||
int row_gap = 8;
|
||||
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
||||
int thumb_w = 0, thumb_h = 0;
|
||||
if (opt_overlay_previews) {
|
||||
row_h = overlay_preview_row_h();
|
||||
thumb_w = overlay_preview_thumb_w();
|
||||
thumb_h = overlay_preview_thumb_h();
|
||||
}
|
||||
struct kick_tab *tab;
|
||||
|
||||
int bw = 1;
|
||||
|
|
@ -2932,6 +3149,12 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
|
|||
}
|
||||
int total_w = 0;
|
||||
for (int k = 0; k < n; k++) {
|
||||
if (opt_overlay_previews) {
|
||||
/* No captions in preview mode: fixed thumbnail boxes. */
|
||||
widths[k] = thumb_w;
|
||||
total_w += icon_sz + icon_gap + widths[k];
|
||||
continue;
|
||||
}
|
||||
/* Titles are ellipsized at draw time by the text tile itself.
|
||||
overlay_window_title returns the client string directly when
|
||||
there is nothing to synthesize, so draw the RETURNED pointer:
|
||||
|
|
@ -2947,6 +3170,7 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
|
|||
|
||||
int pen = box_x + boxpad + (row_inner - total_w) / 2;
|
||||
int iy = ry + (row_h - icon_sz) / 2;
|
||||
int ty = ry + (row_h - thumb_h) / 2;
|
||||
if (n == 0) {
|
||||
struct kick_window *first = tab_first_window(tab);
|
||||
const char *ct = (first && first->cmd && first->cmd[0])
|
||||
|
|
@ -2982,9 +3206,15 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass,
|
|||
overlay_pass_rect(pass, mx + 3, my + 3, icon_sz - 6,
|
||||
icon_sz - 6, 0.07f, 0.07f, 0.09f, 1.0f);
|
||||
}
|
||||
if (opt_overlay_previews) {
|
||||
overlay_pass_thumb(pass, w, mx + icon_sz + icon_gap, ty,
|
||||
thumb_w, thumb_h,
|
||||
active ? 1.0f : 0.72f);
|
||||
} else {
|
||||
overlay_pass_text(pass, strs[k], shares[k], mx + icon_sz + icon_gap,
|
||||
ry + (row_h - host.overlay_font_h) / 2,
|
||||
active ? 1.0f : 0.72f * 0.9f);
|
||||
}
|
||||
pen += mw + member_gap;
|
||||
}
|
||||
row++;
|
||||
|
|
@ -3017,6 +3247,11 @@ static bool overlay_hit_test(double ox, double oy,
|
|||
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||
int row_gap = 8;
|
||||
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
||||
int thumb_w = 0;
|
||||
if (opt_overlay_previews) {
|
||||
row_h = overlay_preview_row_h();
|
||||
thumb_w = overlay_preview_thumb_w();
|
||||
}
|
||||
struct kick_tab *tab;
|
||||
|
||||
int row = 0;
|
||||
|
|
@ -3043,6 +3278,11 @@ static bool overlay_hit_test(double ox, double oy,
|
|||
}
|
||||
int total_w = 0;
|
||||
for (int k = 0; k < n; k++) {
|
||||
if (opt_overlay_previews) {
|
||||
widths[k] = thumb_w;
|
||||
total_w += icon_sz + icon_gap + widths[k];
|
||||
continue;
|
||||
}
|
||||
char tb[256];
|
||||
const char *s = overlay_window_title(wins[k], tb, sizeof(tb));
|
||||
struct overlay_text_entry *te = overlay_text_tile(s, share);
|
||||
|
|
@ -3736,6 +3976,7 @@ static void remove_tab(struct kick_tab *tab) {
|
|||
}
|
||||
if (w->icon_wlr)
|
||||
wlr_texture_destroy(w->icon_wlr);
|
||||
thumb_release(w);
|
||||
icon_proto_release(w);
|
||||
if (w->toplevel)
|
||||
window_detach_xdg_listeners(w);
|
||||
|
|
@ -3795,6 +4036,7 @@ static void window_destroy_common(struct kick_window *w) {
|
|||
free(w->tag_desc);
|
||||
if (w->icon_wlr)
|
||||
wlr_texture_destroy(w->icon_wlr);
|
||||
thumb_release(w);
|
||||
icon_proto_release(w);
|
||||
mru_remove(w);
|
||||
free(w);
|
||||
|
|
@ -4263,6 +4505,8 @@ static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
|
|||
negative y once they decide on their titlebar), so keep the scene
|
||||
position in sync with every commit instead of only at activation. */
|
||||
position_window(w);
|
||||
/* --overlay-previews: a commit is a new thumbnail frame. */
|
||||
thumb_capture(w);
|
||||
|
||||
/* [diag] What does this client commit, and is its scene node visible? */
|
||||
struct wlr_surface *s = toplevel->base->surface;
|
||||
|
|
@ -5028,6 +5272,7 @@ static void handle_xs_commit(struct wl_listener *listener, void *data) {
|
|||
(void)data;
|
||||
struct kick_window *w = wl_container_of(listener, w, commit);
|
||||
x11_sync_position(w);
|
||||
thumb_capture(w);
|
||||
}
|
||||
|
||||
static void handle_xs_surface_map(struct wl_listener *listener, void *data) {
|
||||
|
|
@ -9497,6 +9742,58 @@ static void handle_window_meta(struct wl_listener *l, void *data) {
|
|||
so no stale fraction leaks into the next session. */
|
||||
static double overlay_wheel_acc = 0.0;
|
||||
|
||||
/* Preview live tick (~15fps): hidden windows get no scene frame callbacks
|
||||
while the overlay is up, so their previews would freeze on the last frame
|
||||
from before the hold. Pacing them with frame-done keeps GPU clients
|
||||
(terminals, video) rendering into the thumbnails; SHM clients that paint
|
||||
on their own schedule are unaffected. Shown windows already get scene
|
||||
frame callbacks and need nothing. Commits repaint through thumb_capture,
|
||||
so the timer itself schedules nothing. Gated on preview mode: the plain
|
||||
title list never wakes background clients. */
|
||||
#define KICK_PREVIEW_TICK_MS 66
|
||||
/* Frame-done to every surfaced buffer in a hidden window's tree: clients
|
||||
may wait on a subsurface/popup callback rather than the main surface's
|
||||
(foot does), and the scene samples none of them while hidden. Surfaces
|
||||
without a pending callback are a no-op send. */
|
||||
static void preview_tick_tree(struct wlr_scene_tree *tree,
|
||||
const struct timespec *now) {
|
||||
if (!tree) return;
|
||||
struct wlr_scene_node *node;
|
||||
wl_list_for_each(node, &tree->children, link) {
|
||||
if (node->type == WLR_SCENE_NODE_BUFFER) {
|
||||
struct wlr_scene_buffer *sb = wlr_scene_buffer_from_node(node);
|
||||
struct wlr_scene_surface *ss =
|
||||
wlr_scene_surface_try_from_buffer(sb);
|
||||
if (ss && ss->surface)
|
||||
wlr_surface_send_frame_done(ss->surface, now);
|
||||
} else if (node->type == WLR_SCENE_NODE_TREE) {
|
||||
preview_tick_tree(wlr_scene_tree_from_node(node), now);
|
||||
}
|
||||
}
|
||||
}
|
||||
static int preview_tick_cb(void *data) {
|
||||
(void)data;
|
||||
if (!host.overlay_visible || !opt_overlay_previews) return 0;
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
struct kick_tab *tab;
|
||||
wl_list_for_each(tab, &server.tabs, link) {
|
||||
struct kick_window *w, *tmp;
|
||||
wl_list_for_each_safe(w, tmp, &tab->windows, link) {
|
||||
if (!w->mapped || window_is_shown(w)) continue;
|
||||
struct wlr_surface *s = window_surface(w);
|
||||
if (!s) continue;
|
||||
wlr_surface_send_frame_done(s, &now);
|
||||
if (w->tree)
|
||||
preview_tick_tree(w->tree, &now);
|
||||
}
|
||||
}
|
||||
if (server.preview_timer)
|
||||
wl_event_source_timer_update(server.preview_timer,
|
||||
KICK_PREVIEW_TICK_MS);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void update_overlay_visible(bool alt) {
|
||||
if (alt == host.overlay_visible) return;
|
||||
if (alt) {
|
||||
|
|
@ -9524,6 +9821,19 @@ static void update_overlay_visible(bool alt) {
|
|||
host.overlay_visible = alt;
|
||||
kick_log(KICK_LOG_DEBUG, "[render] overlay %s\n",
|
||||
alt ? "show" : "hide");
|
||||
/* Preview live tick: pace hidden windows with frame-done while the
|
||||
preview overlay is up so their thumbnails track live frames;
|
||||
disarm on release so background clients freeze again. */
|
||||
if (alt && opt_overlay_previews) {
|
||||
if (!server.preview_timer)
|
||||
server.preview_timer = wl_event_loop_add_timer(
|
||||
server.loop, preview_tick_cb, NULL);
|
||||
if (server.preview_timer)
|
||||
wl_event_source_timer_update(server.preview_timer,
|
||||
KICK_PREVIEW_TICK_MS);
|
||||
} else if (server.preview_timer) {
|
||||
wl_event_source_timer_update(server.preview_timer, 0);
|
||||
}
|
||||
/* A rejected plane goes back to UNKNOWN on the next hold so the fresh
|
||||
allocation is probed again instead of staying composited forever. */
|
||||
if (alt && server.overlay_plane == OVERLAY_PLANE_OFF)
|
||||
|
|
@ -12870,6 +13180,7 @@ static void sweep_dead_startup_groups(void) {
|
|||
wlr_texture_destroy(w->icon_wlr);
|
||||
w->icon_wlr = NULL;
|
||||
}
|
||||
thumb_release(w);
|
||||
icon_proto_release(w);
|
||||
mru_remove(w);
|
||||
wl_list_remove(&w->link);
|
||||
|
|
@ -13286,6 +13597,13 @@ int main(int argc, char *argv[]) {
|
|||
"(nice +10,\n"
|
||||
" best-effort/7). Off by default; "
|
||||
"needs no sudo.\n"
|
||||
" --overlay-previews Show live window thumbnails "
|
||||
"in the tab overlay\n"
|
||||
" instead of icon+title rows: "
|
||||
"each row is the icon\n"
|
||||
" plus a small live view of "
|
||||
"the window. Off by\n"
|
||||
" default.\n"
|
||||
" -h, --help Show the full help.\n"
|
||||
"\n"
|
||||
"Keys (hold the modifier):\n"
|
||||
|
|
@ -13390,6 +13708,9 @@ int main(int argc, char *argv[]) {
|
|||
} else if (strcmp(argv[i], "--focus-priority") == 0) {
|
||||
opt_focus_priority = true;
|
||||
continue;
|
||||
} else if (strcmp(argv[i], "--overlay-previews") == 0) {
|
||||
opt_overlay_previews = true;
|
||||
continue;
|
||||
} else if (strcmp(argv[i], "--panel") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "kick: --panel needs a value, "
|
||||
|
|
|
|||
|
|
@ -6576,3 +6576,99 @@ def test_alt_tab_hold_repeats():
|
|||
f"held Alt+Tab produced only {settled - base} transitions"
|
||||
assert session.active_count() == settled, \
|
||||
"repeats continued after release"
|
||||
|
||||
|
||||
def _dominant_counts(s, path):
|
||||
"""(red, green) dominant-pixel counts in a SIGUSR1 PPM dump.
|
||||
|
||||
Dominance (channel >120 and 80 above both others) survives the
|
||||
overlay's inactive-tab dimming (0.72), so hidden tabs' thumbnails
|
||||
count while overlay chrome (grays/blues) does not.
|
||||
"""
|
||||
w, h, px = s.ppm_rgb(path)
|
||||
red = green = 0
|
||||
for i in range(0, len(px), 3):
|
||||
r, g, b = px[i], px[i + 1], px[i + 2]
|
||||
if r > 120 and r > g + 80 and r > b + 80:
|
||||
red += 1
|
||||
elif g > 120 and g > r + 80 and g > b + 80:
|
||||
green += 1
|
||||
return red, green
|
||||
|
||||
|
||||
def test_standalone_headless_overlay_previews():
|
||||
"""--overlay-previews shows a live thumbnail per window instead of
|
||||
icon+title rows: the hidden tab's color appears in the overlay while
|
||||
Alt is held (it is invisible without the flag)."""
|
||||
with _run_standalone(
|
||||
{}, "--overlay-previews",
|
||||
"%s '#ff0000'" % WL, "%s '#0000ff'" % WL,
|
||||
wait="any_frame") as s:
|
||||
s.wait_surface_mapped(0)
|
||||
s.wait_surface_mapped(1)
|
||||
red, _ = _dominant_counts(s, s.dump_host())
|
||||
assert red < 500, f"hidden red tab visible without overlay: {red}"
|
||||
s.mods(ALT)
|
||||
try:
|
||||
deadline = time.monotonic() + 15.0
|
||||
while time.monotonic() < deadline:
|
||||
red, _ = _dominant_counts(s, s.dump_host())
|
||||
if red > 2000:
|
||||
break
|
||||
time.sleep(0.2)
|
||||
assert red > 2000, "no red thumbnail while Alt held"
|
||||
finally:
|
||||
s.mods(0)
|
||||
_assert_alive(s)
|
||||
|
||||
|
||||
def test_standalone_headless_overlay_previews_live():
|
||||
"""Preview thumbnails track window commits while the overlay is held:
|
||||
a window mapping mid-hold (the delayed --extra) gains a live thumbnail
|
||||
without reopening the overlay."""
|
||||
with _run_standalone(
|
||||
{}, "--overlay-previews", "--tab-groups", "off",
|
||||
"%s --extra '#00ff00' --extra-delay 6000 '#ff0000'" % WL,
|
||||
"%s '#0000ff'" % WL, wait="any_frame") as s:
|
||||
s.wait_surface_mapped(0)
|
||||
s.wait_surface_mapped(1)
|
||||
s.mods(ALT)
|
||||
try:
|
||||
# The green extra window maps ~6s after client boot, mid-hold.
|
||||
deadline = time.monotonic() + 30.0
|
||||
while time.monotonic() < deadline:
|
||||
_, green = _dominant_counts(s, s.dump_host())
|
||||
if green > 2000:
|
||||
break
|
||||
time.sleep(0.3)
|
||||
assert green > 2000, \
|
||||
"no green thumbnail for the window mapped mid-hold"
|
||||
finally:
|
||||
s.mods(0)
|
||||
_assert_alive(s)
|
||||
|
||||
|
||||
def test_standalone_headless_overlay_previews_pulse():
|
||||
"""A hidden window repainting while the overlay is held shows live
|
||||
frames: a --pulse client flipping red/black swings the overlay's red
|
||||
pixel count between full thumbnail and nothing."""
|
||||
with _run_standalone(
|
||||
{}, "--overlay-previews",
|
||||
"%s --pulse 500 '#ff0000'" % WL,
|
||||
"%s '#0000ff'" % WL, wait="any_frame") as s:
|
||||
s.wait_surface_mapped(0)
|
||||
s.wait_surface_mapped(1)
|
||||
s.mods(ALT)
|
||||
try:
|
||||
reds = []
|
||||
deadline = time.monotonic() + 12.0
|
||||
while time.monotonic() < deadline and len(reds) < 8:
|
||||
red, _ = _dominant_counts(s, s.dump_host())
|
||||
reds.append(red)
|
||||
time.sleep(0.6)
|
||||
swing = max(reds) - min(reds) if reds else 0
|
||||
assert swing > 5000, \
|
||||
f"pulse thumbnail never flipped (red samples: {reds})"
|
||||
finally:
|
||||
s.mods(0)
|
||||
_assert_alive(s)
|
||||
|
|
|
|||
|
|
@ -499,6 +499,14 @@ static bool extra_modal;
|
|||
static int extra_delay_ms;
|
||||
static bool extra_done;
|
||||
static uint8_t EXTRA_PIXEL[4] = { 0x00, 0xff, 0x00, 0xff };
|
||||
/* --pulse MS: repaint alternating between the main color and black every
|
||||
MS milliseconds, driven by frame callbacks (so a hidden client only
|
||||
animates while the compositor keeps sending them). Gives the overlay
|
||||
preview tests a deterministic live source. */
|
||||
static int pulse_ms;
|
||||
static uint8_t PULSE_BASE[4];
|
||||
static bool pulse_phase;
|
||||
static struct wl_callback *pulse_cb;
|
||||
static int EX_W = 640, EX_H = 400;
|
||||
static struct wl_surface *ex_surface;
|
||||
static struct xdg_surface *ex_xdg;
|
||||
|
|
@ -508,6 +516,7 @@ static struct wl_buffer *ex_buffer;
|
|||
static void create_buffer(void);
|
||||
static void extra_open(void);
|
||||
static void request_constraint(void);
|
||||
static void pulse_arm(void);
|
||||
|
||||
static void locked_locked(void *data, struct zwp_locked_pointer_v1 *lp) {
|
||||
(void)data; (void)lp;
|
||||
|
|
@ -818,6 +827,14 @@ static void xdg_surface_configure(void *data, struct xdg_surface *xs, uint32_t s
|
|||
wp_viewport_set_destination(viewport, W, H);
|
||||
wl_surface_attach(surface, buffer, 0, 0);
|
||||
wl_surface_damage_buffer(surface, 0, 0, W, H);
|
||||
if (pulse_ms && !pulse_cb) {
|
||||
/* Start the pulse loop once the window exists; the base color is
|
||||
whatever the command line set. Armed before the commit below
|
||||
so the frame request takes effect immediately. */
|
||||
memcpy(PULSE_BASE, PIXEL, sizeof(PULSE_BASE));
|
||||
pulse_phase = false;
|
||||
pulse_arm();
|
||||
}
|
||||
wl_surface_commit(surface);
|
||||
if (want_extra && !extra_done) {
|
||||
extra_done = true;
|
||||
|
|
@ -1195,6 +1212,61 @@ static void create_buffer(void) {
|
|||
wl_shm_pool_destroy(pool);
|
||||
}
|
||||
|
||||
static uint64_t pulse_now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static void pulse_frame_done(void *data, struct wl_callback *cb,
|
||||
uint32_t time);
|
||||
static const struct wl_callback_listener pulse_frame_listener = {
|
||||
.done = pulse_frame_done,
|
||||
};
|
||||
|
||||
static void pulse_arm(void) {
|
||||
if (pulse_cb)
|
||||
wl_callback_destroy(pulse_cb);
|
||||
pulse_cb = wl_surface_frame(surface);
|
||||
wl_callback_add_listener(pulse_cb, &pulse_frame_listener, NULL);
|
||||
}
|
||||
|
||||
static void pulse_frame_done(void *data, struct wl_callback *cb,
|
||||
uint32_t time) {
|
||||
(void)data;
|
||||
(void)time;
|
||||
if (cb == pulse_cb)
|
||||
pulse_cb = NULL;
|
||||
else
|
||||
wl_callback_destroy(cb);
|
||||
if (!pulse_ms)
|
||||
return;
|
||||
bool phase = (pulse_now_ms() / (uint64_t)pulse_ms) & 1;
|
||||
/* Alternate on wall clock so phases stay aligned no matter how often
|
||||
(or rarely) the compositor sends frames. Always attach+commit:
|
||||
the new frame request below only takes effect on a commit. */
|
||||
if (phase != pulse_phase) {
|
||||
pulse_phase = phase;
|
||||
if (phase) {
|
||||
PIXEL[0] = 0x00;
|
||||
PIXEL[1] = 0x00;
|
||||
PIXEL[2] = 0x00;
|
||||
} else {
|
||||
PIXEL[0] = PULSE_BASE[0];
|
||||
PIXEL[1] = PULSE_BASE[1];
|
||||
PIXEL[2] = PULSE_BASE[2];
|
||||
}
|
||||
create_buffer();
|
||||
wl_surface_attach(surface, buffer, 0, 0);
|
||||
wl_surface_damage_buffer(surface, 0, 0, W, H);
|
||||
} else {
|
||||
wl_surface_attach(surface, buffer, 0, 0);
|
||||
}
|
||||
pulse_arm();
|
||||
wl_surface_commit(surface);
|
||||
wl_display_flush(display);
|
||||
}
|
||||
|
||||
/* ---- the extra (second, same-process) toplevel ---- */
|
||||
|
||||
static void extra_toplevel_configure(void *data, struct xdg_toplevel *xp,
|
||||
|
|
@ -3574,6 +3646,8 @@ static void print_usage(const char *argv0) {
|
|||
"#RRGGBB [WIDTHxHEIGHT]\n"
|
||||
" %s shows a solid-color xdg_toplevel, e.g. #ff0000\n"
|
||||
" --extra also opens a second toplevel from the same client\n"
|
||||
" --pulse MS repaints alternating main color/black every\n"
|
||||
" MS ms on frame callbacks (a live source for previews)\n"
|
||||
" --cursor-shape requests that cursor-shape-v1 shape on "
|
||||
"pointer enter (default|text|pointer|crosshair)\n"
|
||||
" --globals-out lists advertised globals to <file> and exits\n"
|
||||
|
|
@ -3705,6 +3779,17 @@ int main(int argc, char **argv) {
|
|||
return 1;
|
||||
}
|
||||
i += 2;
|
||||
} else if (strcmp(argv[i], "--pulse") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "wltest: --pulse needs ms\n");
|
||||
return 1;
|
||||
}
|
||||
pulse_ms = atoi(argv[i + 1]);
|
||||
if (pulse_ms <= 0) {
|
||||
fprintf(stderr, "wltest: bad --pulse\n");
|
||||
return 1;
|
||||
}
|
||||
i += 2;
|
||||
} else if (strcmp(argv[i], "--tearing") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "wltest: --tearing needs vsync|async\n");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue