From c554ecb9f092b96ccb3fa311ce53510cff05e891 Mon Sep 17 00:00:00 2001 From: Niclas Overby Date: Wed, 19 Aug 2026 08:58:24 +0200 Subject: [PATCH] fix(cider): the Swift Publisher canvas has a size, because new reaches the designated initialiser THE DOCUMENT WINDOW DRAWS. Toolbar, page preview strip with the page in it, inspector with Document Margins and Info, and a canvas area where every run since this task began showed flat grey. LOOKED AT, and the title bar says Untitled - Swift Publisher 5. Two defects, both ours, found by asking what sizes that scroll view rather than by looking at it again. ONE. -[NSViewController init] did not exist, so it was NSObject that answered. AppKit documents init on a view controller as initWithNibName:nil bundle:nil, and that is the method a subclass overrides when it builds its view in code. Swift Publisher does exactly that: its document container is created with [CCCanvasesPreviewAndDocumentSplitViewController new], and the override installs a real NSSplitView subclass as the controller view. Ours never ran, the controller came up with no view, and loadView handed back the empty NSView it gives a controller with no nib. The window then added the toolbar, the page preview and the canvas scroll view to that empty view, which lays nothing out. TWO. -[NSSplitView adjustSubviews] never asked splitView:shouldAdjustSizeOfSubview:, so every pane was elastic and the space was shared by ratio. The application answers YES for the canvas ALONE and NO for the toolbar and the preview above it, which is how a fixed strip and a filling pane live in one split view. Read from its own code: the delegate compares the subview against subviews index 2 and returns whether they are the same object. A pane the delegate holds fixed now keeps its size and leaves the ratio entirely, in both directions. MEASURED, the same view across three builds of the same run: CCDocScrollView 0x0 at 0,0 before CCDocScrollView 766x0 at 0,694 with the initialiser fixed, a pane at last but no height CCDocScrollView 841x568 at 0,127 with the delegate asked AND A CORRECTION TO WHAT I RECORDED LAST RUNG. I wrote that -[CCMainWindowController updateViews] is the only code in the application that ever sizes the document scroll view, and that the greyed Hide Inspector item and the blank canvas were therefore the same thread. Both wrong. updateViews sends moveFrameByCorner:toView:toCorner: to the INSPECTOR view with the scroll view only as the reference corner, so it never sizes the canvas at all. The application never sizes it from anywhere: its two frame helpers have two call sites between them and neither is the canvas, and a whole run shows no setFrame or setFrameSize reaching it. A split view was always supposed to do it. Along the way -[NSTableView setColumnAutoresizingStyle:] turned out to be missing, which only appeared once the initialiser fix let these controllers load their nibs at all: the unrecognised selector escaped the nib load, which is silent from outside and stops the window. The whole NSTableViewColumnAutoresizingStyle family is implemented now, with the old autoresizesAllColumnsToFit kept in step as the uniform style rather than left to disagree. Instruments in the same commit, all gated: the view tree dump now prints each autoresizing mask (CIDER_TRACE_VIEWS), which is what showed the panes were fixed and the canvas was not, and CIDER_TRACE_SVFRAME takes a class name instead of meaning every scroll view. WHAT IS LEFT on this canvas is the doc view inside it, still nan by nan, which the application detects and reports itself at CCDocView.m:1851. That is task 124 and it is now the only thing between this window and a page. --- vendor/pins/cocotron/AppKit/NSSplitView.m | 61 ++++++++++++- vendor/pins/cocotron/AppKit/NSTableView.m | 88 ++++++++++++++++--- vendor/pins/cocotron/AppKit/NSView.m | 27 +++++- .../pins/cocotron/AppKit/NSViewController.m | 18 ++++ vendor/pins/cocotron/AppKit/NSWindow.m | 28 +++++- .../AppKit/include/AppKit/NSTableView.h | 13 +++ 6 files changed, 215 insertions(+), 20 deletions(-) diff --git a/vendor/pins/cocotron/AppKit/NSSplitView.m b/vendor/pins/cocotron/AppKit/NSSplitView.m index 49367dcaf..60fc02d15 100644 --- a/vendor/pins/cocotron/AppKit/NSSplitView.m +++ b/vendor/pins/cocotron/AppKit/NSSplitView.m @@ -154,6 +154,20 @@ NSString *const NSSplitViewWillResizeSubviewsNotification = /** adjust all the non-collapsed subviews so that they are equally spaced * horizontally within the splitview */ +/* + * A PANE THE DELEGATE WILL NOT LET US RESIZE keeps the size it has, and the room left over goes to + * the panes that will. Without this every pane is treated as elastic and the space is shared by + * ratio, which is wrong whenever one pane is a fixed strip: Swift Publisher stacks a toolbar, a + * page preview and the document canvas in one split view and answers YES for the canvas alone, so + * asking made the difference between a canvas with a height and a canvas with none. + */ +- (BOOL) _delegateShouldAdjustSizeOfSubview: (NSView *) subview { + if (![_delegate respondsToSelector: @selector(splitView:shouldAdjustSizeOfSubview:)]) + return YES; + + return [_delegate splitView: self shouldAdjustSizeOfSubview: subview]; +} + - (void) _adjustSubviewWidths { // Set all the subview heights to the bounds height of the split view CGFloat height = NSHeight([self bounds]); @@ -192,6 +206,24 @@ NSString *const NSSplitViewWillResizeSubviewsNotification = visibleCount++; } + /* The panes the delegate holds fixed are subtracted from both sides of the ratio, so what is + * shared out below is only the room the elastic panes may have. */ + CGFloat fixedWidth = 0.0; + + for (i = 0; i < count; i++) { + NSView *subview = [_subviews objectAtIndex: i]; + + if ([self isSubviewCollapsed: subview] == NO && + [self _delegateShouldAdjustSizeOfSubview: subview] == NO) { + fixedWidth += NSWidth([subview frame]); + totalWidthBefore -= NSWidth([subview frame]); + visibleCount--; + } + } + totalWidthAfter -= fixedWidth; + if (!(totalWidthAfter >= 0.0)) + totalWidthAfter = 0.0; + BOOL haveProportions = (totalWidthBefore > 0.0) && (visibleCount > 0); CGFloat delta = haveProportions ? (totalWidthAfter / totalWidthBefore) : 0.0; CGFloat equalShare = (visibleCount > 0) ? (totalWidthAfter / visibleCount) : 0.0; @@ -200,7 +232,11 @@ NSString *const NSSplitViewWillResizeSubviewsNotification = for (i = 0; i < count; i++) { NSView *subview = [_subviews objectAtIndex: i]; if ([self isSubviewCollapsed: subview] == NO) { - frame.size.width = haveProportions ? (NSWidth([subview frame]) * delta) : equalShare; + if ([self _delegateShouldAdjustSizeOfSubview: subview] == NO) + frame.size.width = NSWidth([subview frame]); + else + frame.size.width = + haveProportions ? (NSWidth([subview frame]) * delta) : equalShare; frame.size.width = floor(frame.size.width); if (!(frame.size.width >= 0.0)) /* also false for NAN */ frame.size.width = 0.0; @@ -246,6 +282,23 @@ NSString *const NSSplitViewWillResizeSubviewsNotification = visibleCount++; } + /* Same as the width case: a pane the delegate holds fixed leaves the ratio entirely. */ + CGFloat fixedHeight = 0.0; + + for (i = 0; i < count; i++) { + NSView *subview = [_subviews objectAtIndex: i]; + + if ([self isSubviewCollapsed: subview] == NO && + [self _delegateShouldAdjustSizeOfSubview: subview] == NO) { + fixedHeight += NSHeight([subview frame]); + totalHeightBefore -= NSHeight([subview frame]); + visibleCount--; + } + } + totalHeightAfter -= fixedHeight; + if (!(totalHeightAfter >= 0.0)) + totalHeightAfter = 0.0; + BOOL haveProportions = (totalHeightBefore > 0.0) && (visibleCount > 0); CGFloat delta = haveProportions ? (totalHeightAfter / totalHeightBefore) : 0.0; CGFloat equalShare = (visibleCount > 0) ? (totalHeightAfter / visibleCount) : 0.0; @@ -254,7 +307,11 @@ NSString *const NSSplitViewWillResizeSubviewsNotification = for (i = 0; i < count; i++) { NSView *subview = [_subviews objectAtIndex: i]; if ([self isSubviewCollapsed: subview] == NO) { - frame.size.height = haveProportions ? (NSHeight([subview frame]) * delta) : equalShare; + if ([self _delegateShouldAdjustSizeOfSubview: subview] == NO) + frame.size.height = NSHeight([subview frame]); + else + frame.size.height = + haveProportions ? (NSHeight([subview frame]) * delta) : equalShare; frame.size.height = floor(frame.size.height); if (!(frame.size.height >= 0.0)) /* also false for NAN */ frame.size.height = 0.0; diff --git a/vendor/pins/cocotron/AppKit/NSTableView.m b/vendor/pins/cocotron/AppKit/NSTableView.m index 49f7d0a87..a7b512c6c 100644 --- a/vendor/pins/cocotron/AppKit/NSTableView.m +++ b/vendor/pins/cocotron/AppKit/NSTableView.m @@ -122,6 +122,9 @@ const CGFloat NSTableViewDefaultRowHeight = 16.0f; _allowsColumnReordering = (flags & 0x80000000) ? YES : NO; _allowsColumnResizing = (flags & 0x40000000) ? YES : NO; _autoresizesAllColumnsToFit = (flags & 0x00008000) ? YES : NO; + _columnAutoresizingStyle = _autoresizesAllColumnsToFit + ? NSTableViewUniformColumnAutoresizingStyle + : NSTableViewLastColumnOnlyAutoresizingStyle; _allowsMultipleSelection = (flags & 0x08000000) ? YES : NO; _allowsEmptySelection = (flags & 0x10000000) ? YES : NO; _allowsColumnSelection = (flags & 0x04000000) ? YES : NO; @@ -165,6 +168,7 @@ const CGFloat NSTableViewDefaultRowHeight = 16.0f; _autoresizesAllColumnsToFit = NO; // the default isn't actually given in the spec, but this seems // more like default behavior + _columnAutoresizingStyle = NSTableViewLastColumnOnlyAutoresizingStyle; _allowsMultipleSelection = NO; _allowsEmptySelection = YES; _allowsColumnSelection = YES; @@ -274,6 +278,10 @@ const CGFloat NSTableViewDefaultRowHeight = 16.0f; return _autoresizesAllColumnsToFit; } +- (NSTableViewColumnAutoresizingStyle) columnAutoresizingStyle { + return _columnAutoresizingStyle; +} + - (BOOL) allowsMultipleSelection { return _allowsMultipleSelection; } @@ -649,7 +657,21 @@ static CGFloat rowHeightAtIndex(NSTableView *self, NSInteger index) { _allowsColumnResizing = flag; } +/* + * THE SAME SETTING, TWO SPELLINGS. autoresizesAllColumnsToFit is the older boolean and + * columnAutoresizingStyle replaced it, so they are kept in step here rather than allowed to + * disagree: the boolean is exactly the uniform style, and its NO is last-column-only, which is what + * the resize below already did. + */ +- (void) setColumnAutoresizingStyle: (NSTableViewColumnAutoresizingStyle) style { + _columnAutoresizingStyle = style; + _autoresizesAllColumnsToFit = + (style == NSTableViewUniformColumnAutoresizingStyle) ? YES : NO; +} + - (void) setAutoresizesAllColumnsToFit: (BOOL) flag { + _columnAutoresizingStyle = flag ? NSTableViewUniformColumnAutoresizingStyle + : NSTableViewLastColumnOnlyAutoresizingStyle; _autoresizesAllColumnsToFit = flag; } @@ -1449,26 +1471,68 @@ static CGFloat rowHeightAtIndex(NSTableView *self, NSInteger index) { return result; } -- (void) resizeWithOldSuperviewSize: (NSSize) oldSize { - NSSize size = [self frame].size; - - if (size.width < [[self superview] bounds].size.width) { - size.width = [[self superview] bounds].size.width; - [self setFrameSize: size]; - } +/* + * WHO TAKES UP THE SLACK when the table is wider or narrower than its columns. The styles differ + * only in which columns are offered the difference, so this is one walk with a direction and a + * limit: sequential works from the last column towards the first and reverse sequential from the + * first towards the last, each column taking as much of the difference as its minimum and maximum + * width allow before the next one is asked. + */ +- (void) _autoresizeColumns { + CGFloat delta = [[self enclosingScrollView] contentSize].width - + [self _displayWidthOfColumns]; + NSInteger count = [_tableColumns count]; + NSInteger i; - if (_autoresizesAllColumnsToFit) { - CGFloat delta = [[self enclosingScrollView] contentSize].width - - [self _displayWidthOfColumns]; - NSInteger i, count = [_tableColumns count]; + if (count == 0 || _columnAutoresizingStyle == NSTableViewNoColumnAutoresizing) + return; + switch (_columnAutoresizingStyle) { + case NSTableViewUniformColumnAutoresizingStyle: for (i = 0; i < count; ++i) { NSTableColumn *column = [_tableColumns objectAtIndex: i]; + [column setWidth: [column width] + floor((delta / count))]; } + return; - } else + case NSTableViewLastColumnOnlyAutoresizingStyle: [self sizeLastColumnToFit]; + return; + + case NSTableViewFirstColumnOnlyAutoresizingStyle: { + NSTableColumn *column = [_tableColumns objectAtIndex: 0]; + + [column setWidth: [column width] + delta]; + return; + } + + default: + break; + } + + for (i = 0; i < count && delta != 0; ++i) { + NSInteger index = (_columnAutoresizingStyle == + NSTableViewReverseSequentialColumnAutoresizingStyle) + ? i + : (count - 1 - i); + NSTableColumn *column = [_tableColumns objectAtIndex: index]; + CGFloat was = [column width]; + + [column setWidth: was + delta]; + delta -= [column width] - was; + } +} + +- (void) resizeWithOldSuperviewSize: (NSSize) oldSize { + NSSize size = [self frame].size; + + if (size.width < [[self superview] bounds].size.width) { + size.width = [[self superview] bounds].size.width; + [self setFrameSize: size]; + } + + [self _autoresizeColumns]; [self tile]; } diff --git a/vendor/pins/cocotron/AppKit/NSView.m b/vendor/pins/cocotron/AppKit/NSView.m index a61a3ecca..ff2c1dc91 100644 --- a/vendor/pins/cocotron/AppKit/NSView.m +++ b/vendor/pins/cocotron/AppKit/NSView.m @@ -1280,14 +1280,33 @@ static inline void buildTransformsIfNeeded(NSView *self) { return NSMakeRect(minx, miny, maxx - minx, maxy - miny); } + +/* + * WHICH VIEWS TO FOLLOW. CIDER_TRACE_SVFRAME=1 still means every scroll view, which is what the + * Swift Publisher canvas work needed; any other value is a class NAME SUBSTRING, so a run can + * follow exactly the view that is behaving oddly rather than a category of them. + */ +static BOOL _CiderTraceFrameFor(NSView *view) { + const char *want = getenv("CIDER_TRACE_SVFRAME"); + + if (want == NULL || want[0] == '\0') + return NO; + + if (strcmp(want, "1") == 0) + return [view isKindOfClass: [NSScrollView class]]; + + return strstr(object_getClassName(view), want) != NULL; +} + - (void) setFrame: (NSRect) frame { /* EVERY SIZE A SCROLL VIEW IS GIVEN, WITH THE CALLER. The document scroll view of Swift * Publisher arrives at zero and nothing in AppKit can size it (no mask, plain superview), so * the question is whether the application sizes it later and from where. Subtract the image * base from the caller to read it against the disassembly. */ - if (getenv("CIDER_TRACE_SVFRAME") != NULL && [self isKindOfClass: [NSScrollView class]]) { - fprintf(stderr, "CIDER_SVFRAME %s %s -> %gx%g caller=%p\n", object_getClassName(self), - "setFrame", frame.size.width, frame.size.height, __builtin_return_address(0)); + if (_CiderTraceFrameFor(self)) { + fprintf(stderr, "CIDER_SVFRAME %s %s -> %gx%g at %g,%g caller=%p\n", + object_getClassName(self), "setFrame", frame.size.width, frame.size.height, + frame.origin.x, frame.origin.y, __builtin_return_address(0)); fflush(stderr); } @@ -1404,7 +1423,7 @@ static inline void buildTransformsIfNeeded(NSView *self) { * Publisher arrives at zero and nothing in AppKit can size it (no mask, plain superview), so * the question is whether the application sizes it later and from where. Subtract the image * base from the caller to read it against the disassembly. */ - if (getenv("CIDER_TRACE_SVFRAME") != NULL && [self isKindOfClass: [NSScrollView class]]) { + if (_CiderTraceFrameFor(self)) { fprintf(stderr, "CIDER_SVFRAME %s %s -> %gx%g caller=%p\n", object_getClassName(self), "setFrameSize", size.width, size.height, __builtin_return_address(0)); fflush(stderr); diff --git a/vendor/pins/cocotron/AppKit/NSViewController.m b/vendor/pins/cocotron/AppKit/NSViewController.m index 78aecc851..82414bb97 100644 --- a/vendor/pins/cocotron/AppKit/NSViewController.m +++ b/vendor/pins/cocotron/AppKit/NSViewController.m @@ -8,6 +8,24 @@ @synthesize identifier = _identifier; +/* + * NEW HAS TO REACH THE DESIGNATED INITIALISER. AppKit documents -init on a view controller as + * initWithNibName:nil bundle:nil, and a subclass that builds its view in code overrides that one + * method, because it is the only one every other initialiser funnels through. Without this, an + * ordinary [MyViewController new] runs NSObject's -init, the subclass override never executes, and + * the controller comes up with no view at all. + * + * That is not a theoretical tidiness point. Swift Publisher creates its document container that + * way: the override installs a real NSSplitView subclass as the controller's view, and the window + * then adds the page-preview strip and the canvas scroll view to it as panes, both with no + * autoresizing mask, because a split view sizes its own subviews. Skipping the override left them + * in the empty NSView that loadView hands back for a nib-less controller, which lays nothing out, + * so the canvas scroll view stayed at zero by zero for the life of the window. + */ +- init { + return [self initWithNibName: nil bundle: nil]; +} + - initWithNibName: (NSString *) name bundle: (NSBundle *) bundle { _nibName = [name copy]; _nibBundle = [bundle retain]; diff --git a/vendor/pins/cocotron/AppKit/NSWindow.m b/vendor/pins/cocotron/AppKit/NSWindow.m index 5a26fac68..e0f560278 100644 --- a/vendor/pins/cocotron/AppKit/NSWindow.m +++ b/vendor/pins/cocotron/AppKit/NSWindow.m @@ -230,9 +230,12 @@ static void _CiderDumpViewTree(NSView *view, int depth) else if ([view respondsToSelector: @selector(string)]) text = [(id) view string]; - fprintf(stderr, "CIDER_VIEW %*s%s %.0fx%.0f at %.0f,%.0f%s%s\n", depth * 2, "", + /* The mask belongs next to the frame: a subview that stays empty while its container grows is + * either masked to stay that way or was never given a size to grow from, and the two look + * identical in a frame-only dump. */ + fprintf(stderr, "CIDER_VIEW %*s%s %.0fx%.0f at %.0f,%.0f mask=0x%x%s%s\n", depth * 2, "", object_getClassName(view), frame.size.width, frame.size.height, - frame.origin.x, frame.origin.y, + frame.origin.x, frame.origin.y, (unsigned) [view autoresizingMask], (text != nil && [text length] > 0) ? " text: " : "", (text != nil && [text length] > 0) ? [text UTF8String] : ""); @@ -2374,6 +2377,27 @@ static BOOL _allowsAutomaticWindowTabbing; return; } + /* THE TREE OF EVERY WINDOW THAT EVER DRAWS, ONCE. -display dumps it, but a window that only + * ever goes through displayIfNeeded was invisible to that instrument, which is every document + * window in Swift Publisher. One dump per window is enough to answer whether a layout ran. */ + if (getenv("CIDER_TRACE_VIEWS") != NULL) { + static NSWindow *dumped[64]; + static int dumpedCount = 0; + BOOL seen = NO; + + for (int i = 0; i < dumpedCount; i++) + if (dumped[i] == self) + seen = YES; + + if (!seen && dumpedCount < 64) { + dumped[dumpedCount++] = self; + fprintf(stderr, "CIDER_VIEW tree(displayIfNeeded) of %s title=%s\n", + object_getClassName(self), [[self title] UTF8String] ?: "(none)"); + fflush(stderr); + _CiderDumpViewTree(_backgroundView, 1); + } + } + @autoreleasepool { if ([NSGraphicsContext quartzDebuggingIsEnabled]) { // Show all the views getting redrawn. diff --git a/vendor/pins/cocotron/AppKit/include/AppKit/NSTableView.h b/vendor/pins/cocotron/AppKit/include/AppKit/NSTableView.h index 4a4e0e824..80b5736f0 100644 --- a/vendor/pins/cocotron/AppKit/include/AppKit/NSTableView.h +++ b/vendor/pins/cocotron/AppKit/include/AppKit/NSTableView.h @@ -75,6 +75,16 @@ typedef enum { NSTableViewDropAbove } NSTableViewDropOperation; +enum { + NSTableViewNoColumnAutoresizing = 0, + NSTableViewUniformColumnAutoresizingStyle, + NSTableViewSequentialColumnAutoresizingStyle, + NSTableViewReverseSequentialColumnAutoresizingStyle, + NSTableViewLastColumnOnlyAutoresizingStyle, + NSTableViewFirstColumnOnlyAutoresizingStyle +}; +typedef NSUInteger NSTableViewColumnAutoresizingStyle; + @interface NSTableView : NSControl { id _target; SEL _action; @@ -95,6 +105,7 @@ typedef enum { BOOL _allowsColumnReordering; BOOL _allowsColumnResizing; BOOL _autoresizesAllColumnsToFit; + NSTableViewColumnAutoresizingStyle _columnAutoresizingStyle; BOOL _allowsMultipleSelection; BOOL _allowsEmptySelection; BOOL _allowsColumnSelection; @@ -140,6 +151,7 @@ typedef enum { - (BOOL) allowsColumnReordering; - (BOOL) allowsColumnResizing; - (BOOL) autoresizesAllColumnsToFit; +- (NSTableViewColumnAutoresizingStyle) columnAutoresizingStyle; - (BOOL) allowsMultipleSelection; - (BOOL) allowsEmptySelection; - (BOOL) allowsColumnSelection; @@ -181,6 +193,7 @@ typedef enum { - (void) setAllowsColumnReordering: (BOOL) flag; - (void) setAllowsColumnResizing: (BOOL) flag; - (void) setAutoresizesAllColumnsToFit: (BOOL) flag; +- (void) setColumnAutoresizingStyle: (NSTableViewColumnAutoresizingStyle) style; - (void) setAllowsMultipleSelection: (BOOL) flag; - (void) setAllowsEmptySelection: (BOOL) flag; - (void) setAllowsColumnSelection: (BOOL) flag; -- 2.51.2