Merge df13211557 into 629838b9bd
commit
3643c1c6f0
|
|
@ -27,15 +27,15 @@ final class GhosttyWindowPositionUITests: GhosttyCustomConfigCase {
|
|||
|
||||
app.launch() // window in the center
|
||||
|
||||
// app.menuBarItems["Window"].firstMatch.click()
|
||||
// app.menuItems["_zoomTopLeft:"].firstMatch.click()
|
||||
//
|
||||
// // wait for the animation to finish
|
||||
// try await Task.sleep(for: .seconds(0.5))
|
||||
app.menuBarItems["Window"].firstMatch.click()
|
||||
app.menuItems["_zoomTopLeft:"].firstMatch.click()
|
||||
|
||||
// wait for the animation to finish
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
let window = app.windows.firstMatch
|
||||
let windowFrame = window.frame
|
||||
// XCTAssertEqual(windowFrame.minX, 0, "Window should be on the left")
|
||||
XCTAssertEqual(windowFrame.minX, 0, "Window should be on the left")
|
||||
|
||||
app.typeKey("n", modifierFlags: [.command])
|
||||
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ class AppDelegate: NSObject,
|
|||
case .new_tab:
|
||||
_ = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: TerminalController.preferredParent?.window,
|
||||
from: TerminalController.preferredNewTabParent?.window,
|
||||
withBaseConfig: config
|
||||
)
|
||||
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
||||
|
|
@ -709,8 +709,13 @@ class AppDelegate: NSObject,
|
|||
|
||||
@objc private func ghosttyNewWindow(_ notification: Notification) {
|
||||
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
||||
let surfaceView = notification.object as? NSView
|
||||
let config = configAny as? Ghostty.SurfaceConfiguration
|
||||
_ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
||||
_ = TerminalController.newWindow(
|
||||
ghostty,
|
||||
withBaseConfig: config,
|
||||
withParent: surfaceView?.window,
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func ghosttyNewTab(_ notification: Notification) {
|
||||
|
|
@ -945,7 +950,7 @@ class AppDelegate: NSObject,
|
|||
@IBAction func newTab(_ sender: Any?) {
|
||||
_ = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: TerminalController.preferredParent?.window
|
||||
from: TerminalController.preferredNewTabParent?.window
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,8 +94,6 @@ struct NewTerminalIntent: AppIntent {
|
|||
}
|
||||
|
||||
parent = view
|
||||
} else if let preferred = TerminalController.preferredParent {
|
||||
parent = preferred.focusedSurface ?? preferred.surfaceTree.root?.leftmostLeaf()
|
||||
} else {
|
||||
parent = nil
|
||||
}
|
||||
|
|
@ -116,22 +114,29 @@ struct NewTerminalIntent: AppIntent {
|
|||
}
|
||||
|
||||
case .tab:
|
||||
/// If no parent is specified,
|
||||
/// we find the last window in this screen as parent
|
||||
let parentWindow = parent?.window ?? TerminalController.preferredNewTabParent?.window
|
||||
let newController = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: parent?.window,
|
||||
from: parentWindow,
|
||||
withBaseConfig: config)
|
||||
if let view = newController?.surfaceTree.root?.leftmostLeaf() {
|
||||
return .result(value: TerminalEntity(view))
|
||||
}
|
||||
|
||||
case .splitLeft, .splitRight, .splitUp, .splitDown:
|
||||
guard let parent,
|
||||
let controller = parent.window?.windowController as? BaseTerminalController else {
|
||||
// split parent is like tab parent
|
||||
let splitParent = parent ??
|
||||
TerminalController.preferredNewSplitParent?.focusedSurface ??
|
||||
TerminalController.preferredNewSplitParent?.surfaceTree.root?.leftmostLeaf()
|
||||
guard let splitParent,
|
||||
let controller = splitParent.window?.windowController as? BaseTerminalController else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
if let view = controller.newSplit(
|
||||
at: parent,
|
||||
at: splitParent,
|
||||
direction: location.splitDirection!,
|
||||
baseConfig: config
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ extension NSApplication {
|
|||
|
||||
parentWindow = resolvedWindow
|
||||
} else {
|
||||
parentWindow = TerminalController.preferredParent?.window
|
||||
parentWindow = TerminalController.preferredNewTabParent?.window
|
||||
}
|
||||
|
||||
guard let createdController = TerminalController.newTab(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class ServiceProvider: NSObject {
|
|||
case .tab:
|
||||
_ = TerminalController.newTab(
|
||||
delegate.ghostty,
|
||||
from: TerminalController.preferredParent?.window,
|
||||
from: TerminalController.preferredNewTabParent?.window,
|
||||
withBaseConfig: config)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -753,6 +753,9 @@ class BaseTerminalController: NSWindowController,
|
|||
@objc private func ghosttySurfaceDragEndedNoTarget(_ notification: Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard let targetNode = surfaceTree.root?.node(view: target) else { return }
|
||||
guard let position = notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint else {
|
||||
return
|
||||
}
|
||||
|
||||
// If our tree isn't split, then we never create a new window, because
|
||||
// it is already a single split.
|
||||
|
|
@ -782,7 +785,7 @@ class BaseTerminalController: NSWindowController,
|
|||
_ = TerminalController.newWindow(
|
||||
ghostty,
|
||||
tree: newTree,
|
||||
position: notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint,
|
||||
position: position,
|
||||
confirmUndo: false,
|
||||
inheritBackgroundOpacity: isBackgroundOpaque)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,28 +215,43 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
|
||||
// Keep track of the last point that our window was launched at so that new
|
||||
// windows "cascade" over each other and don't just launch directly on top
|
||||
// of each other.
|
||||
private static var lastCascadePoint = NSPoint(x: 0, y: 0)
|
||||
|
||||
private static func applyCascade(to window: NSWindow, hasFixedPos: Bool) {
|
||||
if hasFixedPos { return }
|
||||
|
||||
if all.count > 1 {
|
||||
lastCascadePoint = window.cascadeTopLeft(from: lastCascadePoint)
|
||||
} else {
|
||||
// We assume the window frame is already correct at this point,
|
||||
// so we pass .zero to let cascade use the current frame position.
|
||||
lastCascadePoint = window.cascadeTopLeft(from: .zero)
|
||||
}
|
||||
// The preferred parent terminal controller for new window.
|
||||
private static var preferredNewWindowParent: TerminalController? {
|
||||
preferredParent(on: .main)
|
||||
}
|
||||
|
||||
// The preferred parent terminal controller.
|
||||
static var preferredParent: TerminalController? {
|
||||
all.first {
|
||||
$0.window?.isMainWindow ?? false
|
||||
} ?? lastMain ?? all.last
|
||||
// The preferred parent terminal controller for new tab.
|
||||
static var preferredNewTabParent: TerminalController? {
|
||||
// We choose a proper window on the current screen first.
|
||||
// If none is found, we use existing windows on another screen.
|
||||
// This could be changed to match `preferredNewWindowParent`,
|
||||
// but for now, we always use an existing window.
|
||||
preferredParent(on: .main) ?? preferredParent(on: nil)
|
||||
}
|
||||
|
||||
// The preferred parent terminal controller for new split.
|
||||
static var preferredNewSplitParent: TerminalController? {
|
||||
preferredNewTabParent
|
||||
}
|
||||
|
||||
/// Preferred parent terminal controller on specified screen
|
||||
private static func preferredParent(on screen: NSScreen?) -> TerminalController? {
|
||||
guard let screen else {
|
||||
return all.first {
|
||||
$0.window?.isMainWindow ?? false
|
||||
} ?? lastMain ?? all.last
|
||||
}
|
||||
return all.last {
|
||||
// find last main window in the screen first
|
||||
$0.window?.screen == screen && $0.window?.isMainWindow == true
|
||||
} ?? all.last {
|
||||
// if no main window was found(typically out of focus)
|
||||
// then just find the last visible window in the screen
|
||||
// AppKit will store closed window for a while,
|
||||
// We want to keep the first visible window
|
||||
// in the same spot
|
||||
$0.window?.screen == screen && $0.window?.isVisible == true
|
||||
}
|
||||
}
|
||||
|
||||
// The last controller to be main. We use this when paired with "preferredParent"
|
||||
|
|
@ -245,7 +260,19 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
// by something like an App Intent) then we prefer the most previous main.
|
||||
static private(set) weak var lastMain: TerminalController?
|
||||
|
||||
/// The "new window" action.
|
||||
/// Creates and presents a new terminal window.
|
||||
///
|
||||
/// The new window cascades from and inherits the style of the parent window.
|
||||
/// If the parent window is fullscreen, the new window will also enter fullscreen.
|
||||
/// If no parent is specified, the most recently focused window on the current screen is used.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - ghostty: The Ghostty application instance used to configure the new window.
|
||||
/// - baseConfig: An optional surface configuration to apply to the new terminal surface.
|
||||
/// If `nil`, the default configuration is used.
|
||||
/// - explicitParent: The parent window from which the new window should cascade and inherit its style.
|
||||
/// If `nil`, the last main window on the current screen is used.
|
||||
/// - Returns: The newly created `TerminalController` managing the new window.
|
||||
static func newWindow(
|
||||
_ ghostty: Ghostty.App,
|
||||
withBaseConfig baseConfig: Ghostty.SurfaceConfiguration? = nil,
|
||||
|
|
@ -255,7 +282,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
// Get our parent. Our parent is the one explicitly given to us,
|
||||
// otherwise the focused terminal, otherwise an arbitrary one.
|
||||
let parent: NSWindow? = explicitParent ?? preferredParent?.window
|
||||
let parent: NSWindow? = explicitParent ?? preferredNewWindowParent?.window
|
||||
if let parentController = parent?.windowController as? TerminalController {
|
||||
c.isBackgroundOpaque = parentController.isBackgroundOpaque
|
||||
}
|
||||
|
|
@ -288,13 +315,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
// that Cocoa is doing that we need to be after.
|
||||
c.scheduleInitialPresentation {
|
||||
c.showWindow(self)
|
||||
|
||||
// Only cascade if we aren't fullscreen.
|
||||
if let window = c.window {
|
||||
if !window.styleMask.contains(.fullScreen) {
|
||||
let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil
|
||||
// Only cascade if we aren't fullscreen, have a parent and don't have fixed position in the config.
|
||||
if
|
||||
let window = c.window,
|
||||
!window.styleMask.contains(.fullScreen),
|
||||
let parent,
|
||||
!hasFixedPos {
|
||||
c.window?.cascadeTopLeft(from: parent.topLeftForNextWindow())
|
||||
}
|
||||
|
||||
// All new_window actions force our app to be active, so that the new
|
||||
|
|
@ -322,7 +350,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
_ = TerminalController.newWindow(
|
||||
ghostty,
|
||||
withBaseConfig: baseConfig,
|
||||
withParent: explicitParent)
|
||||
withParent: explicitParent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -340,7 +369,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
static func newWindow(
|
||||
_ ghostty: Ghostty.App,
|
||||
tree: SplitTree<Ghostty.SurfaceView>,
|
||||
position: NSPoint? = nil,
|
||||
position: NSPoint,
|
||||
confirmUndo: Bool = true,
|
||||
inheritBackgroundOpacity: Bool? = nil
|
||||
) -> TerminalController {
|
||||
|
|
@ -362,13 +391,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
|
||||
if !window.styleMask.contains(.fullScreen) {
|
||||
if let position {
|
||||
window.setFrameTopLeftPoint(position)
|
||||
window.constrainToScreen()
|
||||
} else {
|
||||
let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
window.setFrameTopLeftPoint(position)
|
||||
window.constrainToScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -395,6 +419,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
_ = TerminalController.newWindow(
|
||||
ghostty,
|
||||
tree: tree,
|
||||
position: position,
|
||||
inheritBackgroundOpacity: inheritBackgroundOpacity
|
||||
)
|
||||
}
|
||||
|
|
@ -473,13 +498,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
// take effect. Our best theory is there is some next-event-loop-tick logic
|
||||
// that Cocoa is doing that we need to be after.
|
||||
controller.scheduleInitialPresentation {
|
||||
// Only cascade if we aren't fullscreen and are alone in the tab group.
|
||||
if !window.styleMask.contains(.fullScreen) &&
|
||||
window.tabGroup?.windows.count ?? 1 == 1 {
|
||||
let hasFixedPos = controller.derivedConfig.windowPositionX != nil && controller.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
|
||||
controller.showWindow(self)
|
||||
window.makeKeyAndOrderFront(self)
|
||||
|
||||
|
|
@ -1187,35 +1205,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
super.windowWillClose(notification)
|
||||
cancelPendingInitialPresentation()
|
||||
self.relabelTabs()
|
||||
|
||||
// If we remove a window, we reset the cascade point to the key window so that
|
||||
// the next window cascade's from that one.
|
||||
if let focusedWindow = NSApplication.shared.keyWindow {
|
||||
// If we are NOT the focused window, then we are a tabbed window. If we
|
||||
// are closing a tabbed window, we want to set the cascade point to be
|
||||
// the next cascade point from this window.
|
||||
if focusedWindow != window {
|
||||
// The cascadeTopLeft call below should NOT move the window. Starting with
|
||||
// macOS 15, we found that specifically when used with the new window snapping
|
||||
// features of macOS 15, this WOULD move the frame. So we keep track of the
|
||||
// old frame and restore it if necessary. Issue:
|
||||
// https://github.com/ghostty-org/ghostty/issues/2565
|
||||
let oldFrame = focusedWindow.frame
|
||||
|
||||
Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: .zero)
|
||||
|
||||
if focusedWindow.frame != oldFrame {
|
||||
focusedWindow.setFrame(oldFrame, display: true)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If we are the focused window, then we set the last cascade point to
|
||||
// our own frame so that it shows up in the same spot.
|
||||
let frame = focusedWindow.frame
|
||||
Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY)
|
||||
}
|
||||
}
|
||||
|
||||
override func windowDidBecomeKey(_ notification: Notification) {
|
||||
|
|
|
|||
|
|
@ -109,3 +109,20 @@ extension NSWindow {
|
|||
tabButtonHit(atScreenPoint: screenPoint)?.index
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cascade
|
||||
|
||||
extension NSWindow {
|
||||
/// Return the value of `cascadeTopLeft(from: .zero)` and make sure the window will not move
|
||||
func topLeftForNextWindow() -> CGPoint {
|
||||
// The cascadeTopLeft call below should NOT move the window. Starting with
|
||||
// macOS 15, we found that specifically when used with the new window snapping
|
||||
// features of macOS 15, this WOULD move the frame. So we keep track of the
|
||||
// old frame and restore it if necessary. Issue:
|
||||
// https://github.com/ghostty-org/ghostty/issues/2565
|
||||
let oldFrame = frame
|
||||
let cascaded = cascadeTopLeft(from: .zero)
|
||||
setFrame(oldFrame, display: true)
|
||||
return cascaded
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import Cocoa
|
|||
class LastWindowPosition {
|
||||
static let shared = LastWindowPosition()
|
||||
|
||||
private let positionKey = "NSWindowLastPosition"
|
||||
fileprivate static let rectsKey = "NSWindowLastRectsByScreen"
|
||||
|
||||
let defaults: UserDefaults
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func save(_ window: NSWindow?) -> Bool {
|
||||
|
|
@ -13,9 +19,13 @@ class LastWindowPosition {
|
|||
// with the wrong one when window decorations change while creating,
|
||||
// e.g. adding a toolbar affects the window's frame.
|
||||
guard let window, window.isVisible else { return false }
|
||||
let frame = window.frame
|
||||
let rect = [frame.origin.x, frame.origin.y, frame.size.width, frame.size.height]
|
||||
UserDefaults.ghostty.set(rect, forKey: positionKey)
|
||||
// We don't save the window frame when the window is in native fullscreen mode,
|
||||
// since AppKit doesn't restore .fullScreen correctly.
|
||||
// We should keep the behavior like first-party apps, such as Terminal and Safari.
|
||||
guard !window.styleMask.contains(.fullScreen), let screenID = window.screen?.displayUUID?.uuidString else {
|
||||
return false
|
||||
}
|
||||
savedWindowRectInfo[screenID] = window.frame
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -32,22 +42,22 @@ class LastWindowPosition {
|
|||
func restore(_ window: NSWindow, origin restoreOrigin: Bool = true, size restoreSize: Bool = true) -> Bool {
|
||||
guard restoreOrigin || restoreSize else { return false }
|
||||
|
||||
guard let values = UserDefaults.ghostty.array(forKey: positionKey) as? [Double],
|
||||
values.count >= 2 else { return false }
|
||||
|
||||
let lastPosition = CGPoint(x: values[0], y: values[1])
|
||||
|
||||
guard let screen = window.screen ?? NSScreen.main else { return false }
|
||||
guard
|
||||
let screen = window.screen ?? NSScreen.main,
|
||||
let screenID = screen.displayUUID?.uuidString,
|
||||
let lastFrame = savedWindowRectInfo[screenID]
|
||||
else {
|
||||
return false
|
||||
}
|
||||
let visibleFrame = screen.visibleFrame
|
||||
|
||||
var newFrame = window.frame
|
||||
if restoreOrigin {
|
||||
newFrame.origin = lastPosition
|
||||
newFrame.origin = lastFrame.origin
|
||||
}
|
||||
|
||||
if restoreSize, values.count >= 4 {
|
||||
newFrame.size.width = min(values[2], visibleFrame.width)
|
||||
newFrame.size.height = min(values[3], visibleFrame.height)
|
||||
if restoreSize {
|
||||
newFrame.size.width = min(lastFrame.width, visibleFrame.width)
|
||||
newFrame.size.height = min(lastFrame.height, visibleFrame.height)
|
||||
}
|
||||
|
||||
// If the new frame is not constrained to the visible screen,
|
||||
|
|
@ -65,3 +75,35 @@ class LastWindowPosition {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension LastWindowPosition {
|
||||
var savedWindowRectInfo: [String: CGRect] {
|
||||
get {
|
||||
guard
|
||||
let dict = defaults.dictionary(forKey: LastWindowPosition.rectsKey) as? [String: CFDictionary]
|
||||
else {
|
||||
// Restore previously saved rect on main screen
|
||||
if
|
||||
let rect = CGRect(valueArray: defaults.array(forKey: "NSWindowLastPosition")),
|
||||
let screenID = NSScreen.main?.displayUUID?.uuidString {
|
||||
return [screenID: rect]
|
||||
} else {
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
return dict.compactMapValues(CGRect.init(dictionaryRepresentation:))
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue.mapValues(\.dictionaryRepresentation), forKey: LastWindowPosition.rectsKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGRect {
|
||||
init?(valueArray: [Any]?) {
|
||||
guard let values = valueArray as? [Double], values.count >= 2 else {
|
||||
return nil
|
||||
}
|
||||
self.init(x: values[0], y: values[1], width: values[safe: 2] ?? 0, height: values[safe: 3] ?? 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// LastWindowPositionTests.swift
|
||||
// Ghostty
|
||||
//
|
||||
// Created by Lukas on 04.03.2026.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import AppKit
|
||||
@testable import Ghostty
|
||||
|
||||
@Suite(.serialized)
|
||||
struct LastWindowPositionTests {
|
||||
func usingTemporaryHelper(_ perform: (_ helper: LastWindowPosition) throws -> Void) rethrows {
|
||||
let defaults = MockDefaults()
|
||||
try perform(LastWindowPosition(defaults: defaults))
|
||||
defaults.reset()
|
||||
}
|
||||
|
||||
@Test func restorePoint() throws {
|
||||
try usingTemporaryHelper { helper in
|
||||
helper.defaults.set([20, 20], forKey: "NSWindowLastPosition")
|
||||
let rect = try #require(helper.savedWindowRectInfo.values.first)
|
||||
#expect(rect == CGRect(x: 20, y: 20, width: 0, height: 0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func restoreRect() throws {
|
||||
try usingTemporaryHelper { helper in
|
||||
helper.defaults.set([20, 20, 30, 30], forKey: "NSWindowLastPosition")
|
||||
let rect = try #require(helper.savedWindowRectInfo.values.first)
|
||||
#expect(rect == CGRect(x: 20, y: 20, width: 30, height: 30))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func restoreScreenByRect() throws {
|
||||
usingTemporaryHelper { helper in
|
||||
helper.defaults.set([
|
||||
"main": CGRect(x: 20, y: 20, width: 30, height: 30).dictionaryRepresentation
|
||||
], forKey: "NSWindowLastRectsByScreen")
|
||||
#expect(helper.savedWindowRectInfo == [
|
||||
"main": CGRect(x: 20, y: 20, width: 30, height: 30)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockDefaults: UserDefaults {
|
||||
private var values: [String: Any] = [:]
|
||||
|
||||
func reset() {
|
||||
values.removeAll()
|
||||
}
|
||||
|
||||
override func set(_ value: Any?, forKey defaultName: String) {
|
||||
values[defaultName] = value
|
||||
}
|
||||
|
||||
override func dictionary(forKey defaultName: String) -> [String: Any]? {
|
||||
values[defaultName] as? [String: Any]
|
||||
}
|
||||
|
||||
override func array(forKey defaultName: String) -> [Any]? {
|
||||
values[defaultName] as? [Any]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue