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.
This commit is contained in:
David Richardson
2026-08-14 20:09:17 -06:00
parent 907ff374e6
commit 01f7f9b601
3 changed files with 259 additions and 73 deletions
+1
View File
@@ -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"
+135 -18
View File
@@ -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 <Foundation/CPMapTable.j>
// 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"];
+123 -55
View File
@@ -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;
}
}