From 7d7d8fc906e2d603519581854b0bf894bb1727b1 Mon Sep 17 00:00:00 2001
From: Maic Siemering <maic@siemering.tech>
Date: Tue, 30 Jan 2024 09:21:31 +0100
Subject: [PATCH] Fix #1962 - UIGridLayout handles nested layouts

---
 arcade/gui/widgets/layout.py                | 163 ++++++++++----------
 tests/unit/gui/test_layouting_gridlayout.py |  47 +++++-
 2 files changed, 128 insertions(+), 82 deletions(-)

diff --git a/arcade/gui/widgets/layout.py b/arcade/gui/widgets/layout.py
index 5a7631393..02a09ef02 100644
--- a/arcade/gui/widgets/layout.py
+++ b/arcade/gui/widgets/layout.py
@@ -54,16 +54,16 @@ class UIAnchorLayout(UILayout):
     default_anchor_y = "center"
 
     def __init__(
-        self,
-        x: float = 0,
-        y: float = 0,
-        width: float = 100,
-        height: float = 100,
-        children: Iterable["UIWidget"] = tuple(),
-        size_hint=(1, 1),
-        size_hint_min=None,
-        size_hint_max=None,
-        **kwargs
+            self,
+            x: float = 0,
+            y: float = 0,
+            width: float = 1,
+            height: float = 1,
+            children: Iterable["UIWidget"] = tuple(),
+            size_hint=(1, 1),
+            size_hint_min=None,
+            size_hint_max=None,
+            **kwargs
     ):
         super().__init__(
             x=x,
@@ -82,14 +82,14 @@ def do_layout(self):
             self._place_child(child, **data)
 
     def add(
-        self,
-        child: W,
-        *,
-        anchor_x: Optional[str] = None,
-        align_x: float = 0,
-        anchor_y: Optional[str] = None,
-        align_y: float = 0,
-        **kwargs
+            self,
+            child: W,
+            *,
+            anchor_x: Optional[str] = None,
+            align_x: float = 0,
+            anchor_y: Optional[str] = None,
+            align_y: float = 0,
+            **kwargs
     ) -> W:
         """
         Add a widget to the layout as a child. Added widgets will receive
@@ -119,12 +119,12 @@ def add(
         )
 
     def _place_child(
-        self,
-        child: UIWidget,
-        anchor_x: Optional[str] = None,
-        align_x: float = 0,
-        anchor_y: Optional[str] = None,
-        align_y: float = 0,
+            self,
+            child: UIWidget,
+            anchor_x: Optional[str] = None,
+            align_x: float = 0,
+            anchor_y: Optional[str] = None,
+            align_y: float = 0,
     ):
         anchor_x = anchor_x or self.default_anchor_x
         anchor_y = anchor_y or self.default_anchor_y
@@ -214,19 +214,19 @@ class UIBoxLayout(UILayout):
     """
 
     def __init__(
-        self,
-        x=0,
-        y=0,
-        width=0,
-        height=0,
-        vertical=True,
-        align="center",
-        children: Iterable[UIWidget] = tuple(),
-        size_hint=(0, 0),
-        size_hint_max=None,
-        space_between=0,
-        style=None,
-        **kwargs
+            self,
+            x=0,
+            y=0,
+            width=1,
+            height=1,
+            vertical=True,
+            align="center",
+            children: Iterable[UIWidget] = tuple(),
+            size_hint=(0, 0),
+            size_hint_max=None,
+            space_between=0,
+            style=None,
+            **kwargs
     ):
         super().__init__(
             x=x,
@@ -321,12 +321,12 @@ def do_layout(self):
             # Determine if some space is available for children to grow
             available_height = max(0, self.height - self.size_hint_min[1])
             total_size_hint_height = (
-                sum(
-                    child.size_hint[1] or 0
-                    for child in self.children
-                    if child.size_hint
-                )
-                or 1
+                    sum(
+                        child.size_hint[1] or 0
+                        for child in self.children
+                        if child.size_hint
+                    )
+                    or 1
             )  # Prevent division by zero
 
             for child in self.children:
@@ -343,7 +343,7 @@ def do_layout(self):
 
                     # Maximal growth to parent.width * shw
                     available_growth_height = min_height_value + available_height * (
-                        sh_h / total_size_hint_height
+                            sh_h / total_size_hint_height
                     )
                     max_growth_height = self.height * sh_h
                     new_rect = new_rect.resize(
@@ -388,12 +388,12 @@ def do_layout(self):
             # Calculate if some space is available for children to grow.
             available_width = max(0, self.width - self.size_hint_min[0])
             total_size_hint_width = (
-                sum(
-                    child.size_hint[0] or 0
-                    for child in self.children
-                    if child.size_hint
-                )
-                or 1
+                    sum(
+                        child.size_hint[0] or 0
+                        for child in self.children
+                        if child.size_hint
+                    )
+                    or 1
             )  # Prevent division by zero
 
             # TODO Fix layout algorithm, handle size hints per dimension!
@@ -417,13 +417,13 @@ def do_layout(self):
 
                     # Maximal growth to parent.width * shw
                     available_growth_width = min_width_value + available_width * (
-                        sh_w / total_size_hint_width
+                            sh_w / total_size_hint_width
                     )
                     max_growth_width = self.width * sh_w
                     new_rect = new_rect.resize(
                         width=min(
                             available_growth_width, max_growth_width
-                        )   # This does not enforce the minimum width
+                        )  # This does not enforce the minimum width
                     )
 
                     if shmn_w is not None:
@@ -494,27 +494,28 @@ class UIGridLayout(UILayout):
     """
 
     def __init__(
-        self,
-        x=0,
-        y=0,
-        align_horizontal="center",
-        align_vertical="center",
-        children: Iterable[UIWidget] = tuple(),
-        size_hint=(0, 0),
-        size_hint_max=None,
-        horizontal_spacing: int = 0,
-        vertical_spacing: int = 0,
-        column_count: int = 1,
-        row_count: int = 1,
-        style=None,
-        **kwargs
+            self,
+            *,
+            x=0,
+            y=0,
+            align_horizontal="center",
+            align_vertical="center",
+            children: Iterable[UIWidget] = tuple(),
+            size_hint=(0, 0),
+            size_hint_max=None,
+            horizontal_spacing: int = 0,
+            vertical_spacing: int = 0,
+            column_count: int = 1,
+            row_count: int = 1,
+            style=None,
+            **kwargs
     ):
 
         super(UIGridLayout, self).__init__(
             x=x,
             y=y,
-            width=0,
-            height=0,
+            width=1,
+            height=1,
             children=children,
             size_hint=size_hint,
             size_hint_max=size_hint_max,
@@ -608,23 +609,23 @@ def min_size(child: UIWidget) -> Tuple[float, float]:
         base_height = self._padding_top + self._padding_bottom + 2 * self._border_width
 
         content_height = (
-            sum(principal_height_ratio_list) + self.row_count * self._vertical_spacing
+                sum(principal_height_ratio_list) + self.row_count * self._vertical_spacing
         )
         content_width = (
-            sum(principal_width_ratio_list)
-            + self.column_count * self._horizontal_spacing
+                sum(principal_width_ratio_list)
+                + self.column_count * self._horizontal_spacing
         )
 
         self.size_hint_min = (base_width + content_width, base_height + content_height)
 
     def add(
-        self,
-        child: W,
-        col_num: int = 0,
-        row_num: int = 0,
-        col_span: int = 1,
-        row_span: int = 1,
-        **kwargs
+            self,
+            child: W,
+            col_num: int = 0,
+            row_num: int = 0,
+            col_span: int = 1,
+            row_span: int = 1,
+            **kwargs
     ) -> W:
         """
         Add a widget to the grid layout.
@@ -682,9 +683,9 @@ def do_layout(self):
             max_height_per_row[row_num][col_num] = (child.height, row_span)
 
             for row in child_sorted_row_wise[
-                row_num : row_num + row_span  # noqa: E203
-            ]:
-                row[col_num : col_num + col_span] = [child] * col_span  # noqa: E203
+                       row_num: row_num + row_span  # noqa: E203
+                       ]:
+                row[col_num: col_num + col_span] = [child] * col_span  # noqa: E203
 
         principal_height_ratio_list = []
         principal_width_ratio_list = []
diff --git a/tests/unit/gui/test_layouting_gridlayout.py b/tests/unit/gui/test_layouting_gridlayout.py
index 1ed2bed08..ba5e32955 100644
--- a/tests/unit/gui/test_layouting_gridlayout.py
+++ b/tests/unit/gui/test_layouting_gridlayout.py
@@ -1,4 +1,4 @@
-from arcade.gui import UIDummy
+from arcade.gui import UIDummy, UIManager, UIBoxLayout, UIAnchorLayout
 from arcade.gui.widgets import Rect
 from arcade.gui.widgets.layout import UIGridLayout
 
@@ -240,6 +240,7 @@ def test_size_hint_and_spacing(window):
     subject.do_layout()
     assert dummy1.size == (100, 100)
 
+
 def test_empty_cells(window):
     dummy1 = UIDummy(width=100, height=100)
 
@@ -254,3 +255,47 @@ def test_empty_cells(window):
     subject.do_layout()
 
     assert dummy1.position == (0, 0)
+
+
+def test_nested_grid_layouts(window):
+    ui = UIManager()
+    outer = UIGridLayout(row_count=1, column_count=1)
+    inner = UIGridLayout(row_count=1, column_count=1)
+
+    inner.add(UIDummy(), 0, 0)
+    outer.add(inner, 0, 0)
+    ui.add(outer)
+
+    ui._do_layout()
+
+    assert inner.rect.size == (100, 100)
+    assert outer.rect.size == (100, 100)
+
+
+def test_nested_box_layouts(window):
+    ui = UIManager()
+    outer = UIGridLayout(row_count=1, column_count=1)
+    inner = UIBoxLayout()
+
+    inner.add(UIDummy())
+    outer.add(inner, 0, 0)
+    ui.add(outer)
+
+    ui._do_layout()
+
+    assert inner.rect.size == (100, 100)
+    assert outer.rect.size == (100, 100)
+
+
+def test_nested_anchor_layouts(window):
+    ui = UIManager()
+    outer = UIGridLayout(row_count=1, column_count=1)
+    inner = UIAnchorLayout(size_hint_min=(100, 100))
+
+    outer.add(inner, 0, 0)
+    ui.add(outer)
+
+    ui._do_layout()
+
+    assert inner.rect.size == (100, 100)
+    assert outer.rect.size == (100, 100)