popup: refine the input panel

This commit is contained in:
2026-08-14 20:54:09 +09:00
parent 3576c4ea68
commit f222b4d573
7 changed files with 399 additions and 76 deletions

29
dat.h
View File

@@ -18,6 +18,9 @@ enum
LangVI = 0x16,
Fontsz = 32,
PopupPad = 4,
PopupSep = 1,
PopupNumw = Fontsz,
Maxrunes = 64,
Maxutf = Maxrunes * UTFmax + 1,
};
@@ -28,11 +31,13 @@ enum
Maxkouho = 32,
Maxdisp = 9,
Imgw = (Maxrunes + 3) * Fontsz,
Imgh = (Maxdisp + 2) * Fontsz,
Imgh = 2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep,
Colfg = 0x000000,
Colbg = 0xffffff,
Colsel = 0xcccccc,
Colsep = 0xd0d0d0,
Colsel = 0x333333,
Colselfg = 0xffffff,
};
typedef struct Str Str;
@@ -137,6 +142,26 @@ struct Drawcmd
Caret caret;
};
typedef struct Popup Popup;
struct Popup
{
int w;
int h;
int n;
int prey;
int sepy;
int rowsy;
int numx;
int textx;
int textw;
int markx;
int marky;
int markw;
int selx;
int sely;
int selw;
};
typedef struct Keyreq Keyreq;
typedef struct Keyres Keyres;
enum

3
fn.h
View File

@@ -30,6 +30,8 @@ void dictlookup(Dictreq*, Dictres*);
void drawthread(void*);
void popupposition(Caret*, int, int, int, int, int, int, int*, int*);
int pagemarker(char*, int, int, int, int);
void popuplayout(Drawcmd*, int, Popup*);
void popupdraw(u32int*, Drawcmd*, Popup*);
void imthread(void*);
Emit transmap(Im*, Rune);
Emit transko(Im*, Rune);
@@ -50,3 +52,4 @@ int textinit(void);
int textwidth(Str*);
void textdraw(u32int*, int, int, int, int, Str*);
void textdrawfit(u32int*, int, int, int, int, int, Str*);
void textdrawfitcolor(u32int*, int, int, int, int, int, u32int, Str*);

21
font.c
View File

@@ -117,12 +117,13 @@ textwidth(Str *s)
}
static void
drawtext(u32int *buf, int w, int h, int x, int y, int fit, Str *s)
drawtext(u32int *buf, int w, int h, int x, int y, int fit, u32int color,
Str *s)
{
PangoRectangle r;
cairo_surface_t *surface;
cairo_t *cr;
int stride;
int b, g, red, stride;
if(layout == nil)
return;
@@ -156,7 +157,10 @@ drawtext(u32int *buf, int w, int h, int x, int y, int fit, Str *s)
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);
red = color >> 16 & 0xff;
g = color >> 8 & 0xff;
b = color & 0xff;
cairo_set_source_rgb(cr, red / 255.0, g / 255.0, b / 255.0);
pango_cairo_show_layout(cr, layout);
}
cairo_destroy(cr);
@@ -167,11 +171,18 @@ drawtext(u32int *buf, int w, int h, int x, int y, int fit, Str *s)
void
textdraw(u32int *buf, int w, int h, int x, int y, Str *s)
{
drawtext(buf, w, h, x, y, -1, s);
drawtext(buf, w, h, x, y, -1, Colfg, s);
}
void
textdrawfit(u32int *buf, int w, int h, int x, int y, int fit, Str *s)
{
drawtext(buf, w, h, x, y, fit, s);
drawtext(buf, w, h, x, y, fit, Colfg, s);
}
void
textdrawfitcolor(u32int *buf, int w, int h, int x, int y, int fit,
u32int color, Str *s)
{
drawtext(buf, w, h, x, y, fit, color, s);
}

View File

@@ -1,6 +1,33 @@
#include "dat.h"
#include "fn.h"
enum {
Asciitofull = 0xFEE0,
};
static void
fill(u32int *buf, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
buf[i] = color;
}
static void
fillrect(u32int *buf, int w, int h, int x, int y, int rw, int rh,
u32int color)
{
int x0, x1, y0, y1;
x0 = min(max(x, 0), w);
x1 = min(max(x + max(rw, 0), 0), w);
y0 = min(max(y, 0), h);
y1 = min(max(y + max(rh, 0), 0), h);
for(; y0 < y1; y0++)
fill(buf + y0*w + x0, max(x1 - x0, 0), color);
}
int
pagemarker(char *buf, int nbuf, int first, int shown, int total)
{
@@ -13,6 +40,111 @@ pagemarker(char *buf, int nbuf, int first, int shown, int total)
first + shown, total);
}
void
popuplayout(Drawcmd *dc, int screenw, Popup *p)
{
char buf[32];
vlong width;
Str mark;
int i, maxw, nmark, npre, roww, y;
memset(p, 0, sizeof *p);
p->prey = -1;
p->sepy = -1;
p->rowsy = -1;
p->markx = -1;
p->marky = -1;
p->sely = -1;
p->n = min(max(dc->nkouho, 0), Maxdisp);
npre = dc->pre.n != 0;
if(p->n == 0 && !npre)
return;
maxw = npre ? textwidth(&dc->pre) : 0;
roww = 0;
for(i = 0; i < p->n; i++)
roww = max(roww, textwidth(&dc->kouho[i]));
if(p->n != 0)
maxw = max(maxw, PopupNumw + roww);
nmark = pagemarker(buf, sizeof buf, dc->first, p->n, dc->total);
if(nmark != 0){
sinit(&mark, buf, nmark);
maxw = max(maxw, textwidth(&mark));
}
width = (vlong)maxw + 2*PopupPad;
p->w = min(min(max(width, 1), Imgw), max(screenw, 0));
y = PopupPad;
if(npre){
p->prey = y;
y += Fontsz;
}
if(npre && p->n != 0){
p->sepy = y;
y += PopupSep;
}
if(p->n != 0){
p->rowsy = y;
y += p->n*Fontsz;
}
if(nmark != 0){
p->marky = y;
y += Fontsz;
}
p->h = y + PopupPad;
p->numx = PopupPad;
p->textx = PopupPad + PopupNumw;
p->textw = max(p->w - PopupPad - p->textx, 0);
if(nmark != 0){
p->markw = min(textwidth(&mark), max(p->w - 2*PopupPad, 0));
p->markx = p->w - PopupPad - p->markw;
}
p->selx = PopupPad;
p->selw = max(p->w - 2*PopupPad, 0);
if(dc->sel >= 0 && dc->sel < p->n)
p->sely = p->rowsy + dc->sel*Fontsz;
}
void
popupdraw(u32int *img, Drawcmd *dc, Popup *p)
{
char buf[32];
Str mark, num;
u32int color;
int i, nmark, y;
if(img == nil || p->n < 0 || p->n > Maxdisp ||
p->w <= 0 || p->w > Imgw ||
p->h <= 0 || p->h > Imgh)
return;
fill(img, p->w*p->h, Colbg);
if(p->prey >= 0)
textdrawfit(img, p->w, p->h, PopupPad, p->prey,
max(p->w - 2*PopupPad, 0), &dc->pre);
if(p->sepy >= 0)
fillrect(img, p->w, p->h, PopupPad, p->sepy,
p->w - 2*PopupPad, PopupSep, Colsep);
if(p->sely >= 0)
fillrect(img, p->w, p->h, p->selx, p->sely,
p->selw, Fontsz, Colsel);
for(i = 0, y = p->rowsy; i < p->n; i++, y += Fontsz){
color = i == dc->sel ? Colselfg : Colfg;
sclear(&num);
sputr(&num, '1' + i + Asciitofull);
textdrawfitcolor(img, p->w, p->h, p->numx, y,
min(PopupNumw,
max(p->w - PopupPad - p->numx, 0)), color, &num);
textdrawfitcolor(img, p->w, p->h, p->textx, y,
p->textw, color, &dc->kouho[i]);
}
nmark = pagemarker(buf, sizeof buf, dc->first, p->n, dc->total);
if(nmark != 0 && p->marky >= 0){
sinit(&mark, buf, nmark);
textdrawfit(img, p->w, p->h, p->markx, p->marky,
p->markw, &mark);
}
}
void
popupposition(Caret *caret, int pointerx, int pointery, int screenw,
int screenh, int w, int h, int *x, int *y)

View File

@@ -179,7 +179,7 @@ font_render(struct ct *t)
static char *plain[] = { "A", "", "", "", "", "𠀋" };
u32int *buf, *mem;
Str a, heart, longrow, missing;
int i, natural, w, x, y;
int i, maxx, maxy, minx, miny, natural, w, x, y;
mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]);
fillpixels(mem, Testn + 2 * Guard, guardcolor);
@@ -231,6 +231,21 @@ font_render(struct ct *t)
}
CT_EQ_INT(t, natural, textwidth(&longrow));
fillpixels(buf, Testn, Colsel);
textdrawfitcolor(buf, Testw, Testh, 2*Fontsz, Fontsz,
2*Fontsz, Colselfg, &longrow);
CT_CHECK(t, (Colselfg & Rgbmask) != (Colsel & Rgbmask));
if(CT_CHECK(t, inkbounds(buf, Colsel,
&minx, &miny, &maxx, &maxy))){
CT_CHECK(t, minx >= 2*Fontsz);
CT_CHECK(t, maxx < 4*Fontsz);
CT_CHECK(t, miny >= Fontsz);
CT_CHECK(t, maxy < 2*Fontsz);
}
checkoutside(t, buf, Colsel, Fontsz);
checkguards(t, mem);
CT_EQ_INT(t, natural, textwidth(&longrow));
missing = mkstr("\xF4\x8F\xBF\xBF");
for(i = 0; i < 32; i++){
textdraw(buf, Testw, Testh, Fontsz, Fontsz, &heart);

View File

@@ -1,6 +1,41 @@
#include "test.h"
#include <limits.h>
enum {
Rgbmask = 0xffffff,
};
static u32int
pixel(u32int *img, Popup *p, int x, int y)
{
return img[y*p->w + x] & Rgbmask;
}
static int
rectcolor(u32int *img, Popup *p, int x0, int y0, int x1, int y1,
u32int color)
{
int x, y;
color &= Rgbmask;
for(y = y0; y < y1; y++)
for(x = x0; x < x1; x++)
if(pixel(img, p, x, y) != color)
return 0;
return 1;
}
static void
checkpadding(struct ct *t, u32int *img, Popup *p)
{
CT_CHECK(t, rectcolor(img, p, 0, 0, p->w, PopupPad, Colbg));
CT_CHECK(t, rectcolor(img, p, 0, p->h - PopupPad,
p->w, p->h, Colbg));
CT_CHECK(t, rectcolor(img, p, 0, 0, PopupPad, p->h, Colbg));
CT_CHECK(t, rectcolor(img, p, p->w - PopupPad, 0,
p->w, p->h, Colbg));
}
void
popup_layout(struct ct *t)
{
@@ -22,7 +57,11 @@ popup_layout(struct ct *t)
};
char buf[32];
Caret caret;
int i, n, x, y;
Drawcmd dc;
Popup p;
Str longrow;
u32int c, *img;
int i, ink, markink, n, numink, selbg, textink, x, y;
for(i = 0; i < nelem(markers); i++){
n = pagemarker(buf, sizeof buf, markers[i].first,
@@ -30,7 +69,8 @@ popup_layout(struct ct *t)
CT_EQ_INT(t, strlen(markers[i].want), n);
CT_CHECK(t, strcmp(markers[i].want, buf) == 0);
}
CT_EQ_INT(t, (1 + Maxdisp + 1) * Fontsz, Imgh);
CT_EQ_INT(t, 2*PopupPad + (1 + Maxdisp + 1)*Fontsz + PopupSep,
Imgh);
memset(&caret, 0, sizeof caret);
popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y);
@@ -81,4 +121,157 @@ popup_layout(struct ct *t)
INT_MAX, INT_MAX, &x, &y);
CT_EQ_INT(t, 0, x);
CT_EQ_INT(t, 0, y);
if(!CT_CHECK(t, textinit()))
return;
/* Empty sections reserve nothing. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
popuplayout(&dc, Imgw, &p);
CT_EQ_INT(t, 0, p.w);
CT_EQ_INT(t, 0, p.h);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
/* Preedit only. */
dc.pre = mkstr("preedit");
popuplayout(&dc, Imgw, &p);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, 2*PopupPad + Fontsz, p.h);
/* Candidates only. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
dc.nkouho = 2;
dc.kouho[0] = mkstr("short");
dc.kouho[1] = mkstr("candidate");
popuplayout(&dc, Imgw, &p);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, PopupPad, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, 2*PopupPad + 2*Fontsz, p.h);
CT_EQ_INT(t, PopupPad, p.numx);
CT_EQ_INT(t, p.numx + PopupNumw, p.textx);
CT_EQ_INT(t, p.w - PopupPad, p.textx + p.textw);
/* Preedit and candidates meet at one separator, with no empty rows. */
dc.pre = mkstr("preedit");
popuplayout(&dc, Imgw, &p);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, p.prey + Fontsz, p.sepy);
CT_EQ_INT(t, p.sepy + PopupSep, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, p.rowsy + 2*Fontsz + PopupPad, p.h);
CT_EQ_INT(t, 2*PopupPad + 3*Fontsz + PopupSep, p.h);
/* The largest panel includes all sections and fits its backing image. */
memset(&dc, 0, sizeof dc);
dc.pre = mkstr("preedit");
dc.nkouho = Maxdisp;
dc.sel = 4;
dc.first = 0;
dc.total = Maxdisp + 1;
sclear(&longrow);
for(i = 0; i < Maxrunes; i++)
sputr(&longrow, 'W');
for(i = 0; i < Maxdisp; i++)
dc.kouho[i] = mkstr(i == dc.sel ? "M" : "candidate");
dc.kouho[0] = longrow;
popuplayout(&dc, Imgw, &p);
CT_EQ_INT(t, Maxdisp, p.n);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, p.prey + Fontsz, p.sepy);
CT_EQ_INT(t, p.sepy + PopupSep, p.rowsy);
CT_EQ_INT(t, p.rowsy + Maxdisp*Fontsz, p.marky);
CT_EQ_INT(t, p.marky + Fontsz + PopupPad, p.h);
CT_EQ_INT(t, Imgh, p.h);
CT_CHECK(t, p.w > 2*PopupPad + PopupNumw);
CT_CHECK(t, p.w <= Imgw);
CT_CHECK(t, p.h <= Imgh);
CT_EQ_INT(t, p.w - PopupPad, p.markx + p.markw);
CT_EQ_INT(t, PopupPad, p.selx);
CT_EQ_INT(t, p.w - 2*PopupPad, p.selw);
CT_EQ_INT(t, p.rowsy + dc.sel*Fontsz, p.sely);
CT_EQ_INT(t, PopupPad, p.numx);
CT_EQ_INT(t, p.numx + PopupNumw, p.textx);
CT_EQ_INT(t, p.w - PopupPad, p.textx + p.textw);
img = emalloc(Imgw*Imgh*sizeof img[0]);
popupdraw(img, &dc, &p);
checkpadding(t, img, &p);
for(x = 0; x < p.w; x++)
CT_EQ_UINT(t,
x >= PopupPad && x < p.w - PopupPad ? Colsep : Colbg,
pixel(img, &p, x, p.sepy));
/* Selection paint and both foreground columns stay inside the row. */
selbg = numink = textink = 0;
for(y = p.sely; y < p.sely + Fontsz; y++){
for(x = 0; x < p.w; x++){
c = pixel(img, &p, x, y);
if(x < p.selx || x >= p.selx + p.selw){
CT_EQ_UINT(t, Colbg, c);
continue;
}
if(c == (Colsel & Rgbmask)){
selbg++;
continue;
}
CT_CHECK(t,
(x >= p.numx && x < p.numx + PopupNumw) ||
(x >= p.textx && x < p.textx + p.textw));
if(x < p.numx + PopupNumw)
numink++;
else
textink++;
}
}
CT_CHECK(t, Colsel != Colselfg);
CT_CHECK(t, selbg > p.selw*Fontsz/2);
CT_CHECK(t, numink > 0);
CT_CHECK(t, textink > 0);
/* The marker occupies only its compact, right-aligned footer box. */
markink = 0;
for(y = p.marky; y < p.marky + Fontsz; y++){
for(x = 0; x < p.w; x++){
if(pixel(img, &p, x, y) == (Colbg & Rgbmask))
continue;
markink++;
CT_CHECK(t, x >= p.markx && x < p.markx + p.markw);
}
}
CT_CHECK(t, markink > 0);
/* A fitted long row is bounded by the fixed candidate content area. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
dc.nkouho = 1;
dc.kouho[0] = longrow;
n = textwidth(&longrow);
popuplayout(&dc, 6*Fontsz, &p);
CT_CHECK(t, n > p.textw);
CT_EQ_INT(t, PopupPad, p.rowsy);
CT_EQ_INT(t, p.w - PopupPad, p.textx + p.textw);
popupdraw(img, &dc, &p);
ink = 0;
for(y = p.rowsy; y < p.rowsy + Fontsz; y++){
for(x = 0; x < p.w; x++){
c = pixel(img, &p, x, y);
if(x < PopupPad || x >= p.w - PopupPad)
CT_EQ_UINT(t, Colbg, c);
if(x >= p.textx && x < p.textx + p.textw &&
c != (Colbg & Rgbmask))
ink++;
}
}
CT_CHECK(t, ink > 0);
CT_EQ_INT(t, n, textwidth(&longrow));
free(img);
}

76
win.c
View File

@@ -2,10 +2,6 @@
#include "dat.h"
#include "fn.h"
enum {
Asciitofull = 0xFEE0,
};
static xcb_connection_t *conn;
static xcb_screen_t *scr;
static xcb_window_t win;
@@ -146,46 +142,6 @@ wininit(void)
return 1;
}
static void
fill(u32int *buf, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
buf[i] = color;
}
static void
drawkouho(Drawcmd *dc, int n, int w, int h)
{
char buf[32];
int nmark, npre, sely, y, i;
Str mark, num, *s;
if(w <= 0 || w > Imgw || h <= 0 || h > Imgh)
return;
fill(img, w * h, Colbg);
npre = dc->pre.n != 0;
if(npre)
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];
sclear(&num);
sputr(&num, '1' + i + Asciitofull);
textdraw(img, w, h, 0, y, &num);
textdrawfit(img, w, h, 2*Fontsz, y, w - 2*Fontsz, s);
}
nmark = pagemarker(buf, sizeof buf, dc->first, n, dc->total);
if(nmark != 0){
sinit(&mark, buf, nmark);
textdraw(img, w, h, 0, y, &mark);
}
}
static void
putimage(int w, int h)
{
@@ -207,32 +163,20 @@ winhide(void)
static int
winshow(Drawcmd *dc)
{
char buf[32];
vlong width;
int nmark, npre, px, py, w, h, i, n, maxw;
Str mark;
Popup p;
int px, py;
u32int vals[4];
xcb_query_pointer_reply_t *ptr;
xcb_query_pointer_cookie_t cookie;
n = min(max(dc->nkouho, 0), Maxdisp);
npre = dc->pre.n != 0;
if(n == 0 && npre == 0){
if(dc->nkouho <= 0 && dc->pre.n == 0){
return winhide();
}
maxw = textwidth(&dc->pre);
for(i = 0; i < n; i++)
maxw = max(maxw, textwidth(&dc->kouho[i]));
nmark = pagemarker(buf, sizeof buf, dc->first, n, dc->total);
if(nmark != 0){
sinit(&mark, buf, nmark);
maxw = max(maxw, textwidth(&mark));
}
vals[3] = h = (n + npre + (nmark != 0)) * Fontsz;
width = (vlong)maxw + 3*Fontsz;
vals[2] = w = min(min(max(width, 1), Imgw), scr->width_in_pixels);
if(w <= 0 || w > Imgw || h <= 0 || h > Imgh)
popuplayout(dc, scr->width_in_pixels, &p);
if(p.w <= 0 || p.w > Imgw || p.h <= 0 || p.h > Imgh)
return 0;
vals[2] = p.w;
vals[3] = p.h;
px = py = 0;
if(!dc->caret.valid){
cookie = xcb_query_pointer(conn, scr->root);
@@ -244,7 +188,7 @@ winshow(Drawcmd *dc)
free(ptr);
}
popupposition(&dc->caret, px, py, scr->width_in_pixels,
scr->height_in_pixels, w, h, &px, &py);
scr->height_in_pixels, p.w, p.h, &px, &py);
vals[1] = py;
vals[0] = px;
xcb_configure_window(conn, win,
@@ -252,8 +196,8 @@ winshow(Drawcmd *dc)
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT,
vals);
xcb_map_window(conn, win);
drawkouho(dc, n, w, h);
putimage(w, h);
popupdraw(img, dc, &p);
putimage(p.w, p.h);
return xcb_flush(conn) > 0;
}