Repository: kivy/kivy

Issue:
RecycleGridLayout not able to render viewclass when number of columns is equal to the length of data
**Software Versions**
* Python: 3.8.3
* OS: Windows 10 Home
* Kivy: 2.0.0
* Kivy installation method: installed kivy base through pip

Description:
    The recycle grid layout has an issue in rendering the viewclass objects when the number of columns are equal to the number of viewclass objects, to demonstrate this there is a demo below.

**Expected behavior**
It is supposed to display the objects of the view class in a grid, but here they disappear and only the first object is visible on-screen

**To Reproduce**
To reproduce this bug just make a recycle grid layout in a recycle view with the same number of columns as the number of the objects of the view class you want to add.

**Code and Logs and screenshots**
```python
from kivy.app import App
from kivy.lang import Builder
kv = '''
RecycleView:
    data: [{"text": "Label "+str(x)} for x in range(5)]
    viewclass: 'Button'
    RecycleGridLayout:
        cols: 5
        default_size: None, 200
        default_size_hint: 1, None
'''
class MyApp(App):
    def build(self):
        return Builder.load_string(kv)
MyApp().run()
```
![Screenshot (401)](https://user-images.githubusercontent.com/59286213/101752027-0333ed80-3af7-11eb-9dea-ba54c3ce23a9.png)


**Additional context**
This works in kivy 1.11.1, also this behavior is not present in the regular grid layout.



Discussion:

Resolving patch:
diff --git a/kivy/uix/recyclegridlayout.py b/kivy/uix/recyclegridlayout.py
--- a/kivy/uix/recyclegridlayout.py
+++ b/kivy/uix/recyclegridlayout.py
@@ -14,6 +14,8 @@
 :mod:`~kivy.uix.recycleview` module documentation for more information.
 """
 
+import itertools
+chain_from_iterable = itertools.chain.from_iterable
 from kivy.uix.recyclelayout import RecycleLayout
 from kivy.uix.gridlayout import GridLayout, GridLayoutException, nmax, nmin
 from collections import defaultdict
@@ -220,9 +222,7 @@ def compute_visible_views(self, data, viewport):
         right = x + w
         top = y + h
         at_idx = self.get_view_index_at
-        # 'tl' is not actually 'top-left' unless 'orientation' is 'lr-tb'.
-        # But we can pretend it always is. Same for 'bl' and 'br'.
-        tl, __, bl, br = sorted((
+        tl, tr, bl, br = sorted((
             at_idx((x, y)),
             at_idx((right, y)),
             at_idx((x, top)),
@@ -230,12 +230,16 @@ def compute_visible_views(self, data, viewport):
         ))
 
         n = len(data)
+        if len({tl, tr, bl, br}) < 4:
+            # visible area is one row/column
+            return range(min(n, tl), min(n, br + 1))
         indices = []
         stride = len(self._cols) if self._fills_row_first else len(self._rows)
         if stride:
             x_slice = br - bl + 1
-            for s in range(tl, bl + 1, stride):
-                indices.extend(range(min(s, n), min(n, s + x_slice)))
+            indices = chain_from_iterable(
+                range(min(s, n), min(n, s + x_slice))
+                for s in range(tl, bl + 1, stride))
         return indices
 
     def _calculate_idx_from_a_view_idx(self, n_cols, n_rows, view_idx):


Test patch:
diff --git a/kivy/tests/test_uix_gridlayout.py b/kivy/tests/test_uix_gridlayout.py
--- a/kivy/tests/test_uix_gridlayout.py
+++ b/kivy/tests/test_uix_gridlayout.py
@@ -84,29 +84,160 @@ def test_create_idx_iter2(orientation):
     assert [(0, 0)] == list(index_iter)
 
 
-@pytest.mark.parametrize(
-    "n_cols, n_rows, orientation, n_children, expectation", [
-        (3, None, 'lr-tb', 4, [(0, 15), (10, 15), (20, 15), (0, 0)]),
-        (3, None, 'lr-bt', 4, [(0, 0), (10, 0), (20, 0), (0, 15)]),
-        (3, None, 'rl-tb', 4, [(20, 15), (10, 15), (0, 15), (20, 0)]),
-        (3, None, 'rl-bt', 4, [(20, 0), (10, 0), (0, 0), (20, 15)]),
-        (None, 3, 'tb-lr', 4, [(0, 20), (0, 10), (0, 0), (15, 20)]),
-        (None, 3, 'tb-rl', 4, [(15, 20), (15, 10), (15, 0), (0, 20)]),
-        (None, 3, 'bt-lr', 4, [(0, 0), (0, 10), (0, 20), (15, 0)]),
-        (None, 3, 'bt-rl', 4, [(15, 0), (15, 10), (15, 20), (0, 0)]),
-    ]
-)
-def test_children_pos(n_cols, n_rows, orientation, n_children, expectation):
-    from kivy.uix.widget import Widget
-    from kivy.uix.gridlayout import GridLayout
-    gl = GridLayout(
-        cols=n_cols, rows=n_rows, orientation=orientation,
-        pos=(0, 0), size=(30, 30))
-    for __ in range(n_children):
-        gl.add_widget(Widget())
-    gl.do_layout()
-    actual_layout = [tuple(c.pos) for c in reversed(gl.children)]
-    assert actual_layout == expectation
+class TestLayout_fixed_sized_children:
+    def compute_layout(self, *, n_cols, n_rows, ori, n_children):
+        from kivy.uix.widget import Widget
+        from kivy.uix.gridlayout import GridLayout
+        gl = GridLayout(cols=n_cols, rows=n_rows, orientation=ori, pos=(0, 0))
+        gl.bind(minimum_size=gl.setter("size"))
+        for __ in range(n_children):
+            # set 'pos' to some random value to make this test more reliable
+            gl.add_widget(Widget(
+                size_hint=(None, None), size=(100, 100), pos=(8, 8)))
+        gl.do_layout()
+        return [tuple(c.pos) for c in reversed(gl.children)]
+
+    # |---|
+    # | 0 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 1), (1, 1)])
+    def test_1x1(self, n_cols, n_rows):
+        from kivy.uix.gridlayout import GridLayout
+        for ori in GridLayout.orientation.options:
+            assert [(0, 0), ] == self.compute_layout(
+                n_children=1, ori=ori, n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|---|
+    # | 0 | 1 | 2 |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 1), (3, 1)])
+    @pytest.mark.parametrize("ori", "lr-tb lr-bt tb-lr bt-lr".split())
+    def test_3x1_lr(self, ori, n_cols, n_rows):
+        assert [(0, 0), (100, 0), (200, 0)] == self.compute_layout(
+            n_children=3, ori=ori, n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|---|
+    # | 2 | 1 | 0 |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 1), (3, 1)])
+    @pytest.mark.parametrize("ori", "rl-tb rl-bt tb-rl bt-rl".split())
+    def test_3x1_rl(self, ori, n_cols, n_rows):
+        assert [(200, 0), (100, 0), (0, 0)] == self.compute_layout(
+            n_children=3, ori=ori, n_cols=n_cols, n_rows=n_rows)
+
+    # |---|
+    # | 0 |
+    # |---|
+    # | 1 |
+    # |---|
+    # | 2 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 3), (1, 3)])
+    @pytest.mark.parametrize("ori", "tb-lr tb-rl lr-tb rl-tb".split())
+    def test_1x3_tb(self, ori, n_cols, n_rows):
+        assert [(0, 200), (0, 100), (0, 0)] == self.compute_layout(
+            n_children=3, ori=ori, n_cols=n_cols, n_rows=n_rows)
+
+    # |---|
+    # | 2 |
+    # |---|
+    # | 1 |
+    # |---|
+    # | 0 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 3), (1, 3)])
+    @pytest.mark.parametrize("ori", "bt-lr bt-rl lr-bt rl-bt".split())
+    def test_1x3_bt(self, ori, n_cols, n_rows):
+        assert [(0, 0), (0, 100), (0, 200)] == self.compute_layout(
+            n_children=3, ori=ori, n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 0 | 1 |
+    # |---|---|
+    # | 2 | 3 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_lr_tb(self, n_cols, n_rows):
+        assert [(0, 100), (100, 100), (0, 0), (100, 0)] == \
+            self.compute_layout(
+                n_children=4, ori='lr-tb', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 2 | 3 |
+    # |---|---|
+    # | 0 | 1 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_lr_bt(self, n_cols, n_rows):
+        assert [(0, 0), (100, 0), (0, 100), (100, 100)] == \
+            self.compute_layout(
+                n_children=4, ori='lr-bt', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 1 | 0 |
+    # |---|---|
+    # | 3 | 2 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_rl_tb(self, n_cols, n_rows):
+        assert [(100, 100), (0, 100), (100, 0), (0, 0)] == \
+            self.compute_layout(
+                n_children=4, ori='rl-tb', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 3 | 2 |
+    # |---|---|
+    # | 1 | 0 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_rl_bt(self, n_cols, n_rows):
+        assert [(100, 0), (0, 0), (100, 100), (0, 100)] == \
+            self.compute_layout(
+                n_children=4, ori='rl-bt', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 0 | 2 |
+    # |---|---|
+    # | 1 | 3 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_tb_lr(self, n_cols, n_rows):
+        assert [(0, 100), (0, 0), (100, 100), (100, 0)] == \
+            self.compute_layout(
+                n_children=4, ori='tb-lr', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 2 | 0 |
+    # |---|---|
+    # | 3 | 1 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_tb_rl(self, n_cols, n_rows):
+        assert [(100, 100), (100, 0), (0, 100), (0, 0)] == \
+            self.compute_layout(
+                n_children=4, ori='tb-rl', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 1 | 3 |
+    # |---|---|
+    # | 0 | 2 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_bt_lr(self, n_cols, n_rows):
+        assert [(0, 0), (0, 100), (100, 0), (100, 100)] == \
+            self.compute_layout(
+                n_children=4, ori='bt-lr', n_cols=n_cols, n_rows=n_rows)
+
+    # |---|---|
+    # | 3 | 1 |
+    # |---|---|
+    # | 2 | 0 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_bt_rl(self, n_cols, n_rows):
+        assert [(100, 0), (100, 100), (0, 0), (0, 100)] == \
+            self.compute_layout(
+                n_children=4, ori='bt-rl', n_cols=n_cols, n_rows=n_rows)
 
 
 if __name__ == '__main__':
diff --git a/kivy/tests/test_uix_recyclegridlayout.py b/kivy/tests/test_uix_recyclegridlayout.py
new file mode 100644
--- /dev/null
+++ b/kivy/tests/test_uix_recyclegridlayout.py
@@ -0,0 +1,383 @@
+import pytest
+
+
+class TestLayout_all_the_data_is_visible:
+    def compute_layout(self, *, n_cols, n_rows, orientation, n_data, clock):
+        '''Returns {view-index: pos, view-index: pos, ...}'''
+        from textwrap import dedent
+        from kivy.lang import Builder
+
+        # Use Kv because RecycleView cannot be constructed from python
+        rv = Builder.load_string(dedent(f'''
+            RecycleView:
+                viewclass: 'Widget'
+                size: 300, 300
+                data: ({{}} for __ in range({n_data}))
+                RecycleGridLayout:
+                    id: layout
+                    cols: {n_cols}
+                    rows: {n_rows}
+                    orientation: '{orientation}'
+                    default_size_hint: None, None
+                    default_size: 100, 100
+                    size_hint: None, None
+                    size: self.minimum_size
+            '''))
+        clock.tick()
+        layout = rv.ids.layout
+        return {
+            layout.get_view_index_at(c.center): tuple(c.pos)
+            for c in layout.children
+        }
+
+    # |---|
+    # | 0 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 1), (1, 1)])
+    def test_1x1(self, kivy_clock, n_cols, n_rows):
+        from kivy.uix.recyclegridlayout import RecycleGridLayout
+        for orientation in RecycleGridLayout.orientation.options:
+            assert {0: (0, 0), } == self.compute_layout(
+                n_data=1, orientation=orientation, n_cols=n_cols,
+                n_rows=n_rows, clock=kivy_clock)
+
+    # |---|---|---|
+    # | 0 | 1 | 2 |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 1), (3, 1)])
+    @pytest.mark.parametrize("orientation", "lr-tb lr-bt tb-lr bt-lr".split())
+    def test_3x1_lr(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {0: (0, 0), 1: (100, 0), 2: (200, 0)} == self.compute_layout(
+            n_data=3, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|---|---|
+    # | 2 | 1 | 0 |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 1), (3, 1)])
+    @pytest.mark.parametrize("orientation", "rl-tb rl-bt tb-rl bt-rl".split())
+    def test_3x1_rl(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {0: (200, 0), 1: (100, 0), 2: (0, 0)} == self.compute_layout(
+            n_data=3, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|
+    # | 0 |
+    # |---|
+    # | 1 |
+    # |---|
+    # | 2 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 3), (1, 3)])
+    @pytest.mark.parametrize("orientation", "tb-lr tb-rl lr-tb rl-tb".split())
+    def test_1x3_tb(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {0: (0, 200), 1: (0, 100), 2: (0, 0)} == self.compute_layout(
+            n_data=3, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|
+    # | 2 |
+    # |---|
+    # | 1 |
+    # |---|
+    # | 0 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 3), (1, 3)])
+    @pytest.mark.parametrize("orientation", "bt-lr bt-rl lr-bt rl-bt".split())
+    def test_1x3_bt(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {0: (0, 0), 1: (0, 100), 2: (0, 200)} == self.compute_layout(
+            n_data=3, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|---|
+    # | 0 | 1 |
+    # |---|---|
+    # | 2 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_lr_tb(self, kivy_clock, n_cols, n_rows):
+        assert {0: (0, 100), 1: (100, 100), 2: (0, 0)} == self.compute_layout(
+            n_data=3, orientation='lr-tb', n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|
+    # | 2 |
+    # |---|---|
+    # | 0 | 1 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_lr_bt(self, kivy_clock, n_cols, n_rows):
+        assert {0: (0, 0), 1: (100, 0), 2: (0, 100)} == self.compute_layout(
+            n_data=3, orientation='lr-bt', n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|---|
+    # | 1 | 0 |
+    # |---|---|
+    #     | 2 |
+    #     |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_rl_tb(self, kivy_clock, n_cols, n_rows):
+        assert {0: (100, 100), 1: (0, 100), 2: (100, 0)} == \
+            self.compute_layout(
+                n_data=3, orientation='rl-tb', n_cols=n_cols, n_rows=n_rows,
+                clock=kivy_clock)
+
+    #     |---|
+    #     | 2 |
+    # |---|---|
+    # | 1 | 0 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_rl_bt(self, kivy_clock, n_cols, n_rows):
+        assert {0: (100, 0), 1: (0, 0), 2: (100, 100)} == self.compute_layout(
+            n_data=3, orientation='rl-bt', n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|---|
+    # | 0 | 2 |
+    # |---|---|
+    # | 1 |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_tb_lr(self, kivy_clock, n_cols, n_rows):
+        assert {0: (0, 100), 1: (0, 0), 2: (100, 100)} == self.compute_layout(
+            n_data=3, orientation='tb-lr', n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    # |---|---|
+    # | 2 | 0 |
+    # |---|---|
+    #     | 1 |
+    #     |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_tb_rl(self, kivy_clock, n_cols, n_rows):
+        assert {0: (100, 100), 1: (100, 0), 2: (0, 100)} == \
+            self.compute_layout(
+                n_data=3, orientation='tb-rl', n_cols=n_cols, n_rows=n_rows,
+                clock=kivy_clock)
+
+    # |---|
+    # | 1 |
+    # |---|---|
+    # | 0 | 2 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_bt_lr(self, kivy_clock, n_cols, n_rows):
+        assert {0: (0, 0), 1: (0, 100), 2: (100, 0)} == self.compute_layout(
+            n_data=3, orientation='bt-lr', n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+    #     |---|
+    #     | 1 |
+    # |---|---|
+    # | 2 | 0 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(2, None), (None, 2), (2, 2)])
+    def test_2x2_bt_rl(self, kivy_clock, n_cols, n_rows):
+        assert {0: (100, 0), 1: (100, 100), 2: (0, 0)} == self.compute_layout(
+            n_data=3, orientation='bt-rl', n_cols=n_cols, n_rows=n_rows,
+            clock=kivy_clock)
+
+
+class TestLayout_only_a_part_of_the_data_is_visible:
+    def compute_layout(self, *, n_cols, n_rows, orientation, n_data,
+                       scroll_to, clock):
+        '''Returns {view-index: pos, view-index: pos, ...}'''
+        from textwrap import dedent
+        from kivy.lang import Builder
+
+        # Use Kv because RecycleView cannot be constructed from python
+        rv = Builder.load_string(dedent(f'''
+            RecycleView:
+                viewclass: 'Widget'
+                size: 100, 100
+                data: ({{}} for __ in range({n_data}))
+                RecycleGridLayout:
+                    id: layout
+                    cols: {n_cols}
+                    rows: {n_rows}
+                    orientation: '{orientation}'
+                    default_size_hint: None, None
+                    default_size: 100, 100
+                    size_hint: None, None
+                    size: self.minimum_size
+            '''))
+        clock.tick()
+        layout = rv.ids.layout
+        x, y = scroll_to
+        scrollable_width = layout.width - rv.width
+        if scrollable_width:  # avoids ZeroDivisionError
+            rv.scroll_x = x / scrollable_width
+        scrollable_height = layout.height - rv.height
+        if scrollable_height:  # avoids ZeroDivisionError
+            rv.scroll_y = y / scrollable_height
+        clock.tick()
+        return {
+            layout.get_view_index_at(c.center): tuple(c.pos)
+            for c in layout.children
+        }
+
+    # |---|---|---|---|
+    # |   | 1 | 2 |   |
+    # |---|---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(4, None), (None, 1), (4, 1)])
+    @pytest.mark.parametrize("orientation", "lr-tb lr-bt tb-lr bt-lr".split())
+    def test_4x1_lr(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {1: (100, 0), 2: (200, 0)} == self.compute_layout(
+            n_data=4, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            scroll_to=(150, 0), clock=kivy_clock)
+
+    # |---|---|---|---|
+    # |   | 2 | 1 |   |
+    # |---|---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(4, None), (None, 1), (4, 1)])
+    @pytest.mark.parametrize("orientation", "rl-tb rl-bt tb-rl bt-rl".split())
+    def test_4x1_rl(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {1: (200, 0), 2: (100, 0)} == self.compute_layout(
+            n_data=4, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            scroll_to=(150, 0), clock=kivy_clock)
+
+    # |---|
+    # |   |
+    # |---|
+    # | 1 |
+    # |---|
+    # | 2 |
+    # |---|
+    # |   |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 4), (1, 4)])
+    @pytest.mark.parametrize("orientation", "tb-lr tb-rl lr-tb rl-tb".split())
+    def test_1x4_tb(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {1: (0, 200), 2: (0, 100)} == self.compute_layout(
+            n_data=4, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            scroll_to=(0, 150), clock=kivy_clock)
+
+    # |---|
+    # |   |
+    # |---|
+    # | 2 |
+    # |---|
+    # | 1 |
+    # |---|
+    # |   |
+    # |---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(1, None), (None, 4), (1, 4)])
+    @pytest.mark.parametrize("orientation", "bt-lr bt-rl lr-bt rl-bt".split())
+    def test_1x4_bt(self, kivy_clock, orientation, n_cols, n_rows):
+        assert {1: (0, 100), 2: (0, 200)} == self.compute_layout(
+            n_data=4, orientation=orientation, n_cols=n_cols, n_rows=n_rows,
+            scroll_to=(0, 150), clock=kivy_clock)
+
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    # |   | 4 | 5 |
+    # |---|---|---|
+    # |   | 7 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_lr_tb(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (200, 100), 7: (100, 0)} == \
+            self.compute_layout(
+                n_data=8, orientation='lr-tb', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(150, 50), clock=kivy_clock)
+
+    # |---|---|
+    # |   | 7 |
+    # |---|---|---|
+    # |   | 4 | 5 |
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_lr_bt(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (200, 100), 7: (100, 200)} == \
+            self.compute_layout(
+                n_data=8, orientation='lr-bt', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(150, 150), clock=kivy_clock)
+
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    # | 5 | 4 |   |
+    # |---|---|---|
+    #     | 7 |   |
+    #     |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_rl_tb(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (0, 100), 7: (100, 0)} == \
+            self.compute_layout(
+                n_data=8, orientation='rl-tb', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(50, 50), clock=kivy_clock)
+
+    #     |---|---|
+    #     | 7 |   |
+    # |---|---|---|
+    # | 5 | 4 |   |
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_rl_bt(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (0, 100), 7: (100, 200)} == \
+            self.compute_layout(
+                n_data=8, orientation='rl-bt', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(50, 150), clock=kivy_clock)
+
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    # |   | 4 | 7 |
+    # |---|---|---|
+    # |   | 5 |
+    # |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_tb_lr(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (100, 0), 7: (200, 100)} == \
+            self.compute_layout(
+                n_data=8, orientation='tb-lr', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(150, 50), clock=kivy_clock)
+
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    # | 7 | 4 |   |
+    # |---|---|---|
+    #     | 5 |   |
+    #     |---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_tb_rl(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (100, 0), 7: (0, 100)} == \
+            self.compute_layout(
+                n_data=8, orientation='tb-rl', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(50, 50), clock=kivy_clock)
+
+    # |---|---|
+    # |   | 5 |
+    # |---|---|---|
+    # |   | 4 | 7 |
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_bt_lr(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (100, 200), 7: (200, 100)} == \
+            self.compute_layout(
+                n_data=8, orientation='bt-lr', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(150, 150), clock=kivy_clock)
+
+    #     |---|---|
+    #     | 5 |   |
+    # |---|---|---|
+    # | 7 | 4 |   |
+    # |---|---|---|
+    # |   |   |   |
+    # |---|---|---|
+    @pytest.mark.parametrize("n_cols, n_rows", [(3, None), (None, 3), (3, 3)])
+    def test_3x3_bt_rl(self, kivy_clock, n_cols, n_rows):
+        assert {4: (100, 100), 5: (100, 200), 7: (0, 100)} == \
+            self.compute_layout(
+                n_data=8, orientation='bt-rl', n_cols=n_cols, n_rows=n_rows,
+                scroll_to=(50, 150), clock=kivy_clock)