libui/windows/grid.cpp

659 lines
16 KiB
C++

// 10 june 2016
#include "uipriv_windows.hpp"
// TODO compare with GTK+:
// - what happens if you call InsertAt() twice?
// - what happens if you call Append() twice?
// TODOs
// - the Assorted page has clipping and repositioning issues
struct gridChild {
uiControl *c;
int left;
int top;
int xspan;
int yspan;
int hexpand;
uiAlign halign;
int vexpand;
uiAlign valign;
// have these here so they don't need to be reallocated each relayout
int finalx, finaly;
int finalwidth, finalheight;
int minwidth, minheight;
};
struct uiGrid {
uiWindowsControl c;
HWND hwnd;
std::vector<struct gridChild *> *children;
std::map<uiControl *, size_t> *indexof;
int padded;
int xmin, ymin;
int xmax, ymax;
};
static bool gridRecomputeMinMax(uiGrid *g)
{
bool first = true;
for (struct gridChild *gc : *(g->children)) {
// this is important; we want g->xmin/g->ymin to satisfy gridLayoutData::visibleRow()/visibleColumn()
if (!uiControlVisible(gc->c))
continue;
if (first) {
g->xmin = gc->left;
g->ymin = gc->top;
g->xmax = gc->left + gc->xspan;
g->ymax = gc->top + gc->yspan;
first = false;
continue;
}
if (g->xmin > gc->left)
g->xmin = gc->left;
if (g->ymin > gc->top)
g->ymin = gc->top;
if (g->xmax < (gc->left + gc->xspan))
g->xmax = gc->left + gc->xspan;
if (g->ymax < (gc->top + gc->yspan))
g->ymax = gc->top + gc->yspan;
}
return first != false;
}
#define xcount(g) ((g)->xmax - (g)->xmin)
#define ycount(g) ((g)->ymax - (g)->ymin)
#define toxindex(g, x) ((x) - (g)->xmin)
#define toyindex(g, y) ((y) - (g)->ymin)
class gridLayoutData {
int ycount;
public:
int **gg; // topological map gg[y][x] = control index
int *colwidths;
int *rowheights;
bool *hexpand;
bool *vexpand;
int nVisibleRows;
int nVisibleColumns;
bool noVisible;
gridLayoutData(uiGrid *g)
{
size_t i;
int x, y;
this->noVisible = gridRecomputeMinMax(g);
this->gg = new int *[ycount(g)];
for (y = 0; y < ycount(g); y++) {
this->gg[y] = new int[xcount(g)];
for (x = 0; x < xcount(g); x++)
this->gg[y][x] = -1;
}
for (i = 0; i < g->children->size(); i++) {
struct gridChild *gc;
gc = (*(g->children))[i];
if (!uiControlVisible(gc->c))
continue;
for (y = gc->top; y < gc->top + gc->yspan; y++)
for (x = gc->left; x < gc->left + gc->xspan; x++)
this->gg[toyindex(g, y)][toxindex(g, x)] = i;
}
this->colwidths = new int[xcount(g)];
ZeroMemory(this->colwidths, xcount(g) * sizeof (int));
this->rowheights = new int[ycount(g)];
ZeroMemory(this->rowheights, ycount(g) * sizeof (int));
this->hexpand = new bool[xcount(g)];
ZeroMemory(this->hexpand, xcount(g) * sizeof (bool));
this->vexpand = new bool[ycount(g)];
ZeroMemory(this->vexpand, ycount(g) * sizeof (bool));
this->ycount = ycount(g);
// if a row or column only contains emptys and spanning cells of a opposite-direction spannings, it is invisible and should not be considered for padding amount calculations
// note that the first row and column will always be visible because gridRecomputeMinMax() computed a smallest fitting rectangle
if (this->noVisible)
return;
this->nVisibleRows = 0;
for (y = 0; y < this->ycount; y++)
if (this->visibleRow(g, y))
this->nVisibleRows++;
this->nVisibleColumns = 0;
for (x = 0; x < xcount(g); x++)
if (this->visibleColumn(g, x))
this->nVisibleColumns++;
}
~gridLayoutData()
{
size_t y;
delete[] this->hexpand;
delete[] this->vexpand;
delete[] this->colwidths;
delete[] this->rowheights;
for (y = 0; y < this->ycount; y++)
delete[] this->gg[y];
delete[] this->gg;
}
bool visibleRow(uiGrid *g, int y)
{
int x;
struct gridChild *gc;
for (x = 0; x < xcount(g); x++)
if (this->gg[y][x] != -1) {
gc = (*(g->children))[this->gg[y][x]];
if (gc->yspan == 1 || gc->top - g->ymin == y)
return true;
}
return false;
}
bool visibleColumn(uiGrid *g, int x)
{
int y;
struct gridChild *gc;
for (y = 0; y < this->ycount; y++)
if (this->gg[y][x] != -1) {
gc = (*(g->children))[this->gg[y][x]];
if (gc->xspan == 1 || gc->left - g->xmin == x)
return true;
}
return false;
}
};
static void gridPadding(uiGrid *g, int *xpadding, int *ypadding)
{
uiWindowsSizing sizing;
*xpadding = 0;
*ypadding = 0;
if (g->padded) {
uiWindowsGetSizing(g->hwnd, &sizing);
uiWindowsSizingStandardPadding(&sizing, xpadding, ypadding);
}
}
static void gridRelayout(uiGrid *g)
{
RECT r;
int x, y, width, height;
gridLayoutData *ld;
int xpadding, ypadding;
int ix, iy;
int iwidth, iheight;
int i;
struct gridChild *gc;
int nhexpand, nvexpand;
if (g->children->size() == 0)
return; // nothing to do
uiWindowsEnsureGetClientRect(g->hwnd, &r);
x = r.left;
y = r.top;
width = r.right - r.left;
height = r.bottom - r.top;
gridPadding(g, &xpadding, &ypadding);
ld = new gridLayoutData(g);
if (ld->noVisible) { // nothing to do
delete ld;
return;
}
// 0) discount padding from width/height
width -= (ld->nVisibleColumns - 1) * xpadding;
height -= (ld->nVisibleRows - 1) * ypadding;
// 1) compute colwidths and rowheights before handling expansion
// we only count non-spanning controls to avoid weirdness
for (iy = 0; iy < ycount(g); iy++)
for (ix = 0; ix < xcount(g); ix++) {
i = ld->gg[iy][ix];
if (i == -1)
continue;
gc = (*(g->children))[i];
uiWindowsControlMinimumSize(uiWindowsControl(gc->c), &iwidth, &iheight);
if (gc->xspan == 1)
if (ld->colwidths[ix] < iwidth)
ld->colwidths[ix] = iwidth;
if (gc->yspan == 1)
if (ld->rowheights[iy] < iheight)
ld->rowheights[iy] = iheight;
// save these for step 6
gc->minwidth = iwidth;
gc->minheight = iheight;
}
// 2) figure out which rows/columns expand but not span
// we need to know which expanding rows/columns don't span before we can handle the ones that do
for (i = 0; i < g->children->size(); i++) {
gc = (*(g->children))[i];
if (!uiControlVisible(gc->c))
continue;
if (gc->hexpand && gc->xspan == 1)
ld->hexpand[toxindex(g, gc->left)] = true;
if (gc->vexpand && gc->yspan == 1)
ld->vexpand[toyindex(g, gc->top)] = true;
}
// 3) figure out which rows/columns expand that do span
// the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand
for (i = 0; i < g->children->size(); i++) {
gc = (*(g->children))[i];
if (!uiControlVisible(gc->c))
continue;
if (gc->hexpand && gc->xspan != 1) {
bool doit = true;
for (ix = gc->left; ix < gc->left + gc->xspan; ix++)
if (ld->hexpand[toxindex(g, ix)]) {
doit = false;
break;
}
if (doit)
for (ix = gc->left; ix < gc->left + gc->xspan; ix++)
ld->hexpand[toxindex(g, ix)] = true;
}
if (gc->vexpand && gc->yspan != 1) {
bool doit = true;
for (iy = gc->top; iy < gc->top + gc->yspan; iy++)
if (ld->vexpand[toyindex(g, iy)]) {
doit = false;
break;
}
if (doit)
for (iy = gc->top; iy < gc->top + gc->yspan; iy++)
ld->vexpand[toyindex(g, iy)] = true;
}
}
// 4) compute and assign expanded widths/heights
nhexpand = 0;
nvexpand = 0;
for (i = 0; i < xcount(g); i++)
if (ld->hexpand[i])
nhexpand++;
else
width -= ld->colwidths[i];
for (i = 0; i < ycount(g); i++)
if (ld->vexpand[i])
nvexpand++;
else
height -= ld->rowheights[i];
for (i = 0; i < xcount(g); i++)
if (ld->hexpand[i])
ld->colwidths[i] = width / nhexpand;
for (i = 0; i < ycount(g); i++)
if (ld->vexpand[i])
ld->rowheights[i] = height / nvexpand;
// 5) reset the final coordinates for the next step
for (i = 0; i < g->children->size(); i++) {
gc = (*(g->children))[i];
if (!uiControlVisible(gc->c))
continue;
gc->finalx = 0;
gc->finaly = 0;
gc->finalwidth = 0;
gc->finalheight = 0;
}
// 6) compute cell positions and sizes
for (iy = 0; iy < ycount(g); iy++) {
int curx;
int prev;
curx = 0;
prev = -1;
for (ix = 0; ix < xcount(g); ix++) {
if (!ld->visibleColumn(g, ix))
continue;
i = ld->gg[iy][ix];
if (i != -1) {
gc = (*(g->children))[i];
if (iy == toyindex(g, gc->top)) { // don't repeat this step if the control spans vertically
if (i != prev)
gc->finalx = curx;
else
gc->finalwidth += xpadding;
gc->finalwidth += ld->colwidths[ix];
}
}
curx += ld->colwidths[ix] + xpadding;
prev = i;
}
}
for (ix = 0; ix < xcount(g); ix++) {
int cury;
int prev;
cury = 0;
prev = -1;
for (iy = 0; iy < ycount(g); iy++) {
if (!ld->visibleRow(g, iy))
continue;
i = ld->gg[iy][ix];
if (i != -1) {
gc = (*(g->children))[i];
if (ix == toxindex(g, gc->left)) { // don't repeat this step if the control spans horizontally
if (i != prev)
gc->finaly = cury;
else
gc->finalheight += ypadding;
gc->finalheight += ld->rowheights[iy];
}
}
cury += ld->rowheights[iy] + ypadding;
prev = i;
}
}
// 7) everything as it stands now is set for xalign == Fill yalign == Fill; set the correct alignments
// this is why we saved minwidth/minheight above
for (i = 0; i < g->children->size(); i++) {
gc = (*(g->children))[i];
if (!uiControlVisible(gc->c))
continue;
if (gc->halign != uiAlignFill) {
switch (gc->halign) {
case uiAlignEnd:
gc->finalx += gc->finalwidth - gc->minwidth;
break;
case uiAlignCenter:
gc->finalx += (gc->finalwidth - gc->minwidth) / 2;
break;
}
gc->finalwidth = gc->minwidth; // for all three
}
if (gc->valign != uiAlignFill) {
switch (gc->valign) {
case uiAlignEnd:
gc->finaly += gc->finalheight - gc->minheight;
break;
case uiAlignCenter:
gc->finaly += (gc->finalheight - gc->minheight) / 2;
break;
}
gc->finalheight = gc->minheight; // for all three
}
}
// 8) and FINALLY we resize
for (iy = 0; iy < ycount(g); iy++)
for (ix = 0; ix < xcount(g); ix++) {
i = ld->gg[iy][ix];
if (i != -1) { // treat empty cells like spaces
gc = (*(g->children))[i];
uiWindowsEnsureMoveWindowDuringResize(
(HWND) uiControlHandle(gc->c),
gc->finalx,//TODO + x,
gc->finaly,//TODO + y,
gc->finalwidth,
gc->finalheight);
}
}
delete ld;
}
static void uiGridDestroy(uiControl *c)
{
uiGrid *g = uiGrid(c);
for (struct gridChild *gc : *(g->children)) {
uiControlSetParent(gc->c, NULL);
uiControlDestroy(gc->c);
uiprivFree(gc);
}
delete g->indexof;
delete g->children;
uiWindowsEnsureDestroyWindow(g->hwnd);
uiFreeControl(uiControl(g));
}
uiWindowsControlDefaultHandle(uiGrid)
uiWindowsControlDefaultParent(uiGrid)
uiWindowsControlDefaultSetParent(uiGrid)
uiWindowsControlDefaultToplevel(uiGrid)
uiWindowsControlDefaultVisible(uiGrid)
uiWindowsControlDefaultShow(uiGrid)
uiWindowsControlDefaultHide(uiGrid)
uiWindowsControlDefaultEnabled(uiGrid)
uiWindowsControlDefaultEnable(uiGrid)
uiWindowsControlDefaultDisable(uiGrid)
static void uiGridSyncEnableState(uiWindowsControl *c, int enabled)
{
uiGrid *g = uiGrid(c);
if (uiWindowsShouldStopSyncEnableState(uiWindowsControl(g), enabled))
return;
for (const struct gridChild *gc : *(g->children))
uiWindowsControlSyncEnableState(uiWindowsControl(gc->c), enabled);
}
uiWindowsControlDefaultSetParentHWND(uiGrid)
static void uiGridMinimumSize(uiWindowsControl *c, int *width, int *height)
{
uiGrid *g = uiGrid(c);
int xpadding, ypadding;
gridLayoutData *ld;
int x, y;
int i;
struct gridChild *gc;
int minwid, minht;
int colwidth, rowheight;
*width = 0;
*height = 0;
if (g->children->size() == 0)
return; // nothing to do
gridPadding(g, &xpadding, &ypadding);
ld = new gridLayoutData(g);
if (ld->noVisible) { // nothing to do; return 0x0
delete ld;
return;
}
// 1) compute colwidths and rowheights before handling expansion
// TODO put this in its own function (but careful about the spanning calculation in gridRelayout())
for (y = 0; y < ycount(g); y++)
for (x = 0; x < xcount(g); x++) {
i = ld->gg[y][x];
if (i == -1)
continue;
gc = (*(g->children))[i];
uiWindowsControlMinimumSize(uiWindowsControl(gc->c), &minwid, &minht);
// allot equal space in the presence of spanning to keep things sane
if (ld->colwidths[x] < minwid / gc->xspan)
ld->colwidths[x] = minwid / gc->xspan;
if (ld->rowheights[y] < minht / gc->yspan)
ld->rowheights[y] = minht / gc->yspan;
// save these for step 6
gc->minwidth = minwid;
gc->minheight = minht;
}
// 2) compute total column width/row height
colwidth = 0;
rowheight = 0;
for (x = 0; x < xcount(g); x++)
colwidth += ld->colwidths[x];
for (y = 0; y < ycount(g); y++)
rowheight += ld->rowheights[y];
// and that's it; just account for padding
*width = colwidth + (ld->nVisibleColumns - 1) * xpadding;
*height = rowheight + (ld->nVisibleRows - 1) * ypadding;
}
static void uiGridMinimumSizeChanged(uiWindowsControl *c)
{
uiGrid *g = uiGrid(c);
if (uiWindowsControlTooSmall(uiWindowsControl(g))) {
uiWindowsControlContinueMinimumSizeChanged(uiWindowsControl(g));
return;
}
gridRelayout(g);
}
uiWindowsControlDefaultLayoutRect(uiGrid)
uiWindowsControlDefaultAssignControlIDZOrder(uiGrid)
static void uiGridChildVisibilityChanged(uiWindowsControl *c)
{
// TODO eliminate the redundancy
uiWindowsControlMinimumSizeChanged(c);
}
// must have called gridRecomputeMinMax() first
static void gridArrangeChildren(uiGrid *g)
{
LONG_PTR controlID;
HWND insertAfter;
gridLayoutData *ld;
bool *visited;
int x, y;
int i;
struct gridChild *gc;
if (g->children->size() == 0)
return; // nothing to do
ld = new gridLayoutData(g);
controlID = 100;
insertAfter = NULL;
visited = new bool[g->children->size()];
ZeroMemory(visited, g->children->size() * sizeof (bool));
for (y = 0; y < ycount(g); y++)
for (x = 0; x < xcount(g); x++) {
i = ld->gg[y][x];
if (i == -1)
continue;
if (visited[i])
continue;
visited[i] = true;
gc = (*(g->children))[i];
uiWindowsControlAssignControlIDZOrder(uiWindowsControl(gc->c), &controlID, &insertAfter);
}
delete[] visited;
delete ld;
}
static struct gridChild *toChild(uiControl *c, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
{
struct gridChild *gc;
if (xspan < 0)
uiprivUserBug("You cannot have a negative xspan in a uiGrid cell.");
if (yspan < 0)
uiprivUserBug("You cannot have a negative yspan in a uiGrid cell.");
gc = uiprivNew(struct gridChild);
gc->c = c;
gc->xspan = xspan;
gc->yspan = yspan;
gc->hexpand = hexpand;
gc->halign = halign;
gc->vexpand = vexpand;
gc->valign = valign;
return gc;
}
static void add(uiGrid *g, struct gridChild *gc)
{
uiControlSetParent(gc->c, uiControl(g));
uiWindowsControlSetParentHWND(uiWindowsControl(gc->c), g->hwnd);
g->children->push_back(gc);
(*(g->indexof))[gc->c] = g->children->size() - 1;
gridRecomputeMinMax(g);
gridArrangeChildren(g);
uiWindowsControlMinimumSizeChanged(uiWindowsControl(g));
}
void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
{
struct gridChild *gc;
gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign);
gc->left = left;
gc->top = top;
add(g, gc);
}
// TODO decide what happens if existing is NULL
void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
{
struct gridChild *gc;
struct gridChild *other;
gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign);
other = (*(g->children))[(*(g->indexof))[existing]];
switch (at) {
case uiAtLeading:
gc->left = other->left - gc->xspan;
gc->top = other->top;
break;
case uiAtTop:
gc->left = other->left;
gc->top = other->top - gc->yspan;
break;
case uiAtTrailing:
gc->left = other->left + other->xspan;
gc->top = other->top;
break;
case uiAtBottom:
gc->left = other->left;
gc->top = other->top + other->yspan;
break;
// TODO add error checks to ALL enums
}
add(g, gc);
}
int uiGridPadded(uiGrid *g)
{
return g->padded;
}
void uiGridSetPadded(uiGrid *g, int padded)
{
g->padded = padded;
uiWindowsControlMinimumSizeChanged(uiWindowsControl(g));
}
static void onResize(uiWindowsControl *c)
{
gridRelayout(uiGrid(c));
}
uiGrid *uiNewGrid(void)
{
uiGrid *g;
uiWindowsNewControl(uiGrid, g);
g->hwnd = uiWindowsMakeContainer(uiWindowsControl(g), onResize);
g->children = new std::vector<struct gridChild *>;
g->indexof = new std::map<uiControl *, size_t>;
return g;
}