Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions Sources/App/CmuxMainWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,24 +163,34 @@ final class CmuxMainWindow: NSWindow {
if inLiveResize {
recordUserPlacement()
}
guard !styleMask.contains(.fullScreen) else {
super.setFrame(frameRect, display: flag)
return
}
super.setFrame(frameWithinSizePolicy(frameRect), display: flag)
}

/// The animating variant is a separate entry point that does not route
/// through `setFrame(_:display:)`, so without this override display
/// placement, UI-test placement and automation could size the window
/// below the layout floor. Content that cannot shrink with it then
/// overflows the window, and a pane laid out past the window edge ends
/// at 0x0.
override func setFrame(_ frameRect: NSRect, display displayFlag: Bool, animate animateFlag: Bool) {
super.setFrame(frameWithinSizePolicy(frameRect), display: displayFlag, animate: animateFlag)
}

/// Caps a proposed frame to the display union and raises it to the
/// minimum content size. Full screen frames belong to the system.
private func frameWithinSizePolicy(_ frameRect: NSRect) -> NSRect {
guard !styleMask.contains(.fullScreen) else { return frameRect }
let capped = Self.frameByCappingOversizedDimensions(
frameRect,
displayFrames: NSScreen.screens.map {
(frame: $0.frame, visibleFrame: $0.visibleFrame)
}
)
super.setFrame(
Self.frameByRaisingUndersizedDimensions(
capped,
minimumSize: Self.minimumContentSize,
currentFrame: frame,
isLiveResize: inLiveResize
),
display: flag
return Self.frameByRaisingUndersizedDimensions(
capped,
minimumSize: Self.minimumContentSize,
currentFrame: frame,
isLiveResize: inLiveResize
)
}

Expand Down
39 changes: 39 additions & 0 deletions cmuxTests/MainWindowSelfSizingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,45 @@ final class MainWindowSelfSizingTests: XCTestCase {
)
}

/// `setFrame(_:display:animate:)` is its own entry point: it does not
/// route through `setFrame(_:display:)`, so display placement, UI-test
/// placement and automation that use it once shrank the window below
/// the floor. The content could not follow, overflowed the window, and a
/// split pane laid out past its bottom edge ended at 0x0 (#15347).
@MainActor
func testAnimatingSetFrameRaisesUndersizedFrameToMinimumContentSize() {
for animate in [false, true] {
let window = CmuxMainWindow(
contentRect: NSRect(x: 0, y: 0, width: 900, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
defer {
window.orderOut(nil)
window.close()
}

let minimum = CmuxMainWindow.minimumContentSize
let undersized = NSRect(x: 80, y: 500, width: 800, height: 200)
window.setFrame(undersized, display: false, animate: animate)

XCTAssertEqual(
window.frame.width, 800, accuracy: 0.5,
"animate=\(animate): a width above the floor must be kept"
)
XCTAssertGreaterThanOrEqual(
window.frame.height, minimum.height - 0.5,
"animate=\(animate): a setFrame below the minimum height must be raised to the floor"
)
XCTAssertEqual(
window.frame.maxY, undersized.maxY, accuracy: 0.5,
"animate=\(animate): raising an undersized frame must keep the top edge and extend downward"
)
}
}

/// Ordinary frames must flow through the undersized raise untouched so
/// user-owned placement (partial off-screen, multi-display) is never
/// perturbed by the floor.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"kind": "cmux-fuzz-repro",
"version": 1,
"signature": {
"kind": "invariant",
"key": "pane-view-degenerate",
"title": "Layout invariant broken: pane-view-degenerate",
"digest": "3ad440fc1652"
},
"steps": [
{
"do": "split",
"dir": "d",
"via": "socket"
},
{
"do": "split",
"dir": "u",
"via": "socket"
},
{
"do": "window_resize",
"w": 800,
"h": 200
}
]
}
Loading