diff --git a/ObjectivelyMVC.xcodeproj/project.pbxproj b/ObjectivelyMVC.xcodeproj/project.pbxproj index f2f016c4..580860c4 100644 --- a/ObjectivelyMVC.xcodeproj/project.pbxproj +++ b/ObjectivelyMVC.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ CE423A4D1F5346A7002767E7 /* ShellScript */, ); dependencies = ( + CED40A633046145B0090C57A /* PBXTargetDependency */, CE34C8A51FB2A0210025F231 /* PBXTargetDependency */, CE88199A1F91B0DA000D5AB7 /* PBXTargetDependency */, CE34C8A71FB2A0210025F231 /* PBXTargetDependency */, @@ -35,9 +36,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 10B43D5BFBA4557A7326854D /* Objectively.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE73E9FA2FEEA5F60048BC24 /* Objectively.framework */; }; + 124341A5EE68480EEA539233 /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */; }; 12CB053DBF7CE0752A5AE6E9 /* ScrollBar.c in Sources */ = {isa = PBXBuildFile; fileRef = 7889454C3F84200465B57578 /* ScrollBar.c */; }; + 22FD8C57C4593EFD73A0BA80 /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; 3081E9324C929F44F2424D7C /* ScrollHandle.h in Headers */ = {isa = PBXBuildFile; fileRef = 8F25B4D3F9924D41ADDBF368 /* ScrollHandle.h */; settings = {ATTRIBUTES = (Public, ); }; }; 373CCC1EDB79A83F1F913610 /* ScrollHandle.c in Sources */ = {isa = PBXBuildFile; fileRef = 6B2F1D0FBAAAAB9E9859A08E /* ScrollHandle.c */; }; + 4FC583A880396777FF1EF843 /* ObjectivelyMVC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED157CD1C4BF3AC00FBA2DE /* ObjectivelyMVC.framework */; }; + 872D178C50AF548C35D14652 /* View.c in Sources */ = {isa = PBXBuildFile; fileRef = 58441034DC162E9BE53173F4 /* View.c */; }; B1BB0001000000000000000A /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; B1BB000100000000000000A0 /* SDL3.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B1BB000100000000000000AB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA000100000000000000AB /* Foundation.framework */; }; @@ -205,6 +211,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 142E9647D6EC3FAD6E24CA61 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = CED157CC1C4BF3AC00FBA2DE; + remoteInfo = ObjectivelyMVC; + }; CE12D4631C4C8F5400CD0B13 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; @@ -261,9 +274,25 @@ remoteGlobalIDString = CE88198A1F91B0BE000D5AB7; remoteInfo = "ObjectivelyMVC-Style"; }; + CED40A623046145B0090C57A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8A563153D68C9DE444FBF31C; + remoteInfo = "ObjectivelyMVC-View"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + A3611941E3F959776989AD53 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 7; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B1CC000100000000000000D0 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -320,11 +349,13 @@ /* Begin PBXFileReference section */ 14C7C5885104229637E1E9D5 /* Renderer.frag.glsl */ = {isa = PBXFileReference; lastKnownFileType = text; path = Renderer.frag.glsl; sourceTree = ""; }; + 58441034DC162E9BE53173F4 /* View.c */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.c; path = View.c; sourceTree = ""; }; 64D1E41ED038FCC725227A5B /* ScrollBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ScrollBar.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 6B2F1D0FBAAAAB9E9859A08E /* ScrollHandle.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ScrollHandle.c; sourceTree = ""; }; 7819952C297345768B746A46 /* SlideShowView.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SlideShowView.c; sourceTree = ""; }; 7889454C3F84200465B57578 /* ScrollBar.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ScrollBar.c; sourceTree = ""; }; 7F6735C65F7CAB13D8D96588 /* Renderer.frag.spv.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Renderer.frag.spv.h; sourceTree = ""; }; + 84BBDF9467CD6F80ACF8AADF /* ObjectivelyMVC-View */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "ObjectivelyMVC-View"; sourceTree = BUILT_PRODUCTS_DIR; }; 8D42EC0E254AF0B8789148FA /* Renderer.frag.metal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Renderer.frag.metal.h; sourceTree = ""; }; 8F25B4D3F9924D41ADDBF368 /* ScrollHandle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ScrollHandle.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 965273D7A59E496B86FE11EF /* SlideShowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = SlideShowView.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; @@ -521,6 +552,17 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 552A2017A38847FC4DB96EB2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22FD8C57C4593EFD73A0BA80 /* SDL3.xcframework in Frameworks */, + 10B43D5BFBA4557A7326854D /* Objectively.framework in Frameworks */, + 124341A5EE68480EEA539233 /* ObjectivelyGPU.framework in Frameworks */, + 4FC583A880396777FF1EF843 /* ObjectivelyMVC.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B1CC000100000000000000B0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -739,6 +781,7 @@ CE8819891F91B094000D5AB7 /* Style.c */, CE34C8941FB29F3A0025F231 /* Stylesheet.c */, CE423A2C1F534127002767E7 /* Makefile.am */, + 58441034DC162E9BE53173F4 /* View.c */, ); path = ObjectivelyMVC; sourceTree = ""; @@ -880,6 +923,7 @@ CE55C1C91F944C6D00D5A326 /* ObjectivelyMVC-Selector */, CE34C8A21FB29F700025F231 /* ObjectivelyMVC-Stylesheet */, B1AA000100000000000000ED /* ObjectivelyMVC-Hello-iOS.app */, + 84BBDF9467CD6F80ACF8AADF /* ObjectivelyMVC-View */, ); name = Products; sourceTree = ""; @@ -953,6 +997,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 8A563153D68C9DE444FBF31C /* ObjectivelyMVC-View */ = { + isa = PBXNativeTarget; + buildConfigurationList = 326EB91F357CD75FD3012D86 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-View" */; + buildPhases = ( + 09AB70E63430F8B404005FCA /* Sources */, + 552A2017A38847FC4DB96EB2 /* Frameworks */, + A3611941E3F959776989AD53 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + A1D73378A0C8B95650970925 /* PBXTargetDependency */, + ); + name = "ObjectivelyMVC-View"; + productName = "ObjectivelyMVC-View"; + productReference = 84BBDF9467CD6F80ACF8AADF /* ObjectivelyMVC-View */; + productType = "com.apple.product-type.tool"; + }; B1DD00010000000000000001 /* ObjectivelyMVC-Hello-iOS */ = { isa = PBXNativeTarget; buildConfigurationList = B1EE000100000000000000C0 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-Hello-iOS" */; @@ -1107,6 +1169,7 @@ CE55C1BC1F944C6D00D5A326 /* ObjectivelyMVC-Selector */, CE88198A1F91B0BE000D5AB7 /* ObjectivelyMVC-Style */, CE34C8951FB29F700025F231 /* ObjectivelyMVC-Stylesheet */, + 8A563153D68C9DE444FBF31C /* ObjectivelyMVC-View */, CE423A451F534657002767E7 /* ObjectivelyMVC-Tests */, CE12D4541C4C8E8E00CD0B13 /* ObjectivelyMVC-Hello */, B1DD00010000000000000001 /* ObjectivelyMVC-Hello-iOS */, @@ -1250,6 +1313,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 09AB70E63430F8B404005FCA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 872D178C50AF548C35D14652 /* View.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B1CC000100000000000000A0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1354,6 +1425,12 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + A1D73378A0C8B95650970925 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ObjectivelyMVC; + target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; + targetProxy = 142E9647D6EC3FAD6E24CA61 /* PBXContainerItemProxy */; + }; CE12D4641C4C8F5400CD0B13 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; @@ -1394,9 +1471,29 @@ target = CE88198A1F91B0BE000D5AB7 /* ObjectivelyMVC-Style */; targetProxy = CE8819991F91B0DA000D5AB7 /* PBXContainerItemProxy */; }; + CED40A633046145B0090C57A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8A563153D68C9DE444FBF31C /* ObjectivelyMVC-View */; + targetProxy = CED40A623046145B0090C57A /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 7A49C7A9A3020379E61EC10B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; B1EE000100000000000000A0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1483,6 +1580,21 @@ }; name = Release; }; + B74C1EEAAC88021A8E16E1BA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; CE12D45A1C4C8E8E00CD0B13 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1520,8 +1632,14 @@ CE34C8A01FB29F700025F231 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - LIBRARY_SEARCH_PATHS = "$(HOMEBREW_PREFIX)/lib"; - OTHER_LDFLAGS = "-lcheck"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -1529,8 +1647,14 @@ CE34C8A11FB29F700025F231 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - LIBRARY_SEARCH_PATHS = "$(HOMEBREW_PREFIX)/lib"; - OTHER_LDFLAGS = "-lcheck"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -1566,8 +1690,14 @@ CE55C1C71F944C6D00D5A326 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - LIBRARY_SEARCH_PATHS = "$(HOMEBREW_PREFIX)/lib"; - OTHER_LDFLAGS = "-lcheck"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -1575,8 +1705,14 @@ CE55C1C81F944C6D00D5A326 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - LIBRARY_SEARCH_PATHS = "$(HOMEBREW_PREFIX)/lib"; - OTHER_LDFLAGS = "-lcheck"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -1584,8 +1720,14 @@ CE8819951F91B0BE000D5AB7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - LIBRARY_SEARCH_PATHS = "$(HOMEBREW_PREFIX)/lib"; - OTHER_LDFLAGS = "-lcheck"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -1593,8 +1735,14 @@ CE8819961F91B0BE000D5AB7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - LIBRARY_SEARCH_PATHS = "$(HOMEBREW_PREFIX)/lib"; - OTHER_LDFLAGS = "-lcheck"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(HOMEBREW_PREFIX)/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -1705,6 +1853,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 326EB91F357CD75FD3012D86 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-View" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7A49C7A9A3020379E61EC10B /* Debug */, + B74C1EEAAC88021A8E16E1BA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B1EE000100000000000000C0 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-Hello-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcscheme b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcscheme new file mode 100644 index 00000000..f4abc0db --- /dev/null +++ b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-View.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/ObjectivelyMVC/CollectionView.c b/Sources/ObjectivelyMVC/CollectionView.c index 200fb61c..cc678aed 100644 --- a/Sources/ObjectivelyMVC/CollectionView.c +++ b/Sources/ObjectivelyMVC/CollectionView.c @@ -96,8 +96,7 @@ static void layoutSubviews(View *self) { CollectionItemView *item = (CollectionItemView *) $(items, objectAtIndex, i); - $((View *) item, resize, &this->itemSize); - $((View *) item, layoutIfNeeded); + $((View *) item, layoutWithSize, &this->itemSize); item->view.frame.x = x; item->view.frame.y = y; diff --git a/Sources/ObjectivelyMVC/PageView.c b/Sources/ObjectivelyMVC/PageView.c index 14e1554d..5aa28aaa 100644 --- a/Sources/ObjectivelyMVC/PageView.c +++ b/Sources/ObjectivelyMVC/PageView.c @@ -52,7 +52,7 @@ static void addSubview(View *self, View *subview) { PageView *this = (PageView *) self; - subview->hidden = true; + $(subview, setHidden, true); if (this->currentPage == NULL) { $(this, setCurrentPage, subview); @@ -77,7 +77,7 @@ static void removeSubview(View *self, View *subview) { super(View, self, removeSubview, subview); - subview->hidden = false; + $(subview, setHidden, false); if (subview == this->currentPage) { $(this, setCurrentPage, NULL); @@ -117,9 +117,9 @@ static void setCurrentPage_enumerate(const Array *array, ident obj, ident data) View *subview = obj; if (subview == ((PageView *) data)->currentPage) { - subview->hidden = false; + $(subview, setHidden, false); } else { - subview->hidden = true; + $(subview, setHidden, true); } } diff --git a/Sources/ObjectivelyMVC/Panel.c b/Sources/ObjectivelyMVC/Panel.c index e1553815..10c6be14 100644 --- a/Sources/ObjectivelyMVC/Panel.c +++ b/Sources/ObjectivelyMVC/Panel.c @@ -108,8 +108,9 @@ static void layoutSubviews(View *self) { const SDL_Size size = $(this, contentSize); - $((View *) this->contentView, resize, &size); - $((View *) this->contentView, layoutIfNeeded); + View *contentView = (View *) this->contentView; + + $(contentView, layoutWithSize, &size); super(View, self, layoutSubviews); @@ -118,7 +119,7 @@ static void layoutSubviews(View *self) { resizeHandle->frame.x = self->frame.w - resizeHandle->frame.w; resizeHandle->frame.y = self->frame.h - resizeHandle->frame.h; - resizeHandle->hidden = !this->isResizable; + $(resizeHandle, setHidden, !this->isResizable); } #pragma mark - Control diff --git a/Sources/ObjectivelyMVC/ScrollView.c b/Sources/ObjectivelyMVC/ScrollView.c index 506ac0fc..edfc10e6 100644 --- a/Sources/ObjectivelyMVC/ScrollView.c +++ b/Sources/ObjectivelyMVC/ScrollView.c @@ -86,27 +86,27 @@ static void layoutSubviews(View *self) { ScrollView *this = (ScrollView *) self; - bool showScrollBar; + bool scrollBarHidden; switch (this->scrollBarVisibility) { case ScrollBarShow: - showScrollBar = true; + scrollBarHidden = false; break; case ScrollBarHide: - showScrollBar = false; + scrollBarHidden = true; break; default: { if (this->contentView == NULL) { - showScrollBar = false; + scrollBarHidden = true; } else { const SDL_Size contentSize = $(this->contentView, size); const SDL_Rect bounds = $(self, bounds); - showScrollBar = contentSize.h > bounds.h; + scrollBarHidden = contentSize.h <= bounds.h; } break; } } - ((View *) this->scrollBar)->hidden = !showScrollBar; + $((View *) this->scrollBar, setHidden, scrollBarHidden); super(View, self, layoutSubviews); @@ -114,10 +114,6 @@ static void layoutSubviews(View *self) { this->contentView->frame.x = this->contentOffset.x; this->contentView->frame.y = this->contentOffset.y; } - - if (showScrollBar) { - ((View *) this->scrollBar)->needsLayout = true; - } } #pragma mark - Control diff --git a/Sources/ObjectivelyMVC/Select.c b/Sources/ObjectivelyMVC/Select.c index 099e5b31..a65a5527 100644 --- a/Sources/ObjectivelyMVC/Select.c +++ b/Sources/ObjectivelyMVC/Select.c @@ -71,11 +71,11 @@ static void layoutSubviews(View *self) { Option *option = $(options, objectAtIndex, i); if (option->isSelected) { - option->view.hidden = false; + $((View *) option, setHidden, false); } else if ($(control, isHighlighted)) { - option->view.hidden = false; + $((View *) option, setHidden, false); } else { - option->view.hidden = true; + $((View *) option, setHidden, true); } } diff --git a/Sources/ObjectivelyMVC/StackView.c b/Sources/ObjectivelyMVC/StackView.c index 2a2ba963..8105a23b 100644 --- a/Sources/ObjectivelyMVC/StackView.c +++ b/Sources/ObjectivelyMVC/StackView.c @@ -70,8 +70,6 @@ static View *init(View *self) { */ static void layoutSubviews(View *self) { - super(View, self, layoutSubviews); - Array *subviews = $(self, visibleSubviews); if (subviews->count) { @@ -91,18 +89,22 @@ static void layoutSubviews(View *self) { availableSize -= this->spacing * (subviews->count - 1); + const ViewConstraint unspecified = MakeConstraint(ViewConstraintUnspecified, 0); + + SDL_Size sizes[subviews->count]; + for (size_t i = 0; i < subviews->count; i++) { View *subview = $(subviews, objectAtIndex, i); - const SDL_Size subviewSize = $(subview, size); + sizes[i] = $(subview, sizeThatSatisfies, unspecified, unspecified); switch (this->axis) { case StackViewAxisVertical: - requestedSize += subviewSize.h; + requestedSize += sizes[i].h; break; case StackViewAxisHorizontal: - requestedSize += subviewSize.w; + requestedSize += sizes[i].w; break; } } @@ -124,7 +126,7 @@ static void layoutSubviews(View *self) { break; } - SDL_Size subviewSize = $(subview, size); + SDL_Size subviewSize = sizes[i]; switch (this->axis) { case StackViewAxisVertical: @@ -139,6 +141,8 @@ static void layoutSubviews(View *self) { break; } + // Distribute along primary axis + switch (this->distribution) { case StackViewDistributionDefault: break; @@ -157,23 +161,46 @@ static void layoutSubviews(View *self) { case StackViewDistributionFillEqually: switch (this->axis) { case StackViewAxisVertical: - subviewSize.h = availableSize / (float) subviews->count; + subviewSize.h = max(availableSize / (float) subviews->count, sizes[i].h); break; case StackViewAxisHorizontal: - subviewSize.w = availableSize / (float) subviews->count; + subviewSize.w = max(availableSize / (float) subviews->count, sizes[i].w); break; } break; } - $(subview, resize, &subviewSize); - $(subview, layoutIfNeeded); + $(subview, layoutWithSize, &subviewSize); + // Align along secondary axis + switch (this->axis) { case StackViewAxisVertical: + switch (subview->alignment & ViewAlignmentMaskHorizontal) { + case ViewAlignmentLeft: + subview->frame.x = 0; + break; + case ViewAlignmentCenter: + subview->frame.x = (bounds.w - subview->frame.w) * 0.5f; + break; + case ViewAlignmentRight: + subview->frame.x = bounds.w - subview->frame.w; + break; + } pos += subviewSize.h; break; case StackViewAxisHorizontal: + switch (subview->alignment & ViewAlignmentMaskVertical) { + case ViewAlignmentMaskTop: + subview->frame.y = 0; + break; + case ViewAlignmentMaskMiddle: + subview->frame.y = (bounds.h - subview->frame.h) * 0.5f; + break; + case ViewAlignmentMaskBottom: + subview->frame.y = bounds.h - subview->frame.h; + break; + } pos += subviewSize.w; break; } @@ -204,18 +231,14 @@ static SDL_Size sizeThatFits(const View *self) { } Array *subviews = $(self, visibleSubviews); + + const ViewConstraint unspecified = MakeConstraint(ViewConstraintUnspecified, 0); + for (size_t i = 0; i < subviews->count; i++) { - const View *subview = $(subviews, objectAtIndex, i); + View *subview = $(subviews, objectAtIndex, i); - SDL_Size subviewSize; - if (subview->autoresizingMask & ViewAutoresizingContain) { - subviewSize = $(subview, sizeThatContains); - } else if (subview->autoresizingMask & ViewAutoresizingFit) { - subviewSize = $(subview, sizeThatFits); - } else { - subviewSize = $(subview, size); - } + const SDL_Size subviewSize = $(subview, sizeThatSatisfies, unspecified, unspecified); switch (this->axis) { case StackViewAxisVertical: diff --git a/Sources/ObjectivelyMVC/TableView.c b/Sources/ObjectivelyMVC/TableView.c index ca89b440..a7a997a9 100644 --- a/Sources/ObjectivelyMVC/TableView.c +++ b/Sources/ObjectivelyMVC/TableView.c @@ -121,6 +121,10 @@ static void layoutSubviews(View *self) { */ static SDL_Size sizeThatFits(const View *self) { + if ($(self, isContainer) == false) { + return $(self, size); + } + const TableView *this = (TableView *) self; return $(this, naturalSize); diff --git a/Sources/ObjectivelyMVC/Text.c b/Sources/ObjectivelyMVC/Text.c index 07f28f9d..59b80d2f 100644 --- a/Sources/ObjectivelyMVC/Text.c +++ b/Sources/ObjectivelyMVC/Text.c @@ -353,8 +353,7 @@ static void render(View *self, Renderer *renderer) { const float scale = SDL_GetWindowPixelDensity(self->window); if (this->font->scale != scale) { - this->font->scale = scale; - $(this->font, renderDeviceDidReset); + $(self, renderDeviceDidReset); this->texture = release(this->texture); this->textureSize = MakeSize(0, 0); } @@ -449,7 +448,10 @@ static void renderDeviceDidReset(View *self) { Text *this = (Text *) self; this->font->scale = SDL_GetWindowPixelDensity(self->window); + $(this->font, renderDeviceDidReset); + + $(self, sizeToFit); super(View, self, renderDeviceDidReset); } diff --git a/Sources/ObjectivelyMVC/View.c b/Sources/ObjectivelyMVC/View.c index 13584428..5fe8795b 100644 --- a/Sources/ObjectivelyMVC/View.c +++ b/Sources/ObjectivelyMVC/View.c @@ -276,6 +276,15 @@ static void applyStyle(View *self, const Style *style) { ); $(self, bind, inlets, style->attributes); + + // Capture the authored width/height, if either was actually present in this Style -- distinct + // from self->frame.w/h, which layout goes on to freely resize. + if ($(style->attributes, objectForKeyPath, "width")) { + self->styledSize.w = self->frame.w; + } + if ($(style->attributes, objectForKeyPath, "height")) { + self->styledSize.h = self->frame.h; + } } /** @@ -1107,11 +1116,15 @@ static void layoutIfNeeded(View *self) { if (self->needsLayout) { - $(self, clearWarnings, WarningTypeLayout); + // No ancestor is actively arranging self right now, so there's no fresh constraint to + // consult; treat an existing frame as authoritative per-axis (Equal, not Max -- see + // ViewConstraintMax), leaving an axis with no frame yet Unspecified. + const ViewConstraint w = self->frame.w ? + MakeConstraint(ViewConstraintEqual, self->frame.w) : MakeConstraint(ViewConstraintUnspecified, 0); + const ViewConstraint h = self->frame.h ? + MakeConstraint(ViewConstraintEqual, self->frame.h) : MakeConstraint(ViewConstraintUnspecified, 0); - $(self, layoutSubviews); - - self->needsLayout = false; + $(self, layoutWithConstraint, w, h); } } @@ -1121,12 +1134,6 @@ static void layoutIfNeeded(View *self) { */ static void layoutSubviews(View *self) { - if (self->autoresizingMask & ViewAutoresizingContain) { - $(self, sizeToContain); - } else if (self->autoresizingMask & ViewAutoresizingFit) { - $(self, sizeToFit); - } - const SDL_Rect bounds = $(self, bounds); const Array *subviews = (Array *) self->subviews; @@ -1134,18 +1141,13 @@ static void layoutSubviews(View *self) { View *subview = subviews->elements[i]; - SDL_Size subviewSize = $(subview, size); - - if (subview->autoresizingMask & ViewAutoresizingWidth) { - subviewSize.w = bounds.w; - } + const ViewConstraint w = (subview->autoresizingMask & ViewAutoresizingWidth) ? + MakeConstraint(ViewConstraintEqual, bounds.w) : MakeConstraint(ViewConstraintUnspecified, 0); - if (subview->autoresizingMask & ViewAutoresizingHeight) { - subviewSize.h = bounds.h; - } + const ViewConstraint h = (subview->autoresizingMask & ViewAutoresizingHeight) ? + MakeConstraint(ViewConstraintEqual, bounds.h) : MakeConstraint(ViewConstraintUnspecified, 0); - $(subview, resize, &subviewSize); - $(subview, layoutIfNeeded); + $(subview, layoutWithConstraint, w, h); switch (subview->alignment & ViewAlignmentMaskHorizontal) { case ViewAlignmentLeft: @@ -1173,6 +1175,32 @@ static void layoutSubviews(View *self) { } } +/** + * @fn void View::layoutWithConstraint(View *self, ViewConstraint width, ViewConstraint height) + * @memberof View + */ +static void layoutWithConstraint(View *self, ViewConstraint width, ViewConstraint height) { + + $(self, sizeToSatisfy, width, height); + $(self, clearWarnings, WarningTypeLayout); + $(self, layoutSubviews); + + self->needsLayout = false; +} + +/** + * @fn void View::layoutWithSize(View *self, const SDL_Size *size) + * @memberof View + */ +static void layoutWithSize(View *self, const SDL_Size *size) { + + $(self, resize, size); + $(self, clearWarnings, WarningTypeLayout); + $(self, layoutSubviews); + + self->needsLayout = false; +} + /** * @fn bool View::matchesSelector(const View *self, const SimpleSelector *simpleSelector) * @memberof View @@ -1633,6 +1661,22 @@ static View *selectFirst(View *self, const char *rule) { return first; } +/** + * @fn void View::setHidden(View *self, bool hidden) + * @memberof View + */ +static void setHidden(View *self, bool hidden) { + + if (self->hidden != hidden) { + + self->hidden = hidden; + + if (self->superview && $(self->superview, isContainer)) { + self->superview->needsLayout = true; + } + } +} + /** * @fn SDL_Size View::size(const View *self) * @memberof View @@ -1677,30 +1721,16 @@ static SDL_Size sizeThatFits(const View *self) { SDL_Size size = $(self, size); - if (self->autoresizingMask & ViewAutoresizingWidth) { - size.w = 0; - } - - if (self->autoresizingMask & ViewAutoresizingHeight) { - size.h = 0; - } - if ($(self, isContainer)) { size = MakeSize(0, 0); Array *subviews = $(self, visibleSubviews); for (size_t i = 0; i < subviews->count; i++) { - const View *subview = subviews->elements[i]; + View *subview = subviews->elements[i]; - SDL_Size subviewSize; - if (subview->autoresizingMask & ViewAutoresizingContain) { - subviewSize = $(subview, sizeThatContains); - } else if (subview->autoresizingMask & ViewAutoresizingFit) { - subviewSize = $(subview, sizeThatFits); - } else { - subviewSize = $(subview, size); - } + const ViewConstraint unspecified = MakeConstraint(ViewConstraintUnspecified, 0); + const SDL_Size subviewSize = $(subview, sizeThatSatisfies, unspecified, unspecified); SDL_Point subviewOrigin = MakePoint(0, 0); switch (subview->alignment) { @@ -1719,6 +1749,12 @@ static SDL_Size sizeThatFits(const View *self) { size.h += self->padding.top + self->padding.bottom; release(subviews); + + // A styled width/height is authored intent for this View's own frame; floor the + // children-derived sum against it so it survives summing subviews with no size of their + // own to contribute (e.g. Slider, whose bar and handle have none independently). + size.w = max(size.w, self->styledSize.w); + size.h = max(size.h, self->styledSize.h); } size.w = clamp(size.w, self->minSize.w, self->maxSize.w); @@ -1727,6 +1763,43 @@ static SDL_Size sizeThatFits(const View *self) { return size; } +/** + * @brief Resolves an axis's `sizeThatFits` value against an offered ViewConstraint, if the + * View has opted into that axis's autoresizing bit; otherwise returns the value unmodified. + */ +static int resolveViewConstraint(int hasAutoresizingBit, ViewConstraint constraint, int intrinsic) { + + if (!hasAutoresizingBit) { + return intrinsic; + } + + switch (constraint.mode) { + case ViewConstraintEqual: + return constraint.value; + case ViewConstraintMax: + return min(intrinsic, constraint.value); + default: + return intrinsic; + } +} + +/** + * @fn SDL_Size View::sizeThatSatisfies(View *self, ViewConstraint width, ViewConstraint height) + * @memberof View + */ +static SDL_Size sizeThatSatisfies(View *self, ViewConstraint width, ViewConstraint height) { + + SDL_Size size = $(self, sizeThatFits); + + size.w = resolveViewConstraint(self->autoresizingMask & ViewAutoresizingWidth, width, size.w); + size.h = resolveViewConstraint(self->autoresizingMask & ViewAutoresizingHeight, height, size.h); + + size.w = clamp(size.w, self->minSize.w, self->maxSize.w); + size.h = clamp(size.h, self->minSize.h, self->maxSize.h); + + return size; +} + /** * @fn void View::sizeToContain(View *self) * @memberof View @@ -1760,6 +1833,17 @@ static void sizeToFit(View *self) { $(self, resize, &size); } +/** + * @fn void View::sizeToSatisfy(View *self, ViewConstraint width, ViewConstraint height) + * @memberof View + */ +static void sizeToSatisfy(View *self, ViewConstraint width, ViewConstraint height) { + + const SDL_Size size = $(self, sizeThatSatisfies, width, height); + + $(self, resize, &size); +} + /** * @fn View::subviewWithIdentifier(const View *self, const char *identifier) * @memberof View @@ -2003,6 +2087,8 @@ static void initialize(Class *clazz) { ((ViewInterface *) clazz->interface)->isVisible = isVisible; ((ViewInterface *) clazz->interface)->layoutIfNeeded = layoutIfNeeded; ((ViewInterface *) clazz->interface)->layoutSubviews = layoutSubviews; + ((ViewInterface *) clazz->interface)->layoutWithConstraint = layoutWithConstraint; + ((ViewInterface *) clazz->interface)->layoutWithSize = layoutWithSize; ((ViewInterface *) clazz->interface)->matchesSelector = matchesSelector; ((ViewInterface *) clazz->interface)->moveToWindow = moveToWindow; ((ViewInterface *) clazz->interface)->path = path; @@ -2023,13 +2109,16 @@ static void initialize(Class *clazz) { ((ViewInterface *) clazz->interface)->respondToEvent = respondToEvent; ((ViewInterface *) clazz->interface)->select = _select; ((ViewInterface *) clazz->interface)->selectFirst = selectFirst; + ((ViewInterface *) clazz->interface)->setHidden = setHidden; ((ViewInterface *) clazz->interface)->size = size; ((ViewInterface *) clazz->interface)->sizeThatContains = sizeThatContains; ((ViewInterface *) clazz->interface)->sizeThatFills = sizeThatFills; ((ViewInterface *) clazz->interface)->sizeThatFits = sizeThatFits; + ((ViewInterface *) clazz->interface)->sizeThatSatisfies = sizeThatSatisfies; ((ViewInterface *) clazz->interface)->sizeToContain = sizeToContain; ((ViewInterface *) clazz->interface)->sizeToFill = sizeToFill; ((ViewInterface *) clazz->interface)->sizeToFit = sizeToFit; + ((ViewInterface *) clazz->interface)->sizeToSatisfy = sizeToSatisfy; ((ViewInterface *) clazz->interface)->subviewWithIdentifier = subviewWithIdentifier; ((ViewInterface *) clazz->interface)->updateBindings = updateBindings; ((ViewInterface *) clazz->interface)->viewport = viewport; diff --git a/Sources/ObjectivelyMVC/View.h b/Sources/ObjectivelyMVC/View.h index 666c167a..2fd90596 100644 --- a/Sources/ObjectivelyMVC/View.h +++ b/Sources/ObjectivelyMVC/View.h @@ -94,6 +94,44 @@ typedef enum { OBJECTIVELYMVC_EXPORT const EnumName ViewAutoresizingNames[]; +/** + * @brief A constraint offered by an ancestor View to a descendant during View::sizeThatSatisfies. + */ +typedef struct { + enum { + /** + * @brief The offered value is final; the View must resolve to exactly this size. + */ + ViewConstraintEqual, + + /** + * @brief The offered value is an upper bound; the View may resolve to any size up to it. + * @remarks Do not use this for a View's own already-resolved frame (e.g. re-deriving a + * constraint to re-layout a View standalone): a `ViewAutoresizingWidth`/`Height` axis whose + * sizeThatFits value is smaller than that frame would shrink to it, discarding a size a + * parent previously forced with `ViewConstraintEqual`. Use `ViewConstraintEqual` instead to + * treat an existing frame as authoritative. + */ + ViewConstraintMax, + + /** + * @brief No bound is offered; the ancestor is itself unresolved for this axis. + */ + ViewConstraintUnspecified + } mode; + + /** + * @brief The constraint value; a size in pixels. + */ + int value; +} ViewConstraint; + +/** + * @return A ViewConstraint with the given mode and value. + */ +#define MakeConstraint(mode, value) \ + (ViewConstraint) { (mode), (value) } + /** * @brief Spacing applied to the inside of a View's frame. */ @@ -192,6 +230,7 @@ struct View { /** * @brief If `true`, this View is not drawn. + * @see View::setHidden(View *, bool) */ bool hidden; @@ -240,6 +279,17 @@ struct View { */ Style *style; + /** + * @brief The `width`/`height` this View was most recently given via styling, per axis. + * @remarks Written only when a `width` or `height` Style attribute is actually present -- not + * on every View::applyStyle call -- so it reflects the last authored value for an axis, or + * `0` if that axis has never been styled. View::sizeThatFits's container branch floors its + * children-derived sum against this, so an authored size survives being a Contain view with no + * content of its own to size from (e.g. Slider, whose `bar` and `handle` have none + * independently). + */ + SDL_Size styledSize; + /** * @brief An optional Stylesheet. * @remarks If set, this Stylesheet is added to or removed from the current Theme when this @@ -765,20 +815,56 @@ struct ViewInterface { * @fn void View::layoutIfNeeded(View *self) * @brief Recursively updates the layout of this View and its subviews. * @param self The View. + * @remarks Drives the two-phase layout pass for a dirty View: View::sizeThatSatisfies resolves + * this View's own size (bottom-up, from its subviews), if it is a container, before + * View::layoutSubviews arranges its subviews (top-down) using that now-final frame. This + * ordering guarantees every View's size is settled before any View's position is committed. * @memberof View */ void (*layoutIfNeeded)(View *self); /** * @fn View::layoutSubviews(View *self) - * @brief Performs layout for this View's immediate subviews. + * @brief Arranges this View's immediate subviews within its (already final) frame. * @param self The View. - * @remarks Subclasses may override this method to perform their own layout operations. This - * method is called recursively by View::layoutIfNeeded. + * @remarks Subclasses may override this method to perform their own arrangement of subviews. + * This method is called by View::layoutIfNeeded only after a View::sizeThatSatisfies pass has + * already resolved `self->frame`; it must not perform any sizing of its own, only positioning + * and committing of subview frames. + * @remarks The default implementation resolves each subview's size via View::layoutWithConstraint, + * offering `ViewConstraintEqual` for a `ViewAutoresizingWidth`/`Height` subview (since this + * View's bounds are already final) or `ViewConstraintUnspecified` otherwise (so the subview + * sizes itself from its own content). * @memberof View */ void (*layoutSubviews)(View *self); + /** + * @fn void View::layoutWithConstraint(View *self, ViewConstraint width, ViewConstraint height) + * @brief Resolves this View's size against the given ViewConstraints, then arranges it. + * @param self The View. + * @param width The ViewConstraint offered for this View's width. + * @param height The ViewConstraint offered for this View's height. + * @remarks This is the shared tail of View::layoutIfNeeded: resolve self's size via + * View::sizeToSatisfy, then View::layoutSubviews, then clear `needsLayout`. + * @see View::layoutWithSize(View *, const SDL_Size *) + * @memberof View + */ + void (*layoutWithConstraint)(View *self, ViewConstraint width, ViewConstraint height); + + /** + * @fn void View::layoutWithSize(View *self, const SDL_Size *size) + * @brief Resizes this View to the given size, then arranges it. + * @param self The View. + * @param size The size to resize this View to. + * @remarks Like View::layoutWithConstraint, but for a caller that has already computed this + * View's exact final size and must apply it verbatim -- e.g. a StackView applying its + * distribution math to a subview. + * @see View::layoutWithConstraint(View *, ViewConstraint, ViewConstraint) + * @memberof View + */ + void (*layoutWithSize)(View *self, const SDL_Size *size); + /** * @fn bool View::matchesSelector(const View *self, const SimpleSelector *simpleSelector) * @param self The View. @@ -957,6 +1043,18 @@ struct ViewInterface { */ View *(*selectFirst)(View *self, const char *rule); + /** + * @fn void View::setHidden(View *self, bool hidden) + * @brief Sets this View's hidden state. + * @param self The View. + * @param hidden The hidden state. + * @remarks Hiding or unhiding a View changes its superview's View::visibleSubviews, so this + * marks the superview `needsLayout` if it is a container; writing `self->hidden` directly does + * not. + * @memberof View + */ + void (*setHidden)(View *self, bool hidden); + /** * @fn SDL_Size View::size(const View *self) * @param self The View. @@ -978,8 +1076,10 @@ struct ViewInterface { * @fn SDL_Size View::sizeThatFits(const View *self) * @param self The View. * @return An SDL_Size that fits this View's subviews. - * @remarks The default implementation of this method returns the View's current size, or an - * appropriate value based on this View's `autoresizingMask`. + * @remarks The default implementation returns this View's current size if it is not a + * container. If it is a container, it instead returns the smallest size that fits all of its + * subviews, each measured via View::sizeThatSatisfies with `ViewConstraintUnspecified` for + * both axes. Subclasses with their own sizing logic (e.g. Text, TableView) should override this method. * @memberof View */ SDL_Size (*sizeThatFits)(const View *self); @@ -992,6 +1092,19 @@ struct ViewInterface { */ SDL_Size (*sizeThatFills)(const View *self); + /** + * @fn SDL_Size View::sizeThatSatisfies(View *self, ViewConstraint width, ViewConstraint height) + * @brief Resolves the size this View wants to be, within the offered ViewConstraints. + * @param self The View. + * @param width The ViewConstraint offered for this View's width. + * @param height The ViewConstraint offered for this View's height. + * @return The resolved size. + * @remarks Computes this View's sizeThatFits and resolves it against the offered ViewConstraints + * for any axis on which this View has `ViewAutoresizingWidth` or `ViewAutoresizingHeight` set. + * @memberof View + */ + SDL_Size (*sizeThatSatisfies)(View *self, ViewConstraint width, ViewConstraint height); + /** * @fn void View::sizeToContain(View *self) * @brief Resizes this View to contain its subviews. @@ -1019,6 +1132,17 @@ struct ViewInterface { */ void (*sizeToFit)(View *self); + /** + * @fn void View::sizeToSatisfy(View *self, ViewConstraint width, ViewConstraint height) + * @brief Resizes this View to satisfy the given ViewConstraints. + * @param self The View. + * @param width The ViewConstraint offered for this View's width. + * @param height The ViewConstraint offered for this View's height. + * @see View::sizeThatSatisfies(View *, ViewConstraint, ViewConstraint) + * @memberof View + */ + void (*sizeToSatisfy)(View *self, ViewConstraint width, ViewConstraint height); + /** * @fn View *View::subviewWithIdentifier(const View *self, const char *identifier) * @param self The View. diff --git a/Tests/ObjectivelyMVC/.gitignore b/Tests/ObjectivelyMVC/.gitignore index 9111c0a9..f584bd1e 100644 --- a/Tests/ObjectivelyMVC/.gitignore +++ b/Tests/ObjectivelyMVC/.gitignore @@ -4,3 +4,4 @@ Constraint Selector Style Stylesheet +View diff --git a/Tests/ObjectivelyMVC/Makefile.am b/Tests/ObjectivelyMVC/Makefile.am index e5fa33f9..32897b09 100644 --- a/Tests/ObjectivelyMVC/Makefile.am +++ b/Tests/ObjectivelyMVC/Makefile.am @@ -8,7 +8,8 @@ DEFAULT_INCLUDES = \ TESTS = \ Selector \ Style \ - Stylesheet + Stylesheet \ + View CFLAGS += \ -I$(top_srcdir)/Sources \ diff --git a/Tests/ObjectivelyMVC/View.c b/Tests/ObjectivelyMVC/View.c new file mode 100644 index 00000000..b39b0425 --- /dev/null +++ b/Tests/ObjectivelyMVC/View.c @@ -0,0 +1,316 @@ +/* + * ObjectivelyMVC: Object oriented MVC framework for SDL3 and C. + * Copyright (C) 2014 Jay Dolan + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include + +#include "ObjectivelyMVC.h" + +/** + * @brief Allocates a leaf View with a fixed, locked size (minSize == maxSize == size). + */ +static View *fixedView(int w, int h) { + + View *view = $(alloc(View), initWithFrame, NULL); + + const SDL_Size size = MakeSize(w, h); + + view->minSize = size; + view->maxSize = size; + + view->frame.w = w; + view->frame.h = h; + + return view; +} + +START_TEST(fixedSizeLeavesInStackView) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *a = fixedView(40, 20); + View *b = fixedView(60, 30); + + $((View *) stackView, addSubview, a); + $((View *) stackView, addSubview, b); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(60, stackView->view.frame.w); + ck_assert_int_eq(50, stackView->view.frame.h); + + ck_assert_int_eq(0, a->frame.y); + ck_assert_int_eq(20, b->frame.y); + + release(stackView); + +} END_TEST + +START_TEST(nestedContainStackViews) { + + StackView *outer = $(alloc(StackView), initWithFrame, NULL); + outer->axis = StackViewAxisVertical; + + StackView *inner = $(alloc(StackView), initWithFrame, NULL); + inner->axis = StackViewAxisHorizontal; + + View *innerA = fixedView(10, 15); + View *innerB = fixedView(20, 25); + + $((View *) inner, addSubview, innerA); + $((View *) inner, addSubview, innerB); + + View *sibling = fixedView(50, 5); + + $((View *) outer, addSubview, (View *) inner); + $((View *) outer, addSubview, sibling); + + $((View *) outer, layoutIfNeeded); + + ck_assert_int_eq(30, inner->view.frame.w); + ck_assert_int_eq(25, inner->view.frame.h); + + ck_assert_int_eq(50, outer->view.frame.w); + ck_assert_int_eq(30, outer->view.frame.h); + + ck_assert_int_eq(0, inner->view.frame.y); + ck_assert_int_eq(25, sibling->frame.y); + + release(outer); + +} END_TEST + +START_TEST(containStackViewWithFillChild) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + StackView *fillChild = $(alloc(StackView), initWithFrame, NULL); + fillChild->view.autoresizingMask |= ViewAutoresizingWidth; + + View *grandchild = fixedView(35, 10); + $((View *) fillChild, addSubview, grandchild); + + $((View *) stackView, addSubview, (View *) fillChild); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(35, stackView->view.frame.w); + ck_assert_int_eq(35, fillChild->view.frame.w); + + const SDL_Rect frame = fillChild->view.frame; + + stackView->view.needsLayout = true; + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(frame.w, fillChild->view.frame.w); + ck_assert_int_eq(frame.h, fillChild->view.frame.h); + ck_assert_int_eq(35, stackView->view.frame.w); + + release(stackView); + +} END_TEST + +START_TEST(containStackViewWithFillAndFixedSiblings) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *fixedSibling = fixedView(80, 20); + + View *fillSibling = fixedView(30, 10); + fillSibling->autoresizingMask |= ViewAutoresizingWidth; + fillSibling->maxSize.w = INT32_MAX; + + $((View *) stackView, addSubview, fixedSibling); + $((View *) stackView, addSubview, fillSibling); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(80, stackView->view.frame.w); + ck_assert_int_eq(80, fillSibling->frame.w); + + release(stackView); + +} END_TEST + +START_TEST(stackViewMeasuresFreshNotStale) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *child = fixedView(10, 10); + $((View *) stackView, addSubview, child); + + const SDL_Size grown = MakeSize(90, 40); + child->minSize = grown; + child->maxSize = grown; + child->frame.w = grown.w; + child->frame.h = grown.h; + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(90, stackView->view.frame.w); + ck_assert_int_eq(40, stackView->view.frame.h); + + release(stackView); + +} END_TEST + +START_TEST(standaloneRelayoutDoesNotShrinkFillChild) { + + // Mirrors a selected TableRowView: a Contain|Width StackView (row) whose own intrinsic + // content is much narrower than the width its Contain parent (table) already forced it to. + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *wideSibling = fixedView(200, 10); + $((View *) stackView, addSubview, wideSibling); + + StackView *row = $(alloc(StackView), initWithFrame, NULL); + row->view.autoresizingMask |= ViewAutoresizingWidth; + + View *cell = fixedView(50, 10); + $((View *) row, addSubview, cell); + + $((View *) stackView, addSubview, (View *) row); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(200, stackView->view.frame.w); + ck_assert_int_eq(200, row->view.frame.w); + + // Simulate a style rebind on the row alone (e.g. a `:selected` pseudo-class match), which + // marks only the row -- not its superview -- needsLayout, per View::_bind's contract. + row->view.needsLayout = true; + $((View *) row, layoutIfNeeded); + + ck_assert_int_eq(200, row->view.frame.w); + + release(stackView); + +} END_TEST + +START_TEST(fillOnlyChildFillsContainBounds) { + + // Mirrors Slider's `bar`: a plain `fill` (Width|Height) leaf, not itself Contain/Fit, whose + // parent is a Contain container with no other content to size from (a bare `fill` leaf has no + // independent intrinsic size of its own, so the parent's floor comes entirely from minSize -- + // exactly how Slider's own width is floored via CSS `min-width`). + + View *outer = $(alloc(View), initWithFrame, NULL); + outer->autoresizingMask = ViewAutoresizingContain; + outer->minSize = MakeSize(100, 50); + + View *child = $(alloc(View), initWithFrame, NULL); + child->autoresizingMask = ViewAutoresizingFill; + + $(outer, addSubview, child); + + $(outer, layoutIfNeeded); + + ck_assert_int_eq(100, outer->frame.w); + ck_assert_int_eq(50, outer->frame.h); + ck_assert_int_eq(100, child->frame.w); + ck_assert_int_eq(50, child->frame.h); + + release(outer); + +} END_TEST + +START_TEST(styledSizeFloorsContainBoundsWithNoMinSize) { + + // Mirrors Slider itself: a styled `width` is the only thing giving a Contain view with no + // sized content of its own (a bare `fill` leaf, contributing nothing) a real width -- with no + // minSize set at all, unlike fillOnlyChildFillsContainBounds above. + + View *outer = $(alloc(View), initWithFrame, NULL); + outer->autoresizingMask = ViewAutoresizingContain; + + Style *style = $(alloc(Style), initWithAttributes, NULL); + $(style, addIntegerAttribute, "width", 140); + $(style, addIntegerAttribute, "height", 24); + + $(outer, applyStyle, style); + release(style); + + View *child = $(alloc(View), initWithFrame, NULL); + child->autoresizingMask = ViewAutoresizingFill; + + $(outer, addSubview, child); + + $(outer, layoutIfNeeded); + + ck_assert_int_eq(140, outer->frame.w); + ck_assert_int_eq(24, outer->frame.h); + ck_assert_int_eq(140, child->frame.w); + ck_assert_int_eq(24, child->frame.h); + + release(outer); + +} END_TEST + +START_TEST(setHiddenMarksSuperviewNeedsLayout) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *a = fixedView(40, 20); + View *b = fixedView(60, 30); + + $((View *) stackView, addSubview, a); + $((View *) stackView, addSubview, b); + + $((View *) stackView, layoutIfNeeded); + + $(a, setHidden, true); + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(60, stackView->view.frame.w); + ck_assert_int_eq(30, stackView->view.frame.h); + ck_assert_int_eq(0, b->frame.y); + + release(stackView); + +} END_TEST + +int main(int argc, char **argv) { + + TCase *tcase = tcase_create("View"); + tcase_add_test(tcase, fixedSizeLeavesInStackView); + tcase_add_test(tcase, nestedContainStackViews); + tcase_add_test(tcase, containStackViewWithFillChild); + tcase_add_test(tcase, containStackViewWithFillAndFixedSiblings); + tcase_add_test(tcase, stackViewMeasuresFreshNotStale); + tcase_add_test(tcase, standaloneRelayoutDoesNotShrinkFillChild); + tcase_add_test(tcase, fillOnlyChildFillsContainBounds); + tcase_add_test(tcase, styledSizeFloorsContainBoundsWithNoMinSize); + tcase_add_test(tcase, setHiddenMarksSuperviewNeedsLayout); + + Suite *suite = suite_create("View"); + suite_add_tcase(suite, tcase); + + SRunner *runner = srunner_create(suite); + + srunner_run_all(runner, CK_VERBOSE); + int failed = srunner_ntests_failed(runner); + + srunner_free(runner); + + return failed; +}