From dd1775a9927922c14f8679ccbc4d3ca8d5917e20 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 21:23:03 +0300 Subject: public api refactor --- tests/x11/test_input.c | 236 ++++++++++++++++++++++++++++++++++++++--- tests/x11/test_tablet_native.c | 10 +- 2 files changed, 226 insertions(+), 20 deletions(-) (limited to 'tests/x11') diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c index 95943d1..c2eca37 100644 --- a/tests/x11/test_input.c +++ b/tests/x11/test_input.c @@ -1,5 +1,7 @@ /* An isolated real X server supplies the keymap; disabling this surface's XIC * deliberately exercises text insertion when no input method is available. */ +#define _POSIX_C_SOURCE 200809L +#include #include "../../src/platform/x11/window.c" static void check_selection_encoding(struct x11_display *d, struct x11_surface *s) @@ -35,34 +37,72 @@ static void check_selection_encoding(struct x11_display *d, struct x11_surface * XFree(data); XDestroyWindow(d->native, requestor); - /* Simulate a legacy owner returning STRING to the paste conversion window, - * then an empty UTF-8 owner and malformed UTF-8. */ - const struct { Atom type; const char *bytes; size_t len; const char *expected; } cases[] = { - {XA_STRING, "caf\xe9 \xa3", 6, "café £"}, - {d->utf8, "", 0, ""}, - {d->utf8, "\xff", 1, NULL}, + /* Simulate legacy STRING, empty UTF-8, malformed UTF-8, and rejected INCR. */ + const struct { + Atom type; + const void *bytes; + size_t len; + int format; + const char *expected; + enum yasl_transfer_status status; + } cases[] = { + {XA_STRING, "caf\xe9 \xa3", 6, 8, "café £", YASL_TRANSFER_OK}, + {d->utf8, "", 0, 8, "", YASL_TRANSFER_OK}, + {d->utf8, "\xff", 1, 8, NULL, YASL_TRANSFER_FAILED}, + {d->incr, NULL, 1, 32, NULL, YASL_TRANSFER_FAILED}, }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { struct paste_request *pending = calloc(1, sizeof(*pending)); assert(pending); + pending->display = d; pending->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), 0, 0, 1, 1, 0, 0, 0); pending->target = s; + pending->id = (uint64_t)i + 1; + pending->timeout = yasl_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, + paste_timeout, pending); + assert(pending->timeout); pending->next = d->pastes; d->pastes = pending; - XChangeProperty(d->native, pending->window, property, cases[i].type, 8, - PropModeReplace, (const unsigned char *)cases[i].bytes, (int)cases[i].len); + unsigned long incr_bytes = 4096; + const void *bytes = cases[i].bytes; + if (cases[i].type == d->incr) bytes = &incr_bytes; + XChangeProperty(d->native, pending->window, property, cases[i].type, + cases[i].format, PropModeReplace, bytes, (int)cases[i].len); XSelectionEvent reply = {.requestor = pending->window, .property = property}; selection_notify(d, &reply); struct yasl_event event; - if (cases[i].expected) { - assert(yasl_surface_poll_event(&s->base, &event)); - assert(event.type == YASL_EVENT_PASTE); + assert(yasl_surface_poll_event(&s->base, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == i + 1); + assert(event.paste.status == cases[i].status); + if (cases[i].status == YASL_TRANSFER_OK) { assert(event.text.len == strlen(cases[i].expected)); assert(strcmp(event.text.data, cases[i].expected) == 0); } + if (cases[i].type == d->incr) { + assert(d->pastes == pending && !pending->target); + assert(yasl_loop_del_timer(d->loop, pending->timeout)); + pending->timeout = 0; + paste_timeout(pending, 0, PASTE_TIMEOUT_NS); + } assert(!yasl_surface_poll_event(&s->base, &event)); } + + struct paste_request *rejected = calloc(1, sizeof(*rejected)); + assert(rejected); + rejected->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + rejected->target = s; + rejected->selection = d->clipboard; + rejected->id = 99; + rejected->next = d->pastes; + d->pastes = rejected; + XSelectionEvent no_property = {.requestor = rejected->window, .property = None}; + selection_notify(d, &no_property); + struct yasl_event event; + assert(yasl_surface_poll_event(&s->base, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == 99); + assert(event.paste.status == YASL_TRANSFER_FAILED); } /* Own the XSETTINGS selection with one Net/DoubleClickTime value. */ @@ -96,6 +136,173 @@ static void check_double_click(struct x11_display *d) assert(yasl_display_double_click_ms(&d->base) == YASL_DOUBLE_CLICK_MS_DEFAULT); } +static struct yasl_event wait_paste(struct yasl_display *display, uint64_t id) +{ + struct yasl_event event; + for (int i = 0; i < 256 && yasl_display_poll_event(display, &event); i++) + if (event.type == YASL_EVENT_PASTE && event.paste.request_id == id) + return event; + assert(!"expected paste completion was not queued"); + return (struct yasl_event){0}; +} + +static void check_request_completion(struct x11_display *d, struct x11_surface *s) +{ + assert(surface_input_queue(&s->base) == &s->input); + assert(copy_text(&d->base, YASL_CLIPBOARD, "")); + XSync(d->native, False); + assert(XGetSelectionOwner(d->native, d->clipboard) == d->selection_window); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 101)); + struct yasl_event event = wait_paste(&d->base, 101); + assert(event.paste.status == YASL_TRANSFER_OK && event.paste.len == 0); + assert(event.paste.data && !event.paste.data[0]); + + XSetSelectionOwner(d->native, d->clipboard, None, CurrentTime); + XSync(d->native, False); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 102)); + XSync(d->native, False); + dispatch(&d->base); + event = wait_paste(&d->base, 102); + assert(event.paste.status == YASL_TRANSFER_UNAVAILABLE); + + Window silent_owner = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + XSetSelectionOwner(d->native, d->clipboard, silent_owner, CurrentTime); + XSync(d->native, False); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 103)); + struct paste_request *timed = d->pastes; + assert(timed && timed->id == 103 && timed->timeout); + assert(yasl_loop_del_timer(d->loop, timed->timeout)); + timed->timeout = 0; + paste_timeout(timed, 0, PASTE_TIMEOUT_NS); + event = wait_paste(&d->base, 103); + assert(event.paste.status == YASL_TRANSFER_FAILED); + + struct yasl_window_spec spec = {.title = "Cancelled paste", .size = {80, 60}}; + struct yasl_surface *cancel_target = yasl_surface_create_toplevel(&d->base, &spec); + assert(cancel_target); + assert(request_paste(&d->base, YASL_CLIPBOARD, cancel_target, 104)); + assert(d->pastes && d->pastes->target == (struct x11_surface *)cancel_target); + yasl_timer_id timeout = d->pastes->timeout; + yasl_surface_destroy(cancel_target); + assert(d->pastes && !d->pastes->target && d->pastes->id == 0); + assert(yasl_loop_del_timer(d->loop, timeout)); + d->pastes->timeout = 0; + XSelectionEvent late_reply = { + .requestor = d->pastes->window, + .selection = d->pastes->selection, + .property = None, + }; + selection_notify(d, &late_reply); + assert(!d->pastes); + XDestroyWindow(d->native, silent_owner); + XSetSelectionOwner(d->native, d->clipboard, None, CurrentTime); + XSync(d->native, False); +} + +static struct yasl_event next_composition(struct yasl_display *display) +{ + struct yasl_event event; + for (int i = 0; i < 256 && yasl_display_poll_event(display, &event); i++) + if (event.type == YASL_EVENT_COMPOSITION) + return event; + assert(!"expected composition snapshot was not queued"); + return (struct yasl_event){0}; +} + +static void drain_display_events(struct yasl_display *display) +{ + struct yasl_event event; + while (yasl_display_poll_event(display, &event)) {} +} + +static void check_xim_callbacks(struct yasl_display *display, struct x11_surface *s) +{ + const char *old_locale = setlocale(LC_CTYPE, NULL); + char *saved_locale = old_locale ? strdup(old_locale) : NULL; + const char *utf8_locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!utf8_locale) utf8_locale = setlocale(LC_CTYPE, "C.utf8"); + assert(utf8_locale); + + drain_display_events(display); + assert(xim_preedit_start(NULL, (XPointer)s, NULL) == -1); + + wchar_t initial[] = {L'A', (wchar_t)0x1f600, L'B'}; + XIMText wide = {.length = 3, .encoding_is_wchar = True, + .string.wide_char = initial}; + XIMPreeditDrawCallbackStruct draw = { + .caret = 2, .chg_first = 0, .chg_length = 0, .text = &wide, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(s->ime_preedit && strcmp(s->ime_preedit, "A😀B") == 0); + struct yasl_event event = next_composition(display); + assert(event.composition.active && event.composition.len == 6); + assert(event.composition.begin == 5 && event.composition.end == 5); + + /* XIMText.length counts characters. The data need not have a terminator. */ + char multibyte[] = {(char)0xc3, (char)0xa9, 'Z', 'x'}; + XIMText mb = {.length = 2, .encoding_is_wchar = False, + .string.multi_byte = multibyte}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 3, .chg_first = 1, .chg_length = 1, .text = &mb, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(s->ime_preedit && strcmp(s->ime_preedit, "AéZB") == 0); + event = next_composition(display); + assert(event.composition.active && event.composition.begin == 4); + assert(event.composition.end == 4); + + /* A NULL text string updates feedback/caret while preserving the snapshot. */ + XIMText feedback = {.length = 4, .encoding_is_wchar = False, + .string.multi_byte = NULL}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 1, .chg_first = 0, .chg_length = 4, .text = &feedback, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(strcmp(s->ime_preedit, "AéZB") == 0); + event = next_composition(display); + assert(event.composition.active && event.composition.begin == 1); + + char malformed[] = {(char)0xff, 'x'}; + XIMText invalid = {.length = 1, .encoding_is_wchar = False, + .string.multi_byte = malformed}; + draw.text = &invalid; + draw.chg_first = 0; + draw.chg_length = 0; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(strcmp(s->ime_preedit, "AéZB") == 0); + struct yasl_event unused; + assert(!yasl_display_poll_event(display, &unused)); + + wchar_t empty[] = {0}; + XIMText deletion = {.length = 0, .encoding_is_wchar = True, + .string.wide_char = empty}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 0, .chg_first = 0, .chg_length = 4, .text = &deletion, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + event = next_composition(display); + assert(!event.composition.active && event.composition.len == 0); + assert(!s->ime_preedit); + + /* Done also publishes an explicit end snapshot. */ + wide = (XIMText){.length = 3, .encoding_is_wchar = True, + .string.wide_char = initial}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 1, .chg_first = 0, .chg_length = 0, .text = &wide, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + (void)next_composition(display); + xim_preedit_done(NULL, (XPointer)s, NULL); + event = next_composition(display); + assert(!event.composition.active && !s->ime_preedit); + + if (saved_locale) { + setlocale(LC_CTYPE, saved_locale); + free(saved_locale); + } +} + int main(void) { struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; @@ -126,10 +333,13 @@ int main(void) XSync(d->native, False); dispatch(base); input_queue_clear(&s->input); + /* Native creation and keymap updates may already have queued focus or + * reset input on the shared display stream. Isolate this synthetic key. */ + struct yasl_event event; + while (yasl_surface_poll_event(surface, &event)) {} XKeyEvent native = {.type = KeyPress, .display = d->native, .window = s->window, .keycode = code}; key_event(s, &native, false); - struct yasl_event event; assert(yasl_surface_poll_event(surface, &event)); assert(event.type == YASL_EVENT_KEY && event.key.key == YASL_KEY_A); assert(event.key.codepoint == cases[i].label); @@ -149,6 +359,8 @@ int main(void) s->ic = saved_ic; check_selection_encoding(d, s); check_double_click(d); + check_request_completion(d, s); + check_xim_callbacks(base, s); /* Test CSD interactive actions, window menu, and IME caret */ assert(yasl_display_caps(base) & YASL_CAP_CSD); diff --git a/tests/x11/test_tablet_native.c b/tests/x11/test_tablet_native.c index b0d1366..706a5cc 100644 --- a/tests/x11/test_tablet_native.c +++ b/tests/x11/test_tablet_native.c @@ -57,14 +57,8 @@ static int connect_tool(const char *path) static void native_draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) { - struct yasl_frame_context context = { - .surface = surface, .size = frame->target.size, - .scale_120 = yasl_surface_geometry(surface).scale_120, - .cpu = {.pixels = frame->target.pixels, - .stride_bytes = frame->target.size.x * (int)sizeof(uint32_t)}, - .internal_handle = frame, - }; - draw(ud, surface, &context); + (void)surface; + draw(ud, surface, frame); } static void native_configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) -- cgit v1.2.3