feat: overlay titles via Pango+Cairo with full Unicode support

This commit is contained in:
Andrew Hamilton 2026-09-13 11:59:07 +10:00
parent 51faf6b7f0
commit cb362b1519
5 changed files with 369 additions and 279 deletions

535
kick.c
View file

@ -70,10 +70,8 @@ const char *__ubsan_default_options(void) {
#define XKB_PREFIX_MOD4_NAME XKB_MOD_NAME_LOGO
#endif
#include <ft2build.h>
#include FT_FREETYPE_H
#include <cairo.h>
#include <pango/pangocairo.h>
#include <librsvg/rsvg.h>
#include <png.h>
@ -692,7 +690,6 @@ struct kick_server {
bool standalone_headless;
struct wlr_session *session;
struct wl_listener new_input;
struct wlr_texture *overlay_atlas_wlr; /* standalone: overlaid atlas */
/* Standalone native keyboard devices (one entry per libinput keyboard;
see standalone_kbdev below). */
@ -716,12 +713,20 @@ static int start_cmd_index = -1;
/* ===================== 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 */
/* One rasterized overlay text run: white coverage tile (grayscale
antialiasing, premultiplied) drawn through the existing tint path, so
row dimming and the translucent cursor highlight behave exactly as
with the old per-glyph quads. Tiles are keyed by (string, width
budget) and cached for the session; Pango shapes UTF-8 directly, so
every glyph the fonts provide renders -- no fixed character set. */
struct overlay_text_entry {
char *text; /* owned UTF-8 copy, sanitized */
int max_px; /* ellipsization budget, INT_MAX = natural */
struct wlr_texture *tex; /* NULL when nothing drawable came out */
int w, h; /* pixel size (ellipsized) */
int baseline; /* pixels from tile top */
bool rastered; /* a raster attempt already ran */
unsigned long last_used; /* LRU clock */
};
struct kick_host {
@ -779,8 +784,8 @@ struct kick_host {
GLuint import_tex;
/* Alt-held tab-title overlay */
bool overlay_visible;
struct overlay_glyph overlay_glyphs[95]; /* chars 32..126 */
int overlay_font_h; /* ascent + descent, pixels */
int overlay_font_asc; /* ascender, 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;
@ -1510,10 +1515,6 @@ static void shutdown_cleanup(void) {
}
wl_list_init(&server.touchdevs);
if (server.overlay_atlas_wlr) {
wlr_texture_destroy(server.overlay_atlas_wlr);
server.overlay_atlas_wlr = NULL;
}
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);
@ -1608,7 +1609,7 @@ static bool gl_setup(void) {
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. */
from a 1x1 white texture. */
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 dst_w, int dst_h,
@ -1682,162 +1683,232 @@ static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
still-black tab shows its command line instead. No per-client listeners
are needed. */
static FT_Library overlay_ft_lib;
static FT_Face overlay_ft_face;
static PangoLayout *overlay_layout;
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",
};
/* Overlay text tile cache: session-persistent, LRU-capped. Titles repeat
across holds (same tabs, mostly same titles), so the first hold warms it
and later holds hit; changed titles are just new keys. Evicted textures
are destroyed inline like icon textures. */
#define OVERLAY_TEXT_CACHE 64
static struct overlay_text_entry overlay_text_cache[OVERLAY_TEXT_CACHE];
static unsigned long overlay_text_clock;
/* KICK_FONT names a Pango font family description ("DejaVu Sans",
"Sans Bold 30"); Pango + fontconfig resolve it and fall back for glyphs
the family lacks (CJK, emoji). */
static const char *overlay_pick_family(void) {
const char *env = getenv("KICK_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;
return (env && env[0]) ? env : "Sans";
}
/* 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. */
/* Sanitize overlay text: ASCII controls (including newline, which Pango
would break lines on) become spaces, preserving the old atlas behavior
where bytes <32 drew blank. Multi-byte UTF-8 passes through untouched. */
static size_t overlay_sanitize(char *dst, const char *src, size_t cap) {
size_t n = 0;
for (const unsigned char *p = (const unsigned char *)src;
*p && n + 1 < cap; p++) {
dst[n++] = (*p < 32 || *p == 127) ? ' ' : (char)*p;
}
dst[n] = '\0';
return n;
}
/* Back an over-long synthesized title off to a UTF-8 character boundary
so buffers never end mid-sequence (Pango would show replacement glyphs
for the split tail). */
static void overlay_utf8_cut(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] & 0xC0) == 0x80)
n--;
s[n] = '\0';
}
/* Set up the shared Pango layout for overlay text. CPU-side only (no GL
context needed): rasterization happens per string on first draw. Like
labwc, shaping/fallback come from Pango + fontconfig, so any UTF-8 the
fonts provide renders. Non-fatal: on failure the overlay still renders
its boxes, just without text. */
static void overlay_font_init(void) {
static bool overlay_font_done;
if (overlay_font_done) return;
overlay_font_done = true;
int px = OVERLAY_FONT_PX;
static const char *overlay_font_path = NULL;
if (!overlay_ft_lib) {
const char *path = overlay_pick_font();
if (!path) {
kick_log(KICK_LOG_INFO, "[overlay] no font found; "
"set KICK_FONT to enable overlay text\n");
return;
}
overlay_font_path = path;
if (FT_Init_FreeType(&overlay_ft_lib) != 0) {
kick_log(KICK_LOG_INFO, "[overlay] FreeType init failed\n");
return;
}
if (FT_New_Face(overlay_ft_lib, path, 0, &overlay_ft_face) != 0) {
kick_log(KICK_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) {
kick_log(KICK_LOG_INFO, "[overlay] cannot set pixel size %d\n", px);
const char *family = overlay_pick_family();
PangoFontMap *map = pango_cairo_font_map_get_default();
PangoContext *ctx = pango_font_map_create_context(map);
overlay_layout = pango_layout_new(ctx);
g_object_unref(ctx);
if (!overlay_layout) {
kick_log(KICK_LOG_INFO, "[overlay] Pango layout failed\n");
return;
}
PangoFontDescription *desc = pango_font_description_from_string(family);
pango_font_description_set_absolute_size(desc,
(double)OVERLAY_FONT_PX * PANGO_SCALE);
pango_layout_set_font_description(overlay_layout, desc);
PangoContext *lctx = pango_layout_get_context(overlay_layout);
PangoFontMetrics *metrics =
pango_context_get_metrics(lctx, desc, NULL);
int asc = metrics ?
PANGO_PIXELS(pango_font_metrics_get_ascent(metrics)) : OVERLAY_FONT_PX;
int height = metrics ? PANGO_PIXELS(
pango_font_metrics_get_ascent(metrics) +
pango_font_metrics_get_descent(metrics)) : OVERLAY_FONT_PX;
if (metrics)
pango_font_metrics_unref(metrics);
pango_font_description_free(desc);
host.overlay_font_asc = asc > 0 ? asc : OVERLAY_FONT_PX;
host.overlay_font_h = height > 0 ? height : OVERLAY_FONT_PX;
overlay_font_ok = true;
kick_log(KICK_LOG_INFO, "[overlay] font %s ready\n", family);
}
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) {
kick_log(KICK_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;
}
/* Hand the glyph bitmap to the wlroots renderer as a premultiplied-
white texture (r=g=b=a); the render pass tints it via alpha alone. */
{
unsigned char *rgba = malloc((size_t)atlas_w * atlas_h * 4);
if (rgba) {
for (size_t i = 0; i < (size_t)atlas_w * atlas_h; i++) {
unsigned char v = atlas[i * 2];
rgba[i * 4 + 0] = v;
rgba[i * 4 + 1] = v;
rgba[i * 4 + 2] = v;
rgba[i * 4 + 3] = v;
}
server.overlay_atlas_wlr = wlr_texture_from_pixels(
server.renderer, DRM_FORMAT_ARGB8888,
(uint32_t)atlas_w * 4, (uint32_t)atlas_w, (uint32_t)atlas_h,
rgba);
}
free(rgba);
free(atlas);
host.overlay_font_h = height;
overlay_font_ok = server.overlay_atlas_wlr != NULL;
kick_log(KICK_LOG_INFO, "[overlay] font %s glyphs=%d atlas=%dx%d\n",
overlay_font_path, n_glyphs, atlas_w, atlas_h);
static void overlay_text_entry_free(struct overlay_text_entry *e) {
free(e->text);
e->text = NULL;
if (e->tex) {
wlr_texture_destroy(e->tex);
e->tex = NULL;
}
e->w = e->h = e->baseline = 0;
e->rastered = false;
}
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;
for (size_t i = 0; i < OVERLAY_TEXT_CACHE; i++)
overlay_text_entry_free(&overlay_text_cache[i]);
if (overlay_layout) {
g_object_unref(overlay_layout);
overlay_layout = NULL;
}
/* labwc's workaround: release Pango's default font map at exit so its
dedicated thread is gone before our SIGTERM teardown path runs. */
pango_cairo_font_map_set_default(NULL);
overlay_font_ok = false;
}
/* Rasterize one overlay entry into a white coverage tile (grayscale
antialiasing: subpixel rendering over our translucent blending path
misbehaves, per labwc's findings) and upload it like the icon path.
Reconfigures the shared layout from the entry first: other shapes may
have run since this entry was measured. Only called for strings that
are actually drawn, so the raster log proves what reached the screen. */
static struct wlr_texture *overlay_text_raster(struct overlay_text_entry *e) {
char clean[512];
overlay_sanitize(clean, e->text ? e->text : "", sizeof(clean));
pango_layout_set_text(overlay_layout, clean, -1);
if (e->max_px == INT_MAX) {
pango_layout_set_width(overlay_layout, -1);
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_NONE);
} else {
int budget = e->max_px < 0 ? 0 : e->max_px;
pango_layout_set_width(overlay_layout, budget * PANGO_SCALE);
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_END);
}
int w = 0, h = 0;
pango_layout_get_pixel_size(overlay_layout, &w, &h);
e->w = w;
e->h = h;
e->baseline = PANGO_PIXELS(pango_layout_get_baseline(overlay_layout));
e->rastered = true;
if (w <= 0 || h <= 0)
return NULL;
cairo_surface_t *surf =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
cairo_surface_destroy(surf);
return NULL;
}
cairo_t *cr = cairo_create(surf);
cairo_font_options_t *opts = cairo_font_options_create();
cairo_font_options_set_antialias(opts, CAIRO_ANTIALIAS_GRAY);
cairo_set_font_options(cr, opts);
cairo_font_options_destroy(opts);
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
pango_cairo_show_layout(cr, overlay_layout);
cairo_destroy(cr);
cairo_surface_flush(surf);
int stride = cairo_image_surface_get_stride(surf);
unsigned char *src = cairo_image_surface_get_data(surf);
/* Cairo ARGB32 is premultiplied; the source is white, so coverage
equals alpha and the tile uploads as premultiplied white (r=g=b=a)
exactly like the old atlas did. */
unsigned char *rgba = malloc((size_t)w * h * 4);
struct wlr_texture *tex = NULL;
if (rgba && src) {
for (int y = 0; y < h; y++) {
unsigned char *srow = src + (size_t)y * stride;
unsigned char *drow = rgba + (size_t)y * w * 4;
for (int x = 0; x < w; x++) {
unsigned char a = srow[x * 4 + 3];
drow[x * 4 + 0] = a;
drow[x * 4 + 1] = a;
drow[x * 4 + 2] = a;
drow[x * 4 + 3] = a;
}
}
tex = wlr_texture_from_pixels(server.renderer,
DRM_FORMAT_ARGB8888, (uint32_t)w * 4, (uint32_t)w, (uint32_t)h,
rgba);
}
free(rgba);
cairo_surface_destroy(surf);
if (tex)
kick_log(KICK_LOG_DEBUG, "[overlay] raster: '%s' %dx%d\n", clean, w, h);
return tex;
}
/* Cached overlay text run for (string, width budget). Lookup shapes only
(cheap, no GL); the texture rasterizes on first draw, so measuring for
layout never uploads textures nobody paints. Session-persistent LRU:
titles repeat across holds, so later holds hit. */
static void overlay_text_shape(struct overlay_text_entry *e) {
char clean[512];
overlay_sanitize(clean, e->text ? e->text : "", sizeof(clean));
pango_layout_set_text(overlay_layout, clean, -1);
if (e->max_px == INT_MAX) {
pango_layout_set_width(overlay_layout, -1);
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_NONE);
} else {
int budget = e->max_px < 0 ? 0 : e->max_px;
pango_layout_set_width(overlay_layout, budget * PANGO_SCALE);
pango_layout_set_ellipsize(overlay_layout, PANGO_ELLIPSIZE_END);
}
pango_layout_get_pixel_size(overlay_layout, &e->w, &e->h);
e->baseline = PANGO_PIXELS(pango_layout_get_baseline(overlay_layout));
if (pango_layout_is_ellipsized(overlay_layout))
kick_log(KICK_LOG_DEBUG, "[overlay] ellipsis: '%s' budget=%d\n",
e->text ? e->text : "", e->max_px);
}
static struct overlay_text_entry *overlay_text_entry(const char *s,
int max_px) {
if (!s) s = "";
struct overlay_text_entry *oldest = &overlay_text_cache[0];
for (size_t i = 0; i < OVERLAY_TEXT_CACHE; i++) {
struct overlay_text_entry *e = &overlay_text_cache[i];
if (e->text && e->max_px == max_px && strcmp(e->text, s) == 0) {
e->last_used = ++overlay_text_clock;
return e;
}
if (!e->text || e->last_used < oldest->last_used)
oldest = e;
}
overlay_text_entry_free(oldest);
oldest->text = strdup(s);
if (!oldest->text)
return NULL;
oldest->max_px = max_px;
oldest->last_used = ++overlay_text_clock;
overlay_text_shape(oldest);
return oldest;
}
/* ===================== Overlay app icons ===================== */
/* Drawn square of the icon slot, in host pixels. */
@ -2254,74 +2325,29 @@ static void handle_icon_set(struct wl_listener *listener, void *data) {
overlay_refresh_if_visible();
}
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;
/* Cached overlay text run for (string, width budget): lookup shapes only
(cheap, no GL), for measuring layout. Rasterization happens exclusively
in overlay_text_draw_tile, so the raster log proves what reached the
screen and measuring never uploads textures nobody paints. */
static struct overlay_text_entry *overlay_text_tile(const char *s,
int max_px) {
if (!overlay_font_ok || !overlay_layout) return NULL;
return overlay_text_entry(s, max_px);
}
/* Copy `src` into `out`, truncated so that its on-screen width plus a
trailing "..." fits within `max_px` when the title does not already fit
whole. Uses the same advance rules as overlay_text_width(). A title that
fits within `max_px` is copied verbatim, so a row sized to a title's natural
width never grows a needless ellipsis (e.g. "foot" staying "foot" instead of
shrinking to "fo..."). Falls back to an empty string when not even one dot
fits. */
static void fit_text(const char *src, int max_px, char *out, int out_sz)
{
out[0] = '\0';
if (!src || out_sz <= 1) return;
/* Draw-path lookup: measure, then rasterize on first draw. */
static struct overlay_text_entry *overlay_text_draw_tile(const char *s,
int max_px) {
struct overlay_text_entry *e = overlay_text_tile(s, max_px);
if (!e) return NULL;
if (!e->rastered && e->text && e->text[0])
e->tex = overlay_text_raster(e);
return e;
}
if (overlay_text_width(src) <= max_px) {
size_t n = 0;
for (const unsigned char *p = (const unsigned char *)src; *p; p++) {
if (n + 1 >= (size_t)out_sz)
break;
out[n++] = (char)*p;
}
out[n] = '\0';
return;
}
const char *dots = "...";
int dot_w = 0;
for (const unsigned char *p = (const unsigned char *)dots; *p; p++) {
int c = *p;
if (c < 32 || c > 126) { dot_w += 4; continue; }
dot_w += host.overlay_glyphs[c - 32].adv;
}
if (max_px < dot_w) return;
kick_log(KICK_LOG_DEBUG, "[overlay] ellipsis: '%s' budget=%d\n",
src, max_px);
int w = 0;
size_t n = 0;
const unsigned char *p;
for (p = (const unsigned char *)src; *p; p++) {
int c = *p;
int adv = (c < 32 || c > 126) ? 4 : host.overlay_glyphs[c - 32].adv;
if (w + adv + dot_w > max_px)
break;
if (n + 1 >= (size_t)out_sz)
break;
out[n++] = (char)c;
w += adv;
}
if (*p) {
if (n + 4 < (size_t)out_sz) {
memcpy(out + n, dots, 3);
n += 3;
}
}
out[n] = '\0';
static int overlay_text_width(const char *s) {
struct overlay_text_entry *e = overlay_text_tile(s, INT_MAX);
return e ? e->w : 0;
}
/* Row title for an overlay tab: the window the tab is actually showing --
@ -2344,6 +2370,7 @@ static const char *tab_row_title(struct kick_tab *tab) {
kick_log(KICK_LOG_INFO, "[title] app=%d '%s'\n",
tab_index(tab), cur);
snprintf(pick->row_title, sizeof(pick->row_title), "%s", cur);
overlay_utf8_cut(pick->row_title);
}
return t;
}
@ -2377,6 +2404,7 @@ static const char *overlay_window_title(struct kick_window *w,
snprintf(buf, sz, "%s [%s]", base, w->tag);
else
snprintf(buf, sz, "%s", base);
overlay_utf8_cut(buf);
return buf;
}
@ -2402,8 +2430,8 @@ static bool tab_overlay_row(struct kick_tab *tab) {
Both modes composite the scene into an output buffer with wlroots and
paint the overlay onto that buffer as a second render pass -- nested
before blitting the frame to its host window, standalone before commit.
One renderer, one layout, one set of assets: the glyph atlas and icons
exist only as wlr textures. */
One renderer, one layout, one set of assets: the text tiles and icons
exist only as wlr textures. */
static struct wlr_render_color overlay_premul(float r, float g, float b, float a) {
return (struct wlr_render_color){ r * a, g * a, b * a, a };
@ -2422,32 +2450,22 @@ static void overlay_pass_rect(struct wlr_render_pass *pass,
}
static void overlay_pass_text(struct wlr_render_pass *pass, const char *s,
int x, int y, float alpha)
int max_px, int x, int y, float alpha)
{
if (!overlay_font_ok || !server.overlay_atlas_wlr) return;
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];
if (glyph->pw > 0 && glyph->ph > 0) {
struct wlr_render_texture_options o = {
.texture = server.overlay_atlas_wlr,
.src_box = { .x = glyph->ax, .y = glyph->ay,
.width = glyph->pw, .height = glyph->ph },
.dst_box = { .x = pen, .y = y + glyph->top,
.width = glyph->pw, .height = glyph->ph },
.alpha = &alpha,
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
};
wlr_render_pass_add_texture(pass, &o);
}
pen += glyph->adv;
}
if (!overlay_font_ok) return;
struct overlay_text_entry *e = overlay_text_draw_tile(s, max_px);
if (!e || !e->tex) return;
/* Same baseline the old per-glyph quads used (ascender below y). */
struct wlr_render_texture_options o = {
.texture = e->tex,
.src_box = { .x = 0, .y = 0, .width = e->w, .height = e->h },
.dst_box = { .x = x, .y = y + host.overlay_font_asc - e->baseline,
.width = e->w, .height = e->h },
.alpha = &alpha,
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
};
wlr_render_pass_add_texture(pass, &o);
}
static void overlay_pass_icon_tex(struct wlr_render_pass *pass,
@ -2468,7 +2486,7 @@ static void overlay_pass_icon_tex(struct wlr_render_pass *pass,
/* Confirm-before-quit modal, drawn in place of the Alt tab list while a quit
is pending: a centered panel with the live-window count and the y/n keys.
Same styling language as the overlay -- same backdrop, rim strokes, and
glyph atlas. */
text tiles. */
static void draw_confirm_quit_pass(struct wlr_render_pass *pass) {
int n = live_window_count();
char l1[96], l2[96];
@ -2504,8 +2522,8 @@ static void draw_confirm_quit_pass(struct wlr_render_pass *pass) {
int tw = overlay_text_width(s);
int x = box_x + boxpad + (row_inner - tw) / 2;
int y = box_y + boxpad + r * (row_h + row_gap);
overlay_pass_text(pass, s, x, y + (row_h - host.overlay_font_h) / 2,
0.95f);
overlay_pass_text(pass, s, INT_MAX,
x, y + (row_h - host.overlay_font_h) / 2, 0.95f);
}
}
@ -2533,8 +2551,9 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
struct kick_window *w;
wl_list_for_each(w, &tab->windows, link) {
if (!w->mapped) continue;
char tb[64];
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++;
}
@ -2585,7 +2604,9 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
int row_inner = box_w - 2 * boxpad;
struct kick_window *wins[32];
char fitt[32][256];
char tbuf[32][256];
const char *strs[32];
int shares[32];
int widths[32];
int n = 0;
struct kick_window *w;
@ -2605,10 +2626,15 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
}
int total_w = 0;
for (int k = 0; k < n; k++) {
char tb[64];
const char *dt = overlay_window_title(wins[k], tb, sizeof(tb));
fit_text(dt, share, fitt[k], sizeof(fitt[k]));
widths[k] = overlay_text_width(fitt[k]);
/* 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:
the buffer below is only filled in the tag/fallback cases. */
strs[k] = overlay_window_title(wins[k], tbuf[k], sizeof(tbuf[k]));
shares[k] = share;
struct overlay_text_entry *te =
overlay_text_tile(strs[k], share);
widths[k] = te ? te->w : 0;
total_w += icon_sz + icon_gap + widths[k];
}
if (n > 1) total_w += (n - 1) * member_gap;
@ -2619,16 +2645,15 @@ static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
struct kick_window *first = tab_first_window(tab);
const char *ct = (first && first->cmd && first->cmd[0])
? first->cmd : "pending";
char ctb[64];
int cavail = row_inner - (icon_sz + icon_gap) - 4;
if (cavail < 0) cavail = 0;
fit_text(ct, cavail, ctb, sizeof(ctb));
int ctw = overlay_text_width(ctb);
struct overlay_text_entry *cte = overlay_text_tile(ct, cavail);
int ctw = cte ? cte->w : 0;
int cmw = icon_sz + icon_gap + ctw;
int cpen = box_x + boxpad + (row_inner - cmw) / 2;
overlay_pass_rect(pass, cpen - 2, ry - 1, cmw + 4, row_h,
0.22f, 0.22f, 0.25f, 1.0f);
overlay_pass_text(pass, ctb, cpen + icon_sz + icon_gap,
overlay_pass_text(pass, ct, cavail, cpen + icon_sz + icon_gap,
ry + (row_h - host.overlay_font_h) / 2, 0.9f);
}
for (int k = 0; k < n; k++) {
@ -2651,7 +2676,7 @@ 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);
}
overlay_pass_text(pass, fitt[k], mx + icon_sz + icon_gap,
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;
@ -12393,7 +12418,7 @@ int main(int argc, char *argv[]) {
re-enters its dmabuf feedback setup for every instance -- segfault
on the duplicate (VLC died in QWaylandDisplay::registry_global). */
/* The overlay glyph atlas rides the wlr renderer in both modes. */
/* Overlay text setup is CPU-side (Pango) and mode-independent. */
overlay_font_init();
server.allocator = wlr_allocator_autocreate(server.backend, server.renderer);