|
| 1 | +import AppKit |
| 2 | +import Common |
| 3 | + |
| 4 | + |
| 5 | +private enum ResizeEdge { case left, right, up, down } |
| 6 | + |
| 7 | +@MainActor |
| 8 | +private struct CmdRightResizeSession { |
| 9 | + let windowId: UInt32 |
| 10 | + let startPoint: CGPoint |
| 11 | + let edge: ResizeEdge |
| 12 | +} |
| 13 | + |
| 14 | +@MainActor |
| 15 | +private var cmdRightResizeSession: CmdRightResizeSession? = nil |
| 16 | +@MainActor |
| 17 | +private var pendingDragRefreshTask: Task<(), Never>? = nil |
| 18 | + |
| 19 | +private func edgeToDirection(_ e: ResizeEdge) -> CardinalDirection { |
| 20 | + switch e { case .left: .left; case .right: .right; case .up: .up; case .down: .down } |
| 21 | +} |
| 22 | +private func oppositeEdge(_ e: ResizeEdge) -> ResizeEdge { |
| 23 | + switch e { case .left: .right; case .right: .left; case .up: .down; case .down: .up } |
| 24 | +} |
| 25 | + |
| 26 | +@MainActor |
| 27 | +private func resolveParentAndNeighbor(_ window: Window, _ direction: CardinalDirection) -> (parent: TilingContainer, ownIndex: Int, neighborIndex: Int, orientation: Orientation)? { |
| 28 | + guard let (parent, ownIndex) = window.closestParent(hasChildrenInDirection: direction, withLayout: .tiles) else { return nil } |
| 29 | + let neighborIndex = ownIndex + direction.focusOffset |
| 30 | + guard parent.children.indices.contains(neighborIndex) else { return nil } |
| 31 | + return (parent, ownIndex, neighborIndex, parent.orientation) |
| 32 | +} |
| 33 | + |
| 34 | +@MainActor |
| 35 | +func onCmdRightMouseDown() async { |
| 36 | + guard cmdRightResizeSession == nil else { return } |
| 37 | + let point = mouseLocation |
| 38 | + let targetWorkspace = point.monitorApproximation.activeWorkspace |
| 39 | + guard let window = point.findIn(tree: targetWorkspace.rootTilingContainer, virtual: false) else { return } |
| 40 | + guard let rect = window.lastAppliedLayoutPhysicalRect else { return } |
| 41 | + |
| 42 | + let distances: [(ResizeEdge, CGFloat)] = [ |
| 43 | + (.left, abs(point.x - rect.minX)), |
| 44 | + (.right, abs(point.x - rect.maxX)), |
| 45 | + (.up, abs(point.y - rect.minY)), |
| 46 | + (.down, abs(point.y - rect.maxY)), |
| 47 | + ] |
| 48 | + var edge = distances.min(by: { $0.1 < $1.1 })!.0 |
| 49 | + |
| 50 | + func hasNeighbor(_ e: ResizeEdge) -> Bool { resolveParentAndNeighbor(window, edgeToDirection(e)) != nil } |
| 51 | + if !hasNeighbor(edge), hasNeighbor(oppositeEdge(edge)) { edge = oppositeEdge(edge) } |
| 52 | + if !hasNeighbor(edge) { return } |
| 53 | + |
| 54 | + currentlyManipulatedWithMouseWindowId = window.windowId |
| 55 | + cmdRightResizeSession = CmdRightResizeSession(windowId: window.windowId, startPoint: point, edge: edge) |
| 56 | +} |
| 57 | + |
| 58 | +@MainActor |
| 59 | +func onCmdRightMouseDragged() async { |
| 60 | + guard let session = cmdRightResizeSession else { return } |
| 61 | + guard let window = Window.get(byId: session.windowId) else { return } |
| 62 | + |
| 63 | + let point = mouseLocation |
| 64 | + let direction = edgeToDirection(session.edge) |
| 65 | + let delta: CGFloat = (direction.orientation == .h) ? (point.x - session.startPoint.x) : (point.y - session.startPoint.y) |
| 66 | + let diff: CGFloat = direction.isPositive ? delta : -delta |
| 67 | + |
| 68 | + guard let (parent, _, neighborIndex, orientation) = resolveParentAndNeighbor(window, direction) else { return } |
| 69 | + if abs(diff) < 1 { return } |
| 70 | + |
| 71 | + window.parentsWithSelf.lazy |
| 72 | + .prefix(while: { $0 !== parent }) |
| 73 | + .compactMap { node -> TreeNode? in |
| 74 | + let p = node.parent as? TilingContainer |
| 75 | + return (p?.orientation == orientation && p?.layout == .tiles) ? node : nil |
| 76 | + } |
| 77 | + .forEach { $0.setWeight(orientation, $0.getWeightBeforeResize(orientation) + diff) } |
| 78 | + |
| 79 | + let sibling = parent.children[neighborIndex] |
| 80 | + sibling.setWeight(orientation, sibling.getWeightBeforeResize(orientation) - diff) |
| 81 | + |
| 82 | + currentlyManipulatedWithMouseWindowId = window.windowId |
| 83 | + scheduleThrottledRefresh() |
| 84 | +} |
| 85 | + |
| 86 | +@MainActor |
| 87 | +func onCmdRightMouseUp() async { |
| 88 | + cmdRightResizeSession = nil |
| 89 | + pendingDragRefreshTask?.cancel() |
| 90 | + pendingDragRefreshTask = nil |
| 91 | + try? await resetManipulatedWithMouseIfPossible() |
| 92 | +} |
| 93 | + |
| 94 | +@MainActor |
| 95 | +private func scheduleThrottledRefresh() { |
| 96 | + if pendingDragRefreshTask != nil { return } |
| 97 | + pendingDragRefreshTask = Task { @MainActor in |
| 98 | + try? await Task.sleep(for: preferredFrameDuration()) |
| 99 | + runRefreshSession(.globalObserver("cmdRightMouseDragged"), optimisticallyPreLayoutWorkspaces: true) |
| 100 | + pendingDragRefreshTask = nil |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +@MainActor |
| 105 | +private func preferredFrameDuration() -> Duration { |
| 106 | + let maxFps = NSScreen.screens.map { $0.maximumFramesPerSecond }.max() ?? 60 |
| 107 | + let fps = max(maxFps, 1) |
| 108 | + let nanosPerFrame = 1_000_000_000 / fps |
| 109 | + return .nanoseconds(nanosPerFrame) |
| 110 | +} |
0 commit comments