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 <psychon@znc.in>
This commit is contained in:
Albert Safin 2018-11-09 06:19:08 +07:00
parent 9273f67734
commit 1a85619498
12 changed files with 378 additions and 27 deletions

View File

@ -91,7 +91,7 @@ AX_PTHREAD
dnl Each prefix corresponds to a source tarball which users might have dnl Each prefix corresponds to a source tarball which users might have
dnl downloaded in a newer version and would like to overwrite. dnl downloaded in a newer version and would like to overwrite.
PKG_CHECK_MODULES([LIBSN], [libstartup-notification-1.0]) 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], [xcb-event xcb-util])
PKG_CHECK_MODULES([XCB_UTIL_CURSOR], [xcb-cursor]) PKG_CHECK_MODULES([XCB_UTIL_CURSOR], [xcb-cursor])
PKG_CHECK_MODULES([XCB_UTIL_KEYSYMS], [xcb-keysyms]) PKG_CHECK_MODULES([XCB_UTIL_KEYSYMS], [xcb-keysyms])

1
debian/control vendored
View File

@ -13,6 +13,7 @@ Build-Depends: debhelper (>= 9),
libxcb-cursor-dev, libxcb-cursor-dev,
libxcb-xrm-dev, libxcb-xrm-dev,
libxcb-xkb-dev, libxcb-xkb-dev,
libxcb-shape0-dev,
libxkbcommon-dev (>= 0.4.0), libxkbcommon-dev (>= 0.4.0),
libxkbcommon-x11-dev (>= 0.4.0), libxkbcommon-x11-dev (>= 0.4.0),
asciidoc (>= 8.4.4), asciidoc (>= 8.4.4),

View File

@ -484,6 +484,11 @@ struct Window {
/* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */ /* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */
double min_aspect_ratio; double min_aspect_ratio;
double max_aspect_ratio; double max_aspect_ratio;
/** The window has a nonrectangular shape. */
bool shaped;
/** The window has a nonrectangular input shape. */
bool input_shaped;
}; };
/** /**

View File

@ -16,6 +16,7 @@
extern int randr_base; extern int randr_base;
extern int xkb_base; extern int xkb_base;
extern int shape_base;
/** /**
* Adds the given sequence to the list of events which are ignored. * Adds the given sequence to the list of events which are ignored.

View File

@ -14,6 +14,7 @@
#include <sys/time.h> #include <sys/time.h>
#include <sys/resource.h> #include <sys/resource.h>
#include <xcb/shape.h>
#include <xcb/xcb_keysyms.h> #include <xcb/xcb_keysyms.h>
#include <xcb/xkb.h> #include <xcb/xkb.h>
@ -70,7 +71,7 @@ extern uint8_t root_depth;
extern xcb_visualid_t visual_id; extern xcb_visualid_t visual_id;
extern xcb_colormap_t colormap; 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 xcb_window_t root;
extern struct ev_loop *main_loop; extern struct ev_loop *main_loop;
extern bool only_check_config; extern bool only_check_config;

View File

@ -137,3 +137,8 @@ void x_set_warp_to(Rect *rect);
* *
*/ */
void x_mask_event_mask(uint32_t mask); 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);

View File

@ -20,6 +20,7 @@
int randr_base = -1; int randr_base = -1;
int xkb_base = -1; int xkb_base = -1;
int xkb_current_group; int xkb_current_group;
int shape_base = -1;
/* After mapping/unmapping windows, a notify event is generated. However, we dont want it, /* After mapping/unmapping windows, a notify event is generated. However, we dont want it,
since itd trigger an infinite loop of switching between the different windows when since itd 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; 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) { switch (type) {
case XCB_KEY_PRESS: case XCB_KEY_PRESS:
case XCB_KEY_RELEASE: case XCB_KEY_RELEASE:

View 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 */ /* We hope that those are supported and set them to true */
bool xcursor_supported = true; bool xcursor_supported = true;
bool xkb_supported = true; bool xkb_supported = true;
bool shape_supported = true;
bool force_xinerama = false; bool force_xinerama = false;
@ -622,6 +623,9 @@ int main(int argc, char *argv[]) {
xcb_set_root_cursor(XCURSOR_CURSOR_POINTER); xcb_set_root_cursor(XCURSOR_CURSOR_POINTER);
const xcb_query_extension_reply_t *extreply; 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); extreply = xcb_get_extension_data(conn, &xcb_xkb_id);
xkb_supported = extreply->present; xkb_supported = extreply->present;
if (!extreply->present) { if (!extreply->present) {
@ -683,6 +687,23 @@ int main(int argc, char *argv[]) {
xkb_base = extreply->first_event; 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(); restore_connect();
property_handlers_init(); property_handlers_init();

View File

@ -548,6 +548,23 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
* cleanup) */ * cleanup) */
xcb_change_save_set(conn, XCB_SET_MODE_INSERT, window); 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 */ /* Check if any assignments match */
run_assignments(cwindow); run_assignments(cwindow);

View 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 */ * mapped. See https://bugs.i3wm.org/1617 */
xcb_change_save_set(conn, XCB_SET_MODE_DELETE, con->window->id); 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. /* Ignore X11 errors for the ReparentWindow request.
* X11 Errors are returned when the window was already destroyed */ * X11 Errors are returned when the window was already destroyed */
add_ignore_event(cookie.sequence, 0); add_ignore_event(cookie.sequence, 0);

209
src/x.c
View File

@ -51,6 +51,11 @@ typedef struct con_state {
bool need_reparent; bool need_reparent;
xcb_window_t old_frame; 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 rect;
Rect window_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); 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. * 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 */ /* 3: draw a rectangle in border color around the client */
if (p->border_style != BS_NONE && p->con_is_leaf) { if (p->border_style != BS_NONE && p->con_is_leaf) {
/* We might hide some borders adjacent to the screen-edge */ /* Fill the border. We dont just fill the whole rectangle because some
adjacent_t borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; * children are not freely resizable and we want their background color
Rect br = con_border_style_rect(con); * to "shine through". */
xcb_rectangle_t rectangles[4];
/* These rectangles represent the border around the child window size_t rectangles_count = x_get_border_rectangles(con, rectangles);
* (left, bottom and right part). We dont just fill the whole for (size_t i = 0; i < rectangles_count; i++) {
* rectangle because some children are not freely resizable and we want draw_util_rectangle(&(con->frame_buffer), p->color->child_border,
* their background color to "shine through". */ rectangles[i].x,
if (!(borders_to_hide & ADJ_LEFT_SCREEN_EDGE)) { rectangles[i].y,
draw_util_rectangle(&(con->frame_buffer), p->color->child_border, 0, 0, br.x, r->height); rectangles[i].width,
} rectangles[i].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);
} }
/* Highlight the side of the border at which the next window will be /* 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 * opened if we are rendering a single window within a split container
* (which is undistinguishable from a single window outside a split * (which is undistinguishable from a single window outside a split
* container otherwise. */ * container otherwise. */
Rect br = con_border_style_rect(con);
if (TAILQ_NEXT(con, nodes) == NULL && if (TAILQ_NEXT(con, nodes) == NULL &&
TAILQ_PREV(con, nodes_head, nodes) == NULL && TAILQ_PREV(con, nodes_head, nodes) == NULL &&
con->parent->type != CT_FLOATING_CON) { con->parent->type != CT_FLOATING_CON) {
@ -730,6 +774,71 @@ static void set_hidden_state(Con *con) {
state->is_hidden = should_be_hidden; 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 * 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, ). * 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; con->mapped = false;
} }
bool need_reshape = false;
/* reparent the child window (when the window was moved due to a sticky /* reparent the child window (when the window was moved due to a sticky
* container) */ * container) */
if (state->need_reparent && con->window != NULL) { if (state->need_reparent && con->window != NULL) {
@ -793,8 +904,19 @@ void x_push_node(Con *con) {
con->ignore_unmap++; con->ignore_unmap++;
DLOG("ignore_unmap for reparenting of con %p (win 0x%08x) is now %d\n", DLOG("ignore_unmap for reparenting of con %p (win 0x%08x) is now %d\n",
con, con->window->id, con->ignore_unmap); 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 /* The pixmap of a borderless leaf container will not be used except
* for the titlebar in a stack or tabs (issue #1013). */ * for the titlebar in a stack or tabs (issue #1013). */
bool is_pixmap_needed = (con->border_style != BS_NONE || bool is_pixmap_needed = (con->border_style != BS_NONE ||
@ -898,6 +1020,8 @@ void x_push_node(Con *con) {
fake_notify = true; fake_notify = true;
} }
set_shape_state(con, need_reshape);
/* Map if map state changed, also ensure that the child window /* 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. * is changed if we are mapped and there is a new, unmapped child window.
* Unmaps are handled in x_push_node_unmaps(). */ * 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->unmap_now = (state->mapped != con->mapped) && !con->mapped;
state->was_floating = con_is_floating(con);
if (fake_notify) { if (fake_notify) {
DLOG("Sending fake configure notify\n"); 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); 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);
}
}

114
testcases/t/301-shape.t Normal file
View File

@ -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;