Show per-tab app icons in the Alt-hold overlay
This commit is contained in:
parent
db6bbac84c
commit
b8875f7292
5 changed files with 487 additions and 15 deletions
361
tabbed-wlroots.c
361
tabbed-wlroots.c
|
|
@ -48,6 +48,10 @@
|
|||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
#include <cairo.h>
|
||||
#include <librsvg/rsvg.h>
|
||||
#include <png.h>
|
||||
|
||||
#include <wlr/backend.h>
|
||||
#include <wlr/backend/headless.h>
|
||||
#include <wlr/interfaces/wlr_keyboard.h>
|
||||
|
|
@ -89,6 +93,11 @@ struct tabbed_app {
|
|||
struct wl_listener request_minimize;
|
||||
struct wl_listener request_move;
|
||||
struct wl_listener request_resize;
|
||||
|
||||
/* Resolved overlay icon (a GL texture in host pixel space). */
|
||||
GLuint icon_tex;
|
||||
int icon_w, icon_h;
|
||||
bool icon_resolved; /* true once a load attempt has been made */
|
||||
};
|
||||
|
||||
struct tabbed_server {
|
||||
|
|
@ -379,6 +388,10 @@ static void shutdown_cleanup(void) {
|
|||
shutting_down = true;
|
||||
terminate_clients();
|
||||
for (int i = 0; i < server.num_apps; i++) {
|
||||
if (host.egl_ok && server.apps[i]->icon_tex) {
|
||||
glDeleteTextures(1, &server.apps[i]->icon_tex);
|
||||
server.apps[i]->icon_tex = 0;
|
||||
}
|
||||
free(server.apps[i]->cmd);
|
||||
free(server.apps[i]);
|
||||
}
|
||||
|
|
@ -704,6 +717,317 @@ static void overlay_font_fini(void) {
|
|||
overlay_font_ok = false;
|
||||
}
|
||||
|
||||
/* ===================== Overlay app icons ===================== */
|
||||
|
||||
/* Drawn square of the icon slot, in host pixels. */
|
||||
#define OVERLAY_ICON_SZ 28
|
||||
#ifndef MIN
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
static char *overlay_strndup(const char *s, size_t n) {
|
||||
char *r = malloc(n + 1);
|
||||
if (r) {
|
||||
memcpy(r, s, n);
|
||||
r[n] = '\0';
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static bool overlay_endswith(const char *s, const char *suffix) {
|
||||
size_t ls = strlen(s), lx = strlen(suffix);
|
||||
return ls >= lx && strcmp(s + ls - lx, suffix) == 0;
|
||||
}
|
||||
|
||||
/* Read the Icon= value (case-sensitive, not the localized Icon[lang]= form)
|
||||
out of a .desktop file, malloc'd, or NULL. */
|
||||
static char *desktop_icon_from_file(FILE *f) {
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strncmp(line, "Icon=", 5) != 0)
|
||||
continue;
|
||||
if (line[5] == '[') /* Icon[locale]= */
|
||||
continue;
|
||||
size_t n = strlen(line);
|
||||
while (n > 5 && (line[n - 1] == '\n' || line[n - 1] == '\r' ||
|
||||
line[n - 1] == ' ' || line[n - 1] == '\t'))
|
||||
n--;
|
||||
return overlay_strndup(line + 5, n - 5);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Find `<app_id>.desktop` under an XDG data dir and return its Icon= value,
|
||||
or NULL. */
|
||||
static char *desktop_icon_name(const char *app_id) {
|
||||
const char *data_home = getenv("XDG_DATA_HOME");
|
||||
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
||||
if (!data_dirs || !data_dirs[0])
|
||||
data_dirs = "/usr/local/share:/usr/share";
|
||||
char buf[4096];
|
||||
|
||||
if (data_home && data_home[0]) {
|
||||
snprintf(buf, sizeof(buf), "%s/applications/%s.desktop",
|
||||
data_home, app_id);
|
||||
FILE *f = fopen(buf, "r");
|
||||
if (f) {
|
||||
char *icon = desktop_icon_from_file(f);
|
||||
fclose(f);
|
||||
if (icon) return icon;
|
||||
}
|
||||
}
|
||||
char *copy = strdup(data_dirs);
|
||||
if (!copy) return NULL;
|
||||
for (char *p = copy, *dir; p; p = dir) {
|
||||
dir = strchr(p, ':');
|
||||
if (dir) *dir++ = '\0';
|
||||
if (p[0]) {
|
||||
snprintf(buf, sizeof(buf), "%s/applications/%s.desktop", p, app_id);
|
||||
FILE *f = fopen(buf, "r");
|
||||
if (f) {
|
||||
char *icon = desktop_icon_from_file(f);
|
||||
fclose(f);
|
||||
if (icon) {
|
||||
free(copy);
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
free(copy);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Turn an Icon= value (theme name, bare filename or absolute path) into a
|
||||
loadable .png/.svg path, or NULL. */
|
||||
static char *overlay_find_icon_file(const char *name) {
|
||||
static const char *const themes[] = { "Adwaita", "hicolor", "default" };
|
||||
static const char *const sizes[] =
|
||||
{ "scalable", "48x48", "32x32", "24x24" };
|
||||
static const char *const exts[] = { "png", "svg" };
|
||||
char roots[8][512];
|
||||
int n_roots = 0;
|
||||
const char *data_home = getenv("XDG_DATA_HOME");
|
||||
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
||||
if (data_home && data_home[0] && n_roots < 8)
|
||||
snprintf(roots[n_roots++], sizeof(roots[0]), "%s/icons", data_home);
|
||||
if (!data_dirs || !data_dirs[0])
|
||||
data_dirs = "/usr/local/share:/usr/share";
|
||||
char *copy = strdup(data_dirs);
|
||||
for (char *p = copy, *dir; p && n_roots < 8; p = dir) {
|
||||
dir = strchr(p, ':');
|
||||
if (dir) *dir++ = '\0';
|
||||
if (p[0])
|
||||
snprintf(roots[n_roots++], sizeof(roots[0]), "%s/icons", p);
|
||||
}
|
||||
free(copy);
|
||||
|
||||
if (strchr(name, '/') && access(name, R_OK) == 0 &&
|
||||
(overlay_endswith(name, ".png") ||
|
||||
overlay_endswith(name, ".svg")))
|
||||
return strdup(name);
|
||||
|
||||
char buf[4096];
|
||||
for (int tr = 0; tr < n_roots; tr++) {
|
||||
for (size_t t = 0; t < sizeof(themes) / sizeof(themes[0]); t++) {
|
||||
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++) {
|
||||
for (size_t e = 0; e < sizeof(exts) / sizeof(exts[0]); e++) {
|
||||
snprintf(buf, sizeof(buf), "%s/%s/%s/apps/%s.%s",
|
||||
roots[tr], themes[t], sizes[s], name, exts[e]);
|
||||
if (access(buf, R_OK) == 0)
|
||||
return strdup(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static const char *const pixmap_roots[] = {
|
||||
"/usr/share/pixmaps", "/usr/local/share/pixmaps",
|
||||
};
|
||||
for (size_t pr = 0; pr < sizeof(pixmap_roots) / sizeof(pixmap_roots[0]); pr++) {
|
||||
for (size_t e = 0; e < sizeof(exts) / sizeof(exts[0]); e++) {
|
||||
snprintf(buf, sizeof(buf), "%s/%s.%s", pixmap_roots[pr], name, exts[e]);
|
||||
if (access(buf, R_OK) == 0)
|
||||
return strdup(buf);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Decode a PNG into straight (non-premultiplied) RGBA8, malloc'd. */
|
||||
static unsigned char *overlay_load_png(const char *path, int *w, int *h) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL,
|
||||
NULL);
|
||||
if (!png) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
png_infop info = png_create_info_struct(png);
|
||||
if (!info) {
|
||||
png_destroy_read_struct(&png, NULL, NULL);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
if (setjmp(png_jmpbuf(png))) {
|
||||
png_destroy_read_struct(&png, &info, NULL);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
png_init_io(png, f);
|
||||
png_read_info(png, info);
|
||||
int bw = (int)png_get_image_width(png, info);
|
||||
int bh = (int)png_get_image_height(png, info);
|
||||
png_byte ct = png_get_color_type(png, info);
|
||||
png_byte bd = png_get_bit_depth(png, info);
|
||||
if (ct == PNG_COLOR_TYPE_PALETTE)
|
||||
png_set_palette_to_rgb(png);
|
||||
if (ct == PNG_COLOR_TYPE_GRAY && bd < 8)
|
||||
png_set_expand_gray_1_2_4_to_8(png);
|
||||
if (png_get_valid(png, info, PNG_INFO_tRNS))
|
||||
png_set_tRNS_to_alpha(png);
|
||||
if (bd == 16)
|
||||
png_set_strip_16(png);
|
||||
if (ct == PNG_COLOR_TYPE_RGB || ct == PNG_COLOR_TYPE_GRAY)
|
||||
png_set_filler(png, 0xff, PNG_FILLER_AFTER);
|
||||
if (ct == PNG_COLOR_TYPE_GRAY || ct == PNG_COLOR_TYPE_GRAY_ALPHA)
|
||||
png_set_gray_to_rgb(png);
|
||||
png_read_update_info(png, info);
|
||||
|
||||
size_t rowbytes = png_get_rowbytes(png, info);
|
||||
size_t size = (size_t)bh * rowbytes;
|
||||
unsigned char *rgba = malloc(size);
|
||||
if (!rgba) {
|
||||
png_destroy_read_struct(&png, &info, NULL);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
png_bytep *rows = malloc(sizeof(png_bytep) * (size_t)bh);
|
||||
if (!rows) {
|
||||
free(rgba);
|
||||
png_destroy_read_struct(&png, &info, NULL);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
for (int y = 0; y < bh; y++)
|
||||
rows[y] = rgba + (size_t)y * rowbytes;
|
||||
png_read_image(png, rows);
|
||||
free(rows);
|
||||
png_destroy_read_struct(&png, &info, NULL);
|
||||
fclose(f);
|
||||
*w = bw;
|
||||
*h = bh;
|
||||
return rgba;
|
||||
}
|
||||
|
||||
/* Decode an SVG into straight RGBA8 at its intrinsic size, malloc'd. */
|
||||
static unsigned char *overlay_load_svg(const char *path, int *w, int *h) {
|
||||
GError *err = NULL;
|
||||
RsvgHandle *handle = rsvg_handle_new_from_file(path, &err);
|
||||
if (!handle || err) {
|
||||
if (err) g_error_free(err);
|
||||
return NULL;
|
||||
}
|
||||
double iw = 0, ih = 0;
|
||||
if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &iw, &ih))
|
||||
iw = ih = 48.0; /* dimension-less SVG: assume an icon-sized square */
|
||||
if (iw <= 0 || ih <= 0) {
|
||||
g_object_unref(handle);
|
||||
return NULL;
|
||||
}
|
||||
int bw = (int)iw, bh = (int)ih;
|
||||
cairo_surface_t *surf =
|
||||
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, bw, bh);
|
||||
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
|
||||
cairo_surface_destroy(surf);
|
||||
g_object_unref(handle);
|
||||
return NULL;
|
||||
}
|
||||
cairo_t *cr = cairo_create(surf);
|
||||
RsvgRectangle viewport = { 0, 0, iw, ih };
|
||||
if (!rsvg_handle_render_document(handle, cr, &viewport, &err)) {
|
||||
if (err) g_error_free(err);
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surf);
|
||||
g_object_unref(handle);
|
||||
return NULL;
|
||||
}
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_flush(surf);
|
||||
|
||||
/* Cairo's ARGB32 is premultiplied, little-endian byte order B,G,R,A. */
|
||||
int n = bw * bh;
|
||||
unsigned char *rgba = malloc(sizeof(unsigned char) * 4 * (size_t)n);
|
||||
if (rgba) {
|
||||
unsigned char *src = cairo_image_surface_get_data(surf);
|
||||
for (int i = 0; i < n; i++) {
|
||||
unsigned char a = src[i * 4 + 3];
|
||||
unsigned char r = src[i * 4 + 2];
|
||||
unsigned char g = src[i * 4 + 1];
|
||||
unsigned char b = src[i * 4 + 0];
|
||||
if (a > 0) {
|
||||
r = (unsigned char)MIN((r * 255 + a / 2) / a, 255);
|
||||
g = (unsigned char)MIN((g * 255 + a / 2) / a, 255);
|
||||
b = (unsigned char)MIN((b * 255 + a / 2) / a, 255);
|
||||
}
|
||||
rgba[i * 4 + 0] = r;
|
||||
rgba[i * 4 + 1] = g;
|
||||
rgba[i * 4 + 2] = b;
|
||||
rgba[i * 4 + 3] = a;
|
||||
}
|
||||
}
|
||||
cairo_surface_destroy(surf);
|
||||
g_object_unref(handle);
|
||||
*w = bw;
|
||||
*h = bh;
|
||||
return rgba;
|
||||
}
|
||||
|
||||
/* Resolve and upload `app`'s icon as a GL_RGBA texture (host EGL context
|
||||
must be current). Best-effort: on any failure the app keeps no icon and
|
||||
the overlay draws the placeholder instead. */
|
||||
static void overlay_icon_load(struct tabbed_app *app) {
|
||||
app->icon_resolved = true;
|
||||
const char *app_id = app->toplevel ? app->toplevel->app_id : NULL;
|
||||
if (!app_id || !app_id[0])
|
||||
return;
|
||||
|
||||
char *icon = desktop_icon_name(app_id);
|
||||
char *path = overlay_find_icon_file(icon ? icon : app_id);
|
||||
free(icon);
|
||||
if (!path)
|
||||
return;
|
||||
|
||||
int w = 0, h = 0;
|
||||
unsigned char *rgba = NULL;
|
||||
if (overlay_endswith(path, ".svg"))
|
||||
rgba = overlay_load_svg(path, &w, &h);
|
||||
else if (overlay_endswith(path, ".png"))
|
||||
rgba = overlay_load_png(path, &w, &h);
|
||||
free(path);
|
||||
if (!rgba || w <= 0 || h <= 0) {
|
||||
free(rgba);
|
||||
return;
|
||||
}
|
||||
|
||||
GLuint tex;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, 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);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, rgba);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
free(rgba);
|
||||
app->icon_tex = tex;
|
||||
app->icon_w = w;
|
||||
app->icon_h = h;
|
||||
tabbed_log(TABBED_LOG_INFO, "[overlay] icon %s %dx%d\n", app_id, w, h);
|
||||
}
|
||||
|
||||
static void draw_rect(int x, int y, int w, int h,
|
||||
float r, float g, float b, float a)
|
||||
{
|
||||
|
|
@ -755,24 +1079,28 @@ static void draw_tab_overlay(void) {
|
|||
if (!host.overlay_visible || server.num_apps < 1) return;
|
||||
|
||||
int margin = 8, boxpad = 6;
|
||||
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8;
|
||||
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||
int max_title_w = 0, n_rows = 0;
|
||||
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
||||
int max_row_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;
|
||||
if (!app->icon_resolved)
|
||||
overlay_icon_load(app);
|
||||
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;
|
||||
int w = icon_sz + icon_gap + overlay_text_width(title);
|
||||
if (w > max_row_w) max_row_w = w;
|
||||
n_rows++;
|
||||
}
|
||||
if (n_rows == 0) return;
|
||||
|
||||
int box_w = max_title_w + 2 * boxpad;
|
||||
int box_w = max_row_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;
|
||||
|
|
@ -793,11 +1121,28 @@ static void draw_tab_overlay(void) {
|
|||
int ry = box_y + boxpad + row * row_h;
|
||||
bool active = app->toplevel == server.active;
|
||||
if (active) {
|
||||
int hw = overlay_text_width(title) + 4;
|
||||
int hw = icon_sz + icon_gap + 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,
|
||||
int ix = box_x + boxpad;
|
||||
int iy = ry + (row_h - icon_sz) / 2;
|
||||
if (app->icon_tex) {
|
||||
GLfloat tint[4] = { active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.72f, 1.0f };
|
||||
draw_texture_rect(app->icon_tex, app->icon_w, app->icon_h,
|
||||
0, 0, app->icon_w, app->icon_h,
|
||||
ix, iy, icon_sz, icon_sz,
|
||||
host.width, host.height, tint);
|
||||
} else {
|
||||
/* Generic placeholder: a bordered square for apps with no icon. */
|
||||
draw_rect(ix, iy, icon_sz, icon_sz, 0.45f, 0.45f, 0.48f, 0.85f);
|
||||
draw_rect(ix + 3, iy + 3, icon_sz - 6, icon_sz - 6,
|
||||
0.07f, 0.07f, 0.09f, 1.0f);
|
||||
}
|
||||
draw_text(title, ix + icon_sz + icon_gap,
|
||||
ry + (row_h - host.overlay_font_h) / 2,
|
||||
active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.72f,
|
||||
active ? 1.0f : 0.72f,
|
||||
|
|
@ -916,6 +1261,10 @@ static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
|
|||
struct wlr_xdg_toplevel *toplevel = app->toplevel;
|
||||
app->toplevel = NULL;
|
||||
app->tree = NULL;
|
||||
if (app->icon_tex) {
|
||||
glDeleteTextures(1, &app->icon_tex);
|
||||
app->icon_tex = 0;
|
||||
}
|
||||
|
||||
/* wlroots emits destroy with data=NULL and asserts every toplevel
|
||||
event listener list is empty afterwards; remove all listeners we
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue