Cclientpreedit was a one-bit mask that every producer set as "want ? Cclientpreedit : 0" and every consumer masked out again; nothing else was ever going to join it. Keyreq carries clientpre, an int that says whether the client draws the preedit, and the engine and the XIM context keep it under that name.
1170 lines
28 KiB
C
1170 lines
28 KiB
C
#include <xcb/xcb.h>
|
|
#include <xcb-imdkit/imdkit.h>
|
|
|
|
typedef struct Icbinding Icbinding;
|
|
struct Icbinding
|
|
{
|
|
xcb_im_input_context_t *ic;
|
|
void *data;
|
|
uint32_t style;
|
|
xcb_window_t clientwin;
|
|
xcb_window_t focuswin;
|
|
uint32_t preattrmask;
|
|
xcb_im_preedit_attr_t preattr;
|
|
};
|
|
|
|
static Icbinding icbindings[8];
|
|
|
|
static Icbinding*
|
|
wirebinding(xcb_im_input_context_t *ic)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < 8; i++)
|
|
if(icbindings[i].ic == ic)
|
|
return &icbindings[i];
|
|
return 0;
|
|
}
|
|
|
|
static void wirecommit(xcb_im_t*, xcb_im_input_context_t*, uint32_t,
|
|
const char*, uint32_t, uint32_t);
|
|
static void wirestart(xcb_im_t*, xcb_im_input_context_t*);
|
|
static void wiredraw(xcb_im_t*, xcb_im_input_context_t*,
|
|
xcb_im_preedit_draw_fr_t*);
|
|
static void wiredone(xcb_im_t*, xcb_im_input_context_t*);
|
|
static void wireforward(xcb_im_t*, xcb_im_input_context_t*,
|
|
xcb_key_press_event_t*);
|
|
static void wiresetdata(xcb_im_input_context_t*, void*, xcb_im_free_function);
|
|
static xcb_get_geometry_cookie_t wiregeometry(xcb_connection_t*, xcb_drawable_t);
|
|
static xcb_get_geometry_reply_t* wiregeometryreply(xcb_connection_t*,
|
|
xcb_get_geometry_cookie_t, xcb_generic_error_t**);
|
|
static xcb_translate_coordinates_cookie_t wiretranslate(xcb_connection_t*,
|
|
xcb_window_t, xcb_window_t, int16_t, int16_t);
|
|
static xcb_translate_coordinates_reply_t* wiretranslatereply(xcb_connection_t*,
|
|
xcb_translate_coordinates_cookie_t, xcb_generic_error_t**);
|
|
static int wireflush(xcb_connection_t*);
|
|
|
|
#define xcb_im_commit_string wirecommit
|
|
#define xcb_im_preedit_start_callback wirestart
|
|
#define xcb_im_preedit_draw_callback wiredraw
|
|
#define xcb_im_preedit_done_callback wiredone
|
|
#define xcb_im_forward_event wireforward
|
|
#define xcb_im_input_context_set_data wiresetdata
|
|
#define xcb_im_input_context_get_data(ic) \
|
|
(wirebinding(ic) == nil ? nil : wirebinding(ic)->data)
|
|
#define xcb_im_input_context_get_input_style(ic) \
|
|
(wirebinding(ic) == nil ? 0 : wirebinding(ic)->style)
|
|
#define xcb_im_input_context_get_client_window(ic) \
|
|
(wirebinding(ic) == nil ? XCB_NONE : wirebinding(ic)->clientwin)
|
|
#define xcb_im_input_context_get_focus_window(ic) \
|
|
(wirebinding(ic) == nil ? XCB_NONE : wirebinding(ic)->focuswin)
|
|
#define xcb_im_input_context_get_preedit_attr_mask(ic) \
|
|
(wirebinding(ic) == nil ? 0 : wirebinding(ic)->preattrmask)
|
|
#define xcb_im_input_context_get_preedit_attr(ic) \
|
|
(wirebinding(ic) == nil ? nil : &wirebinding(ic)->preattr)
|
|
#define xcb_get_geometry wiregeometry
|
|
#define xcb_get_geometry_reply wiregeometryreply
|
|
#define xcb_translate_coordinates wiretranslate
|
|
#define xcb_translate_coordinates_reply wiretranslatereply
|
|
#define xcb_flush wireflush
|
|
#include "xim.c"
|
|
#undef xcb_im_commit_string
|
|
#undef xcb_im_preedit_start_callback
|
|
#undef xcb_im_preedit_draw_callback
|
|
#undef xcb_im_preedit_done_callback
|
|
#undef xcb_im_forward_event
|
|
#undef xcb_im_input_context_set_data
|
|
#undef xcb_im_input_context_get_data
|
|
#undef xcb_im_input_context_get_input_style
|
|
#undef xcb_im_input_context_get_client_window
|
|
#undef xcb_im_input_context_get_focus_window
|
|
#undef xcb_im_input_context_get_preedit_attr_mask
|
|
#undef xcb_im_input_context_get_preedit_attr
|
|
#undef xcb_get_geometry
|
|
#undef xcb_get_geometry_reply
|
|
#undef xcb_translate_coordinates
|
|
#undef xcb_translate_coordinates_reply
|
|
#undef xcb_flush
|
|
#include "test.h"
|
|
|
|
enum
|
|
{
|
|
Wstart,
|
|
Wdraw,
|
|
Wdone,
|
|
Wcommit,
|
|
Wforward,
|
|
Maxwire = 8,
|
|
};
|
|
|
|
typedef struct Wirecall Wirecall;
|
|
struct Wirecall
|
|
{
|
|
int op;
|
|
xcb_im_input_context_t *ic;
|
|
u32int caret;
|
|
u32int first;
|
|
u32int changed;
|
|
u32int status;
|
|
u32int flag;
|
|
u32int keysym;
|
|
int nbyte;
|
|
int nfeedback;
|
|
uchar text[Maxutf];
|
|
u32int feedback[Maxrunes];
|
|
};
|
|
|
|
typedef struct Wirewin Wirewin;
|
|
struct Wirewin
|
|
{
|
|
xcb_window_t win;
|
|
int geometryok;
|
|
int translateok;
|
|
int rootx;
|
|
int rooty;
|
|
int height;
|
|
};
|
|
|
|
static Wirecall wirecalls[Maxwire];
|
|
static Wirewin wirewins[8];
|
|
static int nwirecalls;
|
|
static int nflush;
|
|
static int transx, transy;
|
|
|
|
static Wirecall*
|
|
newwire(int op, xcb_im_input_context_t *ic)
|
|
{
|
|
Wirecall *call;
|
|
|
|
if(nwirecalls >= nelem(wirecalls))
|
|
return nil;
|
|
call = &wirecalls[nwirecalls++];
|
|
memset(call, 0, sizeof *call);
|
|
call->op = op;
|
|
call->ic = ic;
|
|
return call;
|
|
}
|
|
|
|
static void
|
|
wirecommit(xcb_im_t *im, xcb_im_input_context_t *ic, uint32_t flag,
|
|
const char *text, uint32_t nbyte, uint32_t keysym)
|
|
{
|
|
Wirecall *call;
|
|
|
|
USED(im);
|
|
call = newwire(Wcommit, ic);
|
|
if(call == nil)
|
|
return;
|
|
call->flag = flag;
|
|
call->keysym = keysym;
|
|
call->nbyte = min(nbyte, sizeof call->text);
|
|
if(call->nbyte > 0)
|
|
memmove(call->text, text, call->nbyte);
|
|
}
|
|
|
|
static void
|
|
wirestart(xcb_im_t *im, xcb_im_input_context_t *ic)
|
|
{
|
|
USED(im);
|
|
newwire(Wstart, ic);
|
|
}
|
|
|
|
static void
|
|
wiredraw(xcb_im_t *im, xcb_im_input_context_t *ic,
|
|
xcb_im_preedit_draw_fr_t *frame)
|
|
{
|
|
Wirecall *call;
|
|
|
|
USED(im);
|
|
call = newwire(Wdraw, ic);
|
|
if(call == nil)
|
|
return;
|
|
call->caret = frame->caret;
|
|
call->first = frame->chg_first;
|
|
call->changed = frame->chg_length;
|
|
call->status = frame->status;
|
|
call->nbyte = min(frame->length_of_preedit_string,
|
|
sizeof call->text);
|
|
if(call->nbyte > 0)
|
|
memmove(call->text, frame->preedit_string, call->nbyte);
|
|
call->nfeedback = min(frame->feedback_array.size,
|
|
nelem(call->feedback));
|
|
if(call->nfeedback > 0)
|
|
memmove(call->feedback, frame->feedback_array.items,
|
|
call->nfeedback * sizeof call->feedback[0]);
|
|
}
|
|
|
|
static void
|
|
wiredone(xcb_im_t *im, xcb_im_input_context_t *ic)
|
|
{
|
|
USED(im);
|
|
newwire(Wdone, ic);
|
|
}
|
|
|
|
static void
|
|
wireforward(xcb_im_t *im, xcb_im_input_context_t *ic,
|
|
xcb_key_press_event_t *ev)
|
|
{
|
|
Wirecall *call;
|
|
|
|
USED(im);
|
|
call = newwire(Wforward, ic);
|
|
if(call != nil)
|
|
call->keysym = ev->detail;
|
|
}
|
|
|
|
static void
|
|
wiresetdata(xcb_im_input_context_t *ic, void *data, xcb_im_free_function freefn)
|
|
{
|
|
Icbinding *binding;
|
|
|
|
USED(freefn);
|
|
binding = wirebinding(ic);
|
|
if(binding != nil)
|
|
binding->data = data;
|
|
}
|
|
|
|
static Wirewin*
|
|
wirewindow(xcb_window_t win)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < nelem(wirewins); i++)
|
|
if(wirewins[i].win == win)
|
|
return &wirewins[i];
|
|
return nil;
|
|
}
|
|
|
|
static xcb_get_geometry_cookie_t
|
|
wiregeometry(xcb_connection_t *c, xcb_drawable_t drawable)
|
|
{
|
|
xcb_get_geometry_cookie_t cookie;
|
|
|
|
USED(c);
|
|
cookie.sequence = drawable;
|
|
return cookie;
|
|
}
|
|
|
|
static xcb_get_geometry_reply_t*
|
|
wiregeometryreply(xcb_connection_t *c, xcb_get_geometry_cookie_t cookie,
|
|
xcb_generic_error_t **error)
|
|
{
|
|
xcb_get_geometry_reply_t *reply;
|
|
Wirewin *w;
|
|
|
|
USED(c);
|
|
USED(error);
|
|
w = wirewindow(cookie.sequence);
|
|
if(w == nil || !w->geometryok)
|
|
return nil;
|
|
reply = calloc(1, sizeof *reply);
|
|
if(reply != nil)
|
|
reply->height = w->height;
|
|
return reply;
|
|
}
|
|
|
|
static xcb_translate_coordinates_cookie_t
|
|
wiretranslate(xcb_connection_t *c, xcb_window_t src, xcb_window_t dst,
|
|
int16_t x, int16_t y)
|
|
{
|
|
xcb_translate_coordinates_cookie_t cookie;
|
|
|
|
USED(c);
|
|
USED(dst);
|
|
cookie.sequence = src;
|
|
transx = x;
|
|
transy = y;
|
|
return cookie;
|
|
}
|
|
|
|
static xcb_translate_coordinates_reply_t*
|
|
wiretranslatereply(xcb_connection_t *c,
|
|
xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t **error)
|
|
{
|
|
xcb_translate_coordinates_reply_t *reply;
|
|
Wirewin *w;
|
|
|
|
USED(c);
|
|
USED(error);
|
|
w = wirewindow(cookie.sequence);
|
|
if(w == nil || !w->translateok)
|
|
return nil;
|
|
reply = calloc(1, sizeof *reply);
|
|
if(reply != nil){
|
|
reply->same_screen = 1;
|
|
reply->dst_x = w->rootx + transx;
|
|
reply->dst_y = w->rooty + transy;
|
|
}
|
|
return reply;
|
|
}
|
|
|
|
static int
|
|
wireflush(xcb_connection_t *c)
|
|
{
|
|
USED(c);
|
|
nflush++;
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
wireclear(void)
|
|
{
|
|
memset(wirecalls, 0, sizeof wirecalls);
|
|
memset(icbindings, 0, sizeof icbindings);
|
|
memset(wirewins, 0, sizeof wirewins);
|
|
nwirecalls = 0;
|
|
nflush = 0;
|
|
transx = transy = 0;
|
|
preowner = nil;
|
|
}
|
|
|
|
/* Wire text is the COMPOUND_TEXT of what the engine produced. */
|
|
static int
|
|
checkct(struct ct *t, char *where, uchar *got, size_t ngot, char *utf8)
|
|
{
|
|
char *want;
|
|
size_t nwant;
|
|
int ok;
|
|
|
|
xcb_compound_text_init();
|
|
want = xcb_utf8_to_compound_text(utf8, strlen(utf8), &nwant);
|
|
if(!CT_CHECK(t, want != nil))
|
|
return 0;
|
|
ok = ngot == nwant && memcmp(got, want, nwant) == 0;
|
|
if(!ok)
|
|
CT_ERRORF(t, "%s: wire text differs from COMPOUND_TEXT of %s",
|
|
where, utf8);
|
|
free(want);
|
|
return ok;
|
|
}
|
|
|
|
static void
|
|
wirebind(int n, xcb_im_input_context_t *ic, Ic *state)
|
|
{
|
|
icbindings[n].ic = ic;
|
|
icbindings[n].data = state;
|
|
}
|
|
|
|
static Wirewin*
|
|
wirewin(int n, xcb_window_t win, int x, int y, int h)
|
|
{
|
|
Wirewin *w;
|
|
|
|
w = &wirewins[n];
|
|
memset(w, 0, sizeof *w);
|
|
w->win = win;
|
|
w->geometryok = 1;
|
|
w->translateok = 1;
|
|
w->rootx = x;
|
|
w->rooty = y;
|
|
w->height = h;
|
|
return w;
|
|
}
|
|
|
|
static struct xkb_state*
|
|
testkeystate(char *layout)
|
|
{
|
|
struct xkb_context *context;
|
|
struct xkb_keymap *keymap;
|
|
struct xkb_rule_names names;
|
|
struct xkb_state *state;
|
|
|
|
memset(&names, 0, sizeof names);
|
|
names.layout = layout;
|
|
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
if(context == nil)
|
|
return nil;
|
|
keymap = xkb_keymap_new_from_names(context, &names,
|
|
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
xkb_context_unref(context);
|
|
if(keymap == nil)
|
|
return nil;
|
|
state = xkb_state_new(keymap);
|
|
xkb_keymap_unref(keymap);
|
|
return state;
|
|
}
|
|
|
|
typedef struct Ximfix Ximfix;
|
|
struct Ximfix
|
|
{
|
|
Channel *oldreply;
|
|
Pump pump;
|
|
};
|
|
|
|
typedef struct Freejob Freejob;
|
|
struct Freejob
|
|
{
|
|
Ic *state;
|
|
Channel *done;
|
|
};
|
|
|
|
static int
|
|
ximbegin(struct ct *t, Ximfix *f, int lang)
|
|
{
|
|
Drawcmd dc;
|
|
|
|
memset(f, 0, sizeof *f);
|
|
while(channbrecv(drawc, &dc) > 0)
|
|
;
|
|
testengineinit(lang);
|
|
f->oldreply = replyc;
|
|
replyc = chancreate(sizeof(Keyres), 0);
|
|
pumpstart(&f->pump, 16);
|
|
USED(t);
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
ximend(Ximfix *f)
|
|
{
|
|
Drawcmd dc;
|
|
|
|
pumpstop(&f->pump);
|
|
testengineinit(LangEN);
|
|
while(channbrecv(drawc, &dc) > 0)
|
|
;
|
|
chanfree(replyc);
|
|
replyc = f->oldreply;
|
|
}
|
|
|
|
static Keyreq
|
|
nexttracecap(struct ct *t, Ximfix *f, int op, Ic *owner, int cap)
|
|
{
|
|
Keyreq req;
|
|
|
|
memset(&req, 0, sizeof req);
|
|
if(!CT_CHECK(t, channbrecv(f->pump.trace, &req) > 0))
|
|
return req;
|
|
CT_EQ_INT(t, op, req.op);
|
|
CT_EQ_PTR(t, owner, req.owner);
|
|
CT_EQ_INT(t, cap, req.clientpre);
|
|
CT_EQ_PTR(t, replyc, req.reply);
|
|
return req;
|
|
}
|
|
|
|
static Keyreq
|
|
nexttrace(struct ct *t, Ximfix *f, int op, Ic *state)
|
|
{
|
|
return nexttracecap(t, f, op, state, state->clientpre);
|
|
}
|
|
|
|
static void
|
|
notrace(struct ct *t, Ximfix *f)
|
|
{
|
|
Keyreq req;
|
|
|
|
CT_CHECK(t, channbrecv(f->pump.trace, &req) <= 0);
|
|
}
|
|
|
|
void
|
|
xim_adapter_key_contract(struct ct *t)
|
|
{
|
|
Ximfix f;
|
|
Ic state;
|
|
Keyreq req;
|
|
Keyres res;
|
|
u32int allmod;
|
|
|
|
memset(&state, 0, sizeof state);
|
|
if(!ximbegin(t, &f, LangJP))
|
|
goto cleanup;
|
|
allmod = ~0;
|
|
keypress(&state, 'x', allmod, &res);
|
|
req = nexttrace(t, &f, Keypress, &state);
|
|
CT_EQ_UINT(t, Mmask, req.mod);
|
|
CT_CHECK(t, !res.eaten);
|
|
keypress(&state, 'k', 0, &res);
|
|
req = nexttrace(t, &f, Keypress, &state);
|
|
CT_EQ_UINT(t, 0, req.mod);
|
|
CT_CHECK(t, res.eaten);
|
|
keypress(&state, 'a', 0, &res);
|
|
nexttrace(t, &f, Keypress, &state);
|
|
CT_CHECK(t, res.eaten);
|
|
checkstr(t, "preedit", "か", &res.preedit);
|
|
|
|
keypress(&state, Kspec|0x57, 0, &res);
|
|
nexttrace(t, &f, Keypress, &state);
|
|
CT_CHECK(t, !res.eaten);
|
|
checkstr(t, "preedit", "か", &res.commit);
|
|
checkstr(t, "preedit", "", &res.preedit);
|
|
cleanup:
|
|
ximend(&f);
|
|
}
|
|
|
|
void
|
|
xim_adapter_release_lifecycle(struct ct *t)
|
|
{
|
|
Ximfix f;
|
|
Ic a, b, *dead;
|
|
Keyres res;
|
|
int deadcap;
|
|
|
|
memset(&a, 0, sizeof a);
|
|
memset(&b, 0, sizeof b);
|
|
dead = nil;
|
|
if(!ximbegin(t, &f, LangJP))
|
|
goto cleanup;
|
|
keypress(&a, 'k', 0, &res);
|
|
nexttrace(t, &f, Keypress, &a);
|
|
keypress(&a, 'a', 0, &res);
|
|
nexttrace(t, &f, Keypress, &a);
|
|
keypress(&b, 'n', 0, &res);
|
|
nexttrace(t, &f, Keypress, &b);
|
|
checkstr(t, "preedit", "ん", &res.preedit);
|
|
CT_EQ_PTR(t, &b, testengineowner());
|
|
|
|
/* ResetIC and focus loss use the same idempotent release path. */
|
|
release(&a);
|
|
nexttrace(t, &f, Keyrelease, &a);
|
|
checkenginepreedit(t, "ん");
|
|
CT_EQ_PTR(t, &b, testengineowner());
|
|
release(&a);
|
|
notrace(t, &f);
|
|
|
|
release(&b);
|
|
nexttrace(t, &f, Keyrelease, &b);
|
|
CT_EQ_PTR(t, nil, testengineowner());
|
|
keypress(&b, 'k', 0, &res);
|
|
nexttrace(t, &f, Keypress, &b);
|
|
release(&b);
|
|
nexttrace(t, &f, Keyrelease, &b);
|
|
release(&b);
|
|
notrace(t, &f);
|
|
|
|
dead = calloc(1, sizeof *dead);
|
|
if(!CT_CHECK(t, dead != nil))
|
|
goto cleanup;
|
|
dead->next = ics;
|
|
ics = dead;
|
|
keypress(dead, 'n', 0, &res);
|
|
nexttrace(t, &f, Keypress, dead);
|
|
CT_EQ_PTR(t, dead, testengineowner());
|
|
deadcap = dead->clientpre;
|
|
icfree(dead);
|
|
nexttracecap(t, &f, Keyrelease, dead, deadcap);
|
|
dead = nil;
|
|
CT_EQ_PTR(t, nil, ics);
|
|
CT_EQ_PTR(t, nil, testengineowner());
|
|
cleanup:
|
|
if(dead != nil){
|
|
icunlink(dead);
|
|
free(dead);
|
|
}
|
|
ximend(&f);
|
|
}
|
|
|
|
static void
|
|
freeworker(void *arg)
|
|
{
|
|
Freejob *job;
|
|
uchar token;
|
|
|
|
job = arg;
|
|
icfree(job->state);
|
|
token = 0;
|
|
chansend(job->done, &token);
|
|
}
|
|
|
|
void
|
|
xim_adapter_free_waits_for_release(struct ct *t)
|
|
{
|
|
Ximfix f;
|
|
Freejob job;
|
|
Ic *state;
|
|
Channel *freed;
|
|
Keyreq req;
|
|
Keyres res;
|
|
uchar token;
|
|
int workeractive;
|
|
|
|
state = nil;
|
|
freed = nil;
|
|
workeractive = 0;
|
|
if(!ximbegin(t, &f, LangJP))
|
|
goto cleanup;
|
|
state = calloc(1, sizeof *state);
|
|
freed = chancreate(sizeof(uchar), 0);
|
|
if(!CT_CHECK(t, state != nil && freed != nil))
|
|
goto cleanup;
|
|
state->next = ics;
|
|
ics = state;
|
|
keypress(state, 'n', 0, &res);
|
|
nexttrace(t, &f, Keypress, state);
|
|
/* Hold the release: the free must not complete before the engine
|
|
* acknowledges it, since the engine still points at this Ic. */
|
|
pumphold(&f.pump, Keyrelease);
|
|
job.state = state;
|
|
job.done = freed;
|
|
workeractive = threadcreate(freeworker, &job, 8192) >= 0;
|
|
if(!CT_CHECK(t, workeractive))
|
|
goto cleanup;
|
|
chanrecv(f.pump.trace, &req);
|
|
CT_EQ_INT(t, Keyrelease, req.op);
|
|
CT_EQ_PTR(t, state, req.owner);
|
|
CT_CHECK(t, channbrecv(freed, &token) <= 0);
|
|
CT_EQ_PTR(t, state, ics);
|
|
CT_EQ_PTR(t, state, testengineowner());
|
|
chansend(f.pump.go, &token);
|
|
chanrecv(freed, &token);
|
|
workeractive = 0;
|
|
state = nil;
|
|
CT_EQ_PTR(t, nil, ics);
|
|
CT_EQ_PTR(t, nil, testengineowner());
|
|
cleanup:
|
|
if(workeractive){
|
|
chansend(f.pump.go, &token);
|
|
chanrecv(freed, &token);
|
|
state = nil;
|
|
}
|
|
if(state != nil){
|
|
icunlink(state);
|
|
free(state);
|
|
}
|
|
if(freed != nil)
|
|
chanfree(freed);
|
|
ximend(&f);
|
|
}
|
|
|
|
void
|
|
xim_adapter_styles(struct ct *t)
|
|
{
|
|
static u32int want[] = {
|
|
XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
|
|
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
|
|
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
|
|
};
|
|
xcb_im_input_context_t *ic;
|
|
Ic *state;
|
|
int i;
|
|
|
|
CT_EQ_SIZE(t, 1, nelem(encs));
|
|
CT_EQ_STR(t, "COMPOUND_TEXT", encs[0]);
|
|
CT_EQ_SIZE(t, nelem(want), nelem(styles));
|
|
/* Only PreeditCallbacks clients draw the preedit themselves. */
|
|
for(i = 0; i < nelem(want); i++){
|
|
CT_EQ_UINT(t, want[i], styles[i]);
|
|
wireclear();
|
|
ic = (xcb_im_input_context_t*)(uintptr)(80+i);
|
|
wirebind(0, ic, nil);
|
|
icbindings[0].style = want[i];
|
|
iccreate(nil, ic);
|
|
state = icbindings[0].data;
|
|
if(!CT_CHECK(t, state != nil && state == ics))
|
|
continue;
|
|
CT_EQ_INT(t, want[i] & XCB_IM_PreeditCallbacks ? 1 : 0,
|
|
state->clientpre);
|
|
CT_EQ_PTR(t, ic, state->xic);
|
|
icunlink(state);
|
|
free(state);
|
|
}
|
|
}
|
|
|
|
void
|
|
xim_adapter_placement(struct ct *t)
|
|
{
|
|
/* Spot location first, then the bottom of the focus window, then of
|
|
* the client window; an unset focus window is the client window. */
|
|
const struct {
|
|
u32int mask;
|
|
int focuswin;
|
|
int fgeo;
|
|
int ftrans;
|
|
int cgeo;
|
|
int ctrans;
|
|
int valid;
|
|
int x;
|
|
int y;
|
|
int h;
|
|
} cases[] = {
|
|
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 1, 1, 1, 1, 103, 204-Fontsz, Fontsz},
|
|
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 0, 1, 1, 1, 300, 440, 0},
|
|
{XCB_XIM_XNSpotLocation_MASK, 0, 1, 1, 1, 1, 1, 303, 404-Fontsz, Fontsz},
|
|
{0, 10, 1, 1, 1, 1, 1, 100, 230, 0},
|
|
{0, 0, 1, 1, 1, 1, 1, 300, 440, 0},
|
|
{0, 10, 0, 1, 1, 1, 1, 300, 440, 0},
|
|
{0, 10, 0, 1, 0, 1, 0, 0, 0, 0},
|
|
};
|
|
xcb_im_input_context_t *ic;
|
|
Icbinding *b;
|
|
Wirewin *focus, *client;
|
|
Ic state;
|
|
int i;
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)70;
|
|
for(i = 0; i < nelem(cases); i++){
|
|
wireclear();
|
|
memset(&state, 0, sizeof state);
|
|
state.xic = ic;
|
|
state.caret.valid = 1;
|
|
state.caret.x = state.caret.y = 9;
|
|
wirebind(0, ic, &state);
|
|
b = &icbindings[0];
|
|
b->focuswin = cases[i].focuswin;
|
|
b->clientwin = 20;
|
|
b->preattrmask = cases[i].mask;
|
|
b->preattr.spot_location.x = 3;
|
|
b->preattr.spot_location.y = 4;
|
|
focus = wirewin(0, 10, 100, 200, 30);
|
|
client = wirewin(1, 20, 300, 400, 40);
|
|
focus->geometryok = cases[i].fgeo;
|
|
focus->translateok = cases[i].ftrans;
|
|
client->geometryok = cases[i].cgeo;
|
|
client->translateok = cases[i].ctrans;
|
|
place(&state);
|
|
CT_EQ_INT(t, cases[i].valid, state.caret.valid);
|
|
CT_EQ_INT(t, cases[i].x, state.caret.x);
|
|
CT_EQ_INT(t, cases[i].y, state.caret.y);
|
|
CT_EQ_INT(t, cases[i].h, state.caret.h);
|
|
}
|
|
}
|
|
|
|
void
|
|
xim_adapter_placement_updates(struct ct *t)
|
|
{
|
|
xcb_im_packet_header_fr_t hdr;
|
|
xcb_im_input_context_t *ic;
|
|
xcb_key_press_event_t ev;
|
|
Icbinding *b;
|
|
Ximfix f;
|
|
Keyreq req;
|
|
Ic state;
|
|
struct xkb_state *oldstate;
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)71;
|
|
memset(&state, 0, sizeof state);
|
|
oldstate = kstate;
|
|
if(!ximbegin(t, &f, LangJP))
|
|
goto cleanup;
|
|
kstate = testkeystate("us");
|
|
if(!CT_CHECK(t, kstate != nil))
|
|
goto cleanup;
|
|
wireclear();
|
|
state.xic = ic;
|
|
wirebind(0, ic, &state);
|
|
b = &icbindings[0];
|
|
b->focuswin = 11;
|
|
b->clientwin = 21;
|
|
b->preattrmask = XCB_XIM_XNSpotLocation_MASK;
|
|
b->preattr.spot_location.x = 5;
|
|
b->preattr.spot_location.y = 6;
|
|
wirewin(0, 11, 100, 200, 30);
|
|
memset(&ev, 0, sizeof ev);
|
|
ev.detail = 57;
|
|
kpress(&state, &ev);
|
|
req = nexttrace(t, &f, Keypress, &state);
|
|
CT_CHECK(t, req.caret.valid);
|
|
CT_EQ_INT(t, 105, req.caret.x);
|
|
CT_EQ_INT(t, 206-Fontsz, req.caret.y);
|
|
wirewins[0].rootx = 150;
|
|
wirewins[0].rooty = 250;
|
|
kpress(&state, &ev);
|
|
req = nexttrace(t, &f, Keypress, &state);
|
|
CT_EQ_INT(t, 155, req.caret.x);
|
|
CT_EQ_INT(t, 256-Fontsz, req.caret.y);
|
|
|
|
b->preattr.spot_location.x = 15;
|
|
b->preattr.spot_location.y = 16;
|
|
memset(&hdr, 0, sizeof hdr);
|
|
hdr.major_opcode = XCB_XIM_SET_IC_VALUES;
|
|
callback(nil, nil, ic, &hdr, nil, nil, nil);
|
|
req = nexttrace(t, &f, Keycaret, &state);
|
|
CT_CHECK(t, req.caret.valid);
|
|
CT_EQ_INT(t, 165, req.caret.x);
|
|
CT_EQ_INT(t, 266-Fontsz, req.caret.y);
|
|
b->preattrmask = 0;
|
|
wirewins[0].geometryok = 0;
|
|
hdr.major_opcode = XCB_XIM_SET_IC_FOCUS;
|
|
callback(nil, nil, ic, &hdr, nil, nil, nil);
|
|
req = nexttrace(t, &f, Keycaret, &state);
|
|
CT_CHECK(t, !req.caret.valid);
|
|
cleanup:
|
|
if(kstate != oldstate)
|
|
xkb_state_unref(kstate);
|
|
kstate = oldstate;
|
|
preowner = nil;
|
|
ximend(&f);
|
|
}
|
|
|
|
void
|
|
xim_adapter_callback_replacement(struct ct *t)
|
|
{
|
|
Ic state;
|
|
Str pre;
|
|
xcb_im_input_context_t *ic;
|
|
int i;
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)3;
|
|
memset(&state, 0, sizeof state);
|
|
state.xic = ic;
|
|
state.clientpre = 1;
|
|
wireclear();
|
|
sinit(&pre, "k", strlen("k"));
|
|
updatepreedit(&state, &pre);
|
|
sinit(&pre, "한😀", strlen("한😀"));
|
|
updatepreedit(&state, &pre);
|
|
sclear(&pre);
|
|
updatepreedit(&state, &pre);
|
|
CT_EQ_INT(t, 5, nwirecalls);
|
|
CT_EQ_INT(t, Wstart, wirecalls[0].op);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[1].op);
|
|
CT_EQ_UINT(t, 0, wirecalls[1].first);
|
|
CT_EQ_UINT(t, 0, wirecalls[1].changed);
|
|
CT_EQ_UINT(t, 1, wirecalls[1].caret);
|
|
CT_EQ_INT(t, 1, wirecalls[1].nfeedback);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_UINT(t, 0, wirecalls[2].first);
|
|
CT_EQ_UINT(t, 1, wirecalls[2].changed);
|
|
CT_EQ_UINT(t, 2, wirecalls[2].caret);
|
|
checkct(t, "replacement", wirecalls[2].text, wirecalls[2].nbyte, "한😀");
|
|
CT_EQ_INT(t, 2, wirecalls[2].nfeedback);
|
|
for(i = 0; i < wirecalls[2].nfeedback; i++)
|
|
CT_EQ_UINT(t, XCB_XIM_UNDERLINE, wirecalls[2].feedback[i]);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[3].op);
|
|
CT_EQ_UINT(t, 2, wirecalls[3].changed);
|
|
CT_EQ_UINT(t, 1, wirecalls[3].status);
|
|
CT_EQ_INT(t, 0, wirecalls[3].nbyte);
|
|
CT_EQ_INT(t, Wdone, wirecalls[4].op);
|
|
CT_CHECK(t, !state.prestarted);
|
|
CT_EQ_INT(t, 0, state.nprerune);
|
|
CT_EQ_PTR(t, nil, preowner);
|
|
CT_EQ_INT(t, 1, nflush);
|
|
}
|
|
|
|
void
|
|
xim_adapter_callback_unicode(struct ct *t)
|
|
{
|
|
static char *text[] = {
|
|
"한𠀋",
|
|
"👩💻",
|
|
};
|
|
Ic state;
|
|
Str pre;
|
|
Wirecall *call;
|
|
int i;
|
|
|
|
for(i = 0; i < nelem(text); i++){
|
|
memset(&state, 0, sizeof state);
|
|
state.xic = (xcb_im_input_context_t*)(uintptr)(10+i);
|
|
state.clientpre = 1;
|
|
sinit(&pre, text[i], strlen(text[i]));
|
|
wireclear();
|
|
updatepreedit(&state, &pre);
|
|
CT_EQ_INT(t, 2, nwirecalls);
|
|
call = &wirecalls[1];
|
|
CT_EQ_INT(t, Wdraw, call->op);
|
|
CT_EQ_UINT(t, pre.n, call->caret);
|
|
CT_EQ_INT(t, pre.n, call->nfeedback);
|
|
checkct(t, text[i], call->text, call->nbyte, text[i]);
|
|
}
|
|
}
|
|
|
|
static void
|
|
startstate(Ic *state, xcb_im_input_context_t *ic)
|
|
{
|
|
Str pre;
|
|
|
|
memset(state, 0, sizeof *state);
|
|
state->xic = ic;
|
|
state->clientpre = 1;
|
|
sinit(&pre, "あ", strlen("あ"));
|
|
updatepreedit(state, &pre);
|
|
}
|
|
|
|
void
|
|
xim_adapter_callback_cleanup(struct ct *t)
|
|
{
|
|
static int opcode[] = {
|
|
XCB_XIM_UNSET_IC_FOCUS,
|
|
XCB_XIM_DESTROY_IC,
|
|
};
|
|
xcb_im_packet_header_fr_t hdr;
|
|
xcb_im_reset_ic_reply_fr_t reply;
|
|
xcb_im_client_t *client;
|
|
xcb_im_input_context_t *ic;
|
|
Ximfix f;
|
|
Keyres res;
|
|
Ic state, *dead;
|
|
int i;
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)29;
|
|
memset(&state, 0, sizeof state);
|
|
if(!ximbegin(t, &f, LangJP))
|
|
return;
|
|
wireclear();
|
|
wirebind(0, ic, &state);
|
|
state.xic = ic;
|
|
state.clientpre = 1;
|
|
keypress(&state, 'k', 0, &res);
|
|
nexttrace(t, &f, Keypress, &state);
|
|
keypress(&state, 'a', 0, &res);
|
|
nexttrace(t, &f, Keypress, &state);
|
|
updatepreedit(&state, &res.preedit);
|
|
memset(&hdr, 0, sizeof hdr);
|
|
memset(&reply, 0, sizeof reply);
|
|
hdr.major_opcode = XCB_XIM_RESET_IC;
|
|
callback(nil, nil, ic, &hdr, nil, &reply, nil);
|
|
nexttrace(t, &f, Keyreset, &state);
|
|
notrace(t, &f);
|
|
checkct(t, "reset reply", reply.committed_string,
|
|
reply.byte_length_of_committed_string, "か");
|
|
free(reply.committed_string);
|
|
CT_CHECK(t, state.engaged);
|
|
CT_EQ_PTR(t, &state, testengineowner());
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_INT(t, Wdone, wirecalls[3].op);
|
|
keypress(&state, 'n', 0, &res);
|
|
nexttrace(t, &f, Keypress, &state);
|
|
checkstr(t, "preedit", "ん", &res.preedit);
|
|
release(&state);
|
|
nexttrace(t, &f, Keyrelease, &state);
|
|
preowner = nil;
|
|
ximend(&f);
|
|
|
|
for(i = 0; i < nelem(opcode); i++){
|
|
ic = (xcb_im_input_context_t*)(uintptr)(30+i);
|
|
wireclear();
|
|
wirebind(0, ic, &state);
|
|
startstate(&state, ic);
|
|
memset(&hdr, 0, sizeof hdr);
|
|
hdr.major_opcode = opcode[i];
|
|
callback(nil, nil, ic, &hdr, nil, nil, nil);
|
|
CT_EQ_INT(t, 4, nwirecalls);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_UINT(t, 1, wirecalls[2].changed);
|
|
CT_EQ_INT(t, Wdone, wirecalls[3].op);
|
|
CT_CHECK(t, !state.prestarted);
|
|
CT_EQ_PTR(t, nil, preowner);
|
|
}
|
|
client = (xcb_im_client_t*)(uintptr)40;
|
|
ic = (xcb_im_input_context_t*)(uintptr)40;
|
|
wireclear();
|
|
startstate(&state, ic);
|
|
state.client = client;
|
|
state.next = ics;
|
|
ics = &state;
|
|
hdr.major_opcode = XCB_XIM_DISCONNECT;
|
|
callback(nil, client, nil, &hdr, nil, nil, nil);
|
|
ics = state.next;
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_INT(t, Wdone, wirecalls[3].op);
|
|
CT_CHECK(t, !state.prestarted);
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)41;
|
|
wireclear();
|
|
dead = calloc(1, sizeof *dead);
|
|
if(!CT_CHECK(t, dead != nil))
|
|
return;
|
|
dead->next = ics;
|
|
ics = dead;
|
|
startstate(dead, ic);
|
|
icfree(dead);
|
|
CT_EQ_INT(t, 4, nwirecalls);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_INT(t, Wdone, wirecalls[3].op);
|
|
CT_EQ_PTR(t, nil, preowner);
|
|
|
|
}
|
|
|
|
void
|
|
xim_adapter_callback_transfer(struct ct *t)
|
|
{
|
|
Ximfix f;
|
|
Ic a, b;
|
|
xcb_im_input_context_t *aic;
|
|
Keyres res;
|
|
Str pre;
|
|
|
|
aic = (xcb_im_input_context_t*)(uintptr)50;
|
|
memset(&a, 0, sizeof a);
|
|
memset(&b, 0, sizeof b);
|
|
if(!ximbegin(t, &f, LangJP))
|
|
goto cleanup;
|
|
wireclear();
|
|
a.xic = aic;
|
|
a.clientpre = 1;
|
|
sinit(&pre, "あ", strlen("あ"));
|
|
updatepreedit(&a, &pre);
|
|
keypress(&b, 'x', 0, &res);
|
|
nexttrace(t, &f, Keypress, &b);
|
|
CT_EQ_INT(t, 4, nwirecalls);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_INT(t, Wdone, wirecalls[3].op);
|
|
CT_CHECK(t, !a.prestarted);
|
|
CT_EQ_PTR(t, nil, preowner);
|
|
release(&b);
|
|
nexttrace(t, &f, Keyrelease, &b);
|
|
cleanup:
|
|
preowner = nil;
|
|
ximend(&f);
|
|
}
|
|
|
|
void
|
|
xim_adapter_callback_owner_loss(struct ct *t)
|
|
{
|
|
Ximfix f;
|
|
Ic state;
|
|
xcb_im_input_context_t *ic;
|
|
xcb_im_packet_header_fr_t hdr;
|
|
Keyreq req;
|
|
Keyreq trace;
|
|
Keyres res;
|
|
int foreign;
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)51;
|
|
memset(&state, 0, sizeof state);
|
|
foreign = 0;
|
|
if(!ximbegin(t, &f, LangJP))
|
|
goto cleanup;
|
|
wireclear();
|
|
wirebind(0, ic, &state);
|
|
state.xic = ic;
|
|
state.clientpre = 1;
|
|
keypress(&state, 'k', 0, &res);
|
|
nexttrace(t, &f, Keypress, &state);
|
|
updatepreedit(&state, &res.preedit);
|
|
CT_EQ_INT(t, Wstart, wirecalls[0].op);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[1].op);
|
|
CT_EQ_PTR(t, &state, preowner);
|
|
/* While we still own the engine, the periodic check changes nothing. */
|
|
checkpreowner();
|
|
nexttrace(t, &f, Keycap, &state);
|
|
CT_EQ_PTR(t, &state, preowner);
|
|
CT_EQ_INT(t, 2, nwirecalls);
|
|
|
|
/* Another frontend takes the engine; the next check clears our preedit. */
|
|
memset(&req, 0, sizeof req);
|
|
req.owner = &foreign;
|
|
req.op = Keypress;
|
|
req.ks = 'n';
|
|
req.reply = replyc;
|
|
chansend(keyc, &req);
|
|
chanrecv(replyc, &res);
|
|
memset(&trace, 0, sizeof trace);
|
|
CT_CHECK(t, channbrecv(f.pump.trace, &trace) > 0);
|
|
CT_EQ_INT(t, Keypress, trace.op);
|
|
CT_EQ_PTR(t, &foreign, trace.owner);
|
|
CT_EQ_PTR(t, &foreign, testengineowner());
|
|
checkpreowner();
|
|
nexttrace(t, &f, Keycap, &state);
|
|
CT_EQ_PTR(t, &foreign, testengineowner());
|
|
CT_EQ_INT(t, 4, nwirecalls);
|
|
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
|
|
CT_EQ_UINT(t, 1, wirecalls[2].changed);
|
|
CT_EQ_INT(t, Wdone, wirecalls[3].op);
|
|
CT_CHECK(t, !state.prestarted);
|
|
CT_EQ_PTR(t, nil, preowner);
|
|
checkpreowner();
|
|
notrace(t, &f);
|
|
|
|
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */
|
|
release(&state);
|
|
nexttrace(t, &f, Keyrelease, &state);
|
|
release(&state);
|
|
notrace(t, &f);
|
|
memset(&hdr, 0, sizeof hdr);
|
|
hdr.major_opcode = XCB_XIM_RESET_IC;
|
|
callback(nil, nil, ic, &hdr, nil, nil, nil);
|
|
notrace(t, &f);
|
|
hdr.major_opcode = XCB_XIM_DESTROY_IC;
|
|
callback(nil, nil, ic, &hdr, nil, nil, nil);
|
|
notrace(t, &f);
|
|
CT_EQ_INT(t, 4, nwirecalls);
|
|
|
|
memset(&req, 0, sizeof req);
|
|
req.owner = &foreign;
|
|
req.op = Keyrelease;
|
|
req.reply = replyc;
|
|
chansend(keyc, &req);
|
|
chanrecv(replyc, &res);
|
|
memset(&trace, 0, sizeof trace);
|
|
CT_CHECK(t, channbrecv(f.pump.trace, &trace) > 0);
|
|
CT_EQ_INT(t, Keyrelease, trace.op);
|
|
CT_EQ_PTR(t, &foreign, trace.owner);
|
|
cleanup:
|
|
preowner = nil;
|
|
ximend(&f);
|
|
}
|
|
|
|
void
|
|
xim_adapter_commit_encoding(struct ct *t)
|
|
{
|
|
static char text[] = "한𠀋❤️👩💻";
|
|
Ic state;
|
|
xcb_im_input_context_t *ic;
|
|
|
|
ic = (xcb_im_input_context_t*)(uintptr)60;
|
|
memset(&state, 0, sizeof state);
|
|
state.xic = ic;
|
|
wireclear();
|
|
commit(&state, text, strlen(text));
|
|
CT_EQ_INT(t, 1, nwirecalls);
|
|
CT_EQ_INT(t, Wcommit, wirecalls[0].op);
|
|
CT_EQ_UINT(t, XCB_XIM_LOOKUP_CHARS, wirecalls[0].flag);
|
|
CT_EQ_UINT(t, 0, wirecalls[0].keysym);
|
|
checkct(t, "commit", wirecalls[0].text, wirecalls[0].nbyte, text);
|
|
wireclear();
|
|
commit(&state, text, 0);
|
|
CT_EQ_INT(t, 0, nwirecalls);
|
|
}
|
|
|
|
void
|
|
xim_keymap_lookup(struct ct *t)
|
|
{
|
|
enum { ShiftMask = 1<<0, LockMask = 1<<1, Mod2Mask = 1<<4,
|
|
Mod5Mask = 1<<7, Group1 = 1<<13 };
|
|
struct xkb_context *context;
|
|
struct xkb_keymap *keymap;
|
|
struct xkb_rule_names names;
|
|
struct xkb_state *state;
|
|
|
|
memset(&names, 0, sizeof names);
|
|
names.layout = "de,ru";
|
|
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
keymap = xkb_keymap_new_from_names(context, &names,
|
|
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
if(!CT_CHECK(t, keymap != nil)){
|
|
xkb_context_unref(context);
|
|
return;
|
|
}
|
|
state = xkb_state_new(keymap);
|
|
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, 0));
|
|
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, ShiftMask));
|
|
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, LockMask));
|
|
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, LockMask|ShiftMask));
|
|
CT_EQ_UINT(t, XKB_KEY_EuroSign, keymaplookup(state, 26, Mod5Mask));
|
|
CT_EQ_UINT(t, XKB_KEY_Cyrillic_u, keymaplookup(state, 26, Group1));
|
|
CT_EQ_UINT(t, XKB_KEY_KP_End, keymaplookup(state, 87, 0));
|
|
CT_EQ_UINT(t, XKB_KEY_KP_1, keymaplookup(state, 87, Mod2Mask));
|
|
xkb_state_unref(state);
|
|
xkb_keymap_unref(keymap);
|
|
xkb_context_unref(context);
|
|
}
|
|
|
|
/* imdkit carries any UTF-8 text through COMPOUND_TEXT unchanged. */
|
|
void
|
|
xim_compound_text(struct ct *t)
|
|
{
|
|
static char *text[] = { "A😀한", "한글", "𠀋", "👩💻" };
|
|
char *ct, *utf8;
|
|
size_t clen, len, ulen;
|
|
int i;
|
|
|
|
xcb_compound_text_init();
|
|
for(i = 0; i < nelem(text); i++){
|
|
len = strlen(text[i]);
|
|
ct = xcb_utf8_to_compound_text(text[i], len, &clen);
|
|
if(!CT_CHECK(t, ct != nil))
|
|
continue;
|
|
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
|
|
if(CT_CHECK(t, utf8 != nil)){
|
|
CT_EQ_SIZE(t, len, ulen);
|
|
CT_EQ_MEM(t, text[i], utf8, len);
|
|
}
|
|
free(utf8);
|
|
free(ct);
|
|
}
|
|
}
|