]> git.saurik.com Git - wxWidgets.git/commitdiff
Fix box sizer layout algorithm to respect both proportions and min sizes.
authorVadim Zeitlin <vadim@wxwidgets.org>
Thu, 18 Mar 2010 15:07:24 +0000 (15:07 +0000)
committerVadim Zeitlin <vadim@wxwidgets.org>
Thu, 18 Mar 2010 15:07:24 +0000 (15:07 +0000)
The new version of the algorithm tries to distribute the entire space
allocated to the sizer among its children, just as the version in 2.8 did,
while still respecting minimal children sizes first and foremost. This means
that the space allocated to the item will always be at least its minimal size
if the total space is at least equal to the sum of minimal sizes of the
children but that if there is enough space, the proportions will be respected
too.

Extended the unit test to check that laying out various combinations of three
elements results in the expected results.

Closes #11311.

git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@63706 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

src/common/sizer.cpp
tests/sizers/boxsizer.cpp

index a5a68e5c07f03930192876ad0ecb16f369e264e3..872145da95d120f3fbaa1c743f369e4c38b93320 100644 (file)
@@ -32,6 +32,7 @@
 #endif // WX_PRECOMP
 
 #include "wx/display.h"
+#include "wx/vector.h"
 #include "wx/listimpl.cpp"
 
 
@@ -1987,6 +1988,50 @@ wxSizerItem *wxBoxSizer::AddSpacer(int size)
     return IsVertical() ? Add(0, size) : Add(size, 0);
 }
 
+namespace
+{
+
+/*
+    Helper of RecalcSizes(): checks if there is enough remaining space for the
+    min size of the given item and returns its min size or the entire remaining
+    space depending on which one is greater.
+
+    This function updates the remaining space parameter to account for the size
+    effectively allocated to the item.
+ */
+int
+GetMinOrRemainingSize(int orient, const wxSizerItem *item, int *remainingSpace_)
+{
+    int& remainingSpace = *remainingSpace_;
+
+    wxCoord size;
+    if ( remainingSpace > 0 )
+    {
+        const wxSize sizeMin = item->GetMinSizeWithBorder();
+        size = orient == wxHORIZONTAL ? sizeMin.x : sizeMin.y;
+
+        if ( size >= remainingSpace )
+        {
+            // truncate the item to fit in the remaining space, this is better
+            // than showing it only partially in general, even if both choices
+            // are bad -- but there is nothing else we can do
+            size = remainingSpace;
+        }
+
+        remainingSpace -= size;
+    }
+    else // no remaining space
+    {
+        // no space at all left, no need to even query the item for its min
+        // size as we can't give it to it anyhow
+        size = 0;
+    }
+
+    return size;
+}
+
+} // anonymous namespace
+
 void wxBoxSizer::RecalcSizes()
 {
     if ( m_children.empty() )
@@ -1999,14 +2044,17 @@ void wxBoxSizer::RecalcSizes()
     // stretchable items (i.e. those with non zero proportion)
     int delta = totalMajorSize - GetSizeInMajorDir(m_minSize);
 
+    // declare loop variables used below:
+    wxSizerItemList::const_iterator i;  // iterator in m_children list
+    unsigned n = 0;                     // item index in majorSizes array
 
-    // Inform child items about the size in minor direction, that can
-    // change how much free space we have in major dir and how to distribute it.
-    int majorMinSum = 0;
-    wxSizerItemList::const_iterator i ;
-    for ( i = m_children.begin();
-          i != m_children.end();
-          ++i )
+
+    // First, inform item about the available size in minor direction as this
+    // can change their size in the major direction. Also compute the number of
+    // visible items and sum of their min sizes in major direction.
+
+    int minMajorSize = 0;
+    for ( i = m_children.begin(); i != m_children.end(); ++i )
     {
         wxSizerItem * const item = *i;
 
@@ -2023,64 +2071,174 @@ void wxBoxSizer::RecalcSizes()
             // take too much, so delta should not become negative.
             delta -= deltaChange;
         }
-        majorMinSum += GetSizeInMajorDir(item->GetMinSizeWithBorder());
+        minMajorSize += GetSizeInMajorDir(item->GetMinSizeWithBorder());
     }
-    // And update our min size
-    SizeInMajorDir(m_minSize) = majorMinSum;
 
+    // update our min size and delta which may have changed
+    SizeInMajorDir(m_minSize) = minMajorSize;
+    delta = totalMajorSize - minMajorSize;
 
-    // might have a new delta now
-    delta = totalMajorSize - GetSizeInMajorDir(m_minSize);
 
-    // the position at which we put the next child
-    wxPoint pt(m_position);
+    // space and sum of proportions for the remaining items, both may change
+    // below
+    wxCoord remaining = totalMajorSize;
+    int totalProportion = m_totalProportion;
 
-    // space remaining for the items
-    wxCoord majorRemaining = totalMajorSize;
+    // size of the (visible) items in major direction, -1 means "not fixed yet"
+    wxVector<int> majorSizes(GetItemCount(), wxDefaultCoord);
 
-    int totalProportion = m_totalProportion;
-    for ( i = m_children.begin();
-          i != m_children.end();
-          ++i )
+
+    // Check for the degenerated case when we don't have enough space for even
+    // the min sizes of all the items: in this case we really can't do much
+    // more than to to allocate the min size to as many of fixed size items as
+    // possible (on the assumption that variable size items such as text zones
+    // or list boxes may use scrollbars to show their content even if their
+    // size is less than min size but that fixed size items such as buttons
+    // will suffer even more if we don't give them their min size)
+    if ( totalMajorSize < minMajorSize )
     {
-        wxSizerItem * const item = *i;
+        // Second degenerated case pass: allocate min size to all fixed size
+        // items.
+        for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n )
+        {
+            wxSizerItem * const item = *i;
 
-        if ( !item->IsShown() )
-            continue;
+            if ( !item->IsShown() )
+                continue;
 
-        const wxSize sizeThis(item->GetMinSizeWithBorder());
+            // deal with fixed size items only during this pass
+            if ( item->GetProportion() )
+                continue;
+
+            majorSizes[n] = GetMinOrRemainingSize(m_orient, item, &remaining);
+        }
 
-        // adjust the size in the major direction using the proportion
-        wxCoord majorSize = GetSizeInMajorDir(sizeThis);
 
-        if ( delta > 0 )
+        // Third degenerated case pass: allocate min size to all the remaining,
+        // i.e. non-fixed size, items.
+        for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n )
         {
-            // distribute extra space among the items respecting their
-            // proportions
+            wxSizerItem * const item = *i;
+
+            if ( !item->IsShown() )
+                continue;
+
+            // we've already dealt with fixed size items above
+            if ( !item->GetProportion() )
+                continue;
+
+            majorSizes[n] = GetMinOrRemainingSize(m_orient, item, &remaining);
+        }
+    }
+    else // we do have enough space to give at least min sizes to all items
+    {
+        // Second and maybe more passes in the non-degenerated case: deal with
+        // fixed size items and items whose min size is greater than what we
+        // would allocate to them taking their proportion into account. For
+        // both of them, we will just use their min size, but for the latter we
+        // also need to reexamine all the items as the items which fitted
+        // before we adjusted their size upwards might not fit any more. This
+        // does make for a quadratic algorithm but it's not obvious how to
+        // avoid it and hopefully it's not a huge problem in practice as the
+        // sizers don't have many items usually (and, of course, the algorithm
+        // still reduces into a linear one if there is enough space for all the
+        // min sizes).
+        bool nonFixedSpaceChanged = false;
+        for ( i = m_children.begin(), n = 0; ; ++i, ++n )
+        {
+            if ( nonFixedSpaceChanged )
+            {
+                i = m_children.begin();
+                n = 0;
+                nonFixedSpaceChanged = false;
+            }
+
+            // check for the end of the loop only after the check above as
+            // otherwise we wouldn't do another pass if the last child resulted
+            // in non fixed space reduction
+            if ( i == m_children.end() )
+                break;
+
+            wxSizerItem * const item = *i;
+
+            if ( !item->IsShown() )
+                continue;
+
+            // don't check the item which we had already dealt with during a
+            // previous pass (this is more than an optimization, the code
+            // wouldn't work correctly if we kept adjusting for the same item
+            // over and over again)
+            if ( majorSizes[n] != wxDefaultCoord )
+                continue;
+
+            const wxCoord
+                minMajor = GetSizeInMajorDir(item->GetMinSizeWithBorder());
             const int propItem = item->GetProportion();
             if ( propItem )
             {
-                const int deltaItem = (delta * propItem) / totalProportion;
-
-                majorSize += deltaItem;
+                // is the desired size of this item big enough?
+                if ( (remaining*propItem)/totalProportion >= minMajor )
+                {
+                    // yes, it is, we'll determine the real size of this
+                    // item later, for now just leave it as wxDefaultCoord
+                    continue;
+                }
 
-                delta -= deltaItem;
+                // the proportion of this item won't count, it has
+                // effectively become fixed
                 totalProportion -= propItem;
             }
+
+            // we can already allocate space for this item
+            majorSizes[n] = minMajor;
+
+            // change the amount of the space remaining to the other items,
+            // as this can result in not being able to satisfy their
+            // proportions any more we will need to redo another loop
+            // iteration
+            remaining -= minMajor;
+
+            nonFixedSpaceChanged = true;
         }
-        else // delta < 0
+
+
+        // Last by one pass: distribute the remaining space among the non-fixed
+        // items whose size weren't fixed yet according to their proportions.
+        for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n )
         {
-            // we're not going to have enough space for making all items even
-            // of their minimal size, check if this item still fits at all and
-            // truncate it if it doesn't -- even if it means giving it 0 size
-            // and thus making it invisible because we just can't do anything
-            // else
-            if ( majorSize > majorRemaining )
-                majorSize = majorRemaining;
+            wxSizerItem * const item = *i;
+
+            if ( !item->IsShown() )
+                continue;
 
-            majorRemaining -= majorSize;
+            if ( majorSizes[n] == wxDefaultCoord )
+            {
+                const int propItem = item->GetProportion();
+                majorSizes[n] = (remaining*propItem)/totalProportion;
+
+                remaining -= majorSizes[n];
+                totalProportion -= propItem;
+            }
         }
+    }
+
 
+    // the position at which we put the next child
+    wxPoint pt(m_position);
+
+
+    // Final pass: finally do position the items correctly using their sizes as
+    // determined above.
+    for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n )
+    {
+        wxSizerItem * const item = *i;
+
+        if ( !item->IsShown() )
+            continue;
+
+        const int majorSize = majorSizes[n];
+
+        const wxSize sizeThis(item->GetMinSizeWithBorder());
 
         // apply the alignment in the minor direction
         wxPoint posChild(pt);
index c86e319549f357888cb6bc5c930090d9c158acad..3087f6f07c10257658f06511f63cb92d5479e848 100644 (file)
@@ -42,9 +42,11 @@ public:
 private:
     CPPUNIT_TEST_SUITE( BoxSizerTestCase );
         CPPUNIT_TEST( Size1 );
+        CPPUNIT_TEST( Size3 );
     CPPUNIT_TEST_SUITE_END();
 
     void Size1();
+    void Size3();
 
     wxWindow *m_win;
     wxSizer *m_sizer;
@@ -93,7 +95,7 @@ void BoxSizerTestCase::Size1()
     m_sizer->Add(child);
     m_win->Layout();
     CPPUNIT_ASSERT_EQUAL( sizeChild, child->GetSize() );
-;
+
     m_sizer->Clear();
     m_sizer->Add(child, wxSizerFlags(1));
     m_win->Layout();
@@ -117,3 +119,131 @@ void BoxSizerTestCase::Size1()
     CPPUNIT_ASSERT_EQUAL( sizeTotal, child->GetSize() );
 }
 
+void BoxSizerTestCase::Size3()
+{
+    // check that various combinations of minimal sizes and proportions work as
+    // expected for different window sizes
+    static const struct LayoutTestData
+    {
+        // proportions of the elements
+        int prop[3];
+
+        // minimal sizes of the elements in the sizer direction
+        int minsize[3];
+
+        // total size and the expected sizes of the elements
+        int x,
+            sizes[3];
+
+        // if true, don't try the permutations of our test data
+        bool dontPermute;
+
+
+        // Add the given window to the sizer with the corresponding parameters
+        void AddToSizer(wxSizer *sizer, wxWindow *win, int n) const
+        {
+            sizer->Add(win, wxSizerFlags(prop[n]));
+            sizer->SetItemMinSize(win, wxSize(minsize[n], -1));
+        }
+
+    } layoutTestData[] =
+    {
+        // some really simple cases (no need to permute those, they're
+        // symmetrical anyhow)
+        { { 1, 1, 1, }, {  50,  50,  50, }, 150, {  50,  50,  50, }, true },
+        { { 2, 2, 2, }, {  50,  50,  50, }, 600, { 200, 200, 200, }, true },
+
+        // items with different proportions and min sizes when there is enough
+        // space to lay them out
+        { { 1, 2, 3, }, {   0,   0,   0, }, 600, { 100, 200, 300, } },
+        { { 1, 2, 3, }, { 100, 100, 100, }, 600, { 100, 200, 300, } },
+        { { 1, 2, 3, }, { 100,  50,  50, }, 600, { 100, 200, 300, } },
+        { { 0, 1, 1, }, { 200, 100, 100, }, 600, { 200, 200, 200, } },
+        { { 0, 1, 2, }, { 300, 100, 100, }, 600, { 300, 100, 200, } },
+        { { 0, 1, 1, }, { 100,  50,  50, }, 300, { 100, 100, 100, } },
+        { { 0, 1, 2, }, { 100,  50,  50, }, 400, { 100, 100, 200, } },
+
+        // cases when there is not enough space to lay out the items correctly
+        // while still respecting their min sizes
+        { { 0, 1, 1, }, { 100, 150,  50, }, 300, { 100, 150,  50, } },
+        { { 1, 2, 3, }, { 100, 100, 100, }, 300, { 100, 100, 100, } },
+        { { 1, 2, 3, }, { 100,  50,  50, }, 300, { 100,  80, 120, } },
+        { { 1, 2, 3, }, { 100,  10,  10, }, 150, { 100,  20,  30, } },
+
+        // cases when there is not enough space even for the min sizes (don't
+        // permute in these cases as the layout does depend on the item order
+        // because the first ones have priority)
+        { { 1, 2, 3, }, { 100,  50,  50, }, 150, { 100,  50,   0, }, true },
+        { { 1, 2, 3, }, { 100, 100, 100, }, 200, { 100, 100,   0, }, true },
+        { { 1, 2, 3, }, { 100, 100, 100, }, 150, { 100,  50,   0, }, true },
+        { { 1, 2, 3, }, { 100, 100, 100, },  50, {  50,   0,   0, }, true },
+        { { 1, 2, 3, }, { 100, 100, 100, },   0, {   0,   0,   0, }, true },
+    };
+
+    wxWindow *child[3];
+    child[0] = new wxWindow(m_win, wxID_ANY);
+    child[1] = new wxWindow(m_win, wxID_ANY);
+    child[2] = new wxWindow(m_win, wxID_ANY);
+
+    int j;
+    for ( unsigned i = 0; i < WXSIZEOF(layoutTestData); i++ )
+    {
+        LayoutTestData ltd = layoutTestData[i];
+
+        // the results shouldn't depend on the order of items except in the
+        // case when there is not enough space for even the fixed width items
+        // (in which case the first ones might get enough of it but not the
+        // last ones) so test a couple of permutations of test data unless
+        // specifically disabled for this test case
+        for ( unsigned p = 0; p < 3; p++)
+        {
+            switch ( p )
+            {
+                case 0:
+                    // nothing to do, use original data
+                    break;
+
+                case 1:
+                    // exchange first and last elements
+                    wxSwap(ltd.prop[0], ltd.prop[2]);
+                    wxSwap(ltd.minsize[0], ltd.minsize[2]);
+                    wxSwap(ltd.sizes[0], ltd.sizes[2]);
+                    break;
+
+                case 2:
+                    // exchange the original third and second elements
+                    wxSwap(ltd.prop[0], ltd.prop[1]);
+                    wxSwap(ltd.minsize[0], ltd.minsize[1]);
+                    wxSwap(ltd.sizes[0], ltd.sizes[1]);
+                    break;
+            }
+
+            m_sizer->Clear();
+            for ( j = 0; j < WXSIZEOF(child); j++ )
+                ltd.AddToSizer(m_sizer, child[j], j);
+
+            m_win->SetClientSize(ltd.x, -1);
+            m_win->Layout();
+
+            for ( j = 0; j < WXSIZEOF(child); j++ )
+            {
+                WX_ASSERT_EQUAL_MESSAGE
+                (
+                    (
+                        "test %lu, permutation #%d: wrong size for child #%d "
+                        "for total size %d",
+                        static_cast<unsigned long>(i),
+                        static_cast<unsigned long>(p),
+                        j,
+                        ltd.x
+                    ),
+                    ltd.sizes[j], child[j]->GetSize().x
+                );
+            }
+
+            // don't try other permutations if explicitly disabled
+            if ( ltd.dontPermute )
+                break;
+        }
+    }
+}