Skip to content

Commit

Permalink
Improve windows hiding mechanism
Browse files Browse the repository at this point in the history
_fixes #149

If the monitor configuration is correct, this commit _fixes
#289

I accidentally managed to reduce number of visible pixels to a single
1-pixel vertial line, so it paritally _fixes
#66
  • Loading branch information
nikitabobko committed Aug 3, 2024
1 parent c4f62a5 commit 96ae7a7
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 39 deletions.
2 changes: 1 addition & 1 deletion Sources/AppBundle/command/EnableCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct EnableCommand: Command {
} else {
activateMode(nil)
for workspace in Workspace.all {
workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideViaEmulation() } // todo as!
workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideFromCorner() } // todo as!
workspace.layoutWorkspace()
}
}
Expand Down
32 changes: 30 additions & 2 deletions Sources/AppBundle/layout/refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,42 @@ func refreshObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: Unsaf
refreshAndLayout()
}

enum OptimalHideCorner {
case bottomLeftCorner, bottomRightCorner
}

private func layoutWorkspaces() {
let monitors = monitors
var monitorToOptimalHideCorner: [CGPoint: OptimalHideCorner] = [:]
for monitor in monitors {
let xOff = monitor.width * 0.1
let yOff = monitor.height * 0.1
// brc = bottomRightCorner
let brc1 = monitor.rect.bottomRightCorner + CGPoint(x: 2, y: -yOff)
let brc2 = monitor.rect.bottomRightCorner + CGPoint(x: -xOff, y: 2)
let brc3 = monitor.rect.bottomRightCorner + CGPoint(x: 2, y: 2)

// blc = bottomLeftCorner
let blc1 = monitor.rect.bottomLeftCorner + CGPoint(x: -2, y: -yOff)
let blc2 = monitor.rect.bottomLeftCorner + CGPoint(x: xOff, y: 2)
let blc3 = monitor.rect.bottomLeftCorner + CGPoint(x: -2, y: 2)

let corner: OptimalHideCorner =
monitors.contains(where: { m in m.rect.contains(brc1) || m.rect.contains(brc2) || m.rect.contains(brc3) }) &&
monitors.allSatisfy({ m in !m.rect.contains(blc1) && !m.rect.contains(blc2) && !m.rect.contains(blc3) })
? .bottomLeftCorner
: .bottomRightCorner
monitorToOptimalHideCorner[monitor.rect.topLeftCorner] = corner
}

// to reduce flicker, first unhide visible workspaces, then hide invisible ones
for workspace in Workspace.all where workspace.isVisible {
// todo no need to unhide tiling windows (except for keeping hide/unhide state variables invariants)
workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideViaEmulation() } // todo as!
workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).unhideFromCorner() } // todo as!
}
for workspace in Workspace.all where !workspace.isVisible {
workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).hideViaEmulation() } // todo as!
let corner = monitorToOptimalHideCorner[workspace.workspaceMonitor.rect.topLeftCorner] ?? .bottomRightCorner
workspace.allLeafWindowsRecursive.forEach { ($0 as! MacWindow).hideInCorner(corner) } // todo as!
}

for monitor in monitors {
Expand Down
14 changes: 0 additions & 14 deletions Sources/AppBundle/model/Rect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@ struct Rect: Copyable {
var height: CGFloat
}

extension [Rect] {
func union() -> Rect {
let rects: [Rect] = self
let topLeftY = rects.map(\.minY).minOrThrow()
let topLeftX = rects.map(\.minX).maxOrThrow()
return Rect(
topLeftX: topLeftX,
topLeftY: topLeftY,
width: rects.map(\.maxX).maxOrThrow() - topLeftX,
height: rects.map(\.maxY).maxOrThrow() - topLeftY
)
}
}

extension CGRect {
func monitorFrameNormalized() -> Rect {
let mainMonitorHeight: CGFloat = mainMonitor.height
Expand Down
16 changes: 12 additions & 4 deletions Sources/AppBundle/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ final class MacWindow: Window, CustomStringConvertible {
return AXUIElementPerformAction(closeButton, kAXPressAction as CFString) == AXError.success
}

func hideViaEmulation() {
//guard let monitorApproximation else { return }
func hideInCorner(_ corner: OptimalHideCorner) {
guard let nodeMonitor else { return }
// Don't accidentally override prevUnhiddenEmulationPosition in case of subsequent
// `hideEmulation` calls
if !isHiddenViaEmulation {
Expand All @@ -116,10 +116,18 @@ final class MacWindow: Window, CustomStringConvertible {
prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect =
topLeftCorner - workspace.workspaceMonitor.rect.topLeftCorner
}
_ = setTopLeftCorner(allMonitorsRectsUnion.bottomRightCorner)
let p: CGPoint
switch corner {
case .bottomLeftCorner:
guard let s = getSize() else { fallthrough }
p = nodeMonitor.visibleRect.bottomLeftCorner + CGPoint(x: 1, y: -1) + CGPoint(x: -s.width, y: s.height)
case .bottomRightCorner:
p = nodeMonitor.visibleRect.bottomRightCorner - CGPoint(x: 1, y: 1)
}
_ = setTopLeftCorner(p)
}

func unhideViaEmulation() {
func unhideFromCorner() {
guard let prevUnhiddenEmulationPositionRelativeToWorkspaceAssignedRect else { return }
guard let workspace else { return } // hiding only makes sense for workspace windows

Expand Down
4 changes: 0 additions & 4 deletions Sources/AppBundle/util/util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ private func makeAllWindowsVisibleAndRestoreSize() {
}
}

var allMonitorsRectsUnion: Rect {
monitors.map(\.rect).union()
}

extension String? {
var isNilOrEmpty: Bool { self == nil || self?.isEmpty == true }
}
Expand Down
49 changes: 49 additions & 0 deletions docs/assets/monitor-arrangement-1-bad.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions docs/assets/monitor-arrangement-1-good.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions docs/assets/monitor-arrangement-2-bad.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions docs/assets/monitor-arrangement-2-good.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 63 additions & 14 deletions docs/guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -340,52 +340,101 @@ Native macOS Spaces have a lot of problems
* Apple doesn't provide public API to communicate with Spaces (create/delete/reorder/switch Space and move windows between Spaces)

Since Spaces are so hard to deal with, AeroSpace reimplements Spaces and calls them "Workspaces".
The idea is that if the workspace isn’t active then all of its windows are placed outside the visible area of the screen, in the bottom right corner.
The idea is that if the workspace isn’t active then all of its windows are placed outside the visible area of the screen, in the bottom right or left corner.
Once you switch back to the workspace, (e.g. by the means of xref:commands.adoc#workspace[workspace] command, or `cmd + tab`) windows are placed back to the visible area of the screen.

When you quit the AeroSpace or when the AeroSpace is about to crash, AeroSpace will place all windows back to the visible area of the screen.
When you quit the AeroSpace or when the AeroSpace detects that it's about to crash, AeroSpace will place all windows back to the visible area of the screen.

AeroSpace shows the name of currently active workspace in its tray icon (top right corner), to give users a visual feedback on what workspace is currently active.

NOTE: For better or worse, macOS doesn’t allow to place windows outside the visible area entirely.
You will still be able to see a few pixels of "hidden" windows in the bottom right corner of your screen.
The intended workflow of using AeroSpace workspaces is to only have one macOS Space (or as many monitors you have, if `Displays have separate Spaces` is enabled) and don’t interact with macOS Spaces anymore.

[NOTE]
====
For better or worse, macOS doesn’t allow to place windows outside the visible area entirely.
You will still be able to see a 1 pixel vertical line of "hidden" windows in the bottom right or left corner of your screen.
That means, that if AeroSpace crashes badly you will still be able to manually "unhide" the windows by dragging these few pixels to the center of the screen.
The intended workflow of using AeroSpace workspaces is to only have one macOS Space (or as many monitors you have, if `Displays have separate Spaces` is enabled) and don’t interact with macOS Spaces anymore.
If you want to minimize the visibility of hidden windows, it's recommended to place Dock in the bottom (and additionaly turn automatic hiding)
====


=== Proper monitor arrangement

Since AeroSpace needs a free space to hide windows in,
please make sure to arrange monitors in a way where *every monitor has free space in the bottom right or left corner.* (`System Settings -> Displays -> Arrange...`)

.Bad monitor arrangement. Monitor 2 doesn't have free space in either of the bottom corners
image::./assets/monitor-arrangement-1-bad.svg[,,align="center"]

.Good monitor arrangement. Every monitor has free space in either of the bottom corners
image::./assets/monitor-arrangement-1-good.svg[,,align="center"]

.Bad monitor arrangement. Monitor 1 doesn't have free space in either of the bottom corners
image::./assets/monitor-arrangement-2-bad.svg[,,align="center"]

.Good monitor arrangement. Every monitor has free space in either of the bottom corners
image::./assets/monitor-arrangement-2-good.svg[,,align="center"]

[#a-note-on-mission-control]
=== A note on mission control

For some reason, mission control doesn't like that AeroSpace puts a lot of windows in the bottom right corner of the screen.
Mission control shows windows too small even there is enough space to show them bigger.

To workaround, you can enable `System Settings -> Desktop & Dock -> Group windows by application` setting.
For some weird reason, it helps.
There is a workaround. You can enable `Group windows by application` setting:
[source,bash]
----
defaults write com.apple.dock "expose-group-apps" -bool "true" && killall Dock
----
(or in System Settings: `System Settings -> Desktop & Dock -> Group windows by application`). For whatever weird reason, it helps.

[#a-note-on-displays-have-separate-spaces]
=== A note on '`Displays have separate Spaces`'

AeroSpace doesn't care about `System Settings -> Desktop & Dock -> Displays have separate Spaces` setting.
It works equally good whether this option is enabled or disabled.
My opinion is that macOS works better and more stable if you disable `Displays have separate Spaces`. (It's enabled by default)
People report all sorts of weird issues related to focus and performance when this setting is enabled:

* Wrong window may receive focus in multi monitor setup: https:/nikitabobko/AeroSpace/issues/101[#101] (Bug in Apple API)
* Wrong borderless Alacritty window may receive focus in *single monitor* setup: https:/nikitabobko/AeroSpace/issues/247[#247] (Bug in Apple API)
* Performance issues: https:/nikitabobko/AeroSpace/issues/333[#333]
* macOS randomly switches focus back: https:/nikitabobko/AeroSpace/issues/289[#289]

Overview of '`Displays have separate Spaces`'
When `Displays have separate Spaces` is enabled,
moving windows between monitors causes windows to move between different Spaces which is not correctly handled by the public APIs AeroSpace uses,
apparently, these APIs are not aware about Spaces existence.
Spaces are just cursed in macOS. The less Spaces you have, the better macOS behaves.

|===
| |'`Displays have separate Spaces`' is enabled |'`Displays have separate Spaces`' is disabled

|Is it possible for window to span across several monitors?
|😡 No. macOS limitation
|👍 Yes

|Overal stability and performance
|😡 Weird focus and performance issues may happen (see the list above)
|👍 Public Apple API are more stable (which in turn affects AeroSpace stability)

|When the first monitor is in fullscreen
|👍 Second monitor operates independently
|😡 Second monitor is unusable black screen

|Is it possible for window to span across several monitors?
|😡 No
|👍 Yes

|macOS status bar ...
|... is displayed on both monitors
|... is displayed only on main monitor
|===

If you don't care about macOS native fullscreen in multi monitor setup (which is itself clunky anyway, since it creates a separate Space instance),
I recommend disabling `Displays have separate Spaces`.

You can disable the setting by running:
[source,bash]
----
defaults write com.apple.spaces "spans-displays" -bool "true" && killall SystemUIServer
----
(or in System Settings: `System Settings -> Desktop & Dock -> Displays have separate Spaces`). Logout is required for the setting to take effect.

== Callbacks

[#on-window-detected-callback]
Expand Down

0 comments on commit 96ae7a7

Please sign in to comment.