/* * A test for gravity handling. * * Copyright © 2017 Uli Schlachter * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * * Loosely based on test-gravity.c from metacity, which does not have a * copyright or license header, but Metacity itself is licensed under GPLv2. */ #include #include #include #include #include #include #include #include /* * This program tests various gravity-related things. For each possible gravity, * this does: * - Create a window with this gravity. * [Wait for expose event] * - Check that the window is mapped in the correct place. * - Resize the window. * [Wait for real configure notify] * - Check that the window moves as expected by the gravity. * - Move and resize the window (go back to the original size). * [Wait for real configure notify] * - Check that the window moves as expected by the gravity. * - Move the window. * [Wait for synthetic configure notify] * - Check that the window moves to the expected position. * * This needs a WM that supports _NET_FRAME_EXTENTS. * Also, this assumes that the WM allows and applies window moves and resizes by * the application. */ enum test_state { TEST_STATE_CREATED, TEST_STATE_RESIZED1, TEST_STATE_RESIZED2, TEST_STATE_MOVED, TEST_STATE_DONE, TEST_STATE_DONE_GOT_CONFIGURE, }; #define WIN_POS_X1 50 #define WIN_POS_Y1 70 #define WIN_WIDTH1 30 #define WIN_HEIGHT1 40 #define WIN_POS_X2 42 #define WIN_POS_Y2 43 #define WIN_WIDTH2 12 #define WIN_HEIGHT2 16 static int32_t state_difference[][2] = { [TEST_STATE_CREATED] = { 0, 0 }, [TEST_STATE_RESIZED1] = { WIN_WIDTH1 - WIN_WIDTH2, WIN_HEIGHT1 - WIN_HEIGHT2 }, [TEST_STATE_RESIZED2] = { 0, 0 }, [TEST_STATE_MOVED] = { 0, 0 }, [TEST_STATE_DONE] = { 0, 0 }, [TEST_STATE_DONE_GOT_CONFIGURE] = { 0, 0 }, }; struct window_state { xcb_window_t window; enum test_state state; xcb_gravity_t gravity; bool sent_delayed_proceed; }; static xcb_connection_t *c = NULL; static xcb_screen_t *screen; static struct window_state window_state; static xcb_atom_t net_frame_extents; static bool done; static bool had_error = false; static void check(bool cond, const char *format, ...) __attribute__ ((format(printf, 2, 3))); static void do_log(const char *format, ...) __attribute__ ((format(printf, 1, 2))); static void check(bool cond, const char *format, ...) { va_list ap; if (cond) return; va_start(ap, format); fprintf(stderr, "ERROR: "); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); va_end(ap); if (c) { xcb_no_operation(c); free(xcb_get_input_focus_reply(c, xcb_get_input_focus(c), NULL)); } had_error = true; } static void do_log(const char *format, ...) { va_list ap; va_start(ap, format); fprintf(stdout, "LOG: "); vfprintf(stdout, format, ap); fprintf(stdout, "\n"); va_end(ap); } static void query_frame_extents(xcb_window_t win, uint32_t *left, uint32_t *right, uint32_t *top, uint32_t *bottom) { xcb_get_property_reply_t *reply = xcb_get_property_reply(c, xcb_get_property(c, 0, win, net_frame_extents, XCB_ATOM_CARDINAL, 0, 4), NULL); *left = 0; *right = 0; *top = 0; *bottom = 0; if (reply && reply->length == 4) { uint32_t *data = (uint32_t *) xcb_get_property_value(reply); *left = data[0]; *right = data[1]; *top = data[2]; *bottom = data[3]; } free(reply); } static void delayed_proceed_with_window(void) { xcb_client_message_event_t ev; if (window_state.sent_delayed_proceed) return; memset(&ev, 0, sizeof(ev)); ev.response_type = XCB_CLIENT_MESSAGE; ev.window = window_state.window; ev.format = 32; ev.type = XCB_ATOM_NOTICE; ev.data.data32[0] = (int) window_state.state; xcb_send_event(c, 0, window_state.window, XCB_EVENT_MASK_NO_EVENT, (char *) &ev); window_state.sent_delayed_proceed = true; } static void get_geometry(xcb_window_t win, int32_t *x, int32_t *y, uint32_t *width, uint32_t *height, uint32_t *left, uint32_t *right, uint32_t *top, uint32_t *bottom) { xcb_get_geometry_cookie_t geo_c = xcb_get_geometry_unchecked(c, win); xcb_translate_coordinates_cookie_t coord_c = xcb_translate_coordinates(c, win, screen->root, 0, 0); query_frame_extents(win, left, right, top, bottom); xcb_get_geometry_reply_t *geo = xcb_get_geometry_reply(c, geo_c, NULL); xcb_translate_coordinates_reply_t *coord = xcb_translate_coordinates_reply(c, coord_c, NULL); *x = coord->dst_x; *y = coord->dst_y; *width = geo->width; *height = geo->height; free(coord); free(geo); } static const char *gravity_to_string(xcb_gravity_t gravity) { switch (gravity) { case XCB_GRAVITY_NORTH_WEST: return "NorthWest"; case XCB_GRAVITY_NORTH: return "North"; case XCB_GRAVITY_NORTH_EAST: return "NorthEast"; case XCB_GRAVITY_WEST: return "West"; case XCB_GRAVITY_CENTER: return "Center"; case XCB_GRAVITY_EAST: return "East"; case XCB_GRAVITY_SOUTH_WEST: return "SouthWest"; case XCB_GRAVITY_SOUTH: return "South"; case XCB_GRAVITY_SOUTH_EAST: return "SouthEast"; case XCB_GRAVITY_STATIC: return "Static"; default: fprintf(stderr, "Unknown gravity value %d", (int) gravity); exit(1); } } static const char *state_to_string(enum test_state state) { switch (state) { case TEST_STATE_CREATED: return "CREATED"; case TEST_STATE_RESIZED1: return "RESIZED1"; case TEST_STATE_RESIZED2: return "RESIZED2"; case TEST_STATE_MOVED: return "MOVED"; case TEST_STATE_DONE: return "DONE"; case TEST_STATE_DONE_GOT_CONFIGURE: return "DONE+c"; default: return "UNKNOWN"; } } static int32_t div2(int32_t value, int32_t *rounding) { /* If value is odd, we could round up or down */ if (value & 1) *rounding = 1; return value / 2; } static void check_geometry(int32_t expected_x, int32_t expected_y, uint32_t expected_width, uint32_t expected_height) { int32_t actual_x, actual_y; uint32_t actual_width, actual_height, left, right, top, bottom; get_geometry(window_state.window, &actual_x, &actual_y, &actual_width, &actual_height, &left, &right, &top, &bottom); int32_t offset_x, offset_y; int32_t extra_x = 0, extra_y = 0; int32_t diff_x = state_difference[window_state.state][0], diff_y = state_difference[window_state.state][1]; switch (window_state.gravity) { case XCB_GRAVITY_NORTH_WEST: offset_x = left; offset_y = top; break; case XCB_GRAVITY_NORTH: offset_x = div2(left - right + diff_x, &extra_x);; offset_y = top; break; case XCB_GRAVITY_NORTH_EAST: offset_x = -right + diff_x; offset_y = top; break; case XCB_GRAVITY_WEST: offset_x = left; offset_y = div2(top - bottom + diff_y, &extra_y); break; case XCB_GRAVITY_CENTER: offset_x = div2(left - right + diff_x, &extra_x); offset_y = div2(top - bottom + diff_y, &extra_y); break; case XCB_GRAVITY_EAST: offset_x = -right + diff_x; offset_y = div2(top - bottom + diff_y, &extra_y); break; case XCB_GRAVITY_SOUTH_WEST: offset_x = left; offset_y = -bottom + diff_y; break; case XCB_GRAVITY_SOUTH: offset_x = div2(left - right + diff_x, &extra_x); offset_y = -bottom + diff_y; break; case XCB_GRAVITY_SOUTH_EAST: offset_x = -right + diff_x; offset_y = -bottom + diff_y; break; case XCB_GRAVITY_STATIC: offset_x = 0; offset_y = 0; break; default: fprintf(stderr, "Unknown gravity!?\n"); return; } do_log("Checking if position of window with gravity %s is %dx%d and size " "is %dx%d when frame has size left=%d, right=%d, top=%d, bottom=%d", gravity_to_string(window_state.gravity), expected_x, expected_y, expected_width, expected_height, left, right, top, bottom); check(expected_width == actual_width, "For window with gravity %s in state %s, expected width = %d, but got %d", gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_width, (int) actual_width); check(expected_height == actual_height, "For window with gravity %s in state %s, expected height = %d, but got %d", gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_height, (int) actual_height); check(expected_x + offset_x == actual_x || expected_x + offset_x + extra_x == actual_x, "For window with gravity %s in state %s, expected x = %d+%d (+%d), but got %d", gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_x, (int) offset_x, (int) extra_x, (int) actual_x); check(expected_y + offset_y == actual_y || expected_y + offset_y + extra_y == actual_y, "For window with gravity %s in state %s, expected y = %d+%d (+%d), but got %d", gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_y, (int) offset_y, (int) extra_y, (int) actual_y); } static void init(xcb_gravity_t gravity) { const char *name = gravity_to_string(gravity); xcb_size_hints_t size_hints; memset(&size_hints, 0, sizeof(size_hints)); size_hints.win_gravity = gravity; size_hints.flags = XCB_ICCCM_SIZE_HINT_US_POSITION | XCB_ICCCM_SIZE_HINT_P_WIN_GRAVITY; window_state.window = xcb_generate_id(c); window_state.state = TEST_STATE_CREATED; window_state.gravity = gravity; do_log("creating window at %dx%d with size %dx%d", WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); xcb_create_window(c, screen->root_depth, window_state.window, screen->root, WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, (uint32_t[]) { screen->white_pixel, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_EXPOSURE }); xcb_change_property(c, XCB_PROP_MODE_REPLACE, window_state.window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, strlen(name), name); xcb_icccm_set_wm_size_hints(c, window_state.window, XCB_ATOM_WM_NORMAL_HINTS, &size_hints); xcb_map_window(c, window_state.window); } static void handle_expose(xcb_expose_event_t *ev) { if (window_state.window != ev->window || window_state.state != TEST_STATE_CREATED) return; do_log("Window in state CREATED was exposed, going to next state"); delayed_proceed_with_window(); } static void handle_configure_notify(xcb_configure_notify_event_t *ev) { bool synthetic = ev->response_type & 0x80; if (window_state.window != ev->window) return; switch (window_state.state) { case TEST_STATE_CREATED: return; case TEST_STATE_RESIZED1: if (synthetic) return; do_log("Window in state RESIZED1 got ConfigureNotify, going to next state"); delayed_proceed_with_window(); return; case TEST_STATE_RESIZED2: if (synthetic) return; do_log("Window in state RESIZED2 got ConfigureNotify, going to next state"); delayed_proceed_with_window(); return; case TEST_STATE_MOVED: if (!synthetic) return; do_log("Window in state MOVED got ConfigureNotify, going to next state"); delayed_proceed_with_window(); return; case TEST_STATE_DONE: if (!synthetic) return; } check(false, "Unexpected configure notify for window 0x%x with gravity %d, %dx%d+%d+%d%s", (int) window_state.window, (int) window_state.gravity, ev->width, ev->height, ev->x, ev->y, synthetic ? " (generated)" : ""); } static void wait_for_wm(void) { /* Window managers can be awfully slow and ICCCM does not provide much * possibilities to synchronise with them. We try to do this by creating a * window and resizing it, but without mapping it. */ xcb_generic_event_t *event; xcb_window_t window = xcb_generate_id(c); do_log("waiting for WM"); xcb_create_window(c, screen->root_depth, window, screen->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, XCB_CW_EVENT_MASK, (uint32_t[]) { XCB_EVENT_MASK_STRUCTURE_NOTIFY }); xcb_configure_window(c, window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, (uint32_t[]) { 1, 2, 3, 4 }); xcb_flush(c); while (event = xcb_wait_for_event(c)) { if ((event->response_type & 0x7f) == XCB_CONFIGURE_NOTIFY) { free(event); break; } free(event); } xcb_destroy_window(c, window); } static void handle_client_message(xcb_client_message_event_t *event) { /* We want a full roundtrip between "we decide to proceed" and "we actually * proceed" so that any in-flight events are handled. To do this, we send * ourselves two client messages. (So that any events that are still in * flight when the first message is sent are handled) */ if (event->type == XCB_ATOM_NOTICE) { wait_for_wm(); event->response_type = XCB_CLIENT_MESSAGE; event->type = XCB_ATOM_WM_COMMAND; xcb_send_event(c, 0, event->window, XCB_EVENT_MASK_NO_EVENT, (char *) event); return; } if (event->type != XCB_ATOM_WM_COMMAND) return; check(window_state.window == event->window, "Got weird client message?!?"); if (event->data.data32[0] != (int) window_state.state) return; window_state.sent_delayed_proceed = false; switch (window_state.state) { case TEST_STATE_CREATED: /* Verify the position and size set in init() */ check_geometry(WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); do_log("Resizing window to size %dx%d", WIN_WIDTH2, WIN_HEIGHT2); xcb_configure_window(c, window_state.window, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, (uint32_t[]) { WIN_WIDTH2, WIN_HEIGHT2 }); window_state.state = TEST_STATE_RESIZED1; return; case TEST_STATE_RESIZED1: check_geometry(WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH2, WIN_HEIGHT2); do_log("Moving+resizing window to position %dx%d and size %dx%d", WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); xcb_configure_window(c, window_state.window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, (uint32_t[]) { WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1 }); window_state.state = TEST_STATE_RESIZED2; return; case TEST_STATE_RESIZED2: check_geometry(WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); do_log("Moving window to position %dx%d", WIN_POS_X2, WIN_POS_Y2); xcb_configure_window(c, window_state.window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, (uint32_t[]) { WIN_POS_X2, WIN_POS_Y2 }); window_state.state = TEST_STATE_MOVED; return; case TEST_STATE_MOVED: check_geometry(WIN_POS_X2, WIN_POS_Y2, WIN_WIDTH1, WIN_HEIGHT1); xcb_destroy_window(c, window_state.window); window_state.window = XCB_NONE; window_state.state = TEST_STATE_DONE; done = true; return; } } static void event_loop(void) { xcb_generic_event_t *ev; done = false; xcb_flush(c); while (!done && (ev = xcb_wait_for_event(c))) { uint8_t type = ev->response_type & 0x7f; switch (type) { case XCB_EXPOSE: handle_expose((xcb_expose_event_t *) ev); break; case XCB_CONFIGURE_NOTIFY: handle_configure_notify((xcb_configure_notify_event_t *) ev); break; case XCB_CLIENT_MESSAGE: handle_client_message((xcb_client_message_event_t *) ev); case XCB_REPARENT_NOTIFY: case XCB_MAP_NOTIFY: case XCB_UNMAP_NOTIFY: case XCB_DESTROY_NOTIFY: break; default: printf("Got unexpected event of type 0x%x\n", (int) ev->response_type); } free(ev); xcb_flush(c); } } static xcb_atom_t intern_atom(const char *str) { xcb_atom_t result; xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(c, xcb_intern_atom(c, 0, strlen(str), str), NULL); if (!reply) return XCB_NONE; result = reply->atom; free(reply); return result; } static void run_test(xcb_gravity_t gravity) { do_log("Doing run for gravity %s", gravity_to_string(gravity)); init(gravity); event_loop(); do_log("Finished run for gravity %s", gravity_to_string(gravity)); } int main() { int default_screen; c = xcb_connect(NULL, &default_screen); if (xcb_connection_has_error(c)) { fprintf(stderr, "Could not connect to X11 server: %d", xcb_connection_has_error(c)); return 1; } screen = xcb_aux_get_screen(c, default_screen); net_frame_extents = intern_atom("_NET_FRAME_EXTENTS"); run_test(XCB_GRAVITY_NORTH_WEST); run_test(XCB_GRAVITY_NORTH); run_test(XCB_GRAVITY_NORTH_EAST); run_test(XCB_GRAVITY_WEST); run_test(XCB_GRAVITY_CENTER); run_test(XCB_GRAVITY_EAST); run_test(XCB_GRAVITY_SOUTH_WEST); run_test(XCB_GRAVITY_SOUTH); run_test(XCB_GRAVITY_SOUTH_EAST); run_test(XCB_GRAVITY_STATIC); check(!xcb_connection_has_error(c), "X11 connection has error: %d", xcb_connection_has_error(c)); xcb_disconnect(c); if (had_error) return 1; puts("SUCCESS"); return 0; } // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80