libui/darwin/grid.m

801 lines
20 KiB
Objective-C

// 11 june 2016
#import "uipriv_darwin.h"
// TODO the assorted test doesn't work right at all
@interface gridChild : NSView
@property uiControl *c;
@property int left;
@property int top;
@property int xspan;
@property int yspan;
@property int hexpand;
@property uiAlign halign;
@property int vexpand;
@property uiAlign valign;
@property (strong) NSLayoutConstraint *leadingc;
@property (strong) NSLayoutConstraint *topc;
@property (strong) NSLayoutConstraint *trailingc;
@property (strong) NSLayoutConstraint *bottomc;
@property (strong) NSLayoutConstraint *xcenterc;
@property (strong) NSLayoutConstraint *ycenterc;
@property NSLayoutPriority oldHorzHuggingPri;
@property NSLayoutPriority oldVertHuggingPri;
- (void)setC:(uiControl *)c grid:(uiGrid *)g;
- (void)onDestroy;
- (NSView *)view;
@end
@interface gridView : NSView {
uiGrid *g;
NSMutableArray *children;
int padded;
NSMutableArray *edges;
NSMutableArray *inBetweens;
NSMutableArray *emptyCellViews;
}
- (id)initWithG:(uiGrid *)gg;
- (void)onDestroy;
- (void)removeOurConstraints;
- (void)syncEnableStates:(int)enabled;
- (CGFloat)paddingAmount;
- (void)establishOurConstraints;
- (void)append:(gridChild *)gc;
- (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at;
- (int)isPadded;
- (void)setPadded:(int)p;
- (BOOL)hugsTrailing;
- (BOOL)hugsBottom;
- (int)nhexpand;
- (int)nvexpand;
@end
struct uiGrid {
uiDarwinControl c;
gridView *view;
};
@implementation gridChild
- (void)setC:(uiControl *)c grid:(uiGrid *)g
{
self.c = c;
self.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationHorizontal);
self.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationVertical);
uiControlSetParent(self.c, uiControl(g));
uiDarwinControlSetSuperview(uiDarwinControl(self.c), self);
uiDarwinControlSyncEnableState(uiDarwinControl(self.c), uiControlEnabledToUser(uiControl(g)));
if (self.halign == uiAlignStart || self.halign == uiAlignFill) {
self.leadingc = mkConstraint(self, NSLayoutAttributeLeading,
NSLayoutRelationEqual,
[self view], NSLayoutAttributeLeading,
1, 0,
@"uiGrid child horizontal alignment start constraint");
[self addConstraint:self.leadingc];
}
if (self.halign == uiAlignCenter) {
self.xcenterc = mkConstraint(self, NSLayoutAttributeCenterX,
NSLayoutRelationEqual,
[self view], NSLayoutAttributeCenterX,
1, 0,
@"uiGrid child horizontal alignment center constraint");
[self addConstraint:self.xcenterc];
}
if (self.halign == uiAlignEnd || self.halign == uiAlignFill) {
self.trailingc = mkConstraint(self, NSLayoutAttributeTrailing,
NSLayoutRelationEqual,
[self view], NSLayoutAttributeTrailing,
1, 0,
@"uiGrid child horizontal alignment end constraint");
[self addConstraint:self.trailingc];
}
if (self.valign == uiAlignStart || self.valign == uiAlignFill) {
self.topc = mkConstraint(self, NSLayoutAttributeTop,
NSLayoutRelationEqual,
[self view], NSLayoutAttributeTop,
1, 0,
@"uiGrid child vertical alignment start constraint");
[self addConstraint:self.topc];
}
if (self.valign == uiAlignCenter) {
self.ycenterc = mkConstraint(self, NSLayoutAttributeCenterY,
NSLayoutRelationEqual,
[self view], NSLayoutAttributeCenterY,
1, 0,
@"uiGrid child vertical alignment center constraint");
[self addConstraint:self.ycenterc];
}
if (self.valign == uiAlignEnd || self.valign == uiAlignFill) {
self.bottomc = mkConstraint(self, NSLayoutAttributeBottom,
NSLayoutRelationEqual,
[self view], NSLayoutAttributeBottom,
1, 0,
@"uiGrid child vertical alignment end constraint");
[self addConstraint:self.bottomc];
}
}
- (void)onDestroy
{
if (self.leadingc != nil) {
[self removeConstraint:self.leadingc];
self.leadingc = nil;
}
if (self.topc != nil) {
[self removeConstraint:self.topc];
self.topc = nil;
}
if (self.trailingc != nil) {
[self removeConstraint:self.trailingc];
self.trailingc = nil;
}
if (self.bottomc != nil) {
[self removeConstraint:self.bottomc];
self.bottomc = nil;
}
if (self.xcenterc != nil) {
[self removeConstraint:self.xcenterc];
self.xcenterc = nil;
}
if (self.ycenterc != nil) {
[self removeConstraint:self.ycenterc];
self.ycenterc = nil;
}
uiControlSetParent(self.c, NULL);
uiDarwinControlSetSuperview(uiDarwinControl(self.c), nil);
uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal);
uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldVertHuggingPri, NSLayoutConstraintOrientationVertical);
}
- (NSView *)view
{
return (NSView *) uiControlHandle(self.c);
}
@end
@implementation gridView
- (id)initWithG:(uiGrid *)gg
{
self = [super initWithFrame:NSZeroRect];
if (self != nil) {
self->g = gg;
self->padded = 0;
self->children = [NSMutableArray new];
self->edges = [NSMutableArray new];
self->inBetweens = [NSMutableArray new];
self->emptyCellViews = [NSMutableArray new];
}
return self;
}
- (void)onDestroy
{
gridChild *gc;
[self removeOurConstraints];
[self->edges release];
[self->inBetweens release];
[self->emptyCellViews release];
for (gc in self->children) {
[gc onDestroy];
uiControlDestroy(gc.c);
[gc removeFromSuperview];
}
[self->children release];
}
- (void)removeOurConstraints
{
NSView *v;
if ([self->edges count] != 0) {
[self removeConstraints:self->edges];
[self->edges removeAllObjects];
}
if ([self->inBetweens count] != 0) {
[self removeConstraints:self->inBetweens];
[self->inBetweens removeAllObjects];
}
for (v in self->emptyCellViews)
[v removeFromSuperview];
[self->emptyCellViews removeAllObjects];
}
- (void)syncEnableStates:(int)enabled
{
gridChild *gc;
for (gc in self->children)
uiDarwinControlSyncEnableState(uiDarwinControl(gc.c), enabled);
}
- (CGFloat)paddingAmount
{
if (!self->padded)
return 0.0;
return uiDarwinPaddingAmount(NULL);
}
// LONGTERM stop early if all controls are hidden
- (void)establishOurConstraints
{
gridChild *gc;
CGFloat padding;
int xmin, ymin;
int xmax, ymax;
int xcount, ycount;
BOOL first;
int **gg;
NSView ***gv;
BOOL **gspan;
int x, y;
int i;
NSLayoutConstraint *c;
int firstx, firsty;
BOOL *hexpand, *vexpand;
BOOL doit;
BOOL onlyEmptyAndSpanning;
[self removeOurConstraints];
if ([self->children count] == 0)
return;
padding = [self paddingAmount];
// first, figure out the minimum and maximum row and column numbers
// ignore hidden controls
first = YES;
for (gc in self->children) {
// this bit is important: it ensures row ymin and column xmin have at least one cell to draw, so the onlyEmptyAndSpanning logic below will never run on those rows
if (!uiControlVisible(gc.c))
continue;
if (first) {
xmin = gc.left;
ymin = gc.top;
xmax = gc.left + gc.xspan;
ymax = gc.top + gc.yspan;
first = NO;
continue;
}
if (xmin > gc.left)
xmin = gc.left;
if (ymin > gc.top)
ymin = gc.top;
if (xmax < (gc.left + gc.xspan))
xmax = gc.left + gc.xspan;
if (ymax < (gc.top + gc.yspan))
ymax = gc.top + gc.yspan;
}
if (first != NO) // the entire grid is hidden; do nothing
return;
xcount = xmax - xmin;
ycount = ymax - ymin;
// now build a topological map of the grid gg[y][x]
// also figure out which cells contain spanned views so they can be ignored later
// treat hidden controls by keeping the indices -1
gg = (int **) uiAlloc(ycount * sizeof (int *), "int[][]");
gspan = (BOOL **) uiAlloc(ycount * sizeof (BOOL *), "BOOL[][]");
for (y = 0; y < ycount; y++) {
gg[y] = (int *) uiAlloc(xcount * sizeof (int), "int[]");
gspan[y] = (BOOL *) uiAlloc(xcount * sizeof (BOOL), "BOOL[]");
for (x = 0; x < xcount; x++)
gg[y][x] = -1; // empty
}
for (i = 0; i < [self->children count]; i++) {
gc = (gridChild *) [self->children objectAtIndex: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++) {
gg[y - ymin][x - xmin] = i;
if (x != gc.left || y != gc.top)
gspan[y - ymin][x - xmin] = YES;
}
}
// if a row or column only contains emptys and spanning cells of a opposite-direction spannings, remove it by duplicating the previous row or column
for (y = 0; y < ycount; y++) {
onlyEmptyAndSpanning = YES;
for (x = 0; x < xcount; x++)
if (gg[y][x] != -1) {
gc = (gridChild *) [self->children objectAtIndex:gg[y][x]];
if (gc.yspan == 1 || gc.top - ymin == y) {
onlyEmptyAndSpanning = NO;
break;
}
}
if (onlyEmptyAndSpanning)
for (x = 0; x < xcount; x++) {
gg[y][x] = gg[y - 1][x];
gspan[y][x] = YES;
}
}
for (x = 0; x < xcount; x++) {
onlyEmptyAndSpanning = YES;
for (y = 0; y < ycount; y++)
if (gg[y][x] != -1) {
gc = (gridChild *) [self->children objectAtIndex:gg[y][x]];
if (gc.xspan == 1 || gc.left - xmin == x) {
onlyEmptyAndSpanning = NO;
break;
}
}
if (onlyEmptyAndSpanning)
for (y = 0; y < ycount; y++) {
gg[y][x] = gg[y][x - 1];
gspan[y][x] = YES;
}
}
// now build a topological map of the grid's views gv[y][x]
// for any empty cell, create a dummy view
gv = (NSView ***) uiAlloc(ycount * sizeof (NSView **), "NSView *[][]");
for (y = 0; y < ycount; y++) {
gv[y] = (NSView **) uiAlloc(xcount * sizeof (NSView *), "NSView *[]");
for (x = 0; x < xcount; x++)
if (gg[y][x] == -1) {
gv[y][x] = [[NSView alloc] initWithFrame:NSZeroRect];
[gv[y][x] setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:gv[y][x]];
[self->emptyCellViews addObject:gv[y][x]];
} else {
gc = (gridChild *) [self->children objectAtIndex:gg[y][x]];
gv[y][x] = gc;
}
}
// now figure out which rows and columns really expand
hexpand = (BOOL *) uiAlloc(xcount * sizeof (BOOL), "BOOL[]");
vexpand = (BOOL *) uiAlloc(ycount * sizeof (BOOL), "BOOL[]");
// first, which don't span
for (gc in self->children) {
if (!uiControlVisible(gc.c))
continue;
if (gc.hexpand && gc.xspan == 1)
hexpand[gc.left - xmin] = YES;
if (gc.vexpand && gc.yspan == 1)
vexpand[gc.top - ymin] = YES;
}
// second, which do span
// the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand
for (gc in self->children) {
if (!uiControlVisible(gc.c))
continue;
if (gc.hexpand && gc.xspan != 1) {
doit = YES;
for (x = gc.left; x < gc.left + gc.xspan; x++)
if (hexpand[x - xmin]) {
doit = NO;
break;
}
if (doit)
for (x = gc.left; x < gc.left + gc.xspan; x++)
hexpand[x - xmin] = YES;
}
if (gc.vexpand && gc.yspan != 1) {
doit = YES;
for (y = gc.top; y < gc.top + gc.yspan; y++)
if (vexpand[y - ymin]) {
doit = NO;
break;
}
if (doit)
for (y = gc.top; y < gc.top + gc.yspan; y++)
vexpand[y - ymin] = YES;
}
}
// now establish all the edge constraints
// leading and trailing edges
for (y = 0; y < ycount; y++) {
c = mkConstraint(self, NSLayoutAttributeLeading,
NSLayoutRelationEqual,
gv[y][0], NSLayoutAttributeLeading,
1, 0,
@"uiGrid leading edge constraint");
[self addConstraint:c];
[self->edges addObject:c];
c = mkConstraint(self, NSLayoutAttributeTrailing,
NSLayoutRelationEqual,
gv[y][xcount - 1], NSLayoutAttributeTrailing,
1, 0,
@"uiGrid trailing edge constraint");
[self addConstraint:c];
[self->edges addObject:c];
}
// top and bottom edges
for (x = 0; x < xcount; x++) {
c = mkConstraint(self, NSLayoutAttributeTop,
NSLayoutRelationEqual,
gv[0][x], NSLayoutAttributeTop,
1, 0,
@"uiGrid top edge constraint");
[self addConstraint:c];
[self->edges addObject:c];
c = mkConstraint(self, NSLayoutAttributeBottom,
NSLayoutRelationEqual,
gv[ycount - 1][x], NSLayoutAttributeBottom,
1, 0,
@"uiGrid bottom edge constraint");
[self addConstraint:c];
[self->edges addObject:c];
}
// now align leading and top edges
// do NOT align spanning cells!
for (x = 0; x < xcount; x++) {
for (y = 0; y < ycount; y++)
if (!gspan[y][x])
break;
firsty = y;
for (y++; y < ycount; y++) {
if (gspan[y][x])
continue;
c = mkConstraint(gv[firsty][x], NSLayoutAttributeLeading,
NSLayoutRelationEqual,
gv[y][x], NSLayoutAttributeLeading,
1, 0,
@"uiGrid column leading constraint");
[self addConstraint:c];
[self->edges addObject:c];
}
}
for (y = 0; y < ycount; y++) {
for (x = 0; x < xcount; x++)
if (!gspan[y][x])
break;
firstx = x;
for (x++; x < xcount; x++) {
if (gspan[y][x])
continue;
c = mkConstraint(gv[y][firstx], NSLayoutAttributeTop,
NSLayoutRelationEqual,
gv[y][x], NSLayoutAttributeTop,
1, 0,
@"uiGrid row top constraint");
[self addConstraint:c];
[self->edges addObject:c];
}
}
// now string adjacent views together
for (y = 0; y < ycount; y++)
for (x = 1; x < xcount; x++)
if (gv[y][x - 1] != gv[y][x]) {
c = mkConstraint(gv[y][x - 1], NSLayoutAttributeTrailing,
NSLayoutRelationEqual,
gv[y][x], NSLayoutAttributeLeading,
1, -padding,
@"uiGrid internal horizontal constraint");
[self addConstraint:c];
[self->inBetweens addObject:c];
}
for (x = 0; x < xcount; x++)
for (y = 1; y < ycount; y++)
if (gv[y - 1][x] != gv[y][x]) {
c = mkConstraint(gv[y - 1][x], NSLayoutAttributeBottom,
NSLayoutRelationEqual,
gv[y][x], NSLayoutAttributeTop,
1, -padding,
@"uiGrid internal vertical constraint");
[self addConstraint:c];
[self->inBetweens addObject:c];
}
// now set priorities for all widgets that expand or not
// if a cell is in an expanding row, OR If it spans, then it must be willing to stretch
// otherwise, it tries not to
// note we don't use NSLayoutPriorityRequired as that will cause things to squish when they shouldn't
for (gc in self->children) {
NSLayoutPriority priority;
if (!uiControlVisible(gc.c))
continue;
if (hexpand[gc.left - xmin] || gc.xspan != 1)
priority = NSLayoutPriorityDefaultLow;
else
priority = NSLayoutPriorityDefaultHigh;
uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationHorizontal);
// same for vertical direction
if (vexpand[gc.top - ymin] || gc.yspan != 1)
priority = NSLayoutPriorityDefaultLow;
else
priority = NSLayoutPriorityDefaultHigh;
uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationVertical);
}
// TODO make all expanding rows/columns the same height/width
// and finally clean up
uiFree(hexpand);
uiFree(vexpand);
for (y = 0; y < ycount; y++) {
uiFree(gg[y]);
uiFree(gv[y]);
uiFree(gspan[y]);
}
uiFree(gg);
uiFree(gv);
uiFree(gspan);
}
- (void)append:(gridChild *)gc
{
BOOL update;
int oldnh, oldnv;
[gc setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:gc];
// no need to set priority here; that's done in establishOurConstraints
oldnh = [self nhexpand];
oldnv = [self nvexpand];
[self->children addObject:gc];
[self establishOurConstraints];
update = NO;
if (gc.hexpand)
if (oldnh == 0)
update = YES;
if (gc.vexpand)
if (oldnv == 0)
update = YES;
if (update)
uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->g));
[gc release]; // we don't need the initial reference now
}
- (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at
{
gridChild *other;
BOOL found;
found = NO;
for (other in self->children)
if (other.c == c) {
found = YES;
break;
}
if (!found)
userbug("Existing control %p is not in grid %p; you cannot add other controls next to it", c, self->g);
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
}
[self append:gc];
}
- (int)isPadded
{
return self->padded;
}
- (void)setPadded:(int)p
{
CGFloat padding;
NSLayoutConstraint *c;
#if 0 /* TODO */
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC),
dispatch_get_main_queue(),
^{ [[self window] visualizeConstraints:[self constraints]]; }
);
#endif
self->padded = p;
padding = [self paddingAmount];
for (c in self->inBetweens)
switch ([c firstAttribute]) {
case NSLayoutAttributeLeading:
case NSLayoutAttributeTop:
[c setConstant:padding];
break;
case NSLayoutAttributeTrailing:
case NSLayoutAttributeBottom:
[c setConstant:-padding];
break;
}
}
- (BOOL)hugsTrailing
{
// only hug if we have horizontally expanding
return [self nhexpand] != 0;
}
- (BOOL)hugsBottom
{
// only hug if we have vertically expanding
return [self nvexpand] != 0;
}
- (int)nhexpand
{
gridChild *gc;
int n;
n = 0;
for (gc in self->children) {
if (!uiControlVisible(gc.c))
continue;
if (gc.hexpand)
n++;
}
return n;
}
- (int)nvexpand
{
gridChild *gc;
int n;
n = 0;
for (gc in self->children) {
if (!uiControlVisible(gc.c))
continue;
if (gc.vexpand)
n++;
}
return n;
}
@end
static void uiGridDestroy(uiControl *c)
{
uiGrid *g = uiGrid(c);
[g->view onDestroy];
[g->view release];
uiFreeControl(uiControl(g));
}
uiDarwinControlDefaultHandle(uiGrid, view)
uiDarwinControlDefaultParent(uiGrid, view)
uiDarwinControlDefaultSetParent(uiGrid, view)
uiDarwinControlDefaultToplevel(uiGrid, view)
uiDarwinControlDefaultVisible(uiGrid, view)
uiDarwinControlDefaultShow(uiGrid, view)
uiDarwinControlDefaultHide(uiGrid, view)
uiDarwinControlDefaultEnabled(uiGrid, view)
uiDarwinControlDefaultEnable(uiGrid, view)
uiDarwinControlDefaultDisable(uiGrid, view)
static void uiGridSyncEnableState(uiDarwinControl *c, int enabled)
{
uiGrid *g = uiGrid(c);
if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(g), enabled))
return;
[g->view syncEnableStates:enabled];
}
uiDarwinControlDefaultSetSuperview(uiGrid, view)
static BOOL uiGridHugsTrailingEdge(uiDarwinControl *c)
{
uiGrid *g = uiGrid(c);
return [g->view hugsTrailing];
}
static BOOL uiGridHugsBottom(uiDarwinControl *c)
{
uiGrid *g = uiGrid(c);
return [g->view hugsBottom];
}
static void uiGridChildEdgeHuggingChanged(uiDarwinControl *c)
{
uiGrid *g = uiGrid(c);
[g->view establishOurConstraints];
}
uiDarwinControlDefaultHuggingPriority(uiGrid, view)
uiDarwinControlDefaultSetHuggingPriority(uiGrid, view)
static void uiGridChildVisibilityChanged(uiDarwinControl *c)
{
uiGrid *g = uiGrid(c);
[g->view establishOurConstraints];
}
static gridChild *toChild(uiControl *c, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign, uiGrid *g)
{
gridChild *gc;
if (xspan < 0)
userbug("You cannot have a negative xspan in a uiGrid cell.");
if (yspan < 0)
userbug("You cannot have a negative yspan in a uiGrid cell.");
gc = [gridChild new];
gc.xspan = xspan;
gc.yspan = yspan;
gc.hexpand = hexpand;
gc.halign = halign;
gc.vexpand = vexpand;
gc.valign = valign;
[gc setC:c grid:g];
return gc;
}
void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
{
gridChild *gc;
// LONGTERM on other platforms
// or at leat allow this and implicitly turn it into a spacer
if (c == NULL)
userbug("You cannot add NULL to a uiGrid.");
gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g);
gc.left = left;
gc.top = top;
[g->view append:gc];
}
void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
{
gridChild *gc;
gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g);
[g->view insert:gc after:existing at:at];
}
int uiGridPadded(uiGrid *g)
{
return [g->view isPadded];
}
void uiGridSetPadded(uiGrid *g, int padded)
{
[g->view setPadded:padded];
}
uiGrid *uiNewGrid(void)
{
uiGrid *g;
uiDarwinNewControl(uiGrid, g);
g->view = [[gridView alloc] initWithG:g];
return g;
}