Script GUI: added TightGridLayout style (requirement of #1842)

The TightGridLayout fills spaces more aggressively. This is slower but makes for a tighter layout. Finding the best layout is NP-complete. This here is just O(N^2) or so.
directional-lights
David Dormagen 2016-11-05 15:21:29 +01:00
parent 7aeec3279c
commit 8a8593e0ba
5 changed files with 131 additions and 1 deletions

View File

@ -178,6 +178,10 @@
<col>GUI_GridLayout</col>
<col>The sub-windows of this window will automatically be arranged in a grid.</col>
</row>
<row>
<col>GUI_TightGridLayout</col>
<col>Like GUI_GridLayout but might reorder items and lead to less empty space between sub-windows.</col>
</row>
<row>
<col>GUI_VerticalLayout</col>
<col>The sub-windows of this window will automatically be arranged vertically in a list.</col>

View File

@ -107,6 +107,7 @@ func StartMenu(plr)
menu->AddItem(Clonk, "Test Multiple Windows (Player List)", nil, Scenario, "StartPlayerListTest", "Shows how to display a permanent info dialogue.");
menu->AddItem(Lorry, "Tests Two Grid Menus (Trade Menu)", nil, Scenario, "StartTransferTest", "Shows how to work with two grid menus.");
menu->AddItem(Sproutberry, "Test HP Bars (HP Bars!)", nil, Scenario, "StartHPBarTest", "HP BARS!!!");
menu->AddItem(Crate, "Test mixed grid layout.", nil, Scenario, "StartMixedGridTest", "Grid menu with differently-sized items.");
active_menu = GuiOpen(main_menu);
}
@ -459,4 +460,39 @@ func OnHPBarClose()
RemoveEffect("FoolAroundWithHPBar");
HP_bar_menu = nil;
Log("HP bar off!");
}
/* ------------------- mixed grid menu test ---------------*/
func StartMixedGridTest()
{
GuiClose(active_menu);
var menu =
{
toptext = {Bottom = "2em", Text="GUI_TightGridLayout", Style = GUI_TextHCenter},
top = {Top = "2em", Bottom = "50%", Margin = "0.5em", Style = GUI_TightGridLayout, Decoration = GUI_MenuDeco},
bottomtext = {Top = "50%", Bottom = "50% + 2em", Text="GUI_GridLayout", Style = GUI_TextHCenter},
bottom = {Top = "50% + 2em", Margin = "0.5em", Style = GUI_GridLayout, Decoration = GUI_MenuDeco},
BackgroundColor = RGBa(0, 0, 0, 128),
};
GuiAddCloseButton(menu, Scenario, "CloseCurrentMenu");
var seed = 1;
for (var i=0; i < 50; ++i)
{
seed = ((seed * 17) + 7) % 1357;
var w = (seed % 4) + 1;
seed = ((seed * 23) + 13)% 1357;
var h = (seed % 4) + 1;
var item =
{
Right = Format("%dem", w),
Bottom = Format("%dem", h),
Priority = i+1,
BackgroundColor = HSL(seed % 255, 200, 200)
};
GuiAddSubwindow(item, menu.top);
GuiAddSubwindow(item, menu.bottom);
}
active_menu = GuiOpen(menu);
}

View File

@ -3039,6 +3039,7 @@ C4ScriptConstDef C4ScriptGameConstMap[]=
{ "GUI_SetTag" ,C4V_Int, C4ScriptGuiWindowActionID::SetTag },
{ "GUI_Call" ,C4V_Int, C4ScriptGuiWindowActionID::Call },
{ "GUI_GridLayout" ,C4V_Int, C4ScriptGuiWindowStyleFlag::GridLayout },
{ "GUI_TightGridLayout" ,C4V_Int, C4ScriptGuiWindowStyleFlag::TightGridLayout },
{ "GUI_VerticalLayout" ,C4V_Int, C4ScriptGuiWindowStyleFlag::VerticalLayout },
{ "GUI_TextVCenter" ,C4V_Int, C4ScriptGuiWindowStyleFlag::TextVCenter },
{ "GUI_TextHCenter" ,C4V_Int, C4ScriptGuiWindowStyleFlag::TextHCenter },

View File

@ -1445,6 +1445,91 @@ void C4ScriptGuiWindow::UpdateLayoutGrid()
EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
}
// Similar to the grid layout but tries to fill spaces more thoroughly.
// It's slower and might reorder items.
void C4ScriptGuiWindow::UpdateLayoutTightGrid()
{
const int32_t &width = rcBounds.Wdt;
const int32_t &height = rcBounds.Hgt;
const int32_t borderX(0), borderY(0);
int32_t lowestChildRelY = 0;
std::list<C4ScriptGuiWindow*> alreadyPlacedChildren;
for (C4GUI::Element * element : *this)
{
C4ScriptGuiWindow *child = static_cast<C4ScriptGuiWindow*>(element);
// calculate the space the child needs, correctly respecting the margins
const float childLeftMargin = child->CalculateRelativeSize(width, leftMargin, relLeftMargin);
const float childTopMargin = child->CalculateRelativeSize(height, topMargin, relTopMargin);
const float childRightMargin = child->CalculateRelativeSize(width, rightMargin, relRightMargin);
const float childBottomMargin = child->CalculateRelativeSize(height, bottomMargin, relBottomMargin);
const float childWdtF = float(child->rcBounds.Wdt) + childLeftMargin + childRightMargin;
const float childHgtF = float(child->rcBounds.Hgt) + childTopMargin + childBottomMargin;
// do all the rounding after the calculations
const int32_t childWdt = (int32_t)(childWdtF + 0.5f);
const int32_t childHgt = (int32_t)(childHgtF + 0.5f);
// Look for a free spot.
int32_t currentX = borderX;
int32_t currentY = borderY;
bool hadOverlap = false;
int overlapRepeats = 0;
do
{
auto overlapsWithOther = [&currentX, &currentY, &childWdt, &childHgt](C4ScriptGuiWindow *other)
{
if (currentX + childWdt <= other->rcBounds.x) return false;
if (currentY + childHgt <= other->rcBounds.y) return false;
if (currentX >= other->rcBounds.GetRight()) return false;
if (currentY >= other->rcBounds.GetBottom()) return false;
return true;
};
int32_t currentMinY = 0;
hadOverlap = false;
for (auto &other : alreadyPlacedChildren)
{
// Check if the other element is not yet above the new child.
if ((other->rcBounds.GetBottom() > currentY) && other->rcBounds.Hgt > 0)
{
if (currentMinY == 0 || (other->rcBounds.GetBottom() < currentMinY))
currentMinY = other->rcBounds.GetBottom();
}
// If overlapping, we must advance.
if (overlapsWithOther(other))
{
hadOverlap = true;
currentX = other->rcBounds.GetRight();
// Break line if the element doesn't fit anymore.
if (currentX + childWdt > width)
{
currentX = borderX;
// Start forcing change once we start repeating the check. Otherwise, there might
// be a composition of children that lead to infinite loop. The worst-case number
// of sensible checks might O(N^2) be with a really unfortunate children list.
const int32_t forcedMinimalChange = (overlapRepeats > alreadyPlacedChildren.size()) ? 1 : 0;
currentY = std::max(currentY + forcedMinimalChange, currentMinY);
}
}
}
overlapRepeats += 1;
} while (hadOverlap);
alreadyPlacedChildren.push_back(child);
lowestChildRelY = std::max(lowestChildRelY, currentY + childHgt);
child->rcBounds.x = currentX + static_cast<int32_t>(childLeftMargin);
child->rcBounds.y = currentY + static_cast<int32_t>(childTopMargin);
}
// do we need a scroll bar?
EnableScrollBar(lowestChildRelY > height, lowestChildRelY);
}
void C4ScriptGuiWindow::UpdateLayoutVertical()
{
const int32_t borderY(0);
@ -1738,6 +1823,8 @@ bool C4ScriptGuiWindow::UpdateLayout(C4TargetFacet &cgo, float parentWidth, floa
// special layout selected?
if (style & C4ScriptGuiWindowStyleFlag::GridLayout)
UpdateLayoutGrid();
else if (style & C4ScriptGuiWindowStyleFlag::TightGridLayout)
UpdateLayoutTightGrid();
else if (style & C4ScriptGuiWindowStyleFlag::VerticalLayout)
UpdateLayoutVertical();

View File

@ -82,7 +82,8 @@ enum C4ScriptGuiWindowStyleFlag
FitChildren = 64,
Multiple = 128,
IgnoreMouse = 256,
NoCrop = 512
NoCrop = 512,
TightGridLayout = 1024,
};
class C4ScriptGuiWindow;
@ -291,6 +292,7 @@ public:
bool UpdateChildLayout(C4TargetFacet &cgo, float parentWidth, float parentHeight);
// special layouts that are set by styles
void UpdateLayoutGrid();
void UpdateLayoutTightGrid();
void UpdateLayoutVertical();
// the window will be drawn in the context of a viewport BY the viewport
// so just do nothing when TheScreen wants to draw the window