aerospace fork
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277import AppKit
extension Workspace { @MainActor func layoutWorkspace() async throws { if isEffectivelyEmpty { return } let rect = workspaceMonitor.visibleRectPaddedByOuterGaps // If monitors are aligned vertically and the monitor below has smaller width, then macOS may not allow the // window on the upper monitor to take full width. rect.height - 1 resolves this problem // But I also faced this problem in monitors horizontal configuration. ¯\_(ツ)_/¯ try await layoutRecursive(rect.topLeftCorner, width: rect.width, height: rect.height - 1, virtual: rect, LayoutContext(self)) }}
extension TreeNode { @MainActor fileprivate func layoutRecursive(_ point: CGPoint, width: CGFloat, height: CGFloat, virtual: Rect, _ context: LayoutContext) async throws { let physicalRect = Rect(topLeftX: point.x, topLeftY: point.y, width: width, height: height) switch nodeCases { case .workspace(let workspace): lastAppliedLayoutPhysicalRect = physicalRect lastAppliedLayoutVirtualRect = virtual try await workspace.rootTilingContainer.layoutRecursive(point, width: width, height: height, virtual: virtual, context) try await workspace.floatingWindowsContainer.layoutRecursive(point, width: width, height: height, virtual: virtual, context) case .floatingWindowsContainer(let container): for window in container.children.filterIsInstance(of: Window.self) { window.lastAppliedLayoutPhysicalRect = nil window.lastAppliedLayoutVirtualRect = nil try await window.layoutFloatingWindow(context) } case .window(let window): if window.windowId != currentlyManipulatedWithMouseWindowId { lastAppliedLayoutVirtualRect = virtual if window.isFullscreen && window == context.workspace.rootTilingContainer.mostRecentWindowRecursive { lastAppliedLayoutPhysicalRect = nil window.layoutFullscreen(context) } else { lastAppliedLayoutPhysicalRect = physicalRect window.isFullscreen = false window.setAxFrame(point, CGSize(width: width, height: height)) } } case .tilingContainer(let container): lastAppliedLayoutPhysicalRect = physicalRect lastAppliedLayoutVirtualRect = virtual switch container.layout { case .tiles: try await container.layoutTiles(point, width: width, height: height, virtual: virtual, context) case .accordion: try await container.layoutAccordion(point, width: width, height: height, virtual: virtual, context) case .masterStack: try await container.layoutMasterStack(point, width: width, height: height, virtual: virtual, context) } case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosPopupWindowsContainer, .macosHiddenAppsWindowsContainer: return // Nothing to do for weirdos } }}
private struct LayoutContext { let workspace: Workspace let resolvedGaps: ResolvedGaps
@MainActor init(_ workspace: Workspace) { self.workspace = workspace self.resolvedGaps = ResolvedGaps(gaps: config.gaps, monitor: workspace.workspaceMonitor) }}
extension Window { @MainActor fileprivate func layoutFloatingWindow(_ context: LayoutContext) async throws { let workspace = context.workspace
if config.centerFloatingWindows && windowId != currentlyManipulatedWithMouseWindowId { if let windowSize = try await getAxSize(.cancellable) ?? lastFloatingSize { let monitorRect = workspace.workspaceMonitor.visibleRect let topLeft = CGPoint( x: monitorRect.topLeftX + (monitorRect.width - windowSize.width) / 2, y: monitorRect.topLeftY + (monitorRect.height - windowSize.height) / 2, ) setAxFrame(topLeft, nil) } if isFullscreen { layoutFullscreen(context) isFullscreen = false } return }
let windowRect = try await getAxRect(.cancellable) // Probably not idempotent let currentMonitor = windowRect?.center.monitorApproximation if let currentMonitor, let windowRect, workspace != currentMonitor.activeWorkspace { let windowTopLeftCorner = windowRect.topLeftCorner let xProportion = (windowTopLeftCorner.x - currentMonitor.visibleRect.topLeftX) / currentMonitor.visibleRect.width let yProportion = (windowTopLeftCorner.y - currentMonitor.visibleRect.topLeftY) / currentMonitor.visibleRect.height
let workspaceRect = workspace.workspaceMonitor.visibleRect var newX = workspaceRect.topLeftX + xProportion * workspaceRect.width var newY = workspaceRect.topLeftY + yProportion * workspaceRect.height
let windowWidth = windowRect.width let windowHeight = windowRect.height newX = newX.coerce(in: workspaceRect.minX ... max(workspaceRect.minX, workspaceRect.maxX - windowWidth)) newY = newY.coerce(in: workspaceRect.minY ... max(workspaceRect.minY, workspaceRect.maxY - windowHeight))
setAxFrame(CGPoint(x: newX, y: newY), nil) } if isFullscreen { layoutFullscreen(context) isFullscreen = false } }
@MainActor fileprivate func layoutFullscreen(_ context: LayoutContext) { let monitorRect = noOuterGapsInFullscreen ? context.workspace.workspaceMonitor.visibleRect : context.workspace.workspaceMonitor.visibleRectPaddedByOuterGaps setAxFrame(monitorRect.topLeftCorner, CGSize(width: monitorRect.width, height: monitorRect.height)) }}
extension TilingContainer { @MainActor fileprivate func layoutTiles(_ point: CGPoint, width: CGFloat, height: CGFloat, virtual: Rect, _ context: LayoutContext) async throws { var point = point var virtualPoint = virtual.topLeftCorner
guard let delta = ((orientation == .h ? width : height) - CGFloat(children.sumOfDouble { $0.getWeight(orientation) })) .div(children.count) else { return }
let lastIndex = children.indices.last for (i, child) in children.enumerated() { child.setWeight(orientation, child.getWeight(orientation) + delta) let rawGap = context.resolvedGaps.inner.get(orientation).toDouble() // Gaps. Consider 4 cases: // 1. Multiple children. Layout first child // 2. Multiple children. Layout last child // 3. Multiple children. Layout child in the middle // 4. Single child let rawGap = gaps.inner.get(orientation).toDouble() let gap = rawGap - (i == 0 ? rawGap / 2 : 0) - (i == lastIndex ? rawGap / 2 : 0) try await child.layoutRecursive( i == 0 ? point : point.addingOffset(orientation, rawGap / 2), width: orientation == .h ? child.hWeight - gap : width, height: orientation == .v ? child.vWeight - gap : height, virtual: Rect( topLeftX: virtualPoint.x, topLeftY: virtualPoint.y, width: orientation == .h ? child.hWeight : width, height: orientation == .v ? child.vWeight : height, ), context, ) virtualPoint = orientation == .h ? virtualPoint.addingXOffset(child.hWeight) : virtualPoint.addingYOffset(child.vWeight) point = orientation == .h ? point.addingXOffset(child.hWeight) : point.addingYOffset(child.vWeight) } }
/// dwm-style master-stack layout: `children[0]` is the master window occupying an `mfact` share of the /// container's main axis; the remaining children ("stack") evenly share the rest, arranged along the cross axis. /// For horizontal containers, `config.masterPosition` chooses which side (left/right) the master area is on. /// For vertical containers, master is always on top (matches dwm/dwmac semantics). @MainActor fileprivate func layoutMasterStack(_ point: CGPoint, width: CGFloat, height: CGFloat, virtual: Rect, _ context: LayoutContext) async throws { let children = self.children if children.isEmpty { return }
if children.count == 1 { try await children[0].layoutRecursive(point, width: width, height: height, virtual: virtual, context) return }
let gapMain = CGFloat(context.resolvedGaps.inner.get(orientation)) // gap between master and stack areas let gapCross = CGFloat(context.resolvedGaps.inner.get(orientation.opposite)) // gap between stacked windows let mfact = self.mfact let stackCount = children.count - 1
let masterRect: Rect let stackRects: [Rect] let masterVirtual: Rect let stackVirtuals: [Rect]
switch orientation { case .h: let masterWidth = (width - gapMain) * mfact let stackWidth = width - masterWidth - gapMain let stackItemHeight = (height - CGFloat(stackCount - 1) * gapCross) / CGFloat(stackCount)
let masterVirtualWidth = width > 0 ? virtual.width * (masterWidth / width) : virtual.width let stackVirtualWidth = virtual.width - masterVirtualWidth let stackVirtualItemHeight = virtual.height / CGFloat(stackCount)
if config.masterPosition == .right { masterRect = Rect(topLeftX: point.x + stackWidth + gapMain, topLeftY: point.y, width: masterWidth, height: height) masterVirtual = Rect(topLeftX: virtual.topLeftX + stackVirtualWidth, topLeftY: virtual.topLeftY, width: masterVirtualWidth, height: virtual.height) stackRects = (0 ..< stackCount).map { i in Rect(topLeftX: point.x, topLeftY: point.y + CGFloat(i) * (stackItemHeight + gapCross), width: stackWidth, height: stackItemHeight) } stackVirtuals = (0 ..< stackCount).map { i in Rect(topLeftX: virtual.topLeftX, topLeftY: virtual.topLeftY + CGFloat(i) * stackVirtualItemHeight, width: stackVirtualWidth, height: stackVirtualItemHeight) } } else { masterRect = Rect(topLeftX: point.x, topLeftY: point.y, width: masterWidth, height: height) masterVirtual = Rect(topLeftX: virtual.topLeftX, topLeftY: virtual.topLeftY, width: masterVirtualWidth, height: virtual.height) stackRects = (0 ..< stackCount).map { i in Rect(topLeftX: point.x + masterWidth + gapMain, topLeftY: point.y + CGFloat(i) * (stackItemHeight + gapCross), width: stackWidth, height: stackItemHeight) } stackVirtuals = (0 ..< stackCount).map { i in Rect(topLeftX: virtual.topLeftX + masterVirtualWidth, topLeftY: virtual.topLeftY + CGFloat(i) * stackVirtualItemHeight, width: stackVirtualWidth, height: stackVirtualItemHeight) } } case .v: let masterHeight = (height - gapMain) * mfact let stackHeight = height - masterHeight - gapMain let stackItemWidth = (width - CGFloat(stackCount - 1) * gapCross) / CGFloat(stackCount)
let masterVirtualHeight = height > 0 ? virtual.height * (masterHeight / height) : virtual.height let stackVirtualHeight = virtual.height - masterVirtualHeight let stackVirtualItemWidth = virtual.width / CGFloat(stackCount)
masterRect = Rect(topLeftX: point.x, topLeftY: point.y, width: width, height: masterHeight) masterVirtual = Rect(topLeftX: virtual.topLeftX, topLeftY: virtual.topLeftY, width: virtual.width, height: masterVirtualHeight) stackRects = (0 ..< stackCount).map { i in Rect(topLeftX: point.x + CGFloat(i) * (stackItemWidth + gapCross), topLeftY: point.y + masterHeight + gapMain, width: stackItemWidth, height: stackHeight) } stackVirtuals = (0 ..< stackCount).map { i in Rect(topLeftX: virtual.topLeftX + CGFloat(i) * stackVirtualItemWidth, topLeftY: virtual.topLeftY + masterVirtualHeight, width: stackVirtualItemWidth, height: stackVirtualHeight) } }
try await children[0].layoutRecursive(masterRect.topLeftCorner, width: masterRect.width, height: masterRect.height, virtual: masterVirtual, context) for i in 0 ..< stackCount { try await children[i + 1].layoutRecursive( stackRects[i].topLeftCorner, width: stackRects[i].width, height: stackRects[i].height, virtual: stackVirtuals[i], context, ) } }
@MainActor fileprivate func layoutAccordion(_ point: CGPoint, width: CGFloat, height: CGFloat, virtual: Rect, _ context: LayoutContext) async throws { guard let mruIndex: Int = mostRecentChild?.ownIndex else { return } for (index, child) in children.enumerated() { let padding = CGFloat(config.accordionPadding) let (lPadding, rPadding): (CGFloat, CGFloat) = switch index { case 0 where children.count == 1: (0, 0) case 0: (0, padding) case children.indices.last: (padding, 0) case mruIndex - 1: (0, 2 * padding) case mruIndex + 1: (2 * padding, 0) default: (padding, padding) } switch orientation { case .h: try await child.layoutRecursive( point + CGPoint(x: lPadding, y: 0), width: width - rPadding - lPadding, height: height, virtual: virtual, context, ) case .v: try await child.layoutRecursive( point + CGPoint(x: 0, y: lPadding), width: width, height: height - lPadding - rPadding, virtual: virtual, context, ) } } }}