From 54355b819037ed998a3a872c9c5ec13636815a93 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 14:37:30 +0900 Subject: [PATCH] popup: replace FreeType renderer with PangoCairo --- Dockerfile | 6 +- Makefile | 8 +- README.md | 15 ++- docs/PROVENANCE.md | 31 +++--- fn.h | 6 +- font.c | 254 ++++++++++++++++++++++----------------------- popup_layout.c | 12 --- run.sh | 2 + tests/Makefile | 8 +- tests/font_test.c | 239 ++++++++++++++++++++++++++++-------------- tests/popup_test.c | 2 - win.c | 32 +++--- 12 files changed, 349 insertions(+), 266 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9862144..83f873f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,17 @@ FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1 RUN pacman -Syu --noconfirm --needed \ + cairo \ + fontconfig \ diffutils \ - freetype2 \ gcc \ gtk3 \ + harfbuzz \ libxcb \ libxkbcommon \ make \ + noto-fonts-emoji \ + pango \ pkgconf \ plan9port \ python \ diff --git a/Makefile b/Makefile index ef85571..02335b3 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ CC = 9c LD = 9l PKG_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) PKG_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) -FT_CFLAGS = $(shell pkg-config --cflags freetype2) -FT_LIBS = $(shell pkg-config --libs freetype2) +TEXT_CFLAGS = $(shell pkg-config --cflags 'pangocairo >= 1.56' 'cairo >= 1.18') +TEXT_LIBS = $(shell pkg-config --libs 'pangocairo >= 1.56' 'cairo >= 1.18') CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS) PROG = strans DOCKER_IMAGE = strans-build @@ -17,10 +17,10 @@ OBJS = $(SRCS:.c=.o) all: $(PROG) xim gtk $(PROG): $(OBJS) - $(LD) -o $@ $(OBJS) -lthread -lbio -lxcb $(PKG_LIBS) $(FT_LIBS) + $(LD) -o $@ $(OBJS) -lthread -lbio -lxcb $(PKG_LIBS) $(TEXT_LIBS) $(OBJS): dat.h fn.h ipc.h -font.o: CFLAGS += $(FT_CFLAGS) +font.o: CFLAGS += $(TEXT_CFLAGS) clean: rm -f $(OBJS) $(PROG) diff --git a/README.md b/README.md index 6da56f6..488ca18 100644 --- a/README.md +++ b/README.md @@ -51,15 +51,24 @@ dependencies listed in [`Dockerfile`](Dockerfile) are installed. ## Run -The popup needs one to four scalable font files. No fonts are bundled. -`run.sh` uses installed DejaVu, Jigmo, or Noto CJK fonts when available, or -accepts explicit files in fallback order: +The popup needs one to four font files. No fonts are bundled. Each file is +validated and registered with Pango as an application font. Pango shapes the +complete string and chooses glyph fallback from those fonts and the normal +system fonts; command-line file order is not an exact per-glyph fallback +contract. + +`run.sh` uses installed Noto Color Emoji, DejaVu, Jigmo, or Noto CJK fonts when +available, or accepts explicit files. Supply a color emoji font such as Noto +Color Emoji to render emoji in color: ```sh ./run.sh ./run.sh /path/to/primary.ttf /path/to/fallback.ttc ``` +The popup renderer requires Pango/PangoCairo 1.56 or newer and Cairo 1.18 or +newer. + The script starts the daemon and, when `DISPLAY` is set, the XIM frontend. It restarts any existing `strans` processes owned by the current user. diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index c5b3b78..b30e08e 100644 --- a/docs/PROVENANCE.md +++ b/docs/PROVENANCE.md @@ -47,14 +47,20 @@ new SKK revision. ## Fonts No font binaries are bundled. The daemon receives one to four font file paths -on its command line and tries them in that order; it does not discover fonts by -family name or directory contents. `font/PROVENANCE` retains the exact -checksums, source revisions, and license records for the three Noto faces that -were formerly stored in the repository. +on its command line, validates them, and registers them as Pango application +fonts. Pango shapes each complete string and selects glyph fallback from the +registered fonts and the normal system font set. Exact command-line file order +is deliberately not promised: preserving it would require a separate +Fontconfig policy alongside Pango's fallback. `font/PROVENANCE` retains the +exact checksums, source revisions, and license records for the three Noto faces +that were formerly stored in the repository. The Docker renderer tests obtain their fonts from signed Arch packages rather -than the source checkout. On August 13, 2026 the tested package contract was: +than the source checkout. On August 14, 2026 the tested package contract was: +- `noto-fonts-emoji` `1:2.051-1`, providing + `/usr/share/fonts/noto/NotoColorEmoji.ttf` (SHA-256 + `72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b`); - `ttf-dejavu` `2.37+18+g9b5d1b2f-8`, providing `/usr/share/fonts/TTF/DejaVuSans.ttf` (SHA-256 `6038a160b491e121c1f12c7bccb4a9c8730296e3adc1086a059404ed84b7451c`); @@ -64,13 +70,14 @@ than the source checkout. On August 13, 2026 the tested package contract was: and `/usr/share/fonts/TTF/Jigmo2.ttf` (SHA-256 `5da3582efe77e22073b86b3b86b556d7111148a76b957cfb53318a91da2efff0`). -Pacman validates the package checksums and repository signatures. DejaVu Sans -supplies ASCII and the monochrome U+1F600 outline in the tests; Jigmo supplies -Hangul and BMP CJK, and Jigmo2 supplies U+2000B. Their installed license files -are `/usr/share/licenses/ttf-dejavu/LICENSE` and -`/usr/share/licenses/ttf-jigmo/LICENSE.txt`; the packages identify their terms -as the DejaVu custom license and CC0 1.0, respectively. These packages are -test fixtures, not names or paths hard-coded into the daemon. +Pacman validates the package checksums and repository signatures. Noto Color +Emoji supplies the color and shaped-sequence coverage; DejaVu Sans supplies +ASCII, Jigmo supplies Hangul and BMP CJK, and Jigmo2 supplies U+2000B. Their +installed license files are `/usr/share/licenses/noto-fonts-emoji/LICENSE`, +`/usr/share/licenses/ttf-dejavu/LICENSE`, and +`/usr/share/licenses/ttf-jigmo/LICENSE.txt`. The packages identify their terms +as OFL-1.1-no-RFN, the DejaVu custom license, and CC0 1.0, respectively. These +packages are test fixtures, not names or paths hard-coded into the daemon. ## Git submodules diff --git a/fn.h b/fn.h index a0989c4..bcde748 100644 --- a/fn.h +++ b/fn.h @@ -28,7 +28,6 @@ void dictthread(void*); void dictlookup(Dictreq*, Dictres*); void drawthread(void*); -int popupcells(Rune*, int); void popupposition(Caret*, int, int, int, int, int, int, int*, int*); void imthread(void*); Emit transmap(Im*, Rune); @@ -44,5 +43,6 @@ void ibusthread(void*); void* emalloc(ulong); void* erealloc(void*, ulong); -int fontinit(char**, int); -void putfont(u32int*, int, int, int, int, Rune); +int textinit(char**, int); +int textwidth(Str*); +void textdraw(u32int*, int, int, int, int, Str*); diff --git a/font.c b/font.c index d7e2f6f..cc1bbb0 100644 --- a/font.c +++ b/font.c @@ -1,168 +1,166 @@ #include "dat.h" -#include -#include FT_FREETYPE_H +#include #include "fn.h" -typedef struct Font Font; -struct Font +static PangoFontMap *fontmap; +static PangoContext *context; +static PangoLayout *layout; + +static void +textclear(void) { - FT_Face face; - int base; -}; - -static FT_Library lib; -static Font fonts[Maxfonts]; -static int nfonts; -static u32int blendtab[2][256]; - -static u32int -blend(u32int bg, u32int fg, int a) -{ - int r, g, b, inv; - - inv = 255 - a; - r = ((bg >> 16 & 0xff) * inv + (fg >> 16 & 0xff) * a) / 255; - g = ((bg >> 8 & 0xff) * inv + (fg >> 8 & 0xff) * a) / 255; - b = ((bg & 0xff) * inv + (fg & 0xff) * a) / 255; - return (r << 16) | (g << 8) | b; -} - -static int -loadfont(char *path) -{ - FT_Face face; - Font *f; - vlong height, pixels; - - if(nfonts >= nelem(fonts)) - return 0; - if(FT_New_Face(lib, path, 0, &face) != 0){ - fprint(2, "strans: popup: can't load font: %s\n", path); - return 0; - } - height = (vlong)face->ascender - face->descender; - if(height <= 0 || face->units_per_EM == 0){ - fprint(2, "strans: popup: invalid font metrics: %s\n", path); - FT_Done_Face(face); - return 0; - } - pixels = (vlong)Fontsz * face->units_per_EM / height; - if(pixels < 1) - pixels = 1; - if(FT_Set_Pixel_Sizes(face, 0, pixels) != 0){ - fprint(2, "strans: popup: can't size font: %s\n", path); - FT_Done_Face(face); - return 0; - } - f = &fonts[nfonts]; - f->face = face; - f->base = (vlong)Fontsz * face->ascender / height; - nfonts++; - return 1; + if(layout != nil) + g_object_unref(layout); + if(context != nil) + g_object_unref(context); + if(fontmap != nil) + g_object_unref(fontmap); + layout = nil; + context = nil; + fontmap = nil; } static void -clearfonts(void) +setfont(void) { - int i; + PangoFontDescription *font; + PangoRectangle r; + int size; - for(i = 0; i < nfonts; i++){ - FT_Done_Face(fonts[i].face); - fonts[i].face = nil; + font = pango_font_description_new(); + pango_font_description_set_family(font, "sans"); + pango_font_description_set_absolute_size(font, Fontsz * PANGO_SCALE); + pango_layout_set_font_description(layout, font); + pango_layout_set_text(layout, "Mg", -1); + pango_layout_get_pixel_extents(layout, nil, &r); + if(r.height > 0){ + /* Fontsz is the popup row height, not a point size. */ + size = Fontsz * PANGO_SCALE * Fontsz / r.height; + pango_font_description_set_absolute_size(font, max(size, PANGO_SCALE)); + pango_layout_set_font_description(layout, font); } - nfonts = 0; + pango_font_description_free(font); } int -fontinit(char **path, int npath) +textinit(char **path, int npath) { - int i, a; + GError *err; + int i; - if(path == nil || npath < 1 || npath > nelem(fonts)){ - fprint(2, "strans: popup: need 1-%d font files\n", nelem(fonts)); - clearfonts(); + textclear(); + if(path == nil || npath < 1 || npath > Maxfonts){ + fprint(2, "strans: popup: need 1-%d font files\n", Maxfonts); return 0; } - if(lib == nil && FT_Init_FreeType(&lib) != 0){ - fprint(2, "strans: popup: can't initialize FreeType\n"); + fontmap = pango_cairo_font_map_new(); + if(fontmap == nil){ + fprint(2, "strans: popup: can't initialize PangoCairo\n"); return 0; } - clearfonts(); for(i = 0; i < npath; i++){ - if(path[i] == nil || !loadfont(path[i])){ - clearfonts(); + if(path[i] == nil){ + fprint(2, "strans: popup: invalid font file\n"); + textclear(); + return 0; + } + err = nil; + if(!pango_font_map_add_font_file(fontmap, path[i], &err)){ + fprint(2, "strans: popup: can't register font: %s: %s\n", + path[i], err == nil ? "unknown error" : err->message); + if(err != nil) + g_error_free(err); + textclear(); return 0; } } - for(a = 0; a < 256; a++){ - blendtab[0][a] = blend(Colbg, Colfg, a); - blendtab[1][a] = blend(Colsel, Colfg, a); + pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96); + context = pango_font_map_create_context(fontmap); + if(context != nil) + layout = pango_layout_new(context); + if(context == nil || layout == nil){ + fprint(2, "strans: popup: can't create text layout\n"); + textclear(); + return 0; } + pango_layout_set_single_paragraph_mode(layout, TRUE); + setfont(); return 1; } static int -drawglyph(Font *f, u32int *buf, int w, int h, int px, int py) +settext(Str *s) { - FT_Bitmap *b; - FT_GlyphSlot g; - uchar *row; - vlong pitch, x0, x1, y0, y1; - int a, sel, x, xa, xb, y, ya, yb; - u32int *p; + char utf[Maxutf]; + int n; - g = f->face->glyph; - b = &g->bitmap; - if(b->pixel_mode != FT_PIXEL_MODE_GRAY || b->num_grays != 256) + if(layout == nil || s == nil || s->n <= 0) return 0; - if(b->width == 0 || b->rows == 0) - return 1; - pitch = b->pitch; - if(pitch < 0) - pitch = -pitch; - if(b->buffer == nil || pitch < b->width) + n = stoutf(s, utf, sizeof utf); + pango_layout_set_text(layout, utf, n); + return n > 0; +} + +static void +textextents(PangoRectangle *r) +{ + PangoRectangle ink, logical; + int x0, y0, x1, y1; + + pango_layout_get_pixel_extents(layout, &ink, &logical); + x0 = min(ink.x, logical.x); + y0 = min(ink.y, logical.y); + x1 = max(ink.x + ink.width, logical.x + logical.width); + y1 = max(ink.y + ink.height, logical.y + logical.height); + r->x = x0; + r->y = y0; + r->width = max(x1 - x0, 0); + r->height = max(y1 - y0, 0); +} + +int +textwidth(Str *s) +{ + PangoRectangle r; + + if(!settext(s)) return 0; - x0 = (vlong)px + g->bitmap_left; - y0 = (vlong)py + f->base - g->bitmap_top; - x1 = x0 + b->width; - y1 = y0 + b->rows; - if(x1 <= 0 || y1 <= 0 || x0 >= w || y0 >= h) - return 1; - xa = x0 < 0 ? 0 : x0; - xb = x1 > w ? w : x1; - ya = y0 < 0 ? 0 : y0; - yb = y1 > h ? h : y1; - for(y = ya; y < yb; y++){ - if(b->pitch < 0) - row = b->buffer + ((vlong)b->rows - 1 - (y - y0)) * pitch; - else - row = b->buffer + ((vlong)y - y0) * pitch; - for(x = xa; x < xb; x++){ - a = row[(vlong)x - x0]; - if(a != 0){ - p = &buf[y * w + x]; - sel = *p == Colsel; - *p = blendtab[sel][a]; - } - } - } - return 1; + textextents(&r); + return r.width; } void -putfont(u32int *buf, int w, int h, int px, int py, Rune r) +textdraw(u32int *buf, int w, int h, int x, int y, Str *s) { - FT_UInt g; - int f; + PangoRectangle r; + cairo_surface_t *surface; + cairo_t *cr; + int stride; - if(buf == nil || w <= 0 || h <= 0) + if(buf == nil || w <= 0 || h <= 0 || !settext(s)) + return; + stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w); + if(stride < 0 || (vlong)stride != (vlong)w * sizeof buf[0]) + return; + surface = cairo_image_surface_create_for_data((uchar*)buf, + CAIRO_FORMAT_RGB24, w, h, stride); + if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){ + cairo_surface_destroy(surface); return; - for(f = 0; f < nfonts; f++){ - g = FT_Get_Char_Index(fonts[f].face, r); - if(g == 0 || FT_Load_Glyph(fonts[f].face, g, FT_LOAD_DEFAULT) != 0 || - FT_Render_Glyph(fonts[f].face->glyph, FT_RENDER_MODE_NORMAL) != 0) - continue; - if(drawglyph(&fonts[f], buf, w, h, px, py)) - return; } + cairo_surface_mark_dirty(surface); + cr = cairo_create(surface); + if(cairo_status(cr) == CAIRO_STATUS_SUCCESS){ + pango_cairo_update_layout(cr, layout); + textextents(&r); + /* A fallback face must not paint into another popup row. */ + cairo_rectangle(cr, 0, y, w, Fontsz); + cairo_clip(cr); + cairo_move_to(cr, x - r.x, y + (Fontsz - r.height) / 2 - r.y); + cairo_set_source_rgb(cr, 0, 0, 0); + pango_cairo_show_layout(cr, layout); + } + cairo_destroy(cr); + cairo_surface_flush(surface); + cairo_surface_destroy(surface); } diff --git a/popup_layout.c b/popup_layout.c index bedf6e7..bd9252a 100644 --- a/popup_layout.c +++ b/popup_layout.c @@ -1,18 +1,6 @@ #include "dat.h" #include "fn.h" -int -popupcells(Rune *r, int n) -{ - int i, w; - - w = 0; - for(i = 0; i < n; i++) - if(r[i] != 0xfe0e && r[i] != 0xfe0f) - w++; - return w; -} - void popupposition(Caret *caret, int pointerx, int pointery, int screenw, int screenh, int w, int h, int *x, int *y) diff --git a/run.sh b/run.sh index 6df65bd..8b3f7d7 100755 --- a/run.sh +++ b/run.sh @@ -5,11 +5,13 @@ cd "$(dirname "$0")" || exit 1 if test "$#" -eq 0; then set -- for font in \ + /usr/share/fonts/noto/NotoColorEmoji.ttf \ /usr/share/fonts/TTF/DejaVuSans.ttf \ /usr/share/fonts/TTF/Jigmo.ttf \ /usr/share/fonts/TTF/Jigmo2.ttf \ /usr/share/fonts/noto/NotoSansCJK-Regular.ttc do + test "$#" -ge 4 && break test -f "$font" && set -- "$@" "$font" done if test "$#" -eq 0; then diff --git a/tests/Makefile b/tests/Makefile index c20e1b3..a8d23ac 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -2,13 +2,13 @@ CC = 9c LD = 9l HOSTCC = cc CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest -FT_CFLAGS = $(shell pkg-config --cflags freetype2) -FT_LIBS = $(shell pkg-config --libs freetype2) +TEXT_CFLAGS = $(shell pkg-config --cflags 'pangocairo >= 1.56' 'cairo >= 1.18') +TEXT_LIBS = $(shell pkg-config --libs 'pangocairo >= 1.56' 'cairo >= 1.18') DBUS_CFLAGS = $(shell pkg-config --cflags dbus-1) DBUS_LIBS = $(shell pkg-config --libs dbus-1) IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) -LIBS = -lthread -lbio $(FT_LIBS) $(IBUS_LIBS) +LIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) PROG = unit_test LIVE = ibus_live_test ipc_live_test daemon_collision_test daemon_failure_test \ @@ -57,7 +57,7 @@ engine_test.o: ../strans.c ibus_test.o: CFLAGS += $(IBUS_CFLAGS) ibus_test.o: ../ibus.c server_test.o: ../srv.c -unit_font.o: CFLAGS += $(FT_CFLAGS) +unit_font.o: CFLAGS += $(TEXT_CFLAGS) unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h $(CC) $(CFLAGS) -c -o $@ $< diff --git a/tests/font_test.c b/tests/font_test.c index 6c71e9e..06676ae 100644 --- a/tests/font_test.c +++ b/tests/font_test.c @@ -3,14 +3,15 @@ enum { Guard = 8, - Testw = 3 * Fontsz, + Testw = 6 * Fontsz, Testh = 3 * Fontsz, Testn = Testw * Testh, - Missingn = Fontsz * Fontsz, + Rgbmask = 0xffffff, }; static u32int guardcolor = 0x5a5a5a5a; static char *testfonts[] = { + "/usr/share/fonts/noto/NotoColorEmoji.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/TTF/Jigmo.ttf", "/usr/share/fonts/TTF/Jigmo2.ttf", @@ -29,38 +30,84 @@ fillpixels(u32int *p, int n, u32int color) } static int -hasink(u32int *p, int n) +hasink(u32int *p, int n, u32int bg) { int i; + bg &= Rgbmask; for(i = 0; i < n; i++) - if(p[i] != Colbg) + if((p[i] & Rgbmask) != bg) return 1; return 0; } static int -renders(Rune r) +hascolors(u32int *p, int n, u32int bg) { - u32int buf[Fontsz * Fontsz]; + u32int c, first; + int b, found, g, i, r; - fillpixels(buf, nelem(buf), Colbg); - putfont(buf, Fontsz, Fontsz, 0, 0, r); - return hasink(buf, nelem(buf)); + bg &= Rgbmask; + first = 0; + found = 0; + for(i = 0; i < n; i++){ + c = p[i] & Rgbmask; + if(c == bg) + continue; + r = c >> 16 & 0xff; + g = c >> 8 & 0xff; + b = c & 0xff; + if(r == g && g == b) + continue; + if(!found){ + first = c; + found = 1; + }else if(c != first) + return 1; + } + return 0; +} + +static void +checkguards(struct ct *t, u32int *mem) +{ + int i; + + for(i = 0; i < Guard; i++){ + CT_EQ_UINT(t, guardcolor, mem[i]); + CT_EQ_UINT(t, guardcolor, mem[Guard + Testn + i]); + } +} + +static void +checkoutside(struct ct *t, u32int *buf, u32int bg, int row) +{ + int x, y, y0, y1; + + bg &= Rgbmask; + y0 = max(row, 0); + y1 = min(row + Fontsz, Testh); + for(y = 0; y < Testh; y++){ + if(y >= y0 && y < y1) + continue; + for(x = 0; x < Testw; x++) + CT_EQ_UINT(t, bg, buf[y * Testw + x] & Rgbmask); + } } static int -inkbounds(u32int *p, int *minx, int *miny, int *maxx, int *maxy) +inkbounds(u32int *p, u32int bg, int *minx, int *miny, int *maxx, int *maxy) { int x, y; + bg &= Rgbmask; *minx = Testw; *miny = Testh; *maxx = -1; *maxy = -1; for(y = 0; y < Testh; y++){ for(x = 0; x < Testw; x++){ - if(p[y * Testw + x] == Colbg) + if((p[y * Testw + x] & Rgbmask) == bg) continue; *minx = min(*minx, x); *miny = min(*miny, y); @@ -72,86 +119,124 @@ inkbounds(u32int *p, int *minx, int *miny, int *maxx, int *maxy) } static void -checkclip(struct ct *t, u32int *ref, int dx, int dy) +checktext(struct ct *t, u32int *buf, char *utf) { - u32int *buf, *mem, *want; - int i, sx, sy, x, y; + Str s; + int maxx, maxy, minx, miny, w; + + s = mkstr(utf); + w = textwidth(&s); + CT_CHECK(t, w > 0); + CT_CHECK(t, w < Testw - Fontsz); + fillpixels(buf, Testn, Colbg); + textdraw(buf, Testw, Testh, Fontsz, Fontsz, &s); + if(!CT_CHECK(t, inkbounds(buf, Colbg, &minx, &miny, &maxx, &maxy))) + return; + CT_CHECK(t, minx >= 0 && miny >= 0); + CT_CHECK(t, maxx < Testw && maxy < Testh); + CT_CHECK(t, maxx - minx + 1 <= w); +} + +static void +checkcolor(struct ct *t, u32int *buf, char *utf) +{ + Str s; + int w; + + s = mkstr(utf); + w = textwidth(&s); + CT_CHECK(t, w > 0); + if(s.n > 1) + CT_CHECK(t, w < s.n * Fontsz); + fillpixels(buf, Testn, Colbg); + textdraw(buf, Testw, Testh, Fontsz, Fontsz, &s); + CT_CHECK(t, hasink(buf, Testn, Colbg)); + CT_CHECK(t, hascolors(buf, Testn, Colbg)); +} + +static void +checkshape(struct ct *t, u32int *buf, char *utf) +{ + Str s; + int w; + + s = mkstr(utf); + w = textwidth(&s); + CT_CHECK(t, w > 0); + CT_CHECK(t, w < s.n * Fontsz); + fillpixels(buf, Testn, Colbg); + textdraw(buf, Testw, Testh, Fontsz, Fontsz, &s); + CT_CHECK(t, hasink(buf, Testn, Colbg)); +} + +static void +checkclip(struct ct *t, u32int *mem, int x, int y, Str *s) +{ + u32int *buf; - mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]); - want = emalloc(Testn * sizeof want[0]); - fillpixels(mem, Testn + 2 * Guard, guardcolor); buf = mem + Guard; fillpixels(buf, Testn, Colbg); - fillpixels(want, Testn, Colbg); - for(y = 0; y < Testh; y++){ - for(x = 0; x < Testw; x++){ - sx = x - dx; - sy = y - dy; - if(sx >= 0 && sx < Testw && sy >= 0 && sy < Testh) - want[y * Testw + x] = ref[sy * Testw + sx]; - } - } - putfont(buf, Testw, Testh, Fontsz + dx, Fontsz + dy, 'A'); - CT_CHECK(t, hasink(buf, Testn)); - CT_EQ_MEM(t, want, buf, Testn * sizeof buf[0]); - for(i = 0; i < Guard; i++){ - CT_EQ_UINT(t, guardcolor, mem[i]); - CT_EQ_UINT(t, guardcolor, mem[Guard + Testn + i]); - } - free(want); - free(mem); + textdraw(buf, Testw, Testh, x, y, s); + CT_CHECK(t, hasink(buf, Testn, Colbg)); + checkoutside(t, buf, Colbg, y); + checkguards(t, mem); } void font_render(struct ct *t) { - u32int *buf, *missing, *ref; - int i, minx, miny, maxx, maxy; + static char *plain[] = { "A", "가", "あ", "漢", "☆", "𠀋" }; + u32int *buf, *mem; + Str a, heart, missing; + int i, w; - missing = emalloc((Missingn + 2 * Guard) * sizeof missing[0]); - fillpixels(missing, Missingn + 2 * Guard, guardcolor); - buf = missing + Guard; - fillpixels(buf, Missingn, Colbg); - CT_CHECK(t, !fontinit(missingfonts, nelem(missingfonts))); - putfont(buf, Fontsz, Fontsz, 0, 0, 'A'); - for(i = 0; i < Missingn; i++) + mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]); + fillpixels(mem, Testn + 2 * Guard, guardcolor); + buf = mem + Guard; + fillpixels(buf, Testn, Colbg); + a = mkstr("A"); + CT_CHECK(t, !textinit(missingfonts, nelem(missingfonts))); + CT_EQ_INT(t, 0, textwidth(&a)); + textdraw(buf, Testw, Testh, 0, 0, &a); + for(i = 0; i < Testn; i++) CT_EQ_UINT(t, Colbg, buf[i]); - for(i = 0; i < Guard; i++){ - CT_EQ_UINT(t, guardcolor, missing[i]); - CT_EQ_UINT(t, guardcolor, missing[Guard + Missingn + i]); - } + checkguards(t, mem); - if(!CT_CHECK(t, fontinit(testfonts, nelem(testfonts)))){ - free(missing); + if(!CT_CHECK(t, textinit(testfonts, nelem(testfonts)))){ + free(mem); return; } - CT_CHECK(t, renders('A')); - CT_CHECK(t, renders(0xac00)); - CT_CHECK(t, renders(0x4e00)); - CT_CHECK(t, renders(0x1f600)); - CT_CHECK(t, renders(0x2000b)); - fillpixels(missing, Missingn + 2 * Guard, guardcolor); - buf = missing + Guard; - fillpixels(buf, Missingn, Colbg); - putfont(buf, Fontsz, Fontsz, 0, 0, 0x10ffff); - for(i = 0; i < Missingn; i++) - CT_EQ_UINT(t, Colbg, buf[i]); - for(i = 0; i < Guard; i++){ - CT_EQ_UINT(t, guardcolor, missing[i]); - CT_EQ_UINT(t, guardcolor, missing[Guard + Missingn + i]); - } - free(missing); + for(i = 0; i < nelem(plain); i++) + checktext(t, buf, plain[i]); - ref = emalloc(Testn * sizeof ref[0]); - fillpixels(ref, Testn, Colbg); - putfont(ref, Testw, Testh, Fontsz, Fontsz, 'A'); - if(!CT_CHECK(t, inkbounds(ref, &minx, &miny, &maxx, &maxy))){ - free(ref); - return; + checkcolor(t, buf, "😀"); + checkcolor(t, buf, "❤️"); + checkcolor(t, buf, "☕️"); + checkshape(t, buf, "👩‍💻"); + checkshape(t, buf, "👍🏽"); + checkshape(t, buf, "🇰🇷"); + checkshape(t, buf, "☕︎"); + + heart = mkstr("❤️"); + fillpixels(buf, Testn, Colsel); + textdraw(buf, Testw, Testh, Fontsz, Fontsz, &heart); + CT_CHECK(t, hascolors(buf, Testn, Colsel)); + CT_EQ_UINT(t, Colsel, + buf[(Fontsz + Fontsz/2) * Testw + Testw - 1] & Rgbmask); + checkoutside(t, buf, Colsel, Fontsz); + checkguards(t, mem); + + w = textwidth(&a); + checkclip(t, mem, -w / 2, Fontsz, &a); + checkclip(t, mem, Testw - w / 2, Fontsz, &a); + checkclip(t, mem, Fontsz, -Fontsz / 2, &a); + checkclip(t, mem, Fontsz, Testh - Fontsz / 2, &a); + + missing = mkstr("\xF4\x8F\xBF\xBF"); + for(i = 0; i < 32; i++){ + textdraw(buf, Testw, Testh, Fontsz, Fontsz, &heart); + textdraw(buf, Testw, Testh, 0, 0, &missing); } - checkclip(t, ref, -minx - 1, 0); - checkclip(t, ref, Testw - maxx, 0); - checkclip(t, ref, 0, -miny - 1); - checkclip(t, ref, 0, Testh - maxy); - free(ref); + checkguards(t, mem); + free(mem); } diff --git a/tests/popup_test.c b/tests/popup_test.c index 6c413af..d2df539 100644 --- a/tests/popup_test.c +++ b/tests/popup_test.c @@ -4,11 +4,9 @@ void popup_layout(struct ct *t) { - Rune heart[] = { 0x2764, 0xfe0f }; Caret caret; int x, y; - CT_EQ_INT(t, 1, popupcells(heart, nelem(heart))); memset(&caret, 0, sizeof caret); popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y); CT_EQ_INT(t, 50, x); diff --git a/win.c b/win.c index 5c276fa..f082568 100644 --- a/win.c +++ b/win.c @@ -141,7 +141,7 @@ wininit(void) mask = XCB_CW_BACK_PIXMAP; xcb_change_window_attributes(conn, win, mask, &pix); img = emalloc(Imgw * Imgh * sizeof(img[0])); - if(!fontinit(fontfiles, nfontfiles)){ + if(!textinit(fontfiles, nfontfiles)){ fprint(2, "strans: popup disabled: no usable fonts\n"); wincleanup(); return 0; @@ -149,18 +149,6 @@ wininit(void) return 1; } -static void -drawstr(u32int *buf, int x, int y, Rune *r, int n, int maxw, int maxh) -{ - while(n-- > 0){ - if(*r != 0xfe0e && *r != 0xfe0f){ - putfont(buf, maxw, maxh, x, y, *r); - x += Fontsz; - } - r++; - } -} - static void fill(u32int *buf, int n, u32int color) { @@ -174,20 +162,22 @@ static void drawkouho(Drawcmd *dc, int n, int w, int h) { int npre, sely, y, i; - Str *s; + Str num, *s; fill(img, w * h, Colbg); npre = dc->pre.n != 0; if(npre) - drawstr(img, 0, 0, dc->pre.r, dc->pre.n, w, h); + textdraw(img, w, h, 0, 0, &dc->pre); if(dc->sel >= 0 && dc->sel < n){ sely = (npre + dc->sel) * Fontsz; fill(img + sely * w, Fontsz * w, Colsel); } for(i = 0, y = npre * Fontsz; i < n; i++, y += Fontsz){ s = &dc->kouho[i]; - putfont(img, w, h, 0, y, '1' + i + Asciitofull); - drawstr(img, 2*Fontsz, y, s->r, s->n, w, h); + sclear(&num); + sputr(&num, '1' + i + Asciitofull); + textdraw(img, w, h, 0, y, &num); + textdraw(img, w, h, 2*Fontsz, y, s); } } @@ -210,6 +200,7 @@ winhide(void) static int winshow(Drawcmd *dc) { + vlong width; int npre, px, py, w, h, i, n, maxw; u32int vals[4]; xcb_query_pointer_reply_t *ptr; @@ -220,11 +211,12 @@ winshow(Drawcmd *dc) if(n == 0 && npre == 0){ return winhide(); } - maxw = popupcells(dc->pre.r, dc->pre.n); + maxw = textwidth(&dc->pre); for(i = 0; i < n; i++) - maxw = max(maxw, popupcells(dc->kouho[i].r, dc->kouho[i].n)); + maxw = max(maxw, textwidth(&dc->kouho[i])); vals[3] = h = (n + npre) * Fontsz; - vals[2] = w = (maxw + 3) * Fontsz; + width = (vlong)maxw + 3*Fontsz; + vals[2] = w = min(max(width, 1), Imgw); px = py = 0; if(!dc->caret.valid){ cookie = xcb_query_pointer(conn, scr->root);