Repository: kivy/kivy

Issue:
Stacklayout doesn't compute size_hint_y anymore
I guess the commit 1747e97e88e942b62ae96aa3e55bd9a589001d53 is faulty, but when using spacing / padding, it doesn't work really great :/



Discussion:
I just tested it and I confirm something is wrong. However I don't understand the purpose of the change. The stacklayout from 1.8.0 doesn't seem to have any problem with spacing / padding. Can we have some pictures?

In any case, insert on line 216:

```
c.height = c.size_hint[1] * modified_y
```

That should fix it. I'm gonna try to learn to make a pull request.

I ran into more layout issues with StackLayout after making 1747e97 . I have come to the conclusion that while StackLayout is ok for the most usual case, the algorithm definitely does not do what it should at all. There are many different potential configurations that will cause all kinds of errors with the layout algorithm, either not placing widgets within the bounding box at all, not sizing them appropriately, or not appropriately calculating padding/spacing. While this commit addressed one of these cases, there are many others where it is inadequate. However I think its important to note that size_hint under certain circumstances was flat out broken in 1.8.0 and padding/spacing did not calculate correctly, when using the minimum_size property. (The way the algorithm works it does not distribute padding/spacing equaly: 
You end up with [padding] [widget size] [spacing] [widget size] [no padding at end of line where it should be])

What we have is safe in the degenerate case of stacking equal sized widgets in one direction, but nowhere near what the algorithm is attempting to do with stacking widgets to fill space appropriately. 

Things need more analysis but I haven't had time to really look any more deeper than I already have. I tried several more extensive fixes but could never find the general solution for the various corner cases I kept running into. The way I see it if we have 4 widgets:

Widget A: Width: 2x, Height: x
Widget B: Width: x, Height: x
Widget C: width: x, height : x
Widget D: Width 2x, height: x

it should stack like
| A A |
| B C |
| D D |

however usually you will get:
|A A |
|B    |
|C    | 
|D D |

in addition often placing something like A, B, and C widgets will result in
|A A| C
|B   |
with C rendering outside of the stacklayout bounds

instead of
| A A |
| B C |

as expected. This has to do with padding/spacing not being accounted for correctly during sizing the widgets. 

I have also encountered conditions where no widgets are rendering inside of the stacklayout bounds when using certain orientations and size configurations. 

Thanks for clarifying. But honestly, I don't think the algorithm is at fault.
This is my example file:

```
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.uix.stacklayout2 import StackLayout # I just copied stacklaout.py to test things


class RootWidget(StackLayout):

  def __init__(self, **kw):
    super(RootWidget, self).__init__(**kw)
    self.spacing = 80
    self.padding = 80
    a = Button(text='A', size_hint=(1., .4)) # or a = Button(text='A', size_hint=(.8, .4))
    b = Button(text='B', size_hint=(.5, .4)) # or a = Button(text='A', size_hint=(.4, .4))
    c = Button(text='C', size_hint=(.5, .4)) # or a = Button(text='A', size_hint=(.4, .4))
    d = Button(text='D', size_hint=(1., .4)) # or a = Button(text='A', size_hint=(.8, .4))
    self.add_widget(a)
    self.add_widget(b)
    self.add_widget(c)
    self.add_widget(d)


class GameApp(App):

  def build(self):
    return RootWidget()


if __name__ == '__main__':
  GameApp().run()
```

It's totally expected that if size_hint of the button is .5, with 2 buttons side by side there is no room for padding or spacing. Therefore you get:

|A A |
|B |
|C | 
|D D |

Now with .4 for size_hint_x instead of .5, there is enough place for the widgets.

I guess what you meant to say is that we need to introduce padding_hint and spacing_hint to have the intended behavior accross multiple screen/window sizes. However it can still be calculated manually. You would need to check the size of the parent widget and add scaling factor.

```
padding = value * scale # between 0. and 1.
```

I did an even better test with:

```
self.spacing = 0 # change spacing here
self.padding = 80
a = Button(text='A', size_hint=(1., .2))
b = Button(text='B', size_hint=(.5, .2))
c = Button(text='C', size_hint=(.5, .2))
d = Button(text='D', size_hint=(1., .2))
```

In this example, by alternating between spacing = 0 and spacing = 1, you see that everything works as intended. With no spacing everything is stacked, with 1 spacing everything is pushed around. And this is 1.8.0

All other layouts appropriately account for padding and spacing when calculating size from size_hints. 

I think that StackLayout does not is unexpected behavior compared to the others. 

Thanks for you patience. I tend to be stubborn. I checked BoxLayout and GridLayout. So we're saying that StackLayout doesn't substract some space from the reserved space of the widget to account for spacing (padding seems ok to me). Hence the change you made.

We have 2 problems then:
1. We need to add a boolean to check if the next widget is the first on the line. If it is first then we can substract only half-spacing. If it is in the middle, we substract full spacing, but...
2. We need to know in advance wether or not the next widget to add, will be on the same col / row or not. But the current loop construct doesn't account for that need. I am going to rewrite the for loop with a while loop, and use that index to check the next widget size_hint. If it fits on the same line, I substract half-spacing, otherwise none.

I think that's the intended behavior you described.


Resolving patch:
diff --git a/kivy/uix/stacklayout.py b/kivy/uix/stacklayout.py
--- a/kivy/uix/stacklayout.py
+++ b/kivy/uix/stacklayout.py
@@ -134,6 +134,9 @@ def __init__(self, **kwargs):
             pos=self._trigger_layout)
 
     def do_layout(self, *largs):
+        if not self.children:
+            return
+
         # optimize layout by preventing looking at the same attribute in a loop
         selfpos = self.pos
         selfsize = self.size
@@ -142,8 +145,6 @@ def do_layout(self, *largs):
         padding_top = self.padding[1]
         padding_right = self.padding[2]
         padding_bottom = self.padding[3]
-        children = self.children
-        num_children = len(children)
 
         padding_x = padding_left + padding_right
         padding_y = padding_top + padding_bottom
@@ -186,47 +187,75 @@ def do_layout(self, *largs):
         # space calculation, used for determining when a row or column is full
 
         if orientation[0] in ('lr', 'rl'):
-            lu = self.size[innerattr] - padding_x
             sv = padding_y  # size in v-direction, for minimum_size property
             su = padding_x  # size in h-direction
             spacing_u = spacing_x
             spacing_v = spacing_y
+            padding_u = padding_x
+            padding_v = padding_y
         else:
-            lu = self.size[innerattr] - padding_y
             sv = padding_x  # size in v-direction, for minimum_size property
             su = padding_y  # size in h-direction
             spacing_u = spacing_y
             spacing_v = spacing_x
+            padding_u = padding_y
+            padding_v = padding_x
 
         # space calculation, row height or column width, for arranging widgets
         lv = 0
 
         urev = (deltau < 0)
         vrev = (deltav < 0)
-        for c in reversed(children):
-            if c.size_hint[0]:
-                if urev:
-                    modified_x = (
-                        selfsize[0] - padding_x -
-                        (num_children - 1) * spacing_x)
+        firstchild = self.children[0]
+        sizes = []
+        for c in reversed(self.children):
+            if c.size_hint[outerattr]:
+                c.size[outerattr] = max(
+                    1, c.size_hint[outerattr] *
+                       (selfsize[outerattr] - padding_v))
+
+            # does the widget fit in the row/column?
+            ccount = len(lc)
+            totalsize = availsize = max(
+                0, selfsize[innerattr] - padding_u - spacing_u * ccount)
+            if not lc:
+                if c.size_hint[innerattr]:
+                    childsize = max(1, c.size_hint[innerattr] * totalsize)
                 else:
-                    modified_x = (selfsize[0] - padding_x)
-                c.width = c.size_hint[0] * modified_x
-            if c.size_hint[1]:
-                if vrev:
-                    modified_y = (
-                        selfsize[1] - padding_y -
-                        (num_children - 1) * spacing_y)
+                    childsize = max(0, c.size[innerattr])
+                availsize = selfsize[innerattr] - padding_u - childsize
+                testsizes = [childsize]
+            else:
+                testsizes = [0] * (ccount + 1)
+                for i, child in enumerate(lc):
+                    if availsize <= 0:
+                        # no space left but we're trying to add another widget.
+                        availsize = -1
+                        break
+                    if child.size_hint[innerattr]:
+                        testsizes[i] = childsize = max(
+                            1, child.size_hint[innerattr] * totalsize)
+                    else:
+                        testsizes[i] = childsize = max(0, child.size[innerattr])
+                    availsize -= childsize
+                if c.size_hint[innerattr]:
+                    testsizes[-1] = max(1, c.size_hint[innerattr] * totalsize)
                 else:
-                    modified_y = (selfsize[1] - padding_y)
+                    testsizes[-1] = max(0, c.size[innerattr])
+                availsize -= testsizes[-1]
 
-            # does the widget fit in the row/column?
-            if lu - c.size[innerattr] >= 0:
+            if availsize >= 0 or not lc:
+                # even if there's no space, we always add one widget to a row
                 lc.append(c)
-                lu -= c.size[innerattr] + spacing_u
+                sizes = testsizes
                 lv = max(lv, c.size[outerattr])
                 continue
 
+            # apply the sizes
+            for i, child in enumerate(lc):
+                if child.size_hint[innerattr]:
+                    child.size[innerattr] = sizes[i]
+
             # push the line
             sv += lv + spacing_v
             for c2 in lc:
@@ -249,10 +278,19 @@ def do_layout(self, *largs):
             v += deltav * spacing_v
             lc = [c]
             lv = c.size[outerattr]
-            lu = selfsize[innerattr] - su - c.size[innerattr] - spacing_u
+            if c.size_hint[innerattr]:
+                sizes = [max(1, c.size_hint[innerattr] *
+                             (selfsize[innerattr] - padding_u))]
+            else:
+                sizes = [max(0, c.size[innerattr])]
             u = ustart
 
         if lc:
+            # apply the sizes
+            for i, child in enumerate(lc):
+                if child.size_hint[innerattr]:
+                    child.size[innerattr] = sizes[i]
+
             # push the last (incomplete) line
             sv += lv + spacing_v
             for c2 in lc:


Test patch: