#define _POSIX_C_SOURCE 200809L #include #include "common/backend.h" #include #include #include #include #include #include #if YASL_HAS_COCOA void yasl_test_frame_tick(struct yasl_surface *surface); #endif static void drain(void) { yasl_event e; int n = 0; while (yasl_wait_event(&e, 0) > 0) assert(++n < 1000); } static void *wake_thread(void *unused) { (void)unused; struct timespec delay = {0, 20000000}; nanosleep(&delay, NULL); yasl_wake(); return NULL; } static void *wrong_thread(void *data) { assert(yasl_surface_set_title(data, "wrong") == -EPERM); int w = 1, h = 1; assert(!yasl_cpu_pixels(data, &w, &h) && errno == EPERM && !w && !h); struct yasl_native native; assert(yasl_surface_get_native(data, &native) == -EPERM && !native.wl_display && !native.x11_display); return NULL; } static void push(yasl_surface *s, struct yasl_private_event event) { event.surface = s; assert(yasl_private_events_push(s->display, s, &event)); } static int refreshes; /* Whether the display still lists the surface with this address; compared, never dereferenced. */ static bool listed(struct yasl_private_display *d, uintptr_t id) { for (yasl_surface *p = d->surfaces; p; p = p->event_next) if ((uintptr_t)p == id) return true; return false; } static void refresh(yasl_surface *s) { refreshes++; yasl_event e; assert(yasl_wait_event(&e, 0) == -EBUSY); assert(errno == EBUSY); assert(yasl_surface_destroy(s) == -EBUSY); struct yasl_surface_options options = {.width = 32, .height = 32}; assert(!yasl_surface_create(&options) && errno == EBUSY); options.type = YASL_SURFACE_GL; assert(!yasl_surface_create(&options) && errno == EBUSY); int w, h; assert(yasl_cpu_pixels(s, &w, &h) && yasl_cpu_present(s) == 0); } int main(int argc, char **argv) { assert(argc == 2); enum yasl_backend backend = !strcmp(argv[1], "wayland") ? YASL_BACKEND_WAYLAND : !strcmp(argv[1], "x11") ? YASL_BACKEND_X11 : YASL_BACKEND_COCOA; assert(yasl_init(backend, "org.yasl.surface-test") == 0); assert(yasl_init(backend, NULL) == -EALREADY); assert(yasl_get_backend() == backend); unsigned features = yasl_get_features(); yasl_surface *s = yasl_surface_create( &(struct yasl_surface_options){.title = "CPU contract", .width = 96, .height = 80}); if (!s) { fprintf(stderr, "surface: %s\n", strerror(errno)); return 1; } assert(yasl_surface_set_userdata(s, &backend) == 0); assert(yasl_surface_get_userdata(s) == &backend); struct yasl_native native; assert(yasl_surface_get_native(s, &native) == 0); assert(backend != YASL_BACKEND_WAYLAND || (native.wl_display && native.wl_surface)); assert(backend != YASL_BACKEND_X11 || (native.x11_display && native.x11_window)); assert(backend != YASL_BACKEND_COCOA || (native.ns_window && native.ns_view)); assert(yasl_surface_get_native(s, NULL) == -EINVAL); assert(!yasl_metal_get_layer(s) && errno == EBUSY); if (backend == YASL_BACKEND_COCOA) { assert(!(features & YASL_FEATURE_OPENGL)); assert(!yasl_surface_create(&(struct yasl_surface_options){ .title = "unsupported GL", .width = 64, .height = 64, .type = YASL_SURFACE_GL})); assert(errno == ENOTSUP); assert(yasl_gl_make_current(NULL) == 0); assert(!yasl_gl_get_proc_address("glClear") && errno == ENOTSUP); } /* The first events report the window size, then ask for a frame. */ yasl_event e; int fw = 0, fh = 0; bool window = false; for (int i = 0; i < 1000; i++) { assert(yasl_wait_event(&e, i ? 20 : 0) >= 0); if (e.type == YASL_EVENT_WINDOW) { window = true; fw = e.window.pixel_width; fh = e.window.pixel_height; } if (e.type == YASL_EVENT_REFRESH) { assert(window && fw > 0 && fh > 0 && e.surface == s); break; } } assert(e.type == YASL_EVENT_REFRESH); struct yasl_surface_info info = yasl_surface_get_info(s); assert(info.pixel_width == fw && info.pixel_height == fh && info.scale > 0); /* CPU pixels persist between frames and are the window's own buffer. */ int w, h; uint32_t *pixels = yasl_cpu_pixels(s, &w, &h); assert(pixels && w == fw && h == fh); assert(pixels[0] == 0 && pixels[(size_t)w * h - 1] == 0); for (size_t i = 0; i < (size_t)w * h; i++) pixels[i] = 0xff102030u; pixels[(size_t)3 * w + 2] = 0xffaabbccu; assert(yasl_cpu_present(s) == 0); drain(); pixels = yasl_cpu_pixels(s, &w, &h); assert(pixels[0] == 0xff102030u && pixels[(size_t)3 * w + 2] == 0xffaabbccu); /* Waiting ends only on an event, the timeout or yasl_wake, never early. */ drain(); uint64_t start = yasl_time_ns(); int n = yasl_wait_event(&e, 200); assert(n == 1 || yasl_time_ns() - start >= 190000000ull); drain(); pthread_t thread; assert(!pthread_create(&thread, NULL, wake_thread, NULL)); start = yasl_time_ns(); n = yasl_wait_event(&e, 5000); pthread_join(thread, NULL); assert(n == 1 || (n == 0 && yasl_time_ns() - start < 4000000000ull)); assert(!pthread_create(&thread, NULL, wrong_thread, s)); pthread_join(thread, NULL); yasl_surface *second = yasl_surface_create( &(struct yasl_surface_options){.width = 32, .height = 32, .userdata = &backend}); assert(second && yasl_surface_get_userdata(second) == &backend); assert(yasl_surface_destroy(second) == 0); /* Text input */ int ime = yasl_surface_set_text_input(s, &(struct yasl_text_input){ .text = "a\xc3\xa9z", .length = 4, .selection_begin = 1, .selection_end = 3, .revision = 7, .caret_x = 1, .caret_y = 2, .caret_width = 1, .caret_height = 10}); assert(ime == 0 || ime == -ENOTSUP); assert(yasl_surface_set_text_input(s, &(struct yasl_text_input){ .text = "a\xc3\xa9z", .length = 4, .selection_begin = 2, .selection_end = 3}) == -EILSEQ); assert(yasl_surface_set_text_input(s, &(struct yasl_text_input){ .text = "\xc0\xaf", .length = 2}) == -EILSEQ); assert(yasl_surface_set_text_input(s, &(struct yasl_text_input){ .text = "ab", .length = 2, .selection_end = 3}) == -EINVAL); assert(yasl_surface_set_text_input(s, &(struct yasl_text_input){.caret_x = 1e300}) == -EOVERFLOW); assert(yasl_surface_set_text_input(s, NULL) == 0 || ime == -ENOTSUP); /* Arguments are checked before the backend sees them. */ assert(yasl_surface_begin_move_resize(s, YASL_EDGE_TOP | YASL_EDGE_BOTTOM) == -EINVAL); assert(yasl_surface_begin_move_resize(s, 16) == -EINVAL); assert(yasl_surface_request(s, (enum yasl_request)9) == -EINVAL); assert(yasl_surface_request_size(s, 0, 10) == -EINVAL); assert(yasl_surface_set_cursor(s, (enum yasl_cursor)99) == -EINVAL); int locked = yasl_surface_set_cursor(s, YASL_CURSOR_LOCKED); assert(locked == ((features & YASL_FEATURE_LOCKED_CURSOR) ? 0 : -ENOTSUP)); assert(yasl_surface_set_cursor(s, YASL_CURSOR_ARROW) == 0); assert(yasl_drag_start(s, &(struct yasl_drag_image){.pixel_width = 4, .pixel_height = 4}) == -EINVAL); uint32_t icon[4] = {0}; assert(yasl_drag_start(s, &(struct yasl_drag_image){ .pixels = icon, .pixel_width = 2, .pixel_height = 2, .hotspot_x = 2}) == -EINVAL); assert(!yasl_popup_create(s, &(struct yasl_popup_options){.width = 10, .height = 10, .anchor = YASL_EDGE_LEFT | YASL_EDGE_RIGHT}) && errno == EINVAL); assert(yasl_clipboard_set((enum yasl_selection)5, "x") == -EINVAL); assert(yasl_clipboard_set(YASL_CLIPBOARD, NULL) == -EINVAL); assert(yasl_clipboard_set(YASL_CLIPBOARD, "\xff") == -EILSEQ); assert(yasl_clipboard_set(YASL_PRIMARY, "x") == ((features & YASL_FEATURE_PRIMARY_SELECTION) ? 0 : -ENOTSUP)); drain(); /* Event conversion: positions are logical, text is owned until the next wait. */ uint32_t real_scale = s->scale_120; s->scale_120 = 240; push(s, (struct yasl_private_event){ .type = YASL_PRIVATE_EVENT_PEN, .pen = {.device_id = 10, .x = 25, .y = 18.5, .pressure = 0.3, .axes_valid = YASL_PRIVATE_PEN_PRESSURE, .action = YASL_PRIVATE_PEN_DOWN, .in_contact = true}}); assert(yasl_wait_event(&e, 0) == 1 && e.type == YASL_EVENT_PEN && e.action == YASL_PEN_DOWN); assert(e.x == 12.5 && e.y == 9.25 && e.pen.device_id == 10 && e.pen.pressure == 0.3 && (e.pen.axes & YASL_PEN_PRESSURE) && e.pen.in_contact); push(s, (struct yasl_private_event){ .type = YASL_PRIVATE_EVENT_POINTER_DOWN, .pointer = {.pos = {20, 10}, .button = YASL_PRIVATE_BUTTON_SECONDARY, .buttons = 4, .mods = YASL_PRIVATE_MOD_CTRL}}); assert(yasl_wait_event(&e, 0) == 1 && e.type == YASL_EVENT_POINTER && e.action == YASL_POINTER_DOWN && e.x == 10 && e.y == 5 && e.mods == YASL_MOD_CTRL && e.pointer.button == YASL_BUTTON_SECONDARY && e.pointer.buttons == 4); s->scale_120 = real_scale; push(s, (struct yasl_private_event){.type = YASL_PRIVATE_EVENT_TEXT, .text = {"owned", 5}}); assert(yasl_wait_event(&e, 0) == 1 && e.type == YASL_EVENT_TEXT); assert(e.length == 5 && !strcmp(e.text, "owned")); push(s, (struct yasl_private_event){ .type = YASL_PRIVATE_EVENT_PASTE, .paste = {.data = "", .request_id = 1, .status = YASL_PRIVATE_TRANSFER_UNAVAILABLE}}); assert(yasl_wait_event(&e, 0) == 1 && e.type == YASL_EVENT_CLIPBOARD && e.action == YASL_TRANSFER_UNAVAILABLE && e.length == 0); /* Losing focus releases held input; activation itself comes from the window state. */ push(s, (struct yasl_private_event){.type = YASL_PRIVATE_EVENT_FOCUS, .focus = {.focused = false}}); assert(yasl_wait_event(&e, 0) == 1 && e.type == YASL_EVENT_INPUT_RESET); push(s, (struct yasl_private_event){.type = YASL_PRIVATE_EVENT_FOCUS, .focus = {.focused = true}}); assert(yasl_wait_event(&e, 0) == 0 && e.type == YASL_EVENT_NONE); push(s, (struct yasl_private_event){.type = YASL_PRIVATE_EVENT_DROP_HOVER, .drop = {YASL_PRIVATE_DROP_LEAVE, {0, 0}}}); assert(yasl_wait_event(&e, 0) == 1 && e.type == YASL_EVENT_DROP && e.action == YASL_DROP_LEAVE); /* A resize keeps the overlap of the pixels and zeroes the rest. */ assert(yasl_surface_request_size(s, 112, 96) == 0); uint64_t resize_deadline = yasl_time_ns() + 3000000000ull; do { assert(yasl_wait_event(&e, 20) >= 0); info = yasl_surface_get_info(s); } while ((info.width != 112 || info.height != 96) && yasl_time_ns() < resize_deadline); assert(info.width == 112 && info.height == 96); pixels = yasl_cpu_pixels(s, &w, &h); assert(pixels && w == info.pixel_width && h == info.pixel_height); assert(pixels[0] == 0xff102030u && pixels[(size_t)3 * w + 2] == 0xffaabbccu); assert(pixels[(size_t)w * h - 1] == 0); assert(yasl_cpu_present(s) == 0); /* The refresh callback is reserved for modal loops; ordinary waits deliver REFRESH. */ s->refresh = refresh; for (int i = 0; i < 5; i++) { assert(yasl_cpu_pixels(s, &w, &h) && yasl_cpu_present(s) == 0); assert(yasl_wait_event(&e, 20) >= 0); } assert(refreshes == 0); #if YASL_HAS_COCOA s->display->modal = true; yasl_test_frame_tick(s); uint64_t deadline = yasl_private_loop_now_ns() + 3000000000ull; while (!refreshes && yasl_private_loop_now_ns() < deadline) assert(yasl_wait_event(&e, 20) >= 0); assert(refreshes > 0); s->display->modal = false; #endif s->refresh = NULL; /* Title and window requests: each either works or reports ENOTSUP. */ assert(yasl_surface_set_title(s, "CPU contract renamed") == 0); static const enum yasl_request requests[] = {YASL_REQUEST_RESTORE, YASL_REQUEST_FULLSCREEN, YASL_REQUEST_MAXIMIZE, YASL_REQUEST_MINIMIZE}; for (size_t i = 0; i < sizeof requests / sizeof requests[0]; i++) { int r = yasl_surface_request(s, requests[i]); assert(r == 0 || r == -ENOTSUP); } drain(); int restored = yasl_surface_request(s, YASL_REQUEST_RESTORE); assert(restored == 0 || restored == -ENOTSUP); drain(); /* ENOTSUP is the documented answer when the compositor lacks xdg-system-bell; Wayland * currently reports EIO there, so only the 0-or-errno rule is checked. */ assert(yasl_surface_bell(s) <= 0); /* Desktop queries and the drag cancel, which is a no-op without a drag. */ assert(yasl_get_appearance() <= 7); int double_click = yasl_get_double_click_ms(); assert(double_click >= 1 && double_click <= 10000); assert(yasl_drag_cancel() == 0); /* Without a pointer press these may fail with an errno; they must simply return. */ assert(yasl_surface_begin_move_resize(s, 0) <= 0); assert(yasl_surface_show_window_menu(s, 1, 1) <= 0); assert(yasl_drag_start(s, NULL) <= 0); drain(); /* A popup needs an input grab to open; if it opens, its parent owns it. */ struct yasl_private_display *display = s->display; struct yasl_popup_options popup_options = { .width = 40, .height = 30, .anchor_width = 1, .anchor_height = 1}; errno = 0; yasl_surface *popup = yasl_popup_create(s, &popup_options); uintptr_t popup_id = (uintptr_t)popup; if (!popup) { assert(errno != 0); } else { assert(listed(display, popup_id)); assert(yasl_popup_reposition(popup, &popup_options) <= 0); int pw = 0, ph = 0; assert(yasl_cpu_pixels(popup, &pw, &ph) && pw > 0 && ph > 0); } /* Clipboard: offering text may be refused without input; if accepted, it comes back. */ int offered = yasl_clipboard_set(YASL_CLIPBOARD, "yasl"); assert(offered <= 0); if (offered == 0) { assert(yasl_clipboard_request(s, YASL_CLIPBOARD) == 0); bool got = false; uint64_t clipboard_deadline = yasl_time_ns() + 2000000000ull; while (!got && yasl_time_ns() < clipboard_deadline) { assert(yasl_wait_event(&e, 50) >= 0); if (e.type != YASL_EVENT_CLIPBOARD) continue; got = true; assert(e.surface == s && e.action <= YASL_TRANSFER_FAILED); if (e.action == YASL_TRANSFER_OK) assert(e.length == 4 && !strcmp(e.text, "yasl")); } assert(got); } /* Destroying the parent also destroys its popup. */ assert(yasl_surface_destroy(s) == 0); if (popup) assert(!listed(display, popup_id)); assert(yasl_init(backend, NULL) == -EALREADY); s = yasl_surface_create(&(struct yasl_surface_options){.title = "shared backend remains", .width = 64, .height = 64}); assert(s); yasl_shutdown(); assert(yasl_init(backend, NULL) == 0); s = yasl_surface_create( &(struct yasl_surface_options){.title = "reinitialized", .width = 64, .height = 64}); assert(s); yasl_shutdown(); puts("public surface lifecycle, pixels, ownership, input, wake and refresh passed"); return 0; }