Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Drag a window to the edge of the screen. When the mouse cursor reaches the edge
| Bottom left, center, or right third | Respective third |
| Bottom left or right third, then drag to bottom center | First or last two thirds, respectively |

In **Snap Areas**, enable **Animate windows (experimental)** for smooth window resizing or **Blur footprint** for a blurred snap preview. Both are off by default. Animated snapping keeps the selected edge or center aligned when an app limits its window size or aspect ratio. Dragging a snapped window starts restoring its previous size as the window begins moving, and a quick release lets the remaining restore animation continue. Far-right title-bar grabs can still show visible jumps. Title-bar double-click maximize/restore follows the same animation setting and timing as snapping. Window animation has been tested in known scenarios, but remains experimental because repeatedly updating the window's size and position during the transition may cause issues in some edge cases.

### Ignore an app

Ignoring an app means that when the app is frontmost, keyboard shortcuts are un-registered from macOS. When the app is no longer frontmost, keyboard shortcuts are re-registered with macOS. This is useful for apps that have the same shortcuts like Rectangle and you do not want to change them.
Expand Down Expand Up @@ -84,7 +86,7 @@ Apple never released a public API for doing this. Rectangle Pro has next/prev Sp

### Windows overlap when using thirds or other small layouts

Some apps enforce a minimum window size that is larger than the requested layout. For example, a window with a minimum width of 600 points cannot fit a 504-point third of a display. Rectangle keeps the window on screen and briefly shows a “Window size limited” message when the app leaves it larger than the requested size. Use a larger layout, such as halves, or reduce the adjacent window manually. Rectangle cannot override an app's minimum window size.
Some apps enforce a minimum window size that is larger than the requested layout. For example, a window with a minimum width of 600 points cannot fit a 504-point third of a display. Rectangle keeps the window on screen and briefly shows a “Minimum window size reached” HUD with a window icon when the app leaves it larger than the requested size. Use a larger layout, such as halves, or reduce the adjacent window manually. Rectangle cannot override an app's minimum window size.

### Window resizing is off slightly for iTerm2

Expand Down
4 changes: 4 additions & 0 deletions Rectangle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
AC0100010000000000000001 /* WindowAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0100010000000000000002 /* WindowAnimator.swift */; };
01f52cab9bce43359775bdd7 /* JSONDefaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = de0950b8b00f40308934a523 /* JSONDefaultTests.swift */; };
05DBEA44B55E6F8C5FC3D6E0 /* MiddleLeftTwelfthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEABF10E66D648DDB37DABBA /* MiddleLeftTwelfthCalculation.swift */; };
0F29956694B5BB31EBDAE445 /* UpperMiddleRightSixteenthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B5B09465C38F8B766B3C3C /* UpperMiddleRightSixteenthCalculation.swift */; };
Expand Down Expand Up @@ -230,6 +231,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
AC0100010000000000000002 /* WindowAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAnimator.swift; sourceTree = "<group>"; };
16B4044DB4809116039F96EB /* SixteenthsRepeated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SixteenthsRepeated.swift; sourceTree = "<group>"; };
246168B345ECE6C375ACEEB7 /* BottomLeftTwelfthCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomLeftTwelfthCalculation.swift; sourceTree = "<group>"; };
30166BCF24F27D6A00A38608 /* SpecifiedCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecifiedCalculation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -631,6 +633,7 @@
children = (
9824704A22B189250037B409 /* BestEffortWindowMover.swift */,
9824704622B189240037B409 /* StandardWindowMover.swift */,
AC0100010000000000000002 /* WindowAnimator.swift */,
9824704722B189240037B409 /* WindowMover.swift */,
98B3559723CE025700E410E0 /* FixedSizeWindowMover.swift */,
);
Expand Down Expand Up @@ -1062,6 +1065,7 @@
9821403122B38A0500ABFB3F /* TopHalfCalculation.swift in Sources */,
9818E01428B5A4FD004AA524 /* SixthsCompoundCalculation.swift in Sources */,
9824704B22B189250037B409 /* StandardWindowMover.swift in Sources */,
AC0100010000000000000001 /* WindowAnimator.swift in Sources */,
9821402722B3888100ABFB3F /* ChangeSizeCalculation.swift in Sources */,
6490B39927BF97BB0056C220 /* TopCenterRightEighthCalculation.swift in Sources */,
98B3559823CE025700E410E0 /* FixedSizeWindowMover.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ class AccessibilityViewController: NSViewController {
@IBOutlet weak var padlockField: NSTextField!

override func viewDidLoad() {
if #available(OSX 13, *) {
sysPrefsPathField.stringValue = NSLocalizedString(
super.viewDidLoad()
if #available(macOS 27, *) {
sysPrefsPathField.stringValue = NSLocalizedString(
"Go to System Settings → Privacy & Security → Device Control and Data Access", tableName: "Main", value: "", comment: "")
} else if #available(macOS 13, *) {
sysPrefsPathField.stringValue = NSLocalizedString(
"Go to System Settings → Privacy & Security → Accessibility", tableName: "Main", value: "", comment: "")
}
if #available(macOS 13, *) {
openSysPrefsButton.title = NSLocalizedString(
"Open System Settings", tableName: "Main", value: "", comment: "")
padlockField.isHidden = true
}
sysPrefsPathField.preferredMaxLayoutWidth = 250
sysPrefsPathField.maximumNumberOfLines = 0
}

@IBAction func openSystemPrefs(_ sender: Any) {
Expand Down
80 changes: 71 additions & 9 deletions Rectangle/AccessibilityElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ class AccessibilityElement {
/// The Accessebility API only allows size & position adjustments individually.
/// To handle moving to different displays, we have to adjust the size then the position, then the size again since macOS will enforce sizes that fit on the current display.
/// When windows take a long time to adjust size & position, there is some visual stutter with doing each of these actions. The stutter can be slightly reduced by removing the initial size adjustment, which can make unsnap restore appear smoother.
func setFrame(_ frame: CGRect, adjustSizeFirst: Bool = true) {
func setFrame(_ frame: CGRect, adjustSizeFirst: Bool = true, adjustPosition: Bool = true) {
WindowAnimator.shared.cancel(for: self)
let appElement = applicationElement
let builtInAssistiveTechnologyEnabled = NSWorkspace.shared.isVoiceOverEnabled
|| NSWorkspace.shared.isSwitchControlEnabled
Expand All @@ -149,11 +150,60 @@ class AccessibilityElement {
if adjustSizeFirst {
size = frame.size
}
position = frame.origin
if adjustPosition { position = frame.origin }
size = frame.size
}
)
}

/// Holds the Enhanced UI policy for the transition; returns its cleanup closure.
func beginAnimatedAdjustment() -> () -> Void {
let appElement = applicationElement
let restore = Defaults.enhancedUI.value.beginWindowAdjustment(
bundleIdentifier: appElement?.bundleIdentifier,
builtInAssistiveTechnologyEnabled: NSWorkspace.shared.isVoiceOverEnabled
|| NSWorkspace.shared.isSwitchControlEnabled,
readEnhancedUI: { appElement?.enhancedUserInterface },
writeEnhancedUI: { appElement?.enhancedUserInterface = $0 }
)
// Bound AX calls so an unresponsive app cannot stall the animation.
setMessagingTimeout(0.05)
return { [self] in
setMessagingTimeout(0)
restore()
}
}

/// Writes one frame without readback; completion handles the final placement.
func setAnimationFrame(_ frame: CGRect, resizeOnly: Bool = false) -> Bool {
var size = frame.size
var position = frame.origin
guard let sizeValue = AXValueCreate(.cgSize, &size),
let positionValue = AXValueCreate(.cgPoint, &position) else { return false }
guard AXUIElementSetAttributeValue(wrappedElement, kAXSizeAttribute as CFString, sizeValue) == .success else { return false }
// Native dragging owns position during size restoration.
if !resizeOnly {
guard AXUIElementSetAttributeValue(wrappedElement, kAXPositionAttribute as CFString, positionValue) == .success else { return false }
}
return true
}

func setConstrainedAnimationFrame(_ frame: CGRect, placement: WindowAnimationPlacement,
origin: CGRect, progress: CGFloat) -> CGRect? {
var requestedSize = frame.size
guard let sizeValue = AXValueCreate(.cgSize, &requestedSize),
AXUIElementSetAttributeValue(wrappedElement, kAXSizeAttribute as CFString, sizeValue) == .success,
let actualSize = size,
actualSize.width.isFinite, actualSize.height.isFinite,
actualSize.width > 0, actualSize.height > 0 else { return nil }

// An accepted AX write can still be clamped by the app; position using the achieved size.
let resolved = placement.frame(for: frame, actualSize: actualSize, origin: origin, progress: progress)
var position = resolved.origin
guard let positionValue = AXValueCreate(.cgPoint, &position),
AXUIElementSetAttributeValue(wrappedElement, kAXPositionAttribute as CFString, positionValue) == .success else { return nil }
return resolved
}

private var childElements: [AccessibilityElement]? {
getElementsValue(.children)
Expand Down Expand Up @@ -540,22 +590,34 @@ enum EnhancedUI: Int {
bundleIdentifier: String?,
builtInAssistiveTechnologyEnabled: Bool,
readEnhancedUI: () -> Bool?,
writeEnhancedUI: (Bool) -> Void,
writeEnhancedUI: @escaping (Bool) -> Void,
adjustment: () -> Void
) {
let restore = beginWindowAdjustment(bundleIdentifier: bundleIdentifier,
builtInAssistiveTechnologyEnabled: builtInAssistiveTechnologyEnabled,
readEnhancedUI: readEnhancedUI,
writeEnhancedUI: writeEnhancedUI)
adjustment()
restore()
}

func beginWindowAdjustment(
bundleIdentifier: String?,
builtInAssistiveTechnologyEnabled: Bool,
readEnhancedUI: () -> Bool?,
writeEnhancedUI: @escaping (Bool) -> Void
) -> () -> Void {
let enhancedUIWasEnabled = readEnhancedUI()
if enhancedUIWasEnabled == true {
writeEnhancedUI(false)
}

adjustment()

if enhancedUIWasEnabled == true,
restoresEnhancedUI(
let shouldRestore = enhancedUIWasEnabled == true && restoresEnhancedUI(
bundleIdentifier: bundleIdentifier,
builtInAssistiveTechnologyEnabled: builtInAssistiveTechnologyEnabled
) {
writeEnhancedUI(true)
)
return {
if shouldRestore { writeEnhancedUI(true) }
}
}
}
Loading
Loading