From 8ad433f2c9033e33a49bcb9c63713f9fed770121 Mon Sep 17 00:00:00 2001 From: Todd White Date: Fri, 24 Jul 2026 20:30:02 -0400 Subject: [PATCH] NSGeometry: complete NSIntegralRectWithOptions The function ignored the width and height options, always aligned both the minimum and maximum edge of each axis, did not handle NSAlignRectFlipped and collapsed an empty rectangle to the zero rect. Align each axis from exactly two of its minimum edge, maximum edge and size, raising NSInvalidArgumentException otherwise; round a nearest tie towards positive infinity, or negative infinity for a flipped edge; and align an empty or zero-size rectangle in place rather than discarding it. Behaviour checked against Apple Foundation. --- Source/NSGeometry.m | 131 ++++++++++++++++++++++------- Tests/base/Functions/NSGeometry2.m | 40 +++++++++ 2 files changed, 139 insertions(+), 32 deletions(-) diff --git a/Source/NSGeometry.m b/Source/NSGeometry.m index 1ab86f6e4..8634aa311 100644 --- a/Source/NSGeometry.m +++ b/Source/NSGeometry.m @@ -36,6 +36,7 @@ #import "common.h" #include #import "Foundation/NSGeometry.h" +#import "Foundation/NSException.h" #import "Foundation/NSScanner.h" #import "Foundation/NSNotification.h" #import "GSPrivate.h" @@ -100,47 +101,113 @@ static BOOL GSMacOSXCompatibleGeometry(void) return rect; } -NSRect -NSIntegralRectWithOptions(NSRect aRect, NSAlignmentOptions options) +enum { - NSRect rect; - CGFloat maxX, maxY; + GSAlignUnset = 0, + GSAlignFloor, + GSAlignCeil, + GSAlignNearest +}; + +static CGFloat +gsAlignValue(CGFloat value, int mode, BOOL tieDown) +{ + switch (mode) + { + case GSAlignFloor: + return floor(value); + case GSAlignCeil: + return ceil(value); + case GSAlignNearest: + /* Round to the nearest integer; a tie goes towards positive infinity, + or towards negative infinity for a flipped edge. */ + return tieDown ? ceil(value - 0.5) : floor(value + 0.5); + } + return value; +} - if (NSIsEmptyRect(aRect)) - return NSMakeRect(0, 0, 0, 0); +static BOOL +gsAlignAxis(CGFloat origin, CGFloat length, + int minMode, int maxMode, int sizeMode, BOOL tieDown, + CGFloat *outOrigin, CGFloat *outLength) +{ + int count = (minMode != GSAlignUnset) + + (maxMode != GSAlignUnset) + + (sizeMode != GSAlignUnset); - if (options & NSAlignMinXInward) - rect.origin.x = ceil(NSMinX(aRect)); - else if (options & NSAlignMinXOutward) - rect.origin.x = floor(NSMinX(aRect)); - else - rect.origin.x = round(NSMinX(aRect)); + if (count != 2) + { + return NO; + } - if (options & NSAlignMinYInward) - rect.origin.y = ceil(NSMinY(aRect)); - else if (options & NSAlignMinYOutward) - rect.origin.y = floor(NSMinY(aRect)); - else - rect.origin.y = round(NSMinY(aRect)); + if (minMode != GSAlignUnset && maxMode != GSAlignUnset) + { + CGFloat lo = gsAlignValue(origin, minMode, tieDown); + CGFloat hi = gsAlignValue(origin + length, maxMode, tieDown); - if (options & NSAlignMaxXInward) - maxX = floor(NSMaxX(aRect)); - else if (options & NSAlignMaxXOutward) - maxX = ceil(NSMaxX(aRect)); + *outOrigin = lo; + *outLength = hi - lo; + } + else if (minMode != GSAlignUnset) + { + *outOrigin = gsAlignValue(origin, minMode, tieDown); + *outLength = gsAlignValue(length, sizeMode, NO); + } else - maxX = round(NSMaxX(aRect)); + { + CGFloat hi = gsAlignValue(origin + length, maxMode, tieDown); + CGFloat size = gsAlignValue(length, sizeMode, NO); - if (options & NSAlignMaxYInward) - maxY = floor(NSMaxY(aRect)); - else if (options & NSAlignMaxYOutward) - maxY = ceil(NSMaxY(aRect)); - else - maxY = round(NSMaxY(aRect)); + *outOrigin = hi - size; + *outLength = size; + } + return YES; +} - rect.size.width = maxX - rect.origin.x; - rect.size.height = maxY - rect.origin.y; +NSRect +NSIntegralRectWithOptions(NSRect aRect, NSAlignmentOptions options) +{ + BOOL flipped = (options & NSAlignRectFlipped) ? YES : NO; + int minXMode, maxXMode, widthMode; + int minYMode, maxYMode, heightMode; + CGFloat ox, oy, ow, oh; + + minXMode = (options & NSAlignMinXInward) ? GSAlignCeil + : (options & NSAlignMinXOutward) ? GSAlignFloor + : (options & NSAlignMinXNearest) ? GSAlignNearest + : GSAlignUnset; + maxXMode = (options & NSAlignMaxXInward) ? GSAlignFloor + : (options & NSAlignMaxXOutward) ? GSAlignCeil + : (options & NSAlignMaxXNearest) ? GSAlignNearest + : GSAlignUnset; + widthMode = (options & NSAlignWidthInward) ? GSAlignFloor + : (options & NSAlignWidthOutward) ? GSAlignCeil + : (options & NSAlignWidthNearest) ? GSAlignNearest + : GSAlignUnset; + minYMode = (options & NSAlignMinYInward) ? GSAlignCeil + : (options & NSAlignMinYOutward) ? GSAlignFloor + : (options & NSAlignMinYNearest) ? GSAlignNearest + : GSAlignUnset; + maxYMode = (options & NSAlignMaxYInward) ? GSAlignFloor + : (options & NSAlignMaxYOutward) ? GSAlignCeil + : (options & NSAlignMaxYNearest) ? GSAlignNearest + : GSAlignUnset; + heightMode = (options & NSAlignHeightInward) ? GSAlignFloor + : (options & NSAlignHeightOutward) ? GSAlignCeil + : (options & NSAlignHeightNearest) ? GSAlignNearest + : GSAlignUnset; + + if (!gsAlignAxis(NSMinX(aRect), NSWidth(aRect), + minXMode, maxXMode, widthMode, NO, &ox, &ow) + || !gsAlignAxis(NSMinY(aRect), NSHeight(aRect), + minYMode, maxYMode, heightMode, flipped, &oy, &oh)) + { + [NSException raise: NSInvalidArgumentException + format: @"NSIntegralRectWithOptions: each axis needs exactly" + @" two of its minimum edge, maximum edge and size"]; + } - return rect; + return NSMakeRect(ox, oy, ow, oh); } diff --git a/Tests/base/Functions/NSGeometry2.m b/Tests/base/Functions/NSGeometry2.m index a9e50561f..64ab9fc68 100644 --- a/Tests/base/Functions/NSGeometry2.m +++ b/Tests/base/Functions/NSGeometry2.m @@ -52,6 +52,46 @@ int main(void) "NSIntegralRect of an empty rect is the zero rect"); END_SET("NSIntegralRect") + START_SET("NSIntegralRectWithOptions") + /* Each axis is aligned to whole units by the options; the expected values + were checked against Apple Foundation. */ + NSRect o = R(10.3, 20.7, 5.4, 6.8); + + PASS(NSEqualRects(NSIntegralRectWithOptions(o, NSAlignAllEdgesNearest), + R(10, 21, 6, 7)), "all edges nearest rounds each edge to whole units"); + PASS(NSEqualRects(NSIntegralRectWithOptions(o, NSAlignAllEdgesInward), + R(11, 21, 4, 6)), "all edges inward shrinks to whole units"); + PASS(NSEqualRects(NSIntegralRectWithOptions(o, NSAlignAllEdgesOutward), + R(10, 20, 6, 8)), "all edges outward grows to whole units"); + + PASS(NSEqualRects(NSIntegralRectWithOptions(o, + NSAlignMinXNearest | NSAlignWidthNearest + | NSAlignMinYNearest | NSAlignHeightNearest), R(10, 21, 5, 7)), + "a min edge with a size rounds the origin and size independently"); + PASS(NSEqualRects(NSIntegralRectWithOptions(o, + NSAlignMaxXNearest | NSAlignWidthNearest + | NSAlignMaxYNearest | NSAlignHeightNearest), R(11, 21, 5, 7)), + "a max edge with a size anchors the far edge and rounds the size"); + PASS(NSEqualRects(NSIntegralRectWithOptions(o, + NSAlignAllEdgesNearest | NSAlignRectFlipped), R(10, 21, 6, 6)), + "the flipped flag rounds a tie on the max Y edge downward"); + + PASS(NSEqualRects(NSIntegralRectWithOptions(R(5.3, 6.7, 0, 0), + NSAlignAllEdgesNearest), R(5, 7, 0, 0)), + "a zero-size rect keeps its aligned origin"); + PASS(NSEqualRects(NSIntegralRectWithOptions(R(5.3, 6.7, 0, 0), + NSAlignAllEdgesOutward), R(5, 6, 1, 1)), + "outward grows a zero-size rect to one unit"); + + PASS(NSEqualRects(NSIntegralRectWithOptions(R(-2.5, -3.5, 1, 1), + NSAlignAllEdgesNearest), R(-2, -3, 1, 1)), + "a tie rounds towards positive infinity"); + + PASS_EXCEPTION(({ NSRect x = NSIntegralRectWithOptions(o, NSAlignMinXInward); + (void)x; }), NSInvalidArgumentException, + "an axis without exactly two of the min, max and size raises"); + END_SET("NSIntegralRectWithOptions") + START_SET("NSUnionRect") PASS(NSEqualRects(NSUnionRect(R(0, 0, 4, 4), R(2, 2, 4, 4)), R(0, 0, 6, 6)), "NSUnionRect is the bounding box of two overlapping rects");