popup: replace FreeType renderer with PangoCairo

This commit is contained in:
2026-08-14 14:37:30 +09:00
parent 279b9230a4
commit 54355b8190
12 changed files with 349 additions and 266 deletions

View File

@@ -1,13 +1,17 @@
FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1 FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1
RUN pacman -Syu --noconfirm --needed \ RUN pacman -Syu --noconfirm --needed \
cairo \
fontconfig \
diffutils \ diffutils \
freetype2 \
gcc \ gcc \
gtk3 \ gtk3 \
harfbuzz \
libxcb \ libxcb \
libxkbcommon \ libxkbcommon \
make \ make \
noto-fonts-emoji \
pango \
pkgconf \ pkgconf \
plan9port \ plan9port \
python \ python \

View File

@@ -2,8 +2,8 @@ CC = 9c
LD = 9l LD = 9l
PKG_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) PKG_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon)
PKG_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) PKG_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon)
FT_CFLAGS = $(shell pkg-config --cflags freetype2) TEXT_CFLAGS = $(shell pkg-config --cflags 'pangocairo >= 1.56' 'cairo >= 1.18')
FT_LIBS = $(shell pkg-config --libs freetype2) TEXT_LIBS = $(shell pkg-config --libs 'pangocairo >= 1.56' 'cairo >= 1.18')
CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS) CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS)
PROG = strans PROG = strans
DOCKER_IMAGE = strans-build DOCKER_IMAGE = strans-build
@@ -17,10 +17,10 @@ OBJS = $(SRCS:.c=.o)
all: $(PROG) xim gtk all: $(PROG) xim gtk
$(PROG): $(OBJS) $(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 $(OBJS): dat.h fn.h ipc.h
font.o: CFLAGS += $(FT_CFLAGS) font.o: CFLAGS += $(TEXT_CFLAGS)
clean: clean:
rm -f $(OBJS) $(PROG) rm -f $(OBJS) $(PROG)

View File

@@ -51,15 +51,24 @@ dependencies listed in [`Dockerfile`](Dockerfile) are installed.
## Run ## Run
The popup needs one to four scalable font files. No fonts are bundled. The popup needs one to four font files. No fonts are bundled. Each file is
`run.sh` uses installed DejaVu, Jigmo, or Noto CJK fonts when available, or validated and registered with Pango as an application font. Pango shapes the
accepts explicit files in fallback order: 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 ```sh
./run.sh ./run.sh
./run.sh /path/to/primary.ttf /path/to/fallback.ttc ./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 The script starts the daemon and, when `DISPLAY` is set, the XIM frontend. It
restarts any existing `strans` processes owned by the current user. restarts any existing `strans` processes owned by the current user.

View File

@@ -47,14 +47,20 @@ new SKK revision.
## Fonts ## Fonts
No font binaries are bundled. The daemon receives one to four font file paths 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 on its command line, validates them, and registers them as Pango application
family name or directory contents. `font/PROVENANCE` retains the exact fonts. Pango shapes each complete string and selects glyph fallback from the
checksums, source revisions, and license records for the three Noto faces that registered fonts and the normal system font set. Exact command-line file order
were formerly stored in the repository. 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 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 - `ttf-dejavu` `2.37+18+g9b5d1b2f-8`, providing
`/usr/share/fonts/TTF/DejaVuSans.ttf` (SHA-256 `/usr/share/fonts/TTF/DejaVuSans.ttf` (SHA-256
`6038a160b491e121c1f12c7bccb4a9c8730296e3adc1086a059404ed84b7451c`); `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 and `/usr/share/fonts/TTF/Jigmo2.ttf` (SHA-256
`5da3582efe77e22073b86b3b86b556d7111148a76b957cfb53318a91da2efff0`). `5da3582efe77e22073b86b3b86b556d7111148a76b957cfb53318a91da2efff0`).
Pacman validates the package checksums and repository signatures. DejaVu Sans Pacman validates the package checksums and repository signatures. Noto Color
supplies ASCII and the monochrome U+1F600 outline in the tests; Jigmo supplies Emoji supplies the color and shaped-sequence coverage; DejaVu Sans supplies
Hangul and BMP CJK, and Jigmo2 supplies U+2000B. Their installed license files ASCII, Jigmo supplies Hangul and BMP CJK, and Jigmo2 supplies U+2000B. Their
are `/usr/share/licenses/ttf-dejavu/LICENSE` and installed license files are `/usr/share/licenses/noto-fonts-emoji/LICENSE`,
`/usr/share/licenses/ttf-jigmo/LICENSE.txt`; the packages identify their terms `/usr/share/licenses/ttf-dejavu/LICENSE`, and
as the DejaVu custom license and CC0 1.0, respectively. These packages are `/usr/share/licenses/ttf-jigmo/LICENSE.txt`. The packages identify their terms
test fixtures, not names or paths hard-coded into the daemon. 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 ## Git submodules

