aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsethpollen <seth.pollen@gmail.com>2024-01-22 13:34:40 -0600
committerGitHub <noreply@github.com>2024-01-22 20:34:40 +0100
commitb660d6a902cf44be22c434101dd2a4e6743e26bc (patch)
treecffb204c8fdcb31a50021c4b55e2958176f9b284
parent9aba43119b1d414ecc217201155e97276b696974 (diff)
downloadi3-b660d6a902cf44be22c434101dd2a4e6743e26bc.tar.gz
i3-b660d6a902cf44be22c434101dd2a4e6743e26bc.zip
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.
-rw-r--r--include/con.h16
-rw-r--r--include/i3-atoms_NET_SUPPORTED.xmacro.h2
-rw-r--r--release-notes/changes/2-net-wrm-state-maximized1
-rw-r--r--src/con.c59
-rw-r--r--src/x.c41
-rw-r--r--testcases/lib/i3test.pm.in15
-rw-r--r--testcases/t/158-wm_take_focus.t7
-rw-r--r--testcases/t/283-net-wm-state-hidden.t23
-rw-r--r--testcases/t/295-net-wm-state-focused.t15
-rw-r--r--testcases/t/551-net-wm-state-maximized.t104
10 files changed, 245 insertions, 38 deletions
diff --git a/include/con.h b/include/con.h
index e1bb6813..52f588b8 100644
--- a/include/con.h
+++ b/include/con.h
@@ -84,6 +84,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
@@ -420,6 +420,65 @@ 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) {
+ /* 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;
@@ -817,6 +819,44 @@ static void set_hidden_state(Con *con) {
}
/*
+ * 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;