feat: overlay titles via Pango+Cairo with full Unicode support
This commit is contained in:
parent
51faf6b7f0
commit
cb362b1519
5 changed files with 369 additions and 279 deletions
535
kick.c
535
kick.c
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue