Skip to content

Commit c933d24

Browse files
committed
Add command right click functionality for window resizing like in i3wm
1 parent e4ce549 commit c933d24

File tree

4 files changed

+171
-5
lines changed

4 files changed

+171
-5
lines changed

Sources/AppBundle/GlobalObserver.swift

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import AppKit
22
import Common
33

4+
@MainActor
45
class GlobalObserver {
5-
private static func onNotif(_ notification: Notification) {
6+
private static var cmdRightTap: CFMachPort? = nil
7+
private static var cmdRightTapSource: CFRunLoopSource? = nil
8+
private nonisolated static func onNotif(_ notification: Notification) {
69
// Third line of defence against lock screen window. See: closedWindowsCache
710
// Second and third lines of defence are technically needed only to avoid potential flickering
811
if (notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication)?.bundleIdentifier == lockScreenAppBundleId {
@@ -19,7 +22,7 @@ class GlobalObserver {
1922
}
2023
}
2124

22-
private static func onHideApp(_ notification: Notification) {
25+
private nonisolated static func onHideApp(_ notification: Notification) {
2326
let notifName = notification.name.rawValue
2427
Task { @MainActor in
2528
guard let token: RunSessionGuard = .isServerEnabled else { return }
@@ -75,5 +78,55 @@ class GlobalObserver {
7578
}
7679
}
7780
}
81+
82+
let mask = (
83+
(1 << CGEventType.rightMouseDown.rawValue) |
84+
(1 << CGEventType.rightMouseDragged.rawValue) |
85+
(1 << CGEventType.rightMouseUp.rawValue) |
86+
(1 << CGEventType.mouseMoved.rawValue) |
87+
(1 << CGEventType.flagsChanged.rawValue)
88+
)
89+
if cmdRightTap == nil,
90+
let tap = CGEvent.tapCreate(
91+
tap: .cgSessionEventTap,
92+
place: .headInsertEventTap,
93+
options: .defaultTap,
94+
eventsOfInterest: CGEventMask(mask),
95+
callback: { _, type, event, _ in
96+
if !TrayMenuModel.shared.isEnabled { return Unmanaged.passUnretained(event) }
97+
let flags = event.flags
98+
let isCmd = flags.contains(.maskCommand)
99+
guard isCmd else { return Unmanaged.passUnretained(event) }
100+
switch type {
101+
case .rightMouseDown:
102+
Task { @MainActor in await onCmdRightMouseDown() }
103+
return nil
104+
case .rightMouseDragged:
105+
Task { @MainActor in await onCmdRightMouseDragged() }
106+
return Unmanaged.passUnretained(event)
107+
case .mouseMoved:
108+
Task { @MainActor in await onCmdRightMouseDragged() }
109+
return Unmanaged.passUnretained(event)
110+
case .rightMouseUp:
111+
Task { @MainActor in await onCmdRightMouseUp() }
112+
return nil
113+
case .flagsChanged:
114+
if !isCmd {
115+
Task { @MainActor in await onCmdRightMouseUp() }
116+
}
117+
return Unmanaged.passUnretained(event)
118+
default:
119+
return Unmanaged.passUnretained(event)
120+
}
121+
},
122+
userInfo: nil
123+
)
124+
{
125+
cmdRightTap = tap
126+
let src = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
127+
cmdRightTapSource = src
128+
CFRunLoopAddSource(CFRunLoopGetCurrent(), src, .commonModes)
129+
CGEvent.tapEnable(tap: tap, enable: true)
130+
}
78131
}
79132
}

Sources/AppBundle/mouse/mouse.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import AppKit
22

33
@MainActor var currentlyManipulatedWithMouseWindowId: UInt32? = nil
4-
var isLeftMouseButtonDown: Bool { NSEvent.pressedMouseButtons == 1 }
4+
private let leftMouseButtonMask = 1 << 0
5+
private let rightMouseButtonMask = 1 << 1
6+
var isLeftMouseButtonDown: Bool { (NSEvent.pressedMouseButtons & leftMouseButtonMask) != 0 }
7+
var isRightMouseButtonDown: Bool { (NSEvent.pressedMouseButtons & rightMouseButtonMask) != 0 }
58

69
@MainActor
710
func isManipulatedWithMouse(_ window: Window) async throws -> Bool {
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
}

Sources/AppBundle/mouse/resizeWithMouse.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ private func resizeWithMouse(_ window: Window) async throws { // todo cover with
8080

8181
extension TreeNode {
8282
@MainActor
83-
fileprivate func getWeightBeforeResize(_ orientation: Orientation) -> CGFloat {
83+
func getWeightBeforeResize(_ orientation: Orientation) -> CGFloat {
8484
let currentWeight = getWeight(orientation) // Check assertions
8585
return getUserData(key: adaptiveWeightBeforeResizeWithMouseKey)
8686
?? (lastAppliedLayoutVirtualRect?.getDimension(orientation) ?? currentWeight)
8787
.also { putUserData(key: adaptiveWeightBeforeResizeWithMouseKey, data: $0) }
8888
}
8989

90-
fileprivate func resetResizeWeightBeforeResizeRecursive() {
90+
func resetResizeWeightBeforeResizeRecursive() {
9191
cleanUserData(key: adaptiveWeightBeforeResizeWithMouseKey)
9292
for child in children {
9393
child.resetResizeWeightBeforeResizeRecursive()

0 commit comments

Comments
 (0)