From 1a8561949882c359fcd0d53a81dde30c0bd0934e Mon Sep 17 00:00:00 2001 From: Albert Safin Date: Fri, 9 Nov 2018 06:19:08 +0700 Subject: [PATCH] Add input and bounding shapes support (#2742) Basic idea: if the window has a shape, set the parent container shape as the union of the window shape and the shape of the frame borders. Co-authored-by: Uli Schlachter --- configure.ac | 2 +- debian/control | 1 + include/data.h | 5 + include/handlers.h | 1 + include/i3.h | 3 +- include/x.h | 5 + src/handlers.c | 22 +++++ src/main.c | 21 ++++ src/manage.c | 17 ++++ src/tree.c | 5 + src/x.c | 209 +++++++++++++++++++++++++++++++++++----- testcases/t/301-shape.t | 114 ++++++++++++++++++++++ 12 files changed, 378 insertions(+), 27 deletions(-) create mode 100644 testcases/t/301-shape.t diff --git a/configure.ac b/configure.ac index 7ae01422..b961f61c 100644 --- a/configure.ac +++ b/configure.ac @@ -91,7 +91,7 @@ AX_PTHREAD dnl Each prefix corresponds to a source tarball which users might have dnl downloaded in a newer version and would like to overwrite. PKG_CHECK_MODULES([LIBSN], [libstartup-notification-1.0]) -PKG_CHECK_MODULES([XCB], [xcb xcb-xkb xcb-xinerama xcb-randr]) +PKG_CHECK_MODULES([XCB], [xcb xcb-xkb xcb-xinerama xcb-randr xcb-shape]) PKG_CHECK_MODULES([XCB_UTIL], [xcb-event xcb-util]) PKG_CHECK_MODULES([XCB_UTIL_CURSOR], [xcb-cursor]) PKG_CHECK_MODULES([XCB_UTIL_KEYSYMS], [xcb-keysyms]) diff --git a/debian/control b/debian/control index 71a2599c..843e6557 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,7 @@ Build-Depends: debhelper (>= 9), libxcb-cursor-dev, libxcb-xrm-dev, libxcb-xkb-dev, + libxcb-shape0-dev, libxkbcommon-dev (>= 0.4.0), libxkbcommon-x11-dev (>= 0.4.0), asciidoc (>= 8.4.4), diff --git a/include/data.h b/include/data.h index d2b501b9..c3cada37 100644 --- a/include/data.h +++ b/include/data.h @@ -484,6 +484,11 @@ struct Window { /* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */ double min_aspect_ratio; double max_aspect_ratio; + + /** The window has a nonrectangular shape. */ + bool shaped; + /** The window has a nonrectangular input shape. */ + bool input_shaped; }; /** diff --git a/include/handlers.h b/include/handlers.h index 1d5a3865..d2c79c59 100644 --- a/include/handlers.h +++ b/include/handlers.h @@ -16,6 +16,7 @@ extern int randr_base; extern int xkb_base; +extern int shape_base; /** * Adds the given sequence to the list of events which are ignored. diff --git a/include/i3.h b/include/i3.h index 93a7e0a3..e7afe7e5 100644 --- a/include/i3.h +++ b/include/i3.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -70,7 +71,7 @@ extern uint8_t root_depth; extern xcb_visualid_t visual_id; extern xcb_colormap_t colormap; -extern bool xcursor_supported, xkb_supported; +extern bool xcursor_supported, xkb_supported, shape_supported; extern xcb_window_t root; extern struct ev_loop *main_loop; extern bool only_check_config; diff --git a/include/x.h b/include/x.h index 8b7664f2..d01709ed 100644 --- a/include/x.h +++ b/include/x.h @@ -137,3 +137,8 @@ void x_set_warp_to(Rect *rect); * */ void x_mask_event_mask(uint32_t mask); + +/** + * Enables or disables nonrectangular shape of the container frame. + */ +void x_set_shape(Con *con, xcb_shape_sk_t kind, bool enable); diff --git a/src/handlers.c b/src/handlers.c index 47abe2b4..5a79ffe1 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -20,6 +20,7 @@ int randr_base = -1; int xkb_base = -1; int xkb_current_group; +int shape_base = -1; /* After mapping/unmapping windows, a notify event is generated. However, we don’t want it, since it’d trigger an infinite loop of switching between the different windows when @@ -1400,6 +1401,27 @@ void handle_event(int type, xcb_generic_event_t *event) { return; } + if (shape_supported && type == shape_base + XCB_SHAPE_NOTIFY) { + xcb_shape_notify_event_t *shape = (xcb_shape_notify_event_t *)event; + + DLOG("shape_notify_event for window 0x%08x, shape_kind = %d, shaped = %d\n", + shape->affected_window, shape->shape_kind, shape->shaped); + + Con *con = con_by_window_id(shape->affected_window); + if (con == NULL) { + LOG("Not a managed window 0x%08x, ignoring shape_notify_event\n", + shape->affected_window); + return; + } + + if (shape->shape_kind == XCB_SHAPE_SK_BOUNDING || + shape->shape_kind == XCB_SHAPE_SK_INPUT) { + x_set_shape(con, shape->shape_kind, shape->shaped); + } + + return; + } + switch (type) { case XCB_KEY_PRESS: case XCB_KEY_RELEASE: diff --git a/src/main.c b/src/main.c index 7eb47c82..b8b4bdf1 100644 --- a/src/main.c +++ b/src/main.c @@ -89,6 +89,7 @@ struct ws_assignments_head ws_assignments = TAILQ_HEAD_INITIALIZER(ws_assignment /* We hope that those are supported and set them to true */ bool xcursor_supported = true; bool xkb_supported = true; +bool shape_supported = true; bool force_xinerama = false; @@ -622,6 +623,9 @@ int main(int argc, char *argv[]) { xcb_set_root_cursor(XCURSOR_CURSOR_POINTER); const xcb_query_extension_reply_t *extreply; + xcb_prefetch_extension_data(conn, &xcb_xkb_id); + xcb_prefetch_extension_data(conn, &xcb_shape_id); + extreply = xcb_get_extension_data(conn, &xcb_xkb_id); xkb_supported = extreply->present; if (!extreply->present) { @@ -683,6 +687,23 @@ int main(int argc, char *argv[]) { xkb_base = extreply->first_event; } + /* Check for Shape extension. We want to handle input shapes which is + * introduced in 1.1. */ + extreply = xcb_get_extension_data(conn, &xcb_shape_id); + if (extreply->present) { + shape_base = extreply->first_event; + xcb_shape_query_version_cookie_t cookie = xcb_shape_query_version(conn); + xcb_shape_query_version_reply_t *version = + xcb_shape_query_version_reply(conn, cookie, NULL); + shape_supported = version && version->minor_version >= 1; + free(version); + } else { + shape_supported = false; + } + if (!shape_supported) { + DLOG("shape 1.1 is not present on this server\n"); + } + restore_connect(); property_handlers_init(); diff --git a/src/manage.c b/src/manage.c index 63cadc0c..c1468123 100644 --- a/src/manage.c +++ b/src/manage.c @@ -548,6 +548,23 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * cleanup) */ xcb_change_save_set(conn, XCB_SET_MODE_INSERT, window); + if (shape_supported) { + /* Receive ShapeNotify events whenever the client altered its window + * shape. */ + xcb_shape_select_input(conn, window, true); + + /* Check if the window is shaped. Sadly, we can check only for the + * bounding shape, not for the input shape. */ + xcb_shape_query_extents_cookie_t cookie = + xcb_shape_query_extents(conn, window); + xcb_shape_query_extents_reply_t *reply = + xcb_shape_query_extents_reply(conn, cookie, NULL); + if (reply != NULL && reply->bounding_shaped) { + cwindow->shaped = true; + } + FREE(reply); + } + /* Check if any assignments match */ run_assignments(cwindow); diff --git a/src/tree.c b/src/tree.c index 99b03619..5023e894 100644 --- a/src/tree.c +++ b/src/tree.c @@ -248,6 +248,11 @@ bool tree_close_internal(Con *con, kill_window_t kill_window, bool dont_kill_par * mapped. See https://bugs.i3wm.org/1617 */ xcb_change_save_set(conn, XCB_SET_MODE_DELETE, con->window->id); + /* Stop receiving ShapeNotify events. */ + if (shape_supported) { + xcb_shape_select_input(conn, con->window->id, false); + } + /* Ignore X11 errors for the ReparentWindow request. * X11 Errors are returned when the window was already destroyed */ add_ignore_event(cookie.sequence, 0); diff --git a/src/x.c b/src/x.c index 45601337..a8e493dd 100644 --- a/src/x.c +++ b/src/x.c @@ -51,6 +51,11 @@ typedef struct con_state { bool need_reparent; xcb_window_t old_frame; + /* The container was child of floating container during the previous call of + * x_push_node(). This is used to remove the shape when the container is no + * longer floating. */ + bool was_floating; + Rect rect; Rect window_rect; @@ -396,6 +401,58 @@ static void x_draw_decoration_after_title(Con *con, struct deco_render_params *p x_draw_title_border(con, p); } +/* + * Get rectangles representing the border around the child window. Some borders + * are adjacent to the screen-edge and thus not returned. Return value is the + * number of rectangles. + * + */ +static size_t x_get_border_rectangles(Con *con, xcb_rectangle_t rectangles[4]) { + size_t count = 0; + int border_style = con_border_style(con); + + if (border_style != BS_NONE && con_is_leaf(con)) { + adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + Rect br = con_border_style_rect(con); + + if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = 0, + .y = 0, + .width = br.x, + .height = con->rect.height, + }; + } + if (!(borders_to_hide & ADJ_RIGHT_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = con->rect.width + (br.width + br.x), + .y = 0, + .width = -(br.width + br.x), + .height = con->rect.height, + }; + } + if (!(borders_to_hide & ADJ_LOWER_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = br.x, + .y = con->rect.height + (br.height + br.y), + .width = con->rect.width + br.width, + .height = -(br.height + br.y), + }; + } + /* pixel border have an additional line at the top */ + if (border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) { + rectangles[count++] = (xcb_rectangle_t){ + .x = br.x, + .y = 0, + .width = con->rect.width + br.width, + .height = br.y, + }; + } + } + + return count; +} + /* * Draws the decoration of the given container onto its parent. * @@ -497,37 +554,24 @@ void x_draw_decoration(Con *con) { /* 3: draw a rectangle in border color around the client */ if (p->border_style != BS_NONE && p->con_is_leaf) { - /* We might hide some borders adjacent to the screen-edge */ - adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; - Rect br = con_border_style_rect(con); - - /* These rectangles represent the border around the child window - * (left, bottom and right part). We don’t just fill the whole - * rectangle because some children are not freely resizable and we want - * their background color to "shine through". */ - if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), p->color->child_border, 0, 0, br.x, r->height); - } - if (!(borders_to_hide & ADJ_RIGHT_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, r->width + (br.width + br.x), 0, - -(br.width + br.x), r->height); - } - if (!(borders_to_hide & ADJ_LOWER_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, br.x, r->height + (br.height + br.y), - r->width + br.width, -(br.height + br.y)); - } - /* pixel border needs an additional line at the top */ - if (p->border_style == BS_PIXEL && !(borders_to_hide & ADJ_UPPER_SCREEN_EDGE)) { - draw_util_rectangle(&(con->frame_buffer), - p->color->child_border, br.x, 0, r->width + br.width, br.y); + /* Fill the border. We don’t just fill the whole rectangle because some + * children are not freely resizable and we want their background color + * to "shine through". */ + xcb_rectangle_t rectangles[4]; + size_t rectangles_count = x_get_border_rectangles(con, rectangles); + for (size_t i = 0; i < rectangles_count; i++) { + draw_util_rectangle(&(con->frame_buffer), p->color->child_border, + rectangles[i].x, + rectangles[i].y, + rectangles[i].width, + rectangles[i].height); } /* Highlight the side of the border at which the next window will be * opened if we are rendering a single window within a split container * (which is undistinguishable from a single window outside a split * container otherwise. */ + Rect br = con_border_style_rect(con); if (TAILQ_NEXT(con, nodes) == NULL && TAILQ_PREV(con, nodes_head, nodes) == NULL && con->parent->type != CT_FLOATING_CON) { @@ -730,6 +774,71 @@ static void set_hidden_state(Con *con) { state->is_hidden = should_be_hidden; } +/* + * Set the container frame shape as the union of the window shape and the + * shape of the frame borders. + */ +static void x_shape_frame(Con *con, xcb_shape_sk_t shape_kind) { + assert(con->window); + + xcb_shape_combine(conn, XCB_SHAPE_SO_SET, shape_kind, shape_kind, + con->frame.id, + con->window_rect.x + con->border_width, + con->window_rect.y + con->border_width, + con->window->id); + xcb_rectangle_t rectangles[4]; + size_t rectangles_count = x_get_border_rectangles(con, rectangles); + if (rectangles_count) { + xcb_shape_rectangles(conn, XCB_SHAPE_SO_UNION, shape_kind, + XCB_CLIP_ORDERING_UNSORTED, con->frame.id, + 0, 0, rectangles_count, rectangles); + } +} + +/* + * Reset the container frame shape. + */ +static void x_unshape_frame(Con *con, xcb_shape_sk_t shape_kind) { + assert(con->window); + + xcb_shape_mask(conn, XCB_SHAPE_SO_SET, shape_kind, con->frame.id, 0, 0, XCB_PIXMAP_NONE); +} + +/* + * Shape or unshape container frame based on the con state. + */ +static void set_shape_state(Con *con, bool need_reshape) { + if (!shape_supported || con->window == NULL) { + return; + } + + struct con_state *state; + if ((state = state_for_frame(con->frame.id)) == NULL) { + ELOG("window state for con %p not found\n", con); + return; + } + + if (need_reshape && con_is_floating(con)) { + /* We need to reshape the window frame only if it already has shape. */ + if (con->window->shaped) { + x_shape_frame(con, XCB_SHAPE_SK_BOUNDING); + } + if (con->window->input_shaped) { + x_shape_frame(con, XCB_SHAPE_SK_INPUT); + } + } + + if (state->was_floating && !con_is_floating(con)) { + /* Remove the shape when container is no longer floating. */ + if (con->window->shaped) { + x_unshape_frame(con, XCB_SHAPE_SK_BOUNDING); + } + if (con->window->input_shaped) { + x_unshape_frame(con, XCB_SHAPE_SK_INPUT); + } + } +} + /* * This function pushes the properties of each node of the layout tree to * X11 if they have changed (like the map state, position of the window, …). @@ -768,6 +877,8 @@ void x_push_node(Con *con) { con->mapped = false; } + bool need_reshape = false; + /* reparent the child window (when the window was moved due to a sticky * container) */ if (state->need_reparent && con->window != NULL) { @@ -793,8 +904,19 @@ void x_push_node(Con *con) { con->ignore_unmap++; DLOG("ignore_unmap for reparenting of con %p (win 0x%08x) is now %d\n", con, con->window->id, con->ignore_unmap); + + need_reshape = true; } + /* We need to update shape when window frame dimensions is updated. */ + need_reshape |= state->rect.width != rect.width || + state->rect.height != rect.height || + state->window_rect.width != con->window_rect.width || + state->window_rect.height != con->window_rect.height; + + /* We need to set shape when container becomes floating. */ + need_reshape |= con_is_floating(con) && !state->was_floating; + /* The pixmap of a borderless leaf container will not be used except * for the titlebar in a stack or tabs (issue #1013). */ bool is_pixmap_needed = (con->border_style != BS_NONE || @@ -898,6 +1020,8 @@ void x_push_node(Con *con) { fake_notify = true; } + set_shape_state(con, need_reshape); + /* Map if map state changed, also ensure that the child window * is changed if we are mapped and there is a new, unmapped child window. * Unmaps are handled in x_push_node_unmaps(). */ @@ -941,6 +1065,7 @@ void x_push_node(Con *con) { } state->unmap_now = (state->mapped != con->mapped) && !con->mapped; + state->was_floating = con_is_floating(con); if (fake_notify) { DLOG("Sending fake configure notify\n"); @@ -1325,3 +1450,37 @@ void x_mask_event_mask(uint32_t mask) { xcb_change_window_attributes(conn, state->id, XCB_CW_EVENT_MASK, values); } } + +/* + * Enables or disables nonrectangular shape of the container frame. + */ +void x_set_shape(Con *con, xcb_shape_sk_t kind, bool enable) { + struct con_state *state; + if ((state = state_for_frame(con->frame.id)) == NULL) { + ELOG("window state for con %p not found\n", con); + return; + } + + switch (kind) { + case XCB_SHAPE_SK_BOUNDING: + con->window->shaped = enable; + break; + case XCB_SHAPE_SK_INPUT: + con->window->input_shaped = enable; + break; + default: + ELOG("Received unknown shape event kind for con %p. This is a bug.\n", + con); + return; + } + + if (con_is_floating(con)) { + if (enable) { + x_shape_frame(con, kind); + } else { + x_unshape_frame(con, kind); + } + + xcb_flush(conn); + } +} diff --git a/testcases/t/301-shape.t b/testcases/t/301-shape.t new file mode 100644 index 00000000..ac0ec5a7 --- /dev/null +++ b/testcases/t/301-shape.t @@ -0,0 +1,114 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Test shape support. +# Ticket: #2742 +use i3test; +use ExtUtils::PkgConfig; + +my %sn_config; +BEGIN { + %sn_config = ExtUtils::PkgConfig->find('xcb-shape'); +} + +use Inline C => Config => LIBS => $sn_config{libs}, CCFLAGS => $sn_config{cflags}; +use Inline C => <<'END_OF_C_CODE'; +#include + +static xcb_connection_t *conn; + +void init_ctx(void *connptr) { + conn = (xcb_connection_t*)connptr; +} + +/* + * Set the shape for the window consisting of the following zones: + * + * +---+---+ + * | A | B | + * +---+---+ + * | C | + * +-------+ + * + * - Zone A is completly opaque. + * - Zone B is clickable through (input shape). + * - Zone C is completly transparent (bounding shape). + */ +void set_shape(long window_id) { + xcb_rectangle_t bounding_rectangle = { 0, 0, 100, 50 }; + xcb_shape_rectangles(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, + XCB_CLIP_ORDERING_UNSORTED, window_id, + 0, 0, 1, &bounding_rectangle); + xcb_rectangle_t input_rectangle = { 0, 0, 50, 50 }; + xcb_shape_rectangles(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, + XCB_CLIP_ORDERING_UNSORTED, window_id, + 0, 0, 1, &input_rectangle); + xcb_flush(conn); +} +END_OF_C_CODE + +init_ctx($x->get_xcb_conn()); + +my ($ws, $win1, $win1_focus, $win2, $win2_focus); + +################################################################################ +# Case 1: make floating window, then set shape +################################################################################ + +$ws = fresh_workspace; + +$win1 = open_floating_window(rect => [0, 0, 100, 100], background_color => '#ff0000'); +$win1_focus = get_focused($ws); + +$win2 = open_floating_window(rect => [0, 0, 100, 100], background_color => '#00ff00'); +$win2_focus = get_focused($ws); +set_shape($win2->id); + +$win1->warp_pointer(75, 25); +sync_with_i3; +is(get_focused($ws), $win1_focus, 'focus switched to the underlying window'); + +$win1->warp_pointer(25, 25); +sync_with_i3; +is(get_focused($ws), $win2_focus, 'focus switched to the top window'); + +kill_all_windows; + +################################################################################ +# Case 2: set shape first, then make window floating +################################################################################ + +$ws = fresh_workspace; + +$win1 = open_window(rect => [0, 0, 100, 100], background_color => '#ff0000'); +$win1_focus = get_focused($ws); +cmd 'floating toggle'; + +$win2 = open_window(rect => [0, 0, 100, 100], background_color => '#00ff00'); +$win2_focus = get_focused($ws); +set_shape($win2->id); +cmd 'floating toggle'; +sync_with_i3; + +$win1->warp_pointer(75, 25); +sync_with_i3; +is(get_focused($ws), $win1_focus, 'focus switched to the underlying window'); + +$win1->warp_pointer(25, 25); +sync_with_i3; +is(get_focused($ws), $win2_focus, 'focus switched to the top window'); + +done_testing; -- 2.39.5