]> git.sur5r.net Git - i3/i3/commitdiff
Add input and bounding shapes support (#2742) 3514/head
authorAlbert Safin <xzfcpw@gmail.com>
Thu, 8 Nov 2018 23:19:08 +0000 (06:19 +0700)
committerAlbert Safin <xzfcpw@gmail.com>
Sat, 1 Dec 2018 04:52:41 +0000 (11:52 +0700)
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 <psychon@znc.in>
12 files changed:
configure.ac
debian/control
include/data.h
include/handlers.h
include/i3.h
include/x.h
src/handlers.c
src/main.c
src/manage.c
src/tree.c
src/x.c
testcases/t/301-shape.t [new file with mode: 0644]

index 7ae014222d9b370673f940ef9d2cff8b86390ee2..b961f61cba4f561fe1c42f5b6914776c5719976b 100644 (file)
@@ -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])
index 71a2599c6418a0dbe2025ea1da6ffb40e63da650..843e6557cfd31281a71fc5553703b6e360bde518 100644 (file)
@@ -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),
index d2b501b966bb7e7497ce260dff5d21827e30d17d..c3cada3702819e0f84f7c65cc12c105e6163c862 100644 (file)
@@ -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;
 };
 
 /**
index 1d5a386522bcbe229c1de411ecf7b5ceab4d5fe8..d2c79c591686ea462e3ad752334c1d48ae94ad37 100644 (file)
@@ -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.
index 93a7e0a34cf6ca5983fe41cf7298b369bd32d9fe..e7afe7e54b9ac48b4c08e0b854ccaa7575d7574f 100644 (file)
@@ -14,6 +14,7 @@
 #include <sys/time.h>
 #include <sys/resource.h>
 
+#include <xcb/shape.h>
 #include <xcb/xcb_keysyms.h>
 #include <xcb/xkb.h>
 
@@ -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;
index 8b7664f2616f6f52d47c4a694d756c70af9bd84c..d01709ed5a77002f97fa5b9eed65f7ba179b1273 100644 (file)
@@ -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);
index 47abe2b49483d5cf0425d23bcf10885151c4f7de..5a79ffe174e6c025b35c09cfb85a1238ba23ea33 100644 (file)
@@ -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:
index 7eb47c822fa83550b0d61df0f8d127a27cac1e8d..b8b4bdf15c1eb2cdb9dd6c5e9951969255ed4e4d 100644 (file)
@@ -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();
index 63cadc0cef89bfd8eccb47fbd75fcb8536480b13..c146812396951683e5bbf4239d4f02bc3aaac69d 100644 (file)
@@ -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);
 
index 99b03619a25ed854f7658ee737e77a4914fe3ef5..5023e89461d0079169804cd0f100dff01828d31a 100644 (file)
@@ -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 456013373df68835129814504c7b5622001e07ea..a8e493dd070ba9372abae701b29bf15747c9162f 100644 (file)
--- 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 (file)
index 0000000..ac0ec5a
--- /dev/null
@@ -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 <xcb/shape.h>
+
+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;