Repository: qtile/qtile

Issue:
Windows in Tile layout grow beyond the edge of the screen
On the latest Qtile on Arch Linux.

Using the `Tile` layout, when I grow windows to the very end on either side, they go on forever. With other layouts such as `Columns`, the windows stop growing at a certain point. The slave windows almost go underneath the master window.

Below you can see a demonstration of this, which you can also see `screenkey` through the terminals to show the windows do in fact grow beyond the screen borders, based on how many times you've pressed the key.
 
https://user-images.githubusercontent.com/76445071/137324841-f3c3538a-c257-4c3f-807d-26e9cb5ca3b8.mp4


Discussion:
Ha. Yeah, that looks like a bug!

Thanks for reporting.
Isn't that the behaviour you'd want? :D 

Resolving patch:
diff --git a/libqtile/layout/tile.py b/libqtile/layout/tile.py
--- a/libqtile/layout/tile.py
+++ b/libqtile/layout/tile.py
@@ -51,6 +51,8 @@ class Tile(_SimpleLayoutBase):
         ("margin", 0, "Margin of the layout (int or list of ints [N E S W])"),
         ("ratio", 0.618,
             "Width-percentage of screen size reserved for master windows."),
+        ("max_ratio", 0.85, "Maximum width of master windows"),
+        ("min_ratio", 0.15, "Minimum width of master windows"),
         ("master_length", 1,
             "Amount of windows displayed in the master stack. Surplus windows "
             "will be moved to the slave stack."),
@@ -76,6 +78,15 @@ class Tile(_SimpleLayoutBase):
     def __init__(self, **config):
         _SimpleLayoutBase.__init__(self, **config)
         self.add_defaults(Tile.defaults)
+        self._initial_ratio = self.ratio
+
+    @property
+    def ratio_size(self):
+        return self.ratio
+
+    @ratio_size.setter
+    def ratio_size(self, ratio):
+        self.ratio = min(max(ratio, self.min_ratio), self.max_ratio)
 
     @property
     def master_windows(self):
@@ -129,16 +140,16 @@ def configure(self, client, screen_rect):
         if self.clients and client in self.clients:
             pos = self.clients.index(client)
             if client in self.master_windows:
-                w = int(screen_width * self.ratio) \
+                w = int(screen_width * self.ratio_size) \
                     if len(self.slave_windows) or not self.expand \
                     else screen_width
                 h = screen_height // self.master_length
                 x = screen_rect.x
                 y = screen_rect.y + pos * h
             else:
-                w = screen_width - int(screen_width * self.ratio)
+                w = screen_width - int(screen_width * self.ratio_size)
                 h = screen_height // (len(self.slave_windows))
-                x = screen_rect.x + int(screen_width * self.ratio)
+                x = screen_rect.x + int(screen_width * self.ratio_size)
                 y = screen_rect.y + self.clients[self.master_length:].index(client) * h
             if client.has_focus:
                 bc = self.border_focus
@@ -171,6 +182,12 @@ def cmd_shuffle_down(self):
     def cmd_shuffle_up(self):
         self.up()
 
+    def cmd_reset(self):
+        self.ratio_size = self._initial_ratio
+        self.group.layout_all()
+
+    cmd_normalize = cmd_reset
+
     cmd_shuffle_left = cmd_shuffle_up
     cmd_shuffle_right = cmd_shuffle_down
 
@@ -182,11 +199,11 @@ def cmd_shuffle_up(self):
     cmd_right = cmd_next
 
     def cmd_decrease_ratio(self):
-        self.ratio -= self.ratio_increment
+        self.ratio_size -= self.ratio_increment
         self.group.layout_all()
 
     def cmd_increase_ratio(self):
-        self.ratio += self.ratio_increment
+        self.ratio_size += self.ratio_increment
         self.group.layout_all()
 
     def cmd_decrease_nmaster(self):


Test patch:
diff --git a/test/layouts/test_tile.py b/test/layouts/test_tile.py
--- a/test/layouts/test_tile.py
+++ b/test/layouts/test_tile.py
@@ -44,7 +44,7 @@ class TileConfig(Config):
     layouts = [
         layout.Tile(),
         layout.Tile(master_length=2),
-        layout.Tile(add_on_top=False)
+        layout.Tile(add_on_top=False),
     ]
     floating_layout = libqtile.resources.default_config.floating_layout
     keys = []
@@ -159,3 +159,31 @@ def test_tile_add_on_top(manager):
     assert manager.c.layout.info()["clients"] == ['one', 'two', 'four', 'three']
     assert manager.c.layout.info()["slave"] == ["two", "four", "three"]
     assert_focus_path(manager, 'three', 'one', 'two', 'four')
+
+
+@tile_config
+def test_tile_min_max_ratios(manager):
+    manager.test_window("one")
+    manager.test_window("two")
+
+    orig_windows = manager.c.windows()
+
+    # Defaul increment is 5% so 20 steps would move by 100%
+    # i.e. past the edge
+    for _ in range(20):
+        manager.c.layout.decrease_ratio()
+
+    # Window should now be 15% of screen width less it's border
+    assert manager.c.windows()[1]["width"] == (800 * 0.15) - 2
+
+    for _ in range(20):
+        manager.c.layout.increase_ratio()
+
+    # Window should now be 85% of screen width less it's border
+    assert manager.c.windows()[1]["width"] == (800 * 0.85) - 2
+
+    # Reset the layout to original settings
+    manager.c.layout.reset()
+
+    # Check the windows match their original sizes
+    assert manager.c.windows() == orig_windows