6
fn.h
View File

@@ -28,7 +28,6 @@ void dictthread(void*);
void dictlookup(Dictreq*, Dictres*); void dictlookup(Dictreq*, Dictres*);
void drawthread(void*); void drawthread(void*);
int popupcells(Rune*, int);
void popupposition(Caret*, int, int, int, int, int, int, int*, int*); void popupposition(Caret*, int, int, int, int, int, int, int*, int*);
void imthread(void*); void imthread(void*);
Emit transmap(Im*, Rune); Emit transmap(Im*, Rune);
@@ -44,5 +43,6 @@ void ibusthread(void*);
void* emalloc(ulong); void* emalloc(ulong);
void* erealloc(void*, ulong); void* erealloc(void*, ulong);
int fontinit(char**, int); int textinit(char**, int);
void putfont(u32int*, int, int, int, int, Rune); int textwidth(Str*);
void textdraw(u32int*, int, int, int, int, Str*);

254
font.c
View File

@@ -1,168 +1,166 @@
#include "dat.h" #include "dat.h"
#include <ft2build.h> #include <pango/pangocairo.h>
#include FT_FREETYPE_H
#include "fn.h" #include "fn.h"
typedef struct Font Font; static PangoFontMap *fontmap;
struct Font static PangoContext *context;
static PangoLayout *layout;
static void
textclear(void)
{ {
FT_Face face; if(layout != nil)
int base; g_object_unref(layout);
}; if(context != nil)
g_object_unref(context);
static FT_Library lib; if(fontmap != nil)
static Font fonts[Maxfonts]; g_object_unref(fontmap);
static int nfonts; layout = nil;
static u32int blendtab[2][256]; context = nil;
fontmap = nil;
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;
} }
static void static void
clearfonts(void) setfont(void)
{ {
int i; PangoFontDescription *font;
PangoRectangle r;
int size;
for(i = 0; i < nfonts; i++){ font = pango_font_description_new();
FT_Done_Face(fonts[i].face); pango_font_description_set_family(font, "sans");
fonts[i].face = nil; 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 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)){ textclear();
fprint(2, "strans: popup: need 1-%d font files\n", nelem(fonts)); if(path == nil || npath < 1 || npath > Maxfonts){
clearfonts(); fprint(2, "strans: popup: need 1-%d font files\n", Maxfonts);
return 0; return 0;
} }
if(lib == nil && FT_Init_FreeType(&lib) != 0){ fontmap = pango_cairo_font_map_new();
fprint(2, "strans: popup: can't initialize FreeType\n"); if(fontmap == nil){
fprint(2, "strans: popup: can't initialize PangoCairo\n");
return 0; return 0;
} }
clearfonts();
for(i = 0; i < npath; i++){ for(i = 0; i < npath; i++){
if(path[i] == nil || !loadfont(path[i])){ if(path[i] == nil){
clearfonts(); 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; return 0;
} }
} }
for(a = 0; a < 256; a++){ pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96);
blendtab[0][a] = blend(Colbg, Colfg, a); context = pango_font_map_create_context(fontmap);
blendtab[1][a] = blend(Colsel, Colfg, a); 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; return 1;
} }
static int static int
drawglyph(Font *f, u32int *buf, int w, int h, int px, int py) settext(Str *s)
{ {
FT_Bitmap *b; char utf[Maxutf];
FT_GlyphSlot g; int n;
uchar *row;
vlong pitch, x0, x1, y0, y1;
int a, sel, x, xa, xb, y, ya, yb;
u32int *p;
g = f->face->glyph; if(layout == nil || s == nil || s->n <= 0)
b = &g->bitmap;
if(b->pixel_mode != FT_PIXEL_MODE_GRAY || b->num_grays != 256)
return 0; return 0;
if(b->width == 0 || b->rows == 0) n = stoutf(s, utf, sizeof utf);
return 1; pango_layout_set_text(layout, utf, n);
pitch = b->pitch; return n > 0;
if(pitch < 0) }
pitch = -pitch;
if(b->buffer == nil || pitch < b->width) 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; return 0;
x0 = (vlong)px + g->bitmap_left; textextents(&r);
y0 = (vlong)py + f->base - g->bitmap_top; return r.width;
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;
} }
void 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; PangoRectangle r;
int f; 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; 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);
} }

