Draw passive Alt-hold tab-title overlay with FreeType text
This commit is contained in:
parent
e52ac98afe
commit
db6bbac84c
4 changed files with 368 additions and 19 deletions
|
|
@ -87,9 +87,16 @@ Notes:
|
|||
|
||||
| Key | Action |
|
||||
|----------------------------|---------------------------------------------------|
|
||||
| `Alt` (hold) | Show the tab-title overlay (top-left) |
|
||||
| `Alt+Up` | Previous tab (hold to keep cycling) |
|
||||
| `Alt+Down` | Next tab (hold to keep cycling) |
|
||||
|
||||
Holding `Alt` shows a passive overlay listing every tab's title with the
|
||||
active tab highlighted; the list updates live as you cycle with `Up`/`Down`
|
||||
and disappears on `Alt` release. It is drawn by tabbed itself, so it works
|
||||
on hosts with or without a title bar (the overlay text uses a system font
|
||||
via FreeType; set `TABBED_FONT` to pick one).
|
||||
|
||||
The tab-cycling hotkey only emits a physical press for the first tab switch;
|
||||
repeats are synthesized, so holding `Alt+Up/Down` keeps stepping through
|
||||
tabs while any other key stops it.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ deps = [
|
|||
dependency('xkbcommon'),
|
||||
dependency('egl'),
|
||||
dependency('glesv2'),
|
||||
dependency('freetype2'),
|
||||
]
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
|
|
|
|||
333
tabbed-wlroots.c
333
tabbed-wlroots.c
|
|
@ -45,6 +45,9 @@
|
|||
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
#include <wlr/backend.h>
|
||||
#include <wlr/backend/headless.h>
|
||||
#include <wlr/interfaces/wlr_keyboard.h>
|
||||
|
|
@ -125,6 +128,14 @@ struct tabbed_server {
|
|||
|
||||
/* ===================== Host-side (nested) state ===================== */
|
||||
|
||||
/* One cached ASCII glyph in the overlay's font atlas texture. */
|
||||
struct overlay_glyph {
|
||||
int ax, ay; /* atlas pixel origin */
|
||||
int pw, ph; /* glyph bitmap size */
|
||||
int adv; /* advance width, pixels */
|
||||
int top; /* y offset of the glyph top from the row top */
|
||||
};
|
||||
|
||||
struct tabbed_host {
|
||||
struct wl_display *display;
|
||||
struct wl_registry *registry;
|
||||
|
|
@ -154,8 +165,15 @@ struct tabbed_host {
|
|||
int32_t axis_discrete[2];
|
||||
/* GL program */
|
||||
GLuint gl_program;
|
||||
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc;
|
||||
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc, gl_color_loc;
|
||||
GLuint import_tex;
|
||||
/* Alt-held tab-title overlay */
|
||||
bool overlay_visible;
|
||||
GLuint overlay_atlas_tex;
|
||||
GLuint overlay_white_tex;
|
||||
int overlay_atlas_w, overlay_atlas_h;
|
||||
struct overlay_glyph overlay_glyphs[95]; /* chars 32..126 */
|
||||
int overlay_font_h; /* ascent + descent, pixels */
|
||||
/* Present gating: at most one buffer aimed at the host compositor at a
|
||||
time, paced by the host surface's wl_callback. */
|
||||
struct wl_callback *frame_cb;
|
||||
|
|
@ -339,6 +357,8 @@ static void remove_socket_file(void) {
|
|||
unlink(lock_path);
|
||||
}
|
||||
|
||||
static void overlay_font_fini(void);
|
||||
|
||||
/* Grow server.apps to hold at least n entries. Returns 0 on success, -1 on
|
||||
allocation failure. The buffer is freed in shutdown_cleanup(). */
|
||||
static int apps_ensure_cap(int n) {
|
||||
|
|
@ -367,6 +387,7 @@ static void shutdown_cleanup(void) {
|
|||
if (server.xkb_state) xkb_state_unref(server.xkb_state);
|
||||
if (server.xkb_keymap) xkb_keymap_unref(server.xkb_keymap);
|
||||
if (server.xkb_ctx) xkb_context_unref(server.xkb_ctx);
|
||||
overlay_font_fini();
|
||||
bridge_cleanup();
|
||||
remove_socket_file();
|
||||
}
|
||||
|
|
@ -414,8 +435,9 @@ static bool gl_setup(void) {
|
|||
"precision mediump float;\n"
|
||||
"varying vec2 v_uv;\n"
|
||||
"uniform sampler2D u_tex;\n"
|
||||
"uniform vec4 u_color;\n"
|
||||
"void main() {\n"
|
||||
" gl_FragColor = texture2D(u_tex, v_uv);\n"
|
||||
" gl_FragColor = texture2D(u_tex, v_uv) * u_color;\n"
|
||||
"}\n";
|
||||
GLuint vsh = compile_shader(GL_VERTEX_SHADER, vs);
|
||||
GLuint fsh = compile_shader(GL_FRAGMENT_SHADER, fs);
|
||||
|
|
@ -439,6 +461,7 @@ static bool gl_setup(void) {
|
|||
host.gl_pos_loc = glGetAttribLocation(host.gl_program, "a_pos");
|
||||
host.gl_uv_loc = glGetAttribLocation(host.gl_program, "a_uv");
|
||||
host.gl_tex_loc = glGetUniformLocation(host.gl_program, "u_tex");
|
||||
host.gl_color_loc = glGetUniformLocation(host.gl_program, "u_color");
|
||||
|
||||
glGenTextures(1, &host.import_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, host.import_tex);
|
||||
|
|
@ -447,39 +470,50 @@ static bool gl_setup(void) {
|
|||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenTextures(1, &host.overlay_white_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, host.overlay_white_tex);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
const GLubyte white_px[4] = { 255, 255, 255, 255 };
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, white_px);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Blit a source rect of a texture into the window at the given destination
|
||||
offset (window coords), clipped to the window. */
|
||||
rect (dst_x, dst_y, dst_w, dst_h, window coords), clipped to the window.
|
||||
The source is stretched uniformly to the destination (1:1 when the rects
|
||||
match). The draw is tinted by `color` (RGBA), so solid rects are drawn
|
||||
from a 1x1 white texture and text glyphs from the font atlas. */
|
||||
static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
|
||||
int src_x, int src_y, int src_w, int src_h,
|
||||
int dst_x, int dst_y, int win_w, int win_h)
|
||||
int dst_x, int dst_y, int dst_w, int dst_h,
|
||||
int win_w, int win_h, const GLfloat color[4])
|
||||
{
|
||||
if (win_w <= 0 || win_h <= 0) return;
|
||||
if (src_w <= 0 || src_h <= 0) return;
|
||||
if (src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0) return;
|
||||
if (src_x >= tex_w || src_y >= tex_h) return;
|
||||
if (src_w > tex_w - src_x) src_w = tex_w - src_x;
|
||||
if (src_h > tex_h - src_y) src_h = tex_h - src_y;
|
||||
int cx = dst_x < 0 ? -dst_x : 0;
|
||||
int cy = dst_y < 0 ? -dst_y : 0;
|
||||
if (cx >= src_w || cy >= src_h) return;
|
||||
if (cx >= dst_w || cy >= dst_h) return;
|
||||
int wx0 = dst_x + cx;
|
||||
int wy0 = dst_y + cy;
|
||||
int cw = src_w - cx;
|
||||
int ch = src_h - cy;
|
||||
int cw = dst_w - cx;
|
||||
int ch = dst_h - cy;
|
||||
int maxw = win_w - wx0;
|
||||
int maxh = win_h - wy0;
|
||||
if (cw > maxw) cw = maxw;
|
||||
if (ch > maxh) ch = maxh;
|
||||
if (cw <= 0 || ch <= 0) return;
|
||||
src_x += cx;
|
||||
src_y += cy;
|
||||
|
||||
float u0 = (float)src_x / (float)tex_w;
|
||||
float u1 = (float)(src_x + cw) / (float)tex_w;
|
||||
float v0 = (float)src_y / (float)tex_h;
|
||||
float v1 = (float)(src_y + ch) / (float)tex_h;
|
||||
float u0 = ((float)src_x + (float)src_w * cx / dst_w) / (float)tex_w;
|
||||
float u1 = ((float)src_x + (float)src_w * (cx + cw) / dst_w) / (float)tex_w;
|
||||
float v0 = ((float)src_y + (float)src_h * cy / dst_h) / (float)tex_h;
|
||||
float v1 = ((float)src_y + (float)src_h * (cy + ch) / dst_h) / (float)tex_h;
|
||||
|
||||
float nx0 = -1.0f + 2.0f * (float)wx0 / (float)win_w;
|
||||
float ny0 = 1.0f - 2.0f * (float)wy0 / (float)win_h;
|
||||
|
|
@ -504,6 +538,7 @@ static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
|
|||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glUniform1i(host.gl_tex_loc, 0);
|
||||
glUniform4fv(host.gl_color_loc, 1, color);
|
||||
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, verts);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
|
@ -516,6 +551,262 @@ static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
|
|||
glDisableVertexAttribArray(1);
|
||||
}
|
||||
|
||||
/* ============ Alt-held tab-title overlay ============
|
||||
While the host Alt modifier is held, a passive list of tab titles is
|
||||
drawn into the top-left of the host window with the active tab
|
||||
highlighted. Up/Down already cycle tabs while Alt is held, so the
|
||||
highlight follows server.active automatically. Titles are read from the
|
||||
wlr_xdg_toplevel at draw time (terminals retitle themselves), so no
|
||||
per-client listeners are needed. */
|
||||
|
||||
static FT_Library overlay_ft_lib;
|
||||
static FT_Face overlay_ft_face;
|
||||
static bool overlay_font_ok;
|
||||
|
||||
/* Fixed rasterized font size; 30px is twice the original 15px constant. */
|
||||
#define OVERLAY_FONT_PX 30
|
||||
|
||||
static const char *overlay_pick_font(void) {
|
||||
static const char *const paths[] = {
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
};
|
||||
const char *env = getenv("TABBED_FONT");
|
||||
if (env && access(env, R_OK) == 0)
|
||||
return env;
|
||||
for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++)
|
||||
if (access(paths[i], R_OK) == 0)
|
||||
return paths[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Build the ASCII glyph atlas (chars 32..126) from the chosen system font.
|
||||
Must be called with the host EGL context current. Non-fatal: on failure
|
||||
the overlay still renders its boxes, just without text. */
|
||||
static void overlay_font_init(void) {
|
||||
int px = OVERLAY_FONT_PX;
|
||||
static const char *overlay_font_path = NULL;
|
||||
if (!overlay_ft_lib) {
|
||||
const char *path = overlay_pick_font();
|
||||
if (!path) {
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] no font found; "
|
||||
"set TABBED_FONT to enable overlay text\n");
|
||||
return;
|
||||
}
|
||||
overlay_font_path = path;
|
||||
if (FT_Init_FreeType(&overlay_ft_lib) != 0) {
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] FreeType init failed\n");
|
||||
return;
|
||||
}
|
||||
if (FT_New_Face(overlay_ft_lib, path, 0, &overlay_ft_face) != 0) {
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] cannot load font %s\n", path);
|
||||
FT_Done_FreeType(overlay_ft_lib);
|
||||
overlay_ft_lib = NULL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (FT_Set_Pixel_Sizes(overlay_ft_face, 0, px) != 0) {
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] cannot set pixel size %d\n", px);
|
||||
return;
|
||||
}
|
||||
|
||||
int height = (int)(overlay_ft_face->size->metrics.height / 64.0f) + 1;
|
||||
int asc = (int)(overlay_ft_face->size->metrics.ascender / 64.0f);
|
||||
int adv_max = 0;
|
||||
for (int c = 32; c <= 126; c++) {
|
||||
if (FT_Load_Char(overlay_ft_face, c, FT_LOAD_DEFAULT) == 0) {
|
||||
int adv = (int)(overlay_ft_face->glyph->advance.x / 64.0f);
|
||||
if (adv > adv_max) adv_max = adv;
|
||||
}
|
||||
}
|
||||
if (adv_max <= 0) adv_max = 8;
|
||||
int cell_w = adv_max + 3;
|
||||
int cell_h = height + 3;
|
||||
int cols = 16;
|
||||
int rows = (95 + cols - 1) / cols;
|
||||
int atlas_w = cols * cell_w;
|
||||
int atlas_h = rows * cell_h;
|
||||
|
||||
unsigned char *atlas = calloc((size_t)atlas_w * atlas_h * 2, 1);
|
||||
if (!atlas) {
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] atlas alloc failed\n");
|
||||
return;
|
||||
}
|
||||
int n_glyphs = 0;
|
||||
for (int c = 32; c <= 126; c++) {
|
||||
struct overlay_glyph *g = &host.overlay_glyphs[c - 32];
|
||||
if (FT_Load_Char(overlay_ft_face, c, FT_LOAD_RENDER) != 0)
|
||||
continue;
|
||||
FT_Bitmap *bmp = &overlay_ft_face->glyph->bitmap;
|
||||
/* A bitmap wider/taller than its cell would corrupt the glyphs it
|
||||
spills into; skip such glyphs rather than damage the atlas. */
|
||||
if ((int)bmp->width > cell_w - 2 || (int)bmp->rows > cell_h - 2)
|
||||
continue;
|
||||
int cell = c - 32;
|
||||
int cx = (cell % cols) * cell_w;
|
||||
int cy = (cell / cols) * cell_h;
|
||||
g->ax = cx + 1;
|
||||
g->ay = cy + 1;
|
||||
g->pw = bmp->width;
|
||||
g->ph = bmp->rows;
|
||||
g->adv = (int)(overlay_ft_face->glyph->advance.x / 64.0f);
|
||||
g->top = asc - overlay_ft_face->glyph->bitmap_top;
|
||||
/* Cells are GL_LUMINANCE_ALPHA, so each 8-bit alpha is duplicated. */
|
||||
for (int r = 0; r < (int)bmp->rows; r++) {
|
||||
const unsigned char *src = bmp->buffer + (size_t)r * bmp->pitch;
|
||||
unsigned char *dst = atlas
|
||||
+ ((size_t)(g->ay + r) * atlas_w + g->ax) * 2;
|
||||
for (int x = 0; x < (int)bmp->width; x++) {
|
||||
dst[x * 2 + 0] = src[x];
|
||||
dst[x * 2 + 1] = src[x];
|
||||
}
|
||||
}
|
||||
n_glyphs++;
|
||||
}
|
||||
if (n_glyphs == 0) {
|
||||
free(atlas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!host.overlay_atlas_tex)
|
||||
glGenTextures(1, &host.overlay_atlas_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, host.overlay_atlas_tex);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, atlas_w, atlas_h, 0,
|
||||
GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, atlas);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
free(atlas);
|
||||
host.overlay_atlas_w = atlas_w;
|
||||
host.overlay_atlas_h = atlas_h;
|
||||
host.overlay_font_h = height;
|
||||
overlay_font_ok = true;
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] font %s glyphs=%d atlas=%dx%d\n",
|
||||
overlay_font_path, n_glyphs, atlas_w, atlas_h);
|
||||
}
|
||||
|
||||
static void overlay_font_fini(void) {
|
||||
if (overlay_ft_face) {
|
||||
FT_Done_Face(overlay_ft_face);
|
||||
overlay_ft_face = NULL;
|
||||
}
|
||||
if (overlay_ft_lib) {
|
||||
FT_Done_FreeType(overlay_ft_lib);
|
||||
overlay_ft_lib = NULL;
|
||||
}
|
||||
overlay_font_ok = false;
|
||||
}
|
||||
|
||||
static void draw_rect(int x, int y, int w, int h,
|
||||
float r, float g, float b, float a)
|
||||
{
|
||||
if (w <= 0 || h <= 0) return;
|
||||
GLfloat color[4] = { r, g, b, a };
|
||||
draw_texture_rect(host.overlay_white_tex, 1, 1,
|
||||
0, 0, 1, 1, x, y, w, h, host.width, host.height, color);
|
||||
}
|
||||
|
||||
/* Draw `s` at (x, y) (window coords, y is the row top) tinted by `color`.
|
||||
Non-ASCII characters are skipped with a narrow advance. */
|
||||
static void draw_text(const char *s, int x, int y,
|
||||
float r, float g, float b, float a)
|
||||
{
|
||||
if (!overlay_font_ok) return;
|
||||
GLfloat color[4] = { r, g, b, a };
|
||||
int pen = x;
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
int c = *p;
|
||||
if (c < 32 || c > 126) {
|
||||
pen += 4;
|
||||
continue;
|
||||
}
|
||||
struct overlay_glyph *glyph = &host.overlay_glyphs[c - 32];
|
||||
draw_texture_rect(host.overlay_atlas_tex, host.overlay_atlas_w,
|
||||
host.overlay_atlas_h,
|
||||
glyph->ax, glyph->ay, glyph->pw, glyph->ph,
|
||||
pen, y + glyph->top, glyph->pw, glyph->ph,
|
||||
host.width, host.height, color);
|
||||
pen += glyph->adv;
|
||||
}
|
||||
}
|
||||
|
||||
static int overlay_text_width(const char *s) {
|
||||
if (!overlay_font_ok) return 0;
|
||||
int w = 0;
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
int c = *p;
|
||||
if (c < 32 || c > 126) {
|
||||
w += 4;
|
||||
continue;
|
||||
}
|
||||
w += host.overlay_glyphs[c - 32].adv;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
static void draw_tab_overlay(void) {
|
||||
if (!host.overlay_visible || server.num_apps < 1) return;
|
||||
|
||||
int margin = 8, boxpad = 6;
|
||||
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||
int max_title_w = 0, n_rows = 0;
|
||||
for (int i = 0; i < server.num_apps; i++) {
|
||||
struct tabbed_app *app = server.apps[i];
|
||||
if (!app->toplevel) continue;
|
||||
const char *title = app->toplevel->title;
|
||||
char fallback[16];
|
||||
if (!title || !title[0]) {
|
||||
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
||||
title = fallback;
|
||||
}
|
||||
int w = overlay_text_width(title);
|
||||
if (w > max_title_w) max_title_w = w;
|
||||
n_rows++;
|
||||
}
|
||||
if (n_rows == 0) return;
|
||||
|
||||
int box_w = max_title_w + 2 * boxpad;
|
||||
int box_h = n_rows * row_h + 2 * boxpad;
|
||||
if (box_w > host.width - 2 * margin) box_w = host.width - 2 * margin;
|
||||
if (box_h > host.height - 2 * margin) box_h = host.height - 2 * margin;
|
||||
int box_x = margin, box_y = margin;
|
||||
|
||||
draw_rect(box_x, box_y, box_w, box_h, 0.07f, 0.07f, 0.09f, 1.0f);
|
||||
|
||||
int row = 0;
|
||||
for (int i = 0; i < server.num_apps; i++) {
|
||||
struct tabbed_app *app = server.apps[i];
|
||||
if (!app->toplevel) continue;
|
||||
const char *title = app->toplevel->title;
|
||||
char fallback[16];
|
||||
if (!title || !title[0]) {
|
||||
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
||||
title = fallback;
|
||||
}
|
||||
int ry = box_y + boxpad + row * row_h;
|
||||
bool active = app->toplevel == server.active;
|
||||
if (active) {
|
||||
int hw = overlay_text_width(title) + 4;
|
||||
draw_rect(box_x + boxpad - 2, ry - 1, hw, row_h,
|
||||
0.24f, 0.36f, 0.60f, 0.9f);
|
||||
}
|
||||
draw_text(title, box_x + boxpad, ry + (row_h - host.overlay_font_h) / 2,
|
||||
active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.9f);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===================== Server-side wlroots setup ===================== */
|
||||
|
||||
static void swapchain_resize(int width, int height) {
|
||||
|
|
@ -2105,6 +2396,10 @@ static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
|
|||
? xkb_state_mod_index_is_active(server.xkb_state,
|
||||
server.xkb_alt_mod, XKB_STATE_MODS_EFFECTIVE)
|
||||
: (host.host_mods_depressed & (1u << 3)) != 0;
|
||||
if (alt != host.overlay_visible) {
|
||||
host.overlay_visible = alt;
|
||||
needs_redraw = true;
|
||||
}
|
||||
char symbuf[32];
|
||||
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
|
||||
tabbed_log(TABBED_LOG_DEBUG,
|
||||
|
|
@ -2171,6 +2466,10 @@ static void host_kbd_modifiers(void *data, struct wl_keyboard *kbd, uint32_t ser
|
|||
? xkb_state_mod_index_is_active(server.xkb_state,
|
||||
server.xkb_alt_mod, XKB_STATE_MODS_EFFECTIVE)
|
||||
: (mods_depressed & (1u << 3)) != 0;
|
||||
if (alt != host.overlay_visible) {
|
||||
host.overlay_visible = alt;
|
||||
needs_redraw = true;
|
||||
}
|
||||
if (host.repeat_key && !alt)
|
||||
kbd_repeat_cancel();
|
||||
wlr_keyboard_notify_modifiers(&server.keyboard, mods_depressed,
|
||||
|
|
@ -2517,6 +2816,7 @@ static bool host_init(void) {
|
|||
host.egl_ok = false;
|
||||
return true;
|
||||
}
|
||||
overlay_font_init();
|
||||
|
||||
host.egl_ok = true;
|
||||
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: EGL (GPU) rendering enabled\n");
|
||||
|
|
@ -2658,10 +2958,13 @@ static void blit_scene_buffer(struct wlr_buffer *buf) {
|
|||
|
||||
bool imported = buf && import_scene_buffer(buf, host.import_tex);
|
||||
if (imported) {
|
||||
static const GLfloat white[4] = { 1, 1, 1, 1 };
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
draw_texture_rect(host.import_tex, buf->width, buf->height,
|
||||
0, 0, buf->width, buf->height, 0, 0, w, h);
|
||||
0, 0, buf->width, buf->height, 0, 0,
|
||||
buf->width, buf->height, w, h, white);
|
||||
draw_tab_overlay();
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
if (dump_host_req) {
|
||||
|
|
|
|||
|
|
@ -221,6 +221,22 @@ class Session:
|
|||
time.sleep(0.1)
|
||||
pytest.fail("both apps never logged a [claim] line")
|
||||
|
||||
def pixel_at(self, path, x, y):
|
||||
"""Pixel (x, y) of an 8-bit P6 ppm as an (R, G, B) tuple.
|
||||
|
||||
The dump writes the back buffer top-down (window coords), so row 0
|
||||
of the file is the top of the window and column 0 is the left edge.
|
||||
"""
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
lines = data.split(b"\n", 3)
|
||||
if len(lines) < 3:
|
||||
raise AssertionError(f"bad PPM {path}")
|
||||
w, h = map(int, lines[1].split())
|
||||
off = (len(lines[0]) + 1 + len(lines[1]) + 1 + len(lines[2]) + 1)
|
||||
i = off + 3 * (y * w + x)
|
||||
return tuple(data[i:i + 3])
|
||||
|
||||
def centre_of(self, path):
|
||||
"""Centre pixel of an 8-bit P6 ppm as an (R, G, B) tuple."""
|
||||
with open(path, "rb") as f:
|
||||
|
|
@ -229,10 +245,7 @@ class Session:
|
|||
if len(lines) < 3:
|
||||
raise AssertionError(f"bad PPM {path}")
|
||||
w, h = map(int, lines[1].split())
|
||||
off = (len(lines[0]) + 1 + len(lines[1]) + 1 + len(lines[2]) + 1)
|
||||
cx, cy = w // 2, h // 2
|
||||
i = off + 3 * (cy * w + cx)
|
||||
return tuple(data[i:i + 3])
|
||||
return self.pixel_at(path, w // 2, h // 2)
|
||||
|
||||
def close(self):
|
||||
if self.proc.poll() is None:
|
||||
|
|
@ -452,6 +465,31 @@ def test_hold_alt_down_repeats(session):
|
|||
assert session.active_count() == settled, "repeats continued after release"
|
||||
|
||||
|
||||
def test_alt_holds_tab_overlay(session):
|
||||
"""Holding Alt shows the passive tab-title overlay (top-left box over
|
||||
the content); releasing Alt hides it again. The overlay is drawn into
|
||||
the host frame in the same blit that produces the SIGUSR1 dump, so the
|
||||
sampled top-left pixel darkens while Alt is held and reverts after."""
|
||||
session.wait_any_active()
|
||||
base = session.dump_host()
|
||||
p_base = session.pixel_at(base, 9, 9)
|
||||
|
||||
session.mods(ALT)
|
||||
over = session.dump_host()
|
||||
p_over = session.pixel_at(over, 9, 9)
|
||||
|
||||
session.mods(0)
|
||||
after = session.dump_host()
|
||||
p_after = session.pixel_at(after, 9, 9)
|
||||
|
||||
assert p_after == p_base, (
|
||||
f"overlay did not hide on Alt release ({p_base} vs {p_after})")
|
||||
assert p_over != p_base, "overlay did not darken the top-left corner"
|
||||
assert sum(p_over) < sum(p_base), (
|
||||
f"overlay pixel not darker than content ({p_over} vs {p_base})")
|
||||
assert session.proc.poll() is None
|
||||
|
||||
|
||||
def test_non_ctrl_key_ignored(session):
|
||||
start = session.wait_any_active()
|
||||
assert start in (0, 1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue