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
98 changes: 98 additions & 0 deletions apps/macos/Sources/FixedWindowFrame.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import AppKit
import SwiftUI

/// Pins the window to a known frame, when the environment asks for one.
///
/// For tests, and inert otherwise: without `JP_WINDOW_FRAME` set, nothing here
/// runs and the window behaves as any other, remembering where it was left.
///
/// That memory is the problem it exists for. A window's frame is autosaved into
/// user defaults, which is a different mechanism from the saved application
/// state `-ApplePersistenceIgnoreState` disables — so a UI test suite can turn
/// off state restoration, as this one does, and still inherit the size the last
/// run left behind. A test that resizes the window then hands the next run a
/// different starting point, and one that grows the window eventually hands it
/// a window with nowhere left to grow.
struct FixedWindowFrame: ViewModifier {
/// The frame to pin to, as `<width>x<height>`.
///
/// `nonisolated` because a name is not UI state: a `ViewModifier` is
/// main-actor isolated, which would otherwise follow this string everywhere
/// it is read.
nonisolated static let environmentKey = "JP_WINDOW_FRAME"

/// The requested frame, if the environment names a usable one.
private static var requested: CGSize? {
guard let value = ProcessInfo.processInfo.environment[environmentKey] else {
return nil
}

let parts = value.split(separator: "x")
guard parts.count == 2,
let width = Double(parts[0]),
let height = Double(parts[1]),
width > 0,
height > 0
else {
return nil
}

return CGSize(width: width, height: height)
}

func body(content: Content) -> some View {
guard let size = Self.requested else {
return AnyView(content)
}

return AnyView(content.background(WindowPinner(size: size)))
}
}

extension View {
/// Pin the window to the frame `JP_WINDOW_FRAME` names, if it names one.
func fixedWindowFrame() -> some View {
modifier(FixedWindowFrame())
}
}

/// Reaches the `NSWindow` behind a SwiftUI scene, to set its frame once.
private struct WindowPinner: NSViewRepresentable {
let size: CGSize

func makeNSView(context: Context) -> NSView {
let view = WindowPinningView()
view.pin = size
return view
}

func updateNSView(_ view: NSView, context: Context) {}
}

/// A view that acts the moment it is put in a window.
///
/// `viewDidMoveToWindow` rather than `updateNSView`, which is called when SwiftUI
/// decides to and can run before the view has a window at all — and then not
/// again, if nothing else changes.
private final class WindowPinningView: NSView {
var pin: CGSize?

override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()

guard let window, let pin else { return }

// Emptying the autosave name is what stops this run from writing its
// size back over the default the next one reads.
_ = window.setFrameAutosaveName("")
window.setContentSize(pin)

// Placed toward the left of the screen rather than centred, so a test
// dragging the right edge outwards has room whatever the screen size.
if let visible = window.screen?.visibleFrame {
window.setFrameOrigin(
CGPoint(x: visible.minX + 40, y: visible.maxY - window.frame.height - 40)
)
}
}
}
4 changes: 4 additions & 0 deletions apps/macos/Sources/WorkspaceWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ struct WorkspaceWindow: View {
// to the scene, because the scene's `defaultSize` applies to a window
// opened fresh and not to one restored into a saved frame.
.frame(minWidth: Self.minimumWindowWidth, minHeight: Self.minimumWindowHeight)
// Inert unless `JP_WINDOW_FRAME` is set, which only a test does. Frame
// autosave outlives `-ApplePersistenceIgnoreState`, so without this a
// suite inherits the window size its last run left behind.
.fixedWindowFrame()
// Carried but not displayed: the window has no title bar to show it in.
// It is still what the Window menu lists the window under, and what an
// external driver addresses it by.
Expand Down
19 changes: 19 additions & 0 deletions apps/macos/Tests/FixedWindowFrameTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Testing

@testable import JP

/// The window frame a UI test pins the app to.
@Suite("FixedWindowFrame")
struct FixedWindowFrameTests {
/// A UI test bundle drives the app from another process and cannot import
/// it, so `AppUnderTest` spells this key out as a literal. Renaming the app's
/// constant without changing that literal would leave every launch inheriting
/// the previous run's window size again — silently, because an unset variable
/// means "behave normally".
///
/// This is the only thing holding the two spellings together.
@Test("is read from the variable the UI tests set")
func keyMatchesTheOneUITestsSet() {
#expect(FixedWindowFrame.environmentKey == "JP_WINDOW_FRAME")
}
}
Loading
Loading