View File

@@ -1,18 +1,6 @@
#include "dat.h" #include "dat.h"
#include "fn.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 void
popupposition(Caret *caret, int pointerx, int pointery, int screenw, popupposition(Caret *caret, int pointerx, int pointery, int screenw,
int screenh, int w, int h, int *x, int *y) int screenh, int w, int h, int *x, int *y)

2
run.sh
View File

@@ -5,11 +5,13 @@ cd "$(dirname "$0")" || exit 1
if test "$#" -eq 0; then if test "$#" -eq 0; then
set -- set --
for font in \ for font in \
/usr/share/fonts/noto/NotoColorEmoji.ttf \
/usr/share/fonts/TTF/DejaVuSans.ttf \ /usr/share/fonts/TTF/DejaVuSans.ttf \
/usr/share/fonts/TTF/Jigmo.ttf \ /usr/share/fonts/TTF/Jigmo.ttf \
/usr/share/fonts/TTF/Jigmo2.ttf \ /usr/share/fonts/TTF/Jigmo2.ttf \
/usr/share/fonts/noto/NotoSansCJK-Regular.ttc /usr/share/fonts/noto/NotoSansCJK-Regular.ttc
do do
test "$#" -ge 4 && break
test -f "$font" && set -- "$@" "$font" test -f "$font" && set -- "$@" "$font"
done done
if test "$#" -eq 0; then if test "$#" -eq 0; then

View File

@@ -2,13 +2,13 @@ CC = 9c
LD = 9l LD = 9l
HOSTCC = cc HOSTCC = cc
CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest
FT_CFLAGS = $(shell pkg-config --cflags freetype2) TEXT_CFLAGS = $(shell pkg-config --cflags 'pangocairo >= 1.56' 'cairo >= 1.18')
FT_LIBS = $(shell pkg-config --libs freetype2) TEXT_LIBS = $(shell pkg-config --libs 'pangocairo >= 1.56' 'cairo >= 1.18')
DBUS_CFLAGS = $(shell pkg-config --cflags dbus-1) DBUS_CFLAGS = $(shell pkg-config --cflags dbus-1)
DBUS_LIBS = $(shell pkg-config --libs dbus-1) DBUS_LIBS = $(shell pkg-config --libs dbus-1)
IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon)
IBUS_LIBS = $(shell pkg-config --libs 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 PROG = unit_test
LIVE = ibus_live_test ipc_live_test daemon_collision_test daemon_failure_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: CFLAGS += $(IBUS_CFLAGS)
ibus_test.o: ../ibus.c ibus_test.o: ../ibus.c
server_test.o: ../srv.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 unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<

View File

@@ -3,14 +3,15 @@
enum enum
{ {
Guard = 8, Guard = 8,
Testw = 3 * Fontsz, Testw = 6 * Fontsz,
Testh = 3 * Fontsz, Testh = 3 * Fontsz,
Testn = Testw * Testh, Testn = Testw * Testh,
Missingn = Fontsz * Fontsz, Rgbmask = 0xffffff,
}; };
static u32int guardcolor = 0x5a5a5a5a; static u32int guardcolor = 0x5a5a5a5a;
static char *testfonts[] = { static char *testfonts[] = {
"/usr/share/fonts/noto/NotoColorEmoji.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/TTF/Jigmo.ttf", "/usr/share/fonts/TTF/Jigmo.ttf",
"/usr/share/fonts/TTF/Jigmo2.ttf", "/usr/share/fonts/TTF/Jigmo2.ttf",
@@ -29,38 +30,84 @@ fillpixels(u32int *p, int n, u32int color)
} }
static int static int
hasink(u32int *p, int n) hasink(u32int *p, int n, u32int bg)
{ {
int i; int i;
bg &= Rgbmask;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
if(p[i] != Colbg) if((p[i] & Rgbmask) != bg)
return 1; return 1;
return 0; return 0;
} }
static int 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); bg &= Rgbmask;
putfont(buf, Fontsz, Fontsz, 0, 0, r); first = 0;
return hasink(buf, nelem(buf)); 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 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; int x, y;
bg &= Rgbmask;
*minx = Testw; *minx = Testw;
*miny = Testh; *miny = Testh;
*maxx = -1; *maxx = -1;
*maxy = -1; *maxy = -1;
for(y = 0; y < Testh; y++){ for(y = 0; y < Testh; y++){
for(x = 0; x < Testw; x++){ for(x = 0; x < Testw; x++){
if(p[y * Testw + x] == Colbg) if((p[y * Testw + x] & Rgbmask) == bg)
continue; continue;
*minx = min(*minx, x); *minx = min(*minx, x);
*miny = min(*miny, y); *miny = min(*miny, y);
@@ -72,86 +119,124 @@ inkbounds(u32int *p, int *minx, int *miny, int *maxx, int *maxy)
} }
static void static void
checkclip(struct ct *t, u32int *ref, int dx, int dy) checktext(struct ct *t, u32int *buf, char *utf)
{ {
u32int *buf, *mem, *want; Str s;
int i, sx, sy, x, y; 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; buf = mem + Guard;
fillpixels(buf, Testn, Colbg); fillpixels(buf, Testn, Colbg);
fillpixels(want, Testn, Colbg); textdraw(buf, Testw, Testh, x, y, s);
for(y = 0; y < Testh; y++){ CT_CHECK(t, hasink(buf, Testn, Colbg));
for(x = 0; x < Testw; x++){ checkoutside(t, buf, Colbg, y);
sx = x - dx; checkguards(t, mem);
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);
} }
void void
font_render(struct ct *t) font_render(struct ct *t)
{ {
u32int *buf, *missing, *ref; static char *plain[] = { "A", "", "", "", "", "𠀋" };
int i, minx, miny, maxx, maxy; u32int *buf, *mem;
Str a, heart, missing;
int i, w;
missing = emalloc((Missingn + 2 * Guard) * sizeof missing[0]); mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]);
fillpixels(missing, Missingn + 2 * Guard, guardcolor); fillpixels(mem, Testn + 2 * Guard, guardcolor);
buf = missing + Guard; buf = mem + Guard;
fillpixels(buf, Missingn, Colbg); fillpixels(buf, Testn, Colbg);
CT_CHECK(t, !fontinit(missingfonts, nelem(missingfonts))); a = mkstr("A");
putfont(buf, Fontsz, Fontsz, 0, 0, 'A'); CT_CHECK(t, !textinit(missingfonts, nelem(missingfonts)));
for(i = 0; i < Missingn; i++) 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]); CT_EQ_UINT(t, Colbg, buf[i]);
for(i = 0; i < Guard; i++){ checkguards(t, mem);
CT_EQ_UINT(t, guardcolor, missing[i]);
CT_EQ_UINT(t, guardcolor, missing[Guard + Missingn + i]);
}
if(!CT_CHECK(t, fontinit(testfonts, nelem(testfonts)))){ if(!CT_CHECK(t, textinit(testfonts, nelem(testfonts)))){
free(missing); free(mem);
return; return;
} }
CT_CHECK(t, renders('A')); for(i = 0; i < nelem(plain); i++)
CT_CHECK(t, renders(0xac00)); checktext(t, buf, plain[i]);
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);
ref = emalloc(Testn * sizeof ref[0]); checkcolor(t, buf, "😀");
fillpixels(ref, Testn, Colbg); checkcolor(t, buf, "❤️");
putfont(ref, Testw, Testh, Fontsz, Fontsz, 'A'); checkcolor(t, buf, "☕️");
if(!CT_CHECK(t, inkbounds(ref, &minx, &miny, &maxx, &maxy))){ checkshape(t, buf, "👩‍💻");
free(ref); checkshape(t, buf, "👍🏽");
return; 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); checkguards(t, mem);
checkclip(t, ref, Testw - maxx, 0); free(mem);
checkclip(t, ref, 0, -miny - 1);
checkclip(t, ref, 0, Testh - maxy);
free(ref);
} }

View File

@@ -4,11 +4,9 @@
void void
popup_layout(struct ct *t) popup_layout(struct ct *t)
{ {
Rune heart[] = { 0x2764, 0xfe0f };
Caret caret; Caret caret;
int x, y; int x, y;
CT_EQ_INT(t, 1, popupcells(heart, nelem(heart)));
memset(&caret, 0, sizeof caret); memset(&caret, 0, sizeof caret);
popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y); popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y);
CT_EQ_INT(t, 50, x); CT_EQ_INT(t, 50, x);

32
win.c
View File

@@ -141,7 +141,7 @@ wininit(void)
mask = XCB_CW_BACK_PIXMAP; mask = XCB_CW_BACK_PIXMAP;
xcb_change_window_attributes(conn, win, mask, &pix); xcb_change_window_attributes(conn, win, mask, &pix);
img = emalloc(Imgw * Imgh * sizeof(img[0])); img = emalloc(Imgw * Imgh * sizeof(img[0]));
if(!fontinit(fontfiles, nfontfiles)){ if(!textinit(fontfiles, nfontfiles)){
fprint(2, "strans: popup disabled: no usable fonts\n"); fprint(2, "strans: popup disabled: no usable fonts\n");
wincleanup(); wincleanup();
return 0; return 0;
@@ -149,18 +149,6 @@ wininit(void)
return 1; 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 static void
fill(u32int *buf, int n, u32int color) fill(u32int *buf, int n, u32int color)
{ {
@@ -174,20 +162,22 @@ static void
drawkouho(Drawcmd *dc, int n, int w, int h) drawkouho(Drawcmd *dc, int n, int w, int h)
{ {
int npre, sely, y, i; int npre, sely, y, i;
Str *s; Str num, *s;
fill(img, w * h, Colbg); fill(img, w * h, Colbg);
npre = dc->pre.n != 0; npre = dc->pre.n != 0;
if(npre) 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){ if(dc->sel >= 0 && dc->sel < n){
sely = (npre + dc->sel) * Fontsz; sely = (npre + dc->sel) * Fontsz;
fill(img + sely * w, Fontsz * w, Colsel); fill(img + sely * w, Fontsz * w, Colsel);
} }
for(i = 0, y = npre * Fontsz; i < n; i++, y += Fontsz){ for(i = 0, y = npre * Fontsz; i < n; i++, y += Fontsz){
s = &dc->kouho[i]; s = &dc->kouho[i];
putfont(img, w, h, 0, y, '1' + i + Asciitofull); sclear(&num);
drawstr(img, 2*Fontsz, y, s->r, s->n, w, h); 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 static int
winshow(Drawcmd *dc) winshow(Drawcmd *dc)
{ {
vlong width;
int npre, px, py, w, h, i, n, maxw; int npre, px, py, w, h, i, n, maxw;
u32int vals[4]; u32int vals[4];
xcb_query_pointer_reply_t *ptr; xcb_query_pointer_reply_t *ptr;
@@ -220,11 +211,12 @@ winshow(Drawcmd *dc)
if(n == 0 && npre == 0){ if(n == 0 && npre == 0){
return winhide(); return winhide();
} }
maxw = popupcells(dc->pre.r, dc->pre.n); maxw = textwidth(&dc->pre);
for(i = 0; i < n; i++) 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[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; px = py = 0;
if(!dc->caret.valid){ if(!dc->caret.valid){
cookie = xcb_query_pointer(conn, scr->root); cookie = xcb_query_pointer(conn, scr->root);