From e6bf32b0f62d0afc19edea5dfbb74b1651aa2a02 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 17:35:33 +0900 Subject: [PATCH] xim: place popup from input context --- tests/test.h | 2 + tests/unit_test.c | 2 + tests/xim_adapter_test.c | 288 +++++++++++++++++++++++++++++++++++++-- xim/xim.c | 132 +++++++++++++++++- 4 files changed, 411 insertions(+), 13 deletions(-) diff --git a/tests/test.h b/tests/test.h index 5411f5a..e4f3fef 100644 --- a/tests/test.h +++ b/tests/test.h @@ -80,6 +80,8 @@ void xim_adapter_key_contract(struct ct*); void xim_adapter_release_lifecycle(struct ct*); void xim_adapter_free_waits_for_release(struct ct*); void xim_adapter_styles(struct ct*); +void xim_adapter_placement(struct ct*); +void xim_adapter_placement_updates(struct ct*); void xim_adapter_encoding_negotiation(struct ct*); void xim_adapter_callback_replacement(struct ct*); void xim_adapter_callback_unicode(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index a3d77b0..af37c1d 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -134,6 +134,8 @@ static const struct ct_test tests[] = { { "xim/adapter-release-lifecycle", xim_adapter_release_lifecycle }, { "xim/adapter-free-waits-release", xim_adapter_free_waits_for_release }, { "xim/styles", xim_adapter_styles }, + { "xim/placement", xim_adapter_placement }, + { "xim/placement-updates", xim_adapter_placement_updates }, { "xim/encoding-negotiation", xim_adapter_encoding_negotiation }, { "xim/callback-replacement", xim_adapter_callback_replacement }, { "xim/callback-unicode", xim_adapter_callback_unicode }, diff --git a/tests/xim_adapter_test.c b/tests/xim_adapter_test.c index 52647cc..b580608 100644 --- a/tests/xim_adapter_test.c +++ b/tests/xim_adapter_test.c @@ -1,20 +1,63 @@ #include #include +typedef struct Icbinding Icbinding; +struct Icbinding +{ + xcb_im_input_context_t *ic; + void *data; + 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* wiregetdata(xcb_im_input_context_t*); +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_input_context_get_data wiregetdata +#define xcb_im_input_context_get_data(ic) \ + (wirebinding(ic) == nil ? nil : wirebinding(ic)->data) +#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/xim.c" #undef xcb_im_commit_string @@ -22,6 +65,14 @@ static int wireflush(xcb_connection_t*); #undef xcb_im_preedit_draw_callback #undef xcb_im_preedit_done_callback #undef xcb_im_input_context_get_data +#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 "cutest/cutest.h" @@ -51,17 +102,23 @@ struct Wirecall u32int feedback[Maxrunes]; }; -typedef struct Icbinding Icbinding; -struct Icbinding +typedef struct Wirewin Wirewin; +struct Wirewin { - xcb_im_input_context_t *ic; - void *data; + xcb_window_t win; + int geometryok; + int translateok; + int samescreen; + int rootx; + int rooty; + int height; }; static Wirecall wirecalls[Maxwire]; -static Icbinding icbindings[8]; +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) @@ -133,17 +190,80 @@ wiredone(xcb_im_t *im, xcb_im_input_context_t *ic) newwire(Wdone, ic); } -static void* -wiregetdata(xcb_im_input_context_t *ic) +static Wirewin* +wirewindow(xcb_window_t win) { int i; - for(i = 0; i < nelem(icbindings); i++) - if(icbindings[i].ic == ic) - return icbindings[i].data; + 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 = w->samescreen; + reply->dst_x = w->rootx + transx; + reply->dst_y = w->rooty + transy; + } + return reply; +} + static int wireflush(xcb_connection_t *c) { @@ -157,8 +277,10 @@ 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; } @@ -169,6 +291,23 @@ wirebind(int n, xcb_im_input_context_t *ic, Ic *state) 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->samescreen = 1; + w->rootx = x; + w->rooty = y; + w->height = h; + return w; +} + void testengineinit(int); void testenginehandle(Keyreq*); void* testengineowner(void); @@ -506,6 +645,131 @@ xim_adapter_styles(struct ct *t) CT_EQ_INT(t, Cclientpreedit, state.cap); } +void +xim_adapter_placement(struct ct *t) +{ + static const struct { + u32int style; + u32int mask; + int fgeo; + int ftrans; + int cgeo; + int ctrans; + int valid; + int x; + int y; + } cases[] = { + {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, + XCB_XIM_XNSpotLocation_MASK, 1, 1, 1, 1, 1, 103, 204}, + {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, + XCB_XIM_XNSpotLocation_MASK, 1, 0, 1, 1, 1, 303, 404}, + {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, + 0, 1, 1, 1, 1, 1, 100, 230}, + {XCB_IM_PreeditNothing | XCB_IM_StatusNothing, + XCB_XIM_XNSpotLocation_MASK, 1, 1, 1, 1, 1, 100, 230}, + {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, + 0, 0, 1, 1, 1, 1, 300, 440}, + {XCB_IM_PreeditPosition | XCB_IM_StatusNothing, + 0, 0, 1, 0, 1, 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.style = cases[i].style; + state.caret.valid = 1; + state.caret.x = state.caret.y = 9; + wirebind(0, ic, &state); + b = &icbindings[0]; + b->focuswin = 10; + 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; + refreshplace(&state); + CT_EQ_UINT(t, 10, state.focuswin); + CT_EQ_UINT(t, 20, state.clientwin); + CT_EQ_INT(t, cases[i].style == + (XCB_IM_PreeditPosition | XCB_IM_StatusNothing) && + cases[i].mask != 0, state.hasspot); + 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, 0, state.caret.h); + } +} + +void +xim_adapter_placement_updates(struct ct *t) +{ + xcb_im_packet_header_fr_t hdr; + xcb_im_input_context_t *ic; + Icbinding *b; + Ximfix f; + Keyreq req; + Keyres res; + Ic state; + + ic = (xcb_im_input_context_t*)(uintptr)71; + memset(&state, 0, sizeof state); + if(!ximbegin(t, &f, LangJP)) + goto cleanup; + wireclear(); + state.xic = ic; + state.style = XCB_IM_PreeditPosition | XCB_IM_StatusNothing; + 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); + CT_CHECK(t, readattrs(&state)); + place(&state); + CT_CHECK(t, !readattrs(&state)); + keypress(&state, 'x', 0, &res); + 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, 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, 115, req.caret.x); + CT_EQ_INT(t, 216, req.caret.y); + CT_EQ_INT(t, 15, state.spot.x); + CT_EQ_INT(t, 16, state.spot.y); + CT_CHECK(t, !readattrs(&state)); + 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: + preowner = nil; + ximend(&f); +} + void xim_adapter_encoding_negotiation(struct ct *t) { diff --git a/xim/xim.c b/xim/xim.c index 3890aaa..ad041a2 100644 --- a/xim/xim.c +++ b/xim/xim.c @@ -21,6 +21,11 @@ struct Ic int encoding; int prestarted; int nprerune; + xcb_window_t clientwin; + xcb_window_t focuswin; + xcb_point_t spot; + int hasspot; + Caret caret; }; typedef struct Clientenc Clientenc; @@ -41,6 +46,7 @@ static xcb_im_t *xim; static xcb_keysym_t *kmap; static uint8_t minkc, maxkc; static uint8_t symsper; +static xcb_window_t rootwin; static int opened; static Ic *ics; static Ic *preowner; @@ -181,6 +187,108 @@ getscreen(int scr) return nil; } +static int +readattrs(Ic *state) +{ + const xcb_im_preedit_attr_t *attr; + xcb_window_t clientwin, focuswin; + xcb_point_t spot; + int changed, hasspot; + + clientwin = xcb_im_input_context_get_client_window(state->xic); + focuswin = xcb_im_input_context_get_focus_window(state->xic); + spot = state->spot; + hasspot = 0; + if(state->style == (XCB_IM_PreeditPosition | XCB_IM_StatusNothing) && + (xcb_im_input_context_get_preedit_attr_mask(state->xic) & + XCB_XIM_XNSpotLocation_MASK)){ + attr = xcb_im_input_context_get_preedit_attr(state->xic); + if(attr != nil){ + spot = attr->spot_location; + hasspot = 1; + } + } + changed = clientwin != state->clientwin || focuswin != state->focuswin || + hasspot != state->hasspot || (hasspot && + (spot.x != state->spot.x || spot.y != state->spot.y)); + state->clientwin = clientwin; + state->focuswin = focuswin; + state->spot = spot; + state->hasspot = hasspot; + return changed; +} + +static int +translate(xcb_window_t win, int x, int y, Caret *caret) +{ + xcb_translate_coordinates_cookie_t cookie; + xcb_translate_coordinates_reply_t *reply; + + if(win == XCB_NONE) + return 0; + cookie = xcb_translate_coordinates(conn, win, rootwin, x, y); + reply = xcb_translate_coordinates_reply(conn, cookie, nil); + if(reply == nil || !reply->same_screen){ + free(reply); + return 0; + } + caret->valid = 1; + caret->x = reply->dst_x; + caret->y = reply->dst_y; + caret->h = 0; + free(reply); + return 1; +} + +static int +placebottom(Ic *state, xcb_window_t win) +{ + xcb_get_geometry_cookie_t gcookie; + xcb_get_geometry_reply_t *geometry; + int height; + + if(win == XCB_NONE) + return 0; + gcookie = xcb_get_geometry(conn, win); + geometry = xcb_get_geometry_reply(conn, gcookie, nil); + if(geometry == nil) + return 0; + height = geometry->height; + free(geometry); + if(!translate(win, 0, 0, &state->caret)) + return 0; + state->caret.y += height; + return 1; +} + +static void +place(Ic *state) +{ + xcb_window_t win[2]; + int i, nwin; + + memset(&state->caret, 0, sizeof state->caret); + win[0] = state->focuswin; + nwin = 1; + if(state->clientwin != state->focuswin) + win[nwin++] = state->clientwin; + if(state->hasspot) + for(i = 0; i < nwin; i++) + if(translate(win[i], state->spot.x, state->spot.y, + &state->caret)) + return; + for(i = 0; i < nwin; i++) + if(placebottom(state, win[i])) + return; +} + +static void +refreshplace(Ic *state) +{ + readattrs(state); + place(state); +} + static void commit(Ic *state, char *s, int nbyte) { @@ -281,6 +389,7 @@ sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res) kr.op = op; kr.ks = key; kr.mod = mod & Mmask; + kr.caret = state->caret; kr.reply = replyc; chansend(keyc, &kr); chanrecv(replyc, res); @@ -317,7 +426,7 @@ kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev) Keyres res; char buf[Maxutf]; u32int key, rune; - int n; + int n, wasvalid; key = kget(ev->detail, ev->state); rune = xkb_keysym_to_utf32(key); @@ -327,6 +436,15 @@ kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev) key = Kspec + (key - 0xff00); else key = rune; + if(meaningful(key)){ + wasvalid = state->caret.valid; + if(!state->engaged) + refreshplace(state); + else if(readattrs(state)) + place(state); + if(state->engaged && wasvalid && !state->caret.valid) + sendrequest(state, Keycaret, 0, 0, &res); + } keypress(state, key, ev->state, &res); n = stoutf(&res.commit, buf, sizeof buf); commit(state, buf, n); @@ -387,6 +505,7 @@ iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic) state->client = client; state->encoding = getencoding(client); setstyle(state, xcb_im_input_context_get_input_style(ic)); + refreshplace(state); state->next = ics; ics = state; xcb_im_input_context_set_data(ic, state, icfree); @@ -432,6 +551,15 @@ callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic, if(ev != nil && (ev->response_type & ~0x80) == XCB_KEY_PRESS) kpress(state, ic, ev); break; + case XCB_XIM_SET_IC_VALUES: + case XCB_XIM_SET_IC_FOCUS: + refreshplace(state); + if(state->engaged){ + Keyres res; + + sendrequest(state, Keycaret, 0, 0, &res); + } + break; case XCB_XIM_DESTROY_IC: case XCB_XIM_RESET_IC: case XCB_XIM_UNSET_IC_FOCUS: @@ -461,6 +589,7 @@ ximclose(void) free(state); } ics = nil; + rootwin = XCB_NONE; free(kmap); kmap = nil; if(conn != nil){ @@ -502,6 +631,7 @@ ximinit(void) ximlog("cannot find X screen"); return -1; } + rootwin = screen->root; if(kinit() < 0){ ximlog("cannot read keyboard mapping"); return -1;