From 01f7f9b601664c04ae6c150f2b485bff507cebf9 Mon Sep 17 00:00:00 2001 From: David Richardson Date: Fri, 14 Aug 2026 20:09:17 -0600 Subject: [PATCH] Fix CPStackView compile errors and add minimal correctness fixes CPStackView.j emited warnings during builds and tests which constituted either real errors or indeterminate state. These included four types that did not exist: CPUserInterfaceLayoutOrientation, CPLayoutAttribute, CPEdgeInsets, and CPMapTable. The missing types are now present. - Add local typedefs for CPUserInterfaceLayoutOrientation and CPLayoutAttribute. Numeric values match the equivalent Cocoa constants. - Add CPEdgeInsets as an alias for the existing CGInset struct. Add CPEdgeInsetsMake and CPEdgeInsetsEqualToEdgeInsets. - Import Foundation/CPMapTable.j. CPMapTable already exists in Foundation. This commit also fixes two logic errors found during review. - arrangedSubviews did not match the true view order after insert or remove. Add _rebuildArrangedSubviews and call it from every method that changes view order. - Trailing-gravity layout added one extra spacing gap past the last view. This shifted the returned layout boundary. Spacing is now added before each view, not after. This commit adds one new property. - Add a distribution property with storage and accessors. It has no effect on layout yet. This makes the property honest: before this change, the type existed but no property did. The manual test, ./Tests/Manual/CPStackViewTest, used a Narwhal-era Jakefile and would not build. This has been updated to reflect usage under the Node toolchain. This commit adds CPStackView.j to AppKit.j. The file was never imported. Only the build script's wildcard file list included it. This commit adds a header comment to CPStackView.j. The comment states that the class is a placeholder. It lists known limitations. It states that a constraint-solver based replacement is planned. Why: CPStackView must compile and pass tests before capp-build can use this tree as a build reference, and a class with unstated limits invites misuse. --- AppKit/AppKit.j | 1 + AppKit/CPStackView.j | 153 +++++++++++++++++++--- Tests/Manual/CPStackViewTest/Jakefile | 178 ++++++++++++++++++-------- 3 files changed, 259 insertions(+), 73 deletions(-) diff --git a/AppKit/AppKit.j b/AppKit/AppKit.j index edf1dd794..4cd54116c 100644 --- a/AppKit/AppKit.j +++ b/AppKit/AppKit.j @@ -93,6 +93,7 @@ @import "CPSlider.j" @import "CPSound.j" @import "CPSplitView.j" +@import "CPStackView.j" @import "CPStepper.j" @import "CPTableColumn.j" @import "CPTableView.j" diff --git a/AppKit/CPStackView.j b/AppKit/CPStackView.j index c05d776b9..856f9fbaf 100644 --- a/AppKit/CPStackView.j +++ b/AppKit/CPStackView.j @@ -20,7 +20,80 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +/* + * PLACEHOLDER IMPLEMENTATION — READ BEFORE USE OR MODIFICATION. + * + * This class lays out views by direct, procedural arithmetic. It has no + * constraint solver. It cannot compress, expand, or negotiate space among + * views the way NSStackView does; it only places views at their existing + * frame size, in order, separated by fixed spacing. + * + * Known, accepted limitations: + * - `distribution` is stored but has no effect on layout. Fill, + * FillEqually, FillProportionally, and EqualSpacing are unimplemented. + * - Center-gravity views are clamped against the Leading edge only; if + * Leading + Center + Trailing content overflows the container, Center + * views can overlap Trailing views instead of compressing. + * - `visibilityPriority:forView:` only supports the two extreme values + * (MustHold / NotVisible). Intermediate priorities are accepted but + * have no defined effect. + * - No guarantee is made of correctness beyond what a single manual + * test (Tests/Manual/CPStackViewTest) exercises: orientation, the + * three gravity areas, alignment switching, spacing, and hidden-view + * detachment. Insertion, removal, custom spacing, and the CPCoding + * archive path are implemented but not verified by that test. + * + * This exists to give AppKit a working CPStackView symbol now, not to be + * a durable design. It is expected to be replaced by a constraint-solver + * based implementation (Kiwi.js) when time permits. Do not build on its + * internal layout algorithm as if it were a stable foundation. + */ + @import "CPView.j" +@import + +// MARK: - +// MARK: Minimal local type definitions +// +// These types support this file only. They are not shared with the rest +// of AppKit. A future constraint-solver based Auto Layout engine will +// replace them. Numeric values match the equivalent Cocoa constants +// (NSUserInterfaceLayoutOrientation, NSLayoutAttribute) so that a later, +// solver-based CPLayoutAttribute can reuse these numbers without a +// renumbering pass. + +@typedef CPUserInterfaceLayoutOrientation + CPUserInterfaceLayoutOrientationHorizontal = 0; + CPUserInterfaceLayoutOrientationVertical = 1; + +@typedef CPLayoutAttribute + CPLayoutAttributeLeft = 1; + CPLayoutAttributeRight = 2; + CPLayoutAttributeTop = 3; + CPLayoutAttributeBottom = 4; + CPLayoutAttributeLeading = 5; + CPLayoutAttributeTrailing = 6; + CPLayoutAttributeWidth = 7; + CPLayoutAttributeHeight = 8; + CPLayoutAttributeCenterX = 9; + CPLayoutAttributeCenterY = 10; + +@typedef CPEdgeInsets + +/*! + Creates a CPEdgeInsets. Argument order matches Cocoa's NSEdgeInsetsMake + (top, left, bottom, right). Storage reuses the existing CGInset struct, + whose field order is (top, right, bottom, left). +*/ +function CPEdgeInsetsMake(top, left, bottom, right) +{ + return CGInsetMake(top, right, bottom, left); +} + +function CPEdgeInsetsEqualToEdgeInsets(lhsInsets, rhsInsets) +{ + return CGInsetEqualToInset(lhsInsets, rhsInsets); +} // Gravity Areas @typedef CPStackViewGravity @@ -60,6 +133,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX { CPUserInterfaceLayoutOrientation _orientation; CPLayoutAttribute _alignment; + CPStackViewDistribution _distribution; float _spacing; CPEdgeInsets _edgeInsets; @@ -99,6 +173,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX { _orientation = CPUserInterfaceLayoutOrientationHorizontal; _alignment = CPLayoutAttributeCenterY; // Default alignment + _distribution = CPStackViewDistributionGravityAreas; _spacing = 8.0; // Default Cocoa spacing _edgeInsets = CPEdgeInsetsMake(0, 0, 0, 0); _detachesHiddenViews = YES; @@ -169,6 +244,27 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX [self setNeedsLayout:YES]; } +/*! + The distribution mode for the stack view. + @note Not yet applied to layout. All views are laid out at their + existing frame size regardless of this value, pending the + constraint-solver based layout engine. The value is stored and + returned so client code can read back what was set. +*/ +- (CPStackViewDistribution)distribution +{ + return _distribution; +} + +- (void)setDistribution:(CPStackViewDistribution)aDistribution +{ + if (_distribution === aDistribution) + return; + + _distribution = aDistribution; + [self setNeedsLayout:YES]; +} + /*! The minimum spacing, in points, between adjacent views in the stack view. */ @@ -233,6 +329,21 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX return _viewsLeading; // Leading or Top } +/*! + Rebuilds _arrangedSubviews from the three gravity containers, in + Leading, Center, Trailing order. Call after any change to a gravity + container so _arrangedSubviews stays a correct, single source of truth + for ordering, rather than an incrementally and separately maintained + (and error-prone) copy. +*/ +- (void)_rebuildArrangedSubviews +{ + _arrangedSubviews = [[CPMutableArray alloc] init]; + [_arrangedSubviews addObjectsFromArray:_viewsLeading]; + [_arrangedSubviews addObjectsFromArray:_viewsCenter]; + [_arrangedSubviews addObjectsFromArray:_viewsTrailing]; +} + /*! Adds a view to the end of the stack view gravity area. */ @@ -245,7 +356,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX [self removeView:aView]; [container addObject:aView]; - [_arrangedSubviews addObject:aView]; + [self _rebuildArrangedSubviews]; // Add as actual subview if ([aView superview] !== self) @@ -269,7 +380,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX else [container insertObject:aView atIndex:index]; - [_arrangedSubviews addObject:aView]; + [self _rebuildArrangedSubviews]; if ([aView superview] !== self) [self addSubview:aView]; @@ -284,13 +395,9 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX { var container = [self _containerForGravity:gravity]; - // Remove old views from arranged list and superview + // Remove old views from superview for (var i = 0; i < [container count]; i++) - { - var oldView = container[i]; - [oldView removeFromSuperview]; - [_arrangedSubviews removeObject:oldView]; - } + [container[i] removeFromSuperview]; [container removeAllObjects]; @@ -298,10 +405,10 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX { var newView = views[i]; [container addObject:newView]; - [_arrangedSubviews addObject:newView]; [self addSubview:newView]; } + [self _rebuildArrangedSubviews]; [self setNeedsLayout:YES]; } @@ -316,7 +423,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX [_viewsLeading removeObject:aView]; [_viewsCenter removeObject:aView]; [_viewsTrailing removeObject:aView]; - [_arrangedSubviews removeObject:aView]; + [self _rebuildArrangedSubviews]; [aView removeFromSuperview]; @@ -531,6 +638,13 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX var limit = (dir === 1) ? count : -1; var step = (dir === 1) ? 1 : -1; + // Spacing is applied as a gap *before* placing an element (except the + // first placed one), rather than trailing off the end after the last + // element. This keeps the returned cursor at the true content edge, + // with no phantom spacing past the final view. + var hasPlacedAny = false; + var pendingSpacing = 0; + for (; i !== limit; i += step) { var view = views[i]; @@ -538,6 +652,9 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX if (_detachesHiddenViews && [view isHidden]) continue; + if (hasPlacedAny) + cursor += (dir === 1) ? pendingSpacing : -pendingSpacing; + var viewFrame = [view frame]; var viewSizePrimary = isVert ? CGRectGetHeight(viewFrame) : CGRectGetWidth(viewFrame); @@ -617,11 +734,10 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX if (dir === 1) { originY = cursor; - cursor += sizeH + [self _spacingAfterView:view]; + cursor += sizeH; } else { cursor -= sizeH; originY = cursor; - cursor -= [self _spacingAfterView:view]; } } else @@ -633,15 +749,17 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX if (dir === 1) { originX = cursor; - cursor += sizeW + [self _spacingAfterView:view]; + cursor += sizeW; } else { cursor -= sizeW; originX = cursor; - cursor -= [self _spacingAfterView:view]; } } [view setFrame:CGRectMake(originX, originY, sizeW, sizeH)]; + + pendingSpacing = [self _spacingAfterView:view]; + hasPlacedAny = true; } return cursor; @@ -657,6 +775,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX { _orientation = [aCoder decodeIntForKey:@"CPStackViewOrientation"]; _alignment = [aCoder decodeIntForKey:@"CPStackViewAlignment"]; + _distribution = [aCoder decodeIntForKey:@"CPStackViewDistribution"]; _spacing = [aCoder decodeFloatForKey:@"CPStackViewSpacing"]; _edgeInsets = [aCoder decodeObjectForKey:@"CPStackViewEdgeInsets"]; // Assuming CPEdgeInsets supports obj coding or manual decode if (!_edgeInsets) _edgeInsets = CPEdgeInsetsMake(0,0,0,0); @@ -668,10 +787,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX _viewsTrailing = [aCoder decodeObjectForKey:@"CPStackViewViewsTrailing"] || []; // Rebuild arranged subviews cache - _arrangedSubviews = [[CPMutableArray alloc] init]; - [_arrangedSubviews addObjectsFromArray:_viewsLeading]; - [_arrangedSubviews addObjectsFromArray:_viewsCenter]; - [_arrangedSubviews addObjectsFromArray:_viewsTrailing]; + [self _rebuildArrangedSubviews]; _customSpacings = [aCoder decodeObjectForKey:@"CPStackViewCustomSpacings"] || [[CPMapTable alloc] init]; _visibilityPriorities = [[CPMapTable alloc] init]; // usually not persisted @@ -684,6 +800,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX [super encodeWithCoder:aCoder]; [aCoder encodeInt:_orientation forKey:@"CPStackViewOrientation"]; [aCoder encodeInt:_alignment forKey:@"CPStackViewAlignment"]; + [aCoder encodeInt:_distribution forKey:@"CPStackViewDistribution"]; [aCoder encodeFloat:_spacing forKey:@"CPStackViewSpacing"]; [aCoder encodeObject:_edgeInsets forKey:@"CPStackViewEdgeInsets"]; [aCoder encodeBool:_detachesHiddenViews forKey:@"CPStackViewDetachesHiddenViews"]; diff --git a/Tests/Manual/CPStackViewTest/Jakefile b/Tests/Manual/CPStackViewTest/Jakefile index a4174055a..65c8df6cf 100644 --- a/Tests/Manual/CPStackViewTest/Jakefile +++ b/Tests/Manual/CPStackViewTest/Jakefile @@ -1,94 +1,162 @@ /* * Jakefile - * CPSplitViewTest + * CPStackViewTest * - * Created by Alexander Ljungberg on January 27, 2012. - * Copyright 2012, WireLoad All rights reserved. + * Created by You on August 14, 2026. + * Copyright 2026, Your Company All rights reserved. */ -var ENV = require("system").env, - FILE = require("file"), - JAKE = require("jake"), +const path = require("path"); +const fs = require("fs"); + +var ENV = process.env, task = JAKE.task, FileList = JAKE.FileList, - app = require("cappuccino/jake").app, + app = CAPPUCCINO.Jake.applicationtask.app, configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug", - OS = require("os"); + OS = require("os"), + projectName = "CPStackViewTest"; -app ("CPSplitViewTest", function(task) +var buildDir = path.resolve(ENV["BUILD_PATH"] || ENV["CAPP_BUILD"] || "Build"); + +app (projectName, function(task) { - task.setBuildIntermediatesPath(FILE.join("Build", "CPSplitViewTest.build", configuration)); - task.setBuildPath(FILE.join("Build", configuration)); + ENV["OBJJ_INCLUDE_PATHS"] = ["Frameworks"]; - task.setProductName("CPSplitViewTest"); - task.setIdentifier("com.yourcompany.CPSplitViewTest"); + if (configuration === "Debug") + ENV["OBJJ_INCLUDE_PATHS"] = path.join(ENV["OBJJ_INCLUDE_PATHS"], configuration); + + task.setBuildIntermediatesPath(path.join(buildDir, "CPStackViewTest.build", configuration)); + task.setBuildPath(path.join(buildDir, configuration)); + + task.setProductName("CPStackViewTest"); + task.setIdentifier("com.yourcompany.CPStackViewTest"); task.setVersion("1.0"); - task.setAuthor("WireLoad"); + task.setAuthor("Your Company"); task.setEmail("feedback @nospam@ yourcompany.com"); - task.setSummary("CPSplitViewTest"); - task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**"))); + task.setSummary("CPStackViewTest"); + task.setSources(new FileList("**/*.j").exclude(path.join("Build", "**")).exclude(path.join("Frameworks", "Source", "**"))); task.setResources(new FileList("Resources/**")); task.setIndexFilePath("index.html"); task.setInfoPlistPath("Info.plist"); - task.setNib2CibFlags("-R Resources/"); if (configuration === "Debug") - task.setCompilerFlags("-DDEBUG -g"); + task.setCompilerFlags("-DDEBUG -g -S --inline-msg-send"); else - task.setCompilerFlags("-O"); + task.setCompilerFlags("-O2"); }); -task ("default", ["CPSplitViewTest"], function() +task ("default", [projectName], function() { printResults(configuration); }); -task ("build", ["default"]); +task ("build", ["default"], function() +{ + updateApplicationSize(); +}); task ("debug", function() { - ENV["CONFIGURATION"] = "Debug"; + configuration = ENV["CONFIGURATION"] = "Debug"; JAKE.subjake(["."], "build", ENV); }); task ("release", function() { - ENV["CONFIGURATION"] = "Release"; + configuration = ENV["CONFIGURATION"] = "Release"; JAKE.subjake(["."], "build", ENV); }); -task ("run", ["debug"], function() -{ - OS.system(["open", FILE.join("Build", "Debug", "CPSplitViewTest", "index.html")]); -}); - -task ("run-release", ["release"], function() -{ - OS.system(["open", FILE.join("Build", "Release", "CPSplitViewTest", "index.html")]); -}); - -task ("deploy", ["release"], function() -{ - FILE.mkdirs(FILE.join("Build", "Deployment", "CPSplitViewTest")); - OS.system(["press", "-f", FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Deployment", "CPSplitViewTest")]); - printResults("Deployment") -}); - -task ("desktop", ["release"], function() -{ - FILE.mkdirs(FILE.join("Build", "Desktop", "CPSplitViewTest")); - require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app")); - printResults("Desktop") -}); - -task ("run-desktop", ["desktop"], function() -{ - OS.system([FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app", "Contents", "MacOS", "NativeHost"), "-i"]); -}); - function printResults(configuration) { - print("----------------------------"); - print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPSplitViewTest")); - print("----------------------------"); + console.log("----------------------------"); + console.log(configuration+" app built at path: "+path.join(buildDir, configuration, projectName)); + console.log("----------------------------"); +} + +function updateApplicationSize() +{ + console.log("Calculating application file sizes..."); + + var contents = fs.readFileSync(path.join(buildDir, configuration, projectName, "Info.plist"), { encoding: "utf8" }), + format = CFPropertyList.sniffedFormatOfString(contents), + plist = CFPropertyList.propertyListFromString(contents), + totalBytes = {executable:0, data:0, mhtml:0}; + + // Get the size of all framework executables and sprite data + var frameworksDir = "Frameworks"; + + if (configuration === "Debug") + frameworksDir = path.join(frameworksDir, "Debug"); + + var frameworks = []; + + if (fs.existsSync(frameworksDir)) { + frameworks = fs.readdirSync(frameworksDir); + } + + frameworks.forEach(function(framework) + { + if (framework !== "Source") + addBundleFileSizes(path.join(frameworksDir, framework), totalBytes); + }); + + // Read in the default theme name, and attempt to get its size + var themeName = plist.valueForKey("CPDefaultTheme") || "Aristo2", + themePath = nil; + + if (themeName === "Aristo" || themeName === "Aristo2") + themePath = path.join(frameworksDir, "AppKit", "Resources", themeName + ".blend"); + else + themePath = path.join("Frameworks", "Resources", themeName + ".blend"); + + if (fs.existsSync(themePath) && fs.lstatSync(themePath).isDirectory()) + addBundleFileSizes(themePath, totalBytes); + + // Add sizes for the app + addBundleFileSizes(path.join(buildDir, configuration, projectName), totalBytes); + + console.log("Executables: " + totalBytes.executable + ", sprite data: " + totalBytes.data + ", total: " + (totalBytes.executable + totalBytes.data)); + + var dict = new CFMutableDictionary(); + + dict.setValueForKey("executable", totalBytes.executable); + dict.setValueForKey("data", totalBytes.data); + dict.setValueForKey("mhtml", totalBytes.mhtml); + + plist.setValueForKey("CPApplicationSize", dict); + fs.writeFileSync(path.join(buildDir, configuration, projectName, "Info.plist"), CFPropertyList.stringFromPropertyList(plist, format), { encoding: "utf8" }); +} + +function addBundleFileSizes(bundlePath, totalBytes) +{ + var bundleName = path.basename(bundlePath), + environment = bundleName === "Foundation" ? "Objj" : "Browser", + bundlePath = path.join(bundlePath, environment + ".environment"); + + if (fs.existsSync(bundlePath) && fs.lstatSync(bundlePath).isDirectory()) + { + var filename = bundleName + ".sj", + filePath = path.join(bundlePath, filename); + + if (fs.existsSync(filePath)) { + totalBytes.executable += fs.lstatSync(filePath).size; + } + + filePath = path.join(bundlePath, "dataURLs.txt"); + + if (fs.existsSync(filePath)) + totalBytes.data += fs.lstatSync(filePath).size; + + filePath = path.join(bundlePath, "MHTMLData.txt"); + + if (fs.existsSync(filePath)) + totalBytes.mhtml += fs.lstatSync(filePath).size; + + filePath = path.join(bundlePath, "MHTMLPaths.txt"); + + if (fs.existsSync(filePath)) + totalBytes.mhtml += fs.lstatSync(filePath).size; + } }