From b660d6a902cf44be22c434101dd2a4e6743e26bc Mon Sep 17 00:00:00 2001 From: sethpollen Date: Mon, 22 Jan 2024 13:34:40 -0600 Subject: Add support for _NET_WM_STATE_MAXIMIZED_{HORZ, VERT} (#5840) If a window occupies the entirety of its workspace vertically and/or horizontally, pass it the _NET_WM_STATE_MAXIMIZED_{HORZ, VERT} atoms. This helps applications like Google Chrome draw the tab bar correctly and handle tab clicks correctly (see https://crbug.com/1495853). This change is based on work from @yshui in #2380. --- include/con.h | 16 ++++ include/i3-atoms_NET_SUPPORTED.xmacro.h | 2 + release-notes/changes/2-net-wrm-state-maximized | 1 + src/con.c | 59 ++++++++++++++ src/x.c | 41 ++++++++++ testcases/lib/i3test.pm.in | 15 ++-- testcases/t/158-wm_take_focus.t | 7 +- testcases/t/283-net-wm-state-hidden.t | 23 +----- testcases/t/295-net-wm-state-focused.t | 15 ++-- testcases/t/551-net-wm-state-maximized.t | 104 ++++++++++++++++++++++++ 10 files changed, 245 insertions(+), 38 deletions(-) create mode 100644 release-notes/changes/2-net-wrm-state-maximized create mode 100644 testcases/t/551-net-wm-state-maximized.t diff --git a/include/con.h b/include/con.h index e1bb6813..52f588b8 100644 --- a/include/con.h +++ b/include/con.h @@ -83,6 +83,22 @@ bool con_is_split(Con *con); */ bool con_is_hidden(Con *con); +/** + * Returns true if the container is maximized in the given orientation. + * + * If the container is floating or fullscreen, it is not considered maximized. + * Otherwise, it is maximized if it doesn't share space with any other + * container in the given orientation. For example, if a workspace contains + * a single splitv container with three children, none of them are considered + * vertically maximized, but they are all considered horizontally maximized. + * + * Passing "maximized" hints to the application can help it make the right + * choices about how to draw its borders. See discussion in + * https://github.com/i3/i3/pull/2380. + * + */ +bool con_is_maximized(Con *con, orientation_t orientation); + /** * Returns whether the container or any of its children is sticky. * diff --git a/include/i3-atoms_NET_SUPPORTED.xmacro.h b/include/i3-atoms_NET_SUPPORTED.xmacro.h index b491da98..90d03c20 100644 --- a/include/i3-atoms_NET_SUPPORTED.xmacro.h +++ b/include/i3-atoms_NET_SUPPORTED.xmacro.h @@ -11,6 +11,8 @@ xmacro(_NET_WM_STATE_DEMANDS_ATTENTION) \ xmacro(_NET_WM_STATE_MODAL) \ xmacro(_NET_WM_STATE_HIDDEN) \ xmacro(_NET_WM_STATE_FOCUSED) \ +xmacro(_NET_WM_STATE_MAXIMIZED_VERT) \ +xmacro(_NET_WM_STATE_MAXIMIZED_HORZ) \ xmacro(_NET_WM_STATE) \ xmacro(_NET_WM_WINDOW_TYPE) \ xmacro(_NET_WM_WINDOW_TYPE_NORMAL) \ diff --git a/release-notes/changes/2-net-wrm-state-maximized b/release-notes/changes/2-net-wrm-state-maximized new file mode 100644 index 00000000..1a3268c5 --- /dev/null +++ b/release-notes/changes/2-net-wrm-state-maximized @@ -0,0 +1 @@ +pass _NET_WM_STATE_MAXIMIZED_{HORZ, VERT} to tiled windows diff --git a/src/con.c b/src/con.c index 5ecc2700..72f3ad6e 100644 --- a/src/con.c +++ b/src/con.c @@ -419,6 +419,65 @@ bool con_is_hidden(Con *con) { return false; } +/* + * Returns true if the container is maximized in the given orientation. + * + * If the container is floating or fullscreen, it is not considered maximized. + * Otherwise, it is maximized if it doesn't share space with any other + * container in the given orientation. For example, if a workspace contains + * a single splitv container with three children, none of them are considered + * vertically maximized, but they are all considered horizontally maximized. + * + * Passing "maximized" hints to the application can help it make the right + * choices about how to draw its borders. See discussion in + * https://github.com/i3/i3/pull/2380. + */ +bool con_is_maximized(Con *con, orientation_t orientation) { + /* Fullscreen containers are not considered maximized. */ + if (con->fullscreen_mode != CF_NONE) { + return false; + } + + /* Look up the container layout which corresponds to the given + * orientation. */ + layout_t layout; + switch (orientation) { + case HORIZ: + layout = L_SPLITH; + break; + case VERT: + layout = L_SPLITV; + break; + default: + assert(false); + } + + /* Go through all parents, stopping once we reach the workspace node. */ + Con *current = con; + while (true) { + Con *parent = current->parent; + if (parent == NULL || parent->type == CT_WORKSPACE) { + /* We are done searching. We found no reason that the container + * should not be considered maximized. */ + return true; + } + + if (parent->layout == layout && con_num_children(parent) > 1) { + /* The parent has a split in the indicated direction, which + * means none of its children are maximized in that direction. */ + return false; + } + + /* Floating containers and their children are not considered + * maximized. */ + if (parent->type == CT_FLOATING_CON) { + return false; + } + + current = parent; + } +} + /* * Returns whether the container or any of its children is sticky. * diff --git a/src/x.c b/src/x.c index 48105b03..a3a6aec5 100644 --- a/src/x.c +++ b/src/x.c @@ -41,6 +41,8 @@ typedef struct con_state { bool unmap_now; bool child_mapped; bool is_hidden; + bool is_maximized_vert; + bool is_maximized_horz; /* The con for which this state is. */ Con *con; @@ -816,6 +818,44 @@ static void set_hidden_state(Con *con) { state->is_hidden = should_be_hidden; } +/* + * Sets or removes _NET_WM_STATE_MAXIMIZE_{HORZ, VERT} on con + * + */ +static void set_maximized_state(Con *con) { + if (!con->window) { + return; + } + + con_state *state = state_for_frame(con->frame.id); + + const bool con_maximized_horz = con_is_maximized(con, HORIZ); + if (con_maximized_horz != state->is_maximized_horz) { + DLOG("setting _NET_WM_STATE_MAXIMIZED_HORZ for con %p(%s) to %d\n", con, con->name, con_maximized_horz); + + if (con_maximized_horz) { + xcb_add_property_atom(conn, con->window->id, A__NET_WM_STATE, A__NET_WM_STATE_MAXIMIZED_HORZ); + } else { + xcb_remove_property_atom(conn, con->window->id, A__NET_WM_STATE, A__NET_WM_STATE_MAXIMIZED_HORZ); + } + + state->is_maximized_horz = con_maximized_horz; + } + + const bool con_maximized_vert = con_is_maximized(con, VERT); + if (con_maximized_vert != state->is_maximized_vert) { + DLOG("setting _NET_WM_STATE_MAXIMIZED_VERT for con %p(%s) to %d\n", con, con->name, con_maximized_vert); + + if (con_maximized_vert) { + xcb_add_property_atom(conn, con->window->id, A__NET_WM_STATE, A__NET_WM_STATE_MAXIMIZED_VERT); + } else { + xcb_remove_property_atom(conn, con->window->id, A__NET_WM_STATE, A__NET_WM_STATE_MAXIMIZED_VERT); + } + + state->is_maximized_vert = con_maximized_vert; + } +} + /* * Set the container frame shape as the union of the window shape and the * shape of the frame borders. @@ -1121,6 +1161,7 @@ void x_push_node(Con *con) { } set_hidden_state(con); + set_maximized_state(con); /* Handle all children and floating windows of this node. We recurse * in focus order to display the focused client in a stack first when diff --git a/testcases/lib/i3test.pm.in b/testcases/lib/i3test.pm.in index 6d73afca..44d4a3d4 100644 --- a/testcases/lib/i3test.pm.in +++ b/testcases/lib/i3test.pm.in @@ -53,7 +53,7 @@ our @EXPORT = qw( kill_all_windows events_for listen_for_binding - is_net_wm_state_focused + net_wm_state_contains cmp_tree ); @@ -1090,18 +1090,19 @@ sub listen_for_binding { return $command; } -=head2 is_net_wm_state_focused +=head2 net_wm_state_contains -Returns true if the given window has the _NET_WM_STATE_FOCUSED atom. +Returns true if the given window has the given _NET_WM_STATE atom. - ok(is_net_wm_state_focused($window), '_NET_WM_STATE_FOCUSED set'); + ok(net_wm_state_contains($window, '_NET_WM_STATE_FOCUSED'), + '_NET_WM_STATE_FOCUSED set'); =cut -sub is_net_wm_state_focused { - my ($window) = @_; +sub net_wm_state_contains { + my ($window, $atom_name) = @_; sync_with_i3; - my $atom = $x->atom(name => '_NET_WM_STATE_FOCUSED'); + my $atom = $x->atom(name => $atom_name); my $cookie = $x->get_property( 0, $window->{id}, diff --git a/testcases/t/158-wm_take_focus.t b/testcases/t/158-wm_take_focus.t index e9a32cd0..c779365b 100644 --- a/testcases/t/158-wm_take_focus.t +++ b/testcases/t/158-wm_take_focus.t @@ -55,8 +55,7 @@ subtest 'Window without WM_TAKE_FOCUS', sub { my $window = open_window; ok(!recv_take_focus($window), 'did not receive ClientMessage'); - ok(is_net_wm_state_focused($window), '_NET_WM_STATE_FOCUSED set'); - + ok(net_wm_state_contains($window, '_NET_WM_STATE_FOCUSED'), '_NET_WM_STATE_FOCUSED set'); my ($nodes) = get_ws_content($ws); my $con = shift @$nodes; ok($con->{focused}, 'con is focused'); @@ -92,7 +91,7 @@ subtest 'Window with WM_TAKE_FOCUS and without InputHint', sub { $window->map; ok(!recv_take_focus($window), 'did not receive ClientMessage'); - ok(is_net_wm_state_focused($window), '_NET_WM_STATE_FOCUSED set'); + ok(net_wm_state_contains($window, '_NET_WM_STATE_FOCUSED'), '_NET_WM_STATE_FOCUSED set'); my ($nodes) = get_ws_content($ws); my $con = shift @$nodes; @@ -114,7 +113,7 @@ subtest 'Window with WM_TAKE_FOCUS and unspecified InputHint', sub { my $window = open_window({ protocols => [ $take_focus ] }); ok(!recv_take_focus($window), 'did not receive ClientMessage'); - ok(is_net_wm_state_focused($window), '_NET_WM_STATE_FOCUSED set'); + ok(net_wm_state_contains($window, '_NET_WM_STATE_FOCUSED'), '_NET_WM_STATE_FOCUSED set'); my ($nodes) = get_ws_content($ws); my $con = shift @$nodes; diff --git a/testcases/t/283-net-wm-state-hidden.t b/testcases/t/283-net-wm-state-hidden.t index d6c7b2f8..0738ee0b 100644 --- a/testcases/t/283-net-wm-state-hidden.t +++ b/testcases/t/283-net-wm-state-hidden.t @@ -20,29 +20,8 @@ use i3test; use X11::XCB qw(:all); sub is_hidden { - sync_with_i3; - my $atom = $x->atom(name => '_NET_WM_STATE_HIDDEN'); - my ($con) = @_; - my $cookie = $x->get_property( - 0, - $con->{id}, - $x->atom(name => '_NET_WM_STATE')->id, - GET_PROPERTY_TYPE_ANY, - 0, - 4096 - ); - - my $reply = $x->get_property_reply($cookie->{sequence}); - my $len = $reply->{length}; - return 0 if $len == 0; - - my @atoms = unpack("L$len", $reply->{value}); - for (my $i = 0; $i < $len; $i++) { - return 1 if $atoms[$i] == $atom->id; - } - - return 0; + return net_wm_state_contains($con, '_NET_WM_STATE_HIDDEN'); } my ($tabA, $tabB, $tabC, $subtabA, $subtabB, $windowA, $windowB); diff --git a/testcases/t/295-net-wm-state-focused.t b/testcases/t/295-net-wm-state-focused.t index 68283e9c..1a9b065e 100644 --- a/testcases/t/295-net-wm-state-focused.t +++ b/testcases/t/295-net-wm-state-focused.t @@ -23,17 +23,22 @@ my ($windowA, $windowB); fresh_workspace; $windowA = open_window; -ok(is_net_wm_state_focused($windowA), 'a newly opened window that is focused should have _NET_WM_STATE_FOCUSED set'); +ok(net_wm_state_contains($windowA, '_NET_WM_STATE_FOCUSED'), + 'a newly opened window that is focused should have _NET_WM_STATE_FOCUSED set'); $windowB = open_window; -ok(!is_net_wm_state_focused($windowA), 'when a another window is focused, the old window should not have _NET_WM_STATE_FOCUSED set'); -ok(is_net_wm_state_focused($windowB), 'a newly opened window that is focused should have _NET_WM_STATE_FOCUSED set'); +ok(!net_wm_state_contains($windowA, '_NET_WM_STATE_FOCUSED'), + 'when a another window is focused, the old window should not have _NET_WM_STATE_FOCUSED set'); +ok(net_wm_state_contains($windowB, '_NET_WM_STATE_FOCUSED'), + 'a newly opened window that is focused should have _NET_WM_STATE_FOCUSED set'); # See issue #3495. cmd 'kill'; -ok(is_net_wm_state_focused($windowA), 'when the second window is closed, the first window should have _NET_WM_STATE_FOCUSED set'); +ok(net_wm_state_contains($windowA, '_NET_WM_STATE_FOCUSED'), + 'when the second window is closed, the first window should have _NET_WM_STATE_FOCUSED set'); fresh_workspace; -ok(!is_net_wm_state_focused($windowA), 'when focus moves to the ewmh support window, no window should have _NET_WM_STATE_FOCUSED set'); +ok(!net_wm_state_contains($windowA, '_NET_WM_STATE_FOCUSED'), + 'when focus moves to the ewmh support window, no window should have _NET_WM_STATE_FOCUSED set'); done_testing; diff --git a/testcases/t/551-net-wm-state-maximized.t b/testcases/t/551-net-wm-state-maximized.t new file mode 100644 index 00000000..4db03ade --- /dev/null +++ b/testcases/t/551-net-wm-state-maximized.t @@ -0,0 +1,104 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • https://i3wm.org/downloads/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Tests for setting and removing the _NET_WM_STATE_MAXIMIZED_VERT and +# _NET_WM_STATE_MAXIMIZED_HORZ atoms. +use i3test; +use X11::XCB qw(:all); + +sub maximized_vert { + my ($window) = @_; + return net_wm_state_contains($window, '_NET_WM_STATE_MAXIMIZED_VERT'); +} + +sub maximized_horz { + my ($window) = @_; + return net_wm_state_contains($window, '_NET_WM_STATE_MAXIMIZED_HORZ'); +} + +# Returns true if the given window is maximized in both directions. +sub maximized_both { + my ($window) = @_; + return maximized_vert($window) && maximized_horz($window); +} + +# Returns true if the given window is maximized in neither direction. +sub maximized_neither { + my ($window) = @_; + return !maximized_vert($window) && !maximized_horz($window); +} + +my ($winA, $winB, $winC); +fresh_workspace; + +$winA = open_window; +ok(maximized_both($winA), 'if there is just one window, it is maximized'); + +cmd 'fullscreen enable'; +ok(maximized_neither($winA), 'fullscreen windows are not maximized'); + +cmd 'fullscreen disable'; +ok(maximized_both($winA), 'disabling fullscreen sets maximized to true again'); + +cmd 'floating enable'; +ok(maximized_neither($winA), 'floating windows are not maximized'); + +cmd 'floating disable'; +ok(maximized_both($winA), 'disabling floating sets maximized to true again'); + +# Open a second window. +$winB = open_window; + +# Windows in stacked or tabbed containers are considered maximized. +cmd 'layout stacking'; +ok(maximized_both($winA) && maximized_both($winB), + 'stacking layout maximizes all windows'); + +cmd 'layout tabbed'; +ok(maximized_both($winA) && maximized_both($winB), + 'tabbed layout maximizes all windows'); + +# Arrange the two windows with a vertical split. +cmd 'layout splitv'; +ok(!maximized_vert($winA) && !maximized_vert($winB), + 'vertical split means children are not maximized vertically'); +ok(maximized_horz($winA) && maximized_horz($winB), + 'children may still be maximized horizontally in a vertical split'); + +# Arrange the two windows with a horizontal split. +cmd 'layout splith'; +ok(maximized_vert($winA) && maximized_vert($winB), + 'children may still be maximized vertically in a horizontal split'); +ok(!maximized_horz($winA) && !maximized_horz($winB), + 'horizontal split means children are not maximized horizontally'); + +# Add a vertical split within the horizontal split, and open a third window. +cmd 'split vertical'; +$winC = open_window; +ok(maximized_vert($winA), 'winA still reaches from top to bottom'); +ok(!maximized_vert($winB) && !maximized_vert($winC), + 'winB and winC are split vertically, so they are not maximized vertically'); +ok(!maximized_horz($winA) && !maximized_horz($winB) && !maximized_horz($winC), + 'horizontal split means children are not maximized horizontally'); + +# Change the vertical split container to a tabbed container. +cmd 'layout tabbed'; +ok(maximized_vert($winA) && maximized_vert($winB) && maximized_vert($winC), + 'all windows now reach from top to bottom'); +ok(!maximized_horz($winA) && !maximized_horz($winB) && !maximized_horz($winC), + 'horizontal split means children are not maximized horizontally'); + +done_testing; -- cgit v1.2.3-54-g00ecf