diff --git a/Headers/AppKit/NSStackView.h b/Headers/AppKit/NSStackView.h index 36134fc3b..93a96e4f7 100644 --- a/Headers/AppKit/NSStackView.h +++ b/Headers/AppKit/NSStackView.h @@ -92,6 +92,8 @@ APPKIT_EXPORT_CLASS NSMapTable *_customSpacingMap; NSMapTable *_visiblePriorityMap; + + NSMutableArray *_internalConstraints; } // Properties diff --git a/Source/NSStackView.m b/Source/NSStackView.m index 6c71f8481..2407f539a 100644 --- a/Source/NSStackView.m +++ b/Source/NSStackView.m @@ -23,6 +23,7 @@ */ #import "AppKit/NSStackView.h" +#import "AppKit/NSLayoutConstraint.h" #import "AppKit/NSTextStorage.h" #import "AppKit/NSTextContainer.h" #import "AppKit/NSLayoutManager.h" @@ -164,94 +165,364 @@ - (void) _addSubviews: (NSArray *)views @implementation NSStackView -- (void) _layoutViewsWithOrientation: (NSUserInterfaceLayoutOrientation)o +- (NSArray *) _layoutArrangedSubviews { - NSRect currentFrame = [self frame]; - NSRect newFrame = currentFrame; - NSArray *sv = [self subviews]; - NSUInteger n = [sv count]; - CGFloat sp = [self spacing]; - CGFloat x = 0.0; - CGFloat y = 0.0; - CGFloat newHeight = 0.0; - NSUInteger i = 0; - - // Advance vertically or horizontally depending on orientation... - if (o == NSUserInterfaceLayoutOrientationVertical) + NSMutableArray *result = [NSMutableArray arrayWithCapacity: + [_arrangedSubviews count]]; + NSUInteger i; + + for (i = 0; i < [_arrangedSubviews count]; i++) { - if (sp == 0.0) + NSView *v = [_arrangedSubviews objectAtIndex: i]; + + if (_detachesHiddenViews && [v isHidden]) { - if (n > 0) + continue; + } + [result addObject: v]; + } + return result; +} + +- (void) _pin: (id)first + attribute: (NSLayoutAttribute)firstAttribute + relatedBy: (NSLayoutRelation)relation + toItem: (id)second + attribute: (NSLayoutAttribute)secondAttribute + multiplier: (CGFloat)multiplier + constant: (CGFloat)constant +{ + NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem: first + attribute: firstAttribute + relatedBy: relation + toItem: second + attribute: secondAttribute + multiplier: multiplier + constant: constant]; + + [_internalConstraints addObject: c]; +} + +- (CGFloat) _spacingAfterView: (NSView *)v +{ + NSNumber *custom = [_customSpacingMap objectForKey: v]; + + if (custom != nil && [custom floatValue] != NSStackViewSpacingUseDefault) + { + return [custom floatValue]; + } + return _spacing; +} + +- (CGFloat) _intrinsicExtentOfView: (NSView *)v +{ + NSSize size = [v intrinsicContentSize]; + + if (_orientation == NSUserInterfaceLayoutOrientationHorizontal) + { + return size.width; + } + return size.height; +} + +/* The space along the main axis that the arranged views have to share. */ +- (CGFloat) _availableExtent +{ + NSSize size = [self frame].size; + + if (_orientation == NSUserInterfaceLayoutOrientationHorizontal) + { + return size.width - _edgeInsets.left - _edgeInsets.right; + } + return size.height - _edgeInsets.top - _edgeInsets.bottom; +} + +/* The extents that Fill and FillEqually give the arranged views, or nil where + the views do not fit and keep their own extent. No view is made smaller + than its own extent: the ones that fit share what the others leave. */ +- (NSArray *) _filledExtentsForViews: (NSArray *)views inSpace: (CGFloat)space +{ + NSUInteger count = [views count]; + NSMutableArray *extents = [NSMutableArray arrayWithCapacity: count]; + NSMutableIndexSet *keepingOwnExtent = [NSMutableIndexSet indexSet]; + CGFloat content = 0.0; + CGFloat remaining = space; + NSUInteger sharing = count; + BOOL changed = YES; + NSUInteger i; + + for (i = 0; i < count; i++) + { + CGFloat intrinsic + = [self _intrinsicExtentOfView: [views objectAtIndex: i]]; + + if (intrinsic < 0.0) + { + intrinsic = 0.0; + } + content += intrinsic; + [extents addObject: [NSNumber numberWithDouble: intrinsic]]; + } + + if (space < content) + { + return nil; + } + + if (_distribution == NSStackViewDistributionFill) + { + [extents replaceObjectAtIndex: 0 + withObject: [NSNumber numberWithDouble: + [[extents objectAtIndex: 0] doubleValue] + space - content]]; + return extents; + } + + while (changed && sharing > 0) + { + CGFloat share = remaining / (CGFloat)sharing; + + changed = NO; + for (i = 0; i < count; i++) + { + if (![keepingOwnExtent containsIndex: i] + && [[extents objectAtIndex: i] doubleValue] > share) { - NSView *v = [sv objectAtIndex: 0]; - sp = [v frame].size.height; + [keepingOwnExtent addIndex: i]; + remaining -= [[extents objectAtIndex: i] doubleValue]; + sharing--; + changed = YES; } } - - newFrame.size.height += sp; // * 2; // expand height - newFrame.origin.y -= (sp / 2.0); // move the view down. - newHeight = newFrame.size.height; // start at top of view... } - else + + if (sharing > 0) { - if (sp == 0.0) + NSNumber *share = [NSNumber numberWithDouble: + round(remaining / (CGFloat)sharing)]; + + for (i = 0; i < count; i++) { - if (n > 0) + if (![keepingOwnExtent containsIndex: i]) { - NSView *v = [sv objectAtIndex: 0]; - sp = [v frame].size.width; + [extents replaceObjectAtIndex: i withObject: share]; } } + } + return extents; +} - newFrame.size.width += sp; +- (void) _addMainAxisConstraintsForViews: (NSArray *)views +{ + BOOL horizontal + = (_orientation == NSUserInterfaceLayoutOrientationHorizontal); + NSLayoutAttribute head + = horizontal ? NSLayoutAttributeLeading : NSLayoutAttributeTop; + NSLayoutAttribute tail + = horizontal ? NSLayoutAttributeTrailing : NSLayoutAttributeBottom; + NSLayoutAttribute extent + = horizontal ? NSLayoutAttributeWidth : NSLayoutAttributeHeight; + CGFloat headInset = horizontal ? _edgeInsets.left : _edgeInsets.top; + CGFloat tailInset = horizontal ? _edgeInsets.right : _edgeInsets.bottom; + /* The main axis runs towards increasing x but decreasing y. */ + CGFloat direction = horizontal ? 1.0 : -1.0; + NSUInteger count = [views count]; + NSView *firstView = [views objectAtIndex: 0]; + CGFloat available = [self _availableExtent]; + NSArray *filled = nil; + CGFloat content = 0.0; + CGFloat gaps = 0.0; + CGFloat proportion = 0.0; + CGFloat equalGap = 0.0; + CGFloat centreStep = 0.0; + NSUInteger i; + + for (i = 0; i < count; i++) + { + content += [self _intrinsicExtentOfView: [views objectAtIndex: i]]; + if (i > 0) + { + gaps += [self _spacingAfterView: [views objectAtIndex: i - 1]]; + } } - - [self setFrame: newFrame]; - FOR_IN(NSView*,v,sv) + + if (count > 1) + { + equalGap = (available - content) / (CGFloat)(count - 1); + centreStep = (available + - [self _intrinsicExtentOfView: firstView] / 2.0 + - [self _intrinsicExtentOfView: [views objectAtIndex: count - 1]] / 2.0) + / (CGFloat)(count - 1); + } + if (content > 0.0) + { + proportion = (available - gaps) / content; + } + + [self _pin: firstView + attribute: head + relatedBy: NSLayoutRelationEqual + toItem: self + attribute: head + multiplier: 1.0 + constant: direction * headInset]; + + for (i = 1; i < count; i++) + { + NSView *v = [views objectAtIndex: i]; + NSView *previous = [views objectAtIndex: i - 1]; + CGFloat gap = [self _spacingAfterView: previous]; + + if (_distribution == NSStackViewDistributionEqualSpacing) + { + gap = MAX(gap, equalGap); + } + else if (_distribution == NSStackViewDistributionEqualCentering) + { + gap = MAX(gap, centreStep + - ([self _intrinsicExtentOfView: previous] + + [self _intrinsicExtentOfView: v]) / 2.0); + } + [self _pin: v + attribute: head + relatedBy: NSLayoutRelationEqual + toItem: previous + attribute: tail + multiplier: 1.0 + constant: direction * gap]; + } + + /* Extents are pinned to a length rather than related to another view by a + multiplier, which the layout engine rejects. A distribution with no room + leaves every view its own extent and runs past the trailing edge. */ + if (_distribution == NSStackViewDistributionFill + || _distribution == NSStackViewDistributionFillEqually) + { + filled = [self _filledExtentsForViews: views inSpace: available - gaps]; + } + + for (i = 0; i < count; i++) { - NSRect f; - NSString *str = nil; - NSRect sr = NSZeroRect; + NSView *v = [views objectAtIndex: i]; + CGFloat wanted = 0.0; - if ([v respondsToSelector: @selector(title)]) + if (filled != nil) { - str = [(NSButton *)v title]; + wanted = [[filled objectAtIndex: i] doubleValue]; } - - f = [v frame]; - if (f.origin.x < 0.0) + else if (_distribution == NSStackViewDistributionFillProportionally + && proportion > 1.0) { - f.origin.x = 0.0; + wanted = round([self _intrinsicExtentOfView: v] * proportion); } - if (str != nil) + if (wanted > 0.0) { - sr = [str _rectOfString]; + [self _pin: v + attribute: extent + relatedBy: NSLayoutRelationEqual + toItem: nil + attribute: NSLayoutAttributeNotAnAttribute + multiplier: 1.0 + constant: wanted]; } + } +} - // Calculate control position... - if (o == NSUserInterfaceLayoutOrientationVertical) +- (void) _addCrossAxisConstraintsForViews: (NSArray *)views +{ + BOOL horizontal + = (_orientation == NSUserInterfaceLayoutOrientationHorizontal); + NSLayoutAttribute head + = horizontal ? NSLayoutAttributeTop : NSLayoutAttributeLeading; + NSLayoutAttribute tail + = horizontal ? NSLayoutAttributeBottom : NSLayoutAttributeTrailing; + NSLayoutAttribute centre + = horizontal ? NSLayoutAttributeCenterY : NSLayoutAttributeCenterX; + NSLayoutAttribute fill + = horizontal ? NSLayoutAttributeHeight : NSLayoutAttributeWidth; + CGFloat headInset = horizontal ? _edgeInsets.top : _edgeInsets.left; + CGFloat tailInset = horizontal ? _edgeInsets.bottom : _edgeInsets.right; + /* The cross axis runs towards decreasing y but increasing x. */ + CGFloat direction = horizontal ? -1.0 : 1.0; + NSUInteger i; + + for (i = 0; i < [views count]; i++) + { + NSView *v = [views objectAtIndex: i]; + + if (_alignment == centre) + { + [self _pin: v + attribute: centre + relatedBy: NSLayoutRelationEqual + toItem: self + attribute: centre + multiplier: 1.0 + constant: 0.0]; + } + else if (_alignment == tail || _alignment == NSLayoutAttributeRight) { - y = newHeight - ((CGFloat)i * sp) - f.size.height; - f.origin.y = y; + [self _pin: v + attribute: tail + relatedBy: NSLayoutRelationEqual + toItem: self + attribute: tail + multiplier: 1.0 + constant: -direction * tailInset]; } else { - x = (CGFloat)i * sp; - f.origin.x = x; + [self _pin: v + attribute: head + relatedBy: NSLayoutRelationEqual + toItem: self + attribute: head + multiplier: 1.0 + constant: direction * headInset]; + + if (_alignment == fill) + { + [self _pin: v + attribute: tail + relatedBy: NSLayoutRelationEqual + toItem: self + attribute: tail + multiplier: 1.0 + constant: -direction * tailInset]; + } } + } +} + +- (void) updateConstraints +{ + NSArray *views = [self _layoutArrangedSubviews]; - // expand width if control is too short for title... - if (f.size.width < sr.size.width) + if (_internalConstraints == nil) + { + _internalConstraints = [[NSMutableArray alloc] initWithCapacity: 8]; + } + else if ([_internalConstraints count] > 0) + { + [NSLayoutConstraint deactivateConstraints: _internalConstraints]; + [_internalConstraints removeAllObjects]; + } + + if ([views count] > 0) + { + NSUInteger i; + + for (i = 0; i < [views count]; i++) { - f.size.width = sr.size.width + 5.0; //+5 to accomodate border... + [[views objectAtIndex: i] + setTranslatesAutoresizingMaskIntoConstraints: NO]; } - - [v setFrame: f]; - i++; + [self _addMainAxisConstraintsForViews: views]; + [self _addCrossAxisConstraintsForViews: views]; + [NSLayoutConstraint activateConstraints: _internalConstraints]; } - END_FOR_IN(sv); - [self setNeedsDisplay: YES]; + + [super updateConstraints]; } - (void) _refreshView @@ -312,7 +583,7 @@ - (void) _refreshView } else { - [self _layoutViewsWithOrientation: _orientation]; + [self setNeedsUpdateConstraints: YES]; } [self setNeedsDisplay: YES]; } @@ -491,6 +762,11 @@ - (void) dealloc DESTROY(_arrangedSubviews); RELEASE(_detachedViews); RELEASE(_views); + if (_internalConstraints != nil) + { + [NSLayoutConstraint deactivateConstraints: _internalConstraints]; + RELEASE(_internalConstraints); + } RELEASE(_customSpacingMap); RELEASE(_visiblePriorityMap); diff --git a/Tests/gui/NSStackView/arrangedLayout.m b/Tests/gui/NSStackView/arrangedLayout.m new file mode 100644 index 000000000..32cae12b1 --- /dev/null +++ b/Tests/gui/NSStackView/arrangedLayout.m @@ -0,0 +1,304 @@ +/* The arranged subviews of a stack view are placed by layout constraints. + Every frame checked here was measured on a macOS runner with the same three + arranged views, whose intrinsic content sizes are 40x20, 60x30 and 20x10, in + a stack view that is a window's content view. Coordinates are unflipped, so + a vertical stack runs from the top down. */ +#import "Testing.h" +#import +#import +#import +#import +#import +#import +#import +#import + +@interface GSFixedSizeView : NSView +{ + NSSize _intrinsic; +} +- (id) initWithIntrinsicSize: (NSSize)size; +@end + +@implementation GSFixedSizeView +- (id) initWithIntrinsicSize: (NSSize)size +{ + self = [super initWithFrame: NSMakeRect(0, 0, size.width, size.height)]; + if (self != nil) + { + _intrinsic = size; + } + return self; +} +- (NSSize) intrinsicContentSize +{ + return _intrinsic; +} +@end + +static NSStackView * +stackOfWidth3(CGFloat width, NSSize a, NSSize b, NSSize c) +{ + NSWindow *w = AUTORELEASE([[NSWindow alloc] + initWithContentRect: NSMakeRect(0, 0, width, 200) + styleMask: NSBorderlessWindowMask + backing: NSBackingStoreBuffered + defer: NO]); + NSStackView *sv = AUTORELEASE([[NSStackView alloc] + initWithFrame: NSMakeRect(0, 0, width, 200)]); + NSSize sizes[3]; + NSUInteger i; + + sizes[0] = a; + sizes[1] = b; + sizes[2] = c; + + [w setContentView: sv]; + for (i = 0; i < 3; i++) + { + [sv addArrangedSubview: AUTORELEASE([[GSFixedSizeView alloc] + initWithIntrinsicSize: sizes[i]])]; + } + return sv; +} + +static NSStackView * +stackOfWidth(CGFloat width) +{ + return stackOfWidth3(width, NSMakeSize(40, 20), NSMakeSize(60, 30), + NSMakeSize(20, 10)); +} + +static BOOL +frameIs(NSStackView *sv, NSUInteger index, NSRect expected) +{ + return NSEqualRects([[[sv arrangedSubviews] objectAtIndex: index] frame], + expected); +} + +/* Whether this build solves layout constraints at all: pin one view 30 points + past the trailing edge of another and see where it lands. */ +static BOOL +engineSolvesConstraints(void) +{ + NSWindow *w = AUTORELEASE([[NSWindow alloc] + initWithContentRect: NSMakeRect(0, 0, 200, 200) + styleMask: NSBorderlessWindowMask + backing: NSBackingStoreBuffered + defer: NO]); + NSView *host = [w contentView]; + NSView *a = AUTORELEASE([[GSFixedSizeView alloc] + initWithIntrinsicSize: NSMakeSize(40, 20)]); + NSView *b = AUTORELEASE([[GSFixedSizeView alloc] + initWithIntrinsicSize: NSMakeSize(60, 30)]); + + [a setTranslatesAutoresizingMaskIntoConstraints: NO]; + [b setTranslatesAutoresizingMaskIntoConstraints: NO]; + [host addSubview: a]; + [host addSubview: b]; + [NSLayoutConstraint activateConstraints: [NSArray arrayWithObjects: + [NSLayoutConstraint constraintWithItem: a + attribute: NSLayoutAttributeLeading relatedBy: NSLayoutRelationEqual + toItem: host attribute: NSLayoutAttributeLeading + multiplier: 1.0 constant: 0], + [NSLayoutConstraint constraintWithItem: b + attribute: NSLayoutAttributeLeading relatedBy: NSLayoutRelationEqual + toItem: a attribute: NSLayoutAttributeTrailing + multiplier: 1.0 constant: 30], nil]]; + [host layoutSubtreeIfNeeded]; + return [b frame].origin.x == 70; +} + +int +main(int argc, const char **argv) +{ + START_SET("NSStackView arranged layout") + + NSStackView *sv; + BOOL solved = NO; + + NS_DURING + [NSApplication sharedApplication]; + NS_HANDLER + if ([[localException name] isEqualToString: NSInternalInconsistencyException]) + SKIP("It looks like GNUstep backend is not yet installed") + NS_ENDHANDLER + + NS_DURING + { + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv layoutSubtreeIfNeeded]; + solved = engineSolvesConstraints(); + if (solved) + { + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(50, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(120, 95, 20, 10)), + "a horizontal stack spaces the views and centres them"); + PASS(NSEqualRects([sv frame], NSMakeRect(0, 0, 200, 200)), + "laying out the arranged views leaves the stack view frame alone"); + + [sv setSpacing: 0]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(40, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(100, 95, 20, 10)), + "spacing 0 leaves no gap between the views"); + + [sv setSpacing: 10]; + [sv setEdgeInsets: NSEdgeInsetsMake(5, 15, 5, 15)]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(15, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(65, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(135, 95, 20, 10)), + "the leading edge inset moves the views"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFill]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 100, 20)) + && frameIs(sv, 1, NSMakeRect(110, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(180, 95, 20, 10)), + "Fill gives the slack to the first view and reaches the trailing edge"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFillEqually]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 60, 20)) + && frameIs(sv, 1, NSMakeRect(70, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(140, 95, 60, 10)), + "FillEqually gives every view the same width"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionEqualSpacing]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(80, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(180, 95, 20, 10)), + "EqualSpacing keeps the widths and equalises the gaps"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionEqualCentering]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(75, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(180, 95, 20, 10)), + "EqualCentering puts the centres an equal distance apart"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFillProportionally]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 60, 20)) + && frameIs(sv, 1, NSMakeRect(70, 85, 90, 30)) + && frameIs(sv, 2, NSMakeRect(170, 95, 30, 10)), + "FillProportionally scales the widths by one factor"); + + sv = stackOfWidth(100); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFill]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(50, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(120, 95, 20, 10)), + "Fill leaves the extents alone and overflows where there is no room"); + + sv = stackOfWidth(100); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFillEqually]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(50, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(120, 95, 20, 10)), + "FillEqually overflows where there is no room"); + + sv = stackOfWidth3(170, NSMakeSize(10, 20), NSMakeSize(10, 20), + NSMakeSize(100, 30)); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFillEqually]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 25, 20)) + && frameIs(sv, 1, NSMakeRect(35, 90, 25, 20)) + && frameIs(sv, 2, NSMakeRect(70, 85, 100, 30)), + "FillEqually keeps a view that is wider than the equal share"); + + sv = stackOfWidth3(200, NSMakeSize(10, 20), NSMakeSize(10, 20), + NSMakeSize(100, 30)); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionFill]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 70, 20)) + && frameIs(sv, 1, NSMakeRect(80, 90, 10, 20)) + && frameIs(sv, 2, NSMakeRect(100, 85, 100, 30)), + "Fill gives the slack to the first view"); + + sv = stackOfWidth(100); + [sv setOrientation: NSUserInterfaceLayoutOrientationHorizontal]; + [sv setSpacing: 10]; + [sv setDistribution: NSStackViewDistributionEqualSpacing]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 90, 40, 20)) + && frameIs(sv, 1, NSMakeRect(50, 85, 60, 30)) + && frameIs(sv, 2, NSMakeRect(120, 95, 20, 10)), + "EqualSpacing falls back to the spacing where there is no room"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationVertical]; + [sv setSpacing: 10]; + [sv setAlignment: NSLayoutAttributeLeading]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(0, 180, 40, 20)) + && frameIs(sv, 1, NSMakeRect(0, 140, 60, 30)) + && frameIs(sv, 2, NSMakeRect(0, 120, 20, 10)), + "a vertical stack runs from the top down, aligned leading"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationVertical]; + [sv setSpacing: 10]; + [sv setAlignment: NSLayoutAttributeCenterX]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(80, 180, 40, 20)) + && frameIs(sv, 1, NSMakeRect(70, 140, 60, 30)) + && frameIs(sv, 2, NSMakeRect(90, 120, 20, 10)), + "alignment NSLayoutAttributeCenterX centres each view"); + + sv = stackOfWidth(200); + [sv setOrientation: NSUserInterfaceLayoutOrientationVertical]; + [sv setSpacing: 10]; + [sv setAlignment: NSLayoutAttributeTrailing]; + [sv layoutSubtreeIfNeeded]; + PASS(frameIs(sv, 0, NSMakeRect(160, 180, 40, 20)) + && frameIs(sv, 1, NSMakeRect(140, 140, 60, 30)) + && frameIs(sv, 2, NSMakeRect(180, 120, 20, 10)), + "alignment NSLayoutAttributeTrailing puts every view at the far edge"); + } + } + NS_HANDLER + if ([[localException name] isEqualToString: NSInternalInconsistencyException] + || [[localException name] isEqualToString: @"NSWindowServerCommunicationException"]) + SKIP("No display available") + NS_ENDHANDLER + + if (!solved) + { + SKIP("this build solves no layout constraints") + } + + END_SET("NSStackView arranged layout") + return 0; +}