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
82 changes: 82 additions & 0 deletions Sources/CodeIsland/AllSpacesAnchor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import AppKit

/// Anchors a window into a dedicated private CoreGraphics "space" that sits far
/// above the ordinary per-desktop Space stack, instead of relying solely on
/// `NSWindow.collectionBehavior`'s `.canJoinAllSpaces`. Public collectionBehavior
/// tells WindowServer the window is a *member* of every Space, but the window is
/// still part of the normal per-Space window pool and can still be nudged during
/// a Space-swipe gesture. Anchoring it into a separate, always-shown space above
/// everything else keeps it outside that pool entirely — the same mechanism
/// several other menu-bar/notch overlay tools rely on for this exact guarantee.
///
/// This uses undocumented CoreGraphics ("CGS") symbols. They've been stable for
/// years across many shipping tools that use this pattern, but Apple could
/// change or remove them in a future release.
final class AllSpacesAnchor {
static let shared = AllSpacesAnchor()

/// Absolute level for the anchor space. Deliberately the maximum 32-bit
/// value CGS's level field accepts — the private API's underlying type
/// is narrower than Swift's `Int`, so this uses the exact tested constant
/// rather than `Int.max`.
private static let maxSpaceLevel = 2_147_483_647

private let spaceID: CGSSpaceID
private var anchoredWindows: Set<NSWindow> = []

private init() {
// The creation option MUST be 1 — with 0, Finder treats the new space
// as a real desktop and starts drawing desktop icons into it.
let desktopIconSuppressionFlag = 1
spaceID = CGSSpaceCreate(CGSMainConnection(), desktopIconSuppressionFlag, nil)
CGSSpaceSetAbsoluteLevel(CGSMainConnection(), spaceID, Self.maxSpaceLevel)
CGSShowSpaces(CGSMainConnection(), [spaceID])
}

/// Moves `window` into the anchor space. Idempotent.
func anchor(_ window: NSWindow) {
guard !anchoredWindows.contains(window) else { return }
anchoredWindows.insert(window)
CGSAddWindowsToSpaces(CGSMainConnection(), [window.windowNumber] as NSArray, [spaceID])
}

/// Removes `window` from the anchor space. Idempotent.
func release(_ window: NSWindow) {
guard anchoredWindows.remove(window) != nil else { return }
CGSRemoveWindowsFromSpaces(CGSMainConnection(), [window.windowNumber] as NSArray, [spaceID])
}

deinit {
CGSHideSpaces(CGSMainConnection(), [spaceID])
CGSSpaceDestroy(CGSMainConnection(), spaceID)
}
}

// MARK: - Private CoreGraphics Spaces bindings

private typealias CGSConnectionID = UInt
private typealias CGSSpaceID = UInt64

@_silgen_name("_CGSDefaultConnection")
private func CGSMainConnection() -> CGSConnectionID

@_silgen_name("CGSSpaceCreate")
private func CGSSpaceCreate(_ cid: CGSConnectionID, _ options: Int, _ properties: NSDictionary?) -> CGSSpaceID

@_silgen_name("CGSSpaceDestroy")
private func CGSSpaceDestroy(_ cid: CGSConnectionID, _ space: CGSSpaceID)

@_silgen_name("CGSSpaceSetAbsoluteLevel")
private func CGSSpaceSetAbsoluteLevel(_ cid: CGSConnectionID, _ space: CGSSpaceID, _ level: Int)

@_silgen_name("CGSAddWindowsToSpaces")
private func CGSAddWindowsToSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray)

@_silgen_name("CGSRemoveWindowsFromSpaces")
private func CGSRemoveWindowsFromSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray)

@_silgen_name("CGSHideSpaces")
private func CGSHideSpaces(_ cid: CGSConnectionID, _ spaces: NSArray)

@_silgen_name("CGSShowSpaces")
private func CGSShowSpaces(_ cid: CGSConnectionID, _ spaces: NSArray)
9 changes: 9 additions & 0 deletions Sources/CodeIsland/DiagnosticsExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ struct DiagnosticsExporter {
"hideWhenNoSession": UserDefaults.standard.bool(forKey: SettingsKey.hideWhenNoSession),
"collapseOnMouseLeave": UserDefaults.standard.bool(forKey: SettingsKey.collapseOnMouseLeave),
"smartSuppress": UserDefaults.standard.bool(forKey: SettingsKey.smartSuppress),
"openOnHover": UserDefaults.standard.object(forKey: SettingsKey.openOnHover) as? Bool
?? SettingsDefaults.openOnHover,
"hoverOpenDelay": HoverOpenDelay.clamped(
UserDefaults.standard.object(forKey: SettingsKey.hoverOpenDelay) as? Double
?? SettingsDefaults.hoverOpenDelay
),
"invertHorizontalSwipeDirection": UserDefaults.standard.object(
forKey: SettingsKey.invertHorizontalSwipeDirection
) as? Bool ?? SettingsDefaults.invertHorizontalSwipeDirection,
"sessionTimeout": UserDefaults.standard.integer(forKey: SettingsKey.sessionTimeout),
"maxVisibleSessions": UserDefaults.standard.integer(forKey: SettingsKey.maxVisibleSessions),
"mascotSpeed": UserDefaults.standard.integer(forKey: SettingsKey.mascotSpeed),
Expand Down
75 changes: 75 additions & 0 deletions Sources/CodeIsland/FourFingerSwipeGesture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import AppKit

enum FourFingerSwipeDirection: Equatable {
case left
case right
}

/// Pure threshold math for a raw four-finger trackpad swipe, independent of the
/// NSTouch plumbing that drives it. Watching for the touches directly (rather
/// than reacting to NSWorkspace.activeSpaceDidChangeNotification after the
/// fact) lets the panel start collapsing as the gesture crosses the threshold,
/// not only once macOS has already finished switching Spaces.
enum FourFingerSwipeGesture {
/// Fraction of the trackpad's normalized width (0...1) the four touches'
/// average position must move before the swipe counts as deliberate.
static let normalizedDisplacementThreshold: CGFloat = 0.10

/// Tolerance for floating-point rounding at the threshold boundary — touch
/// coordinates are sensor-derived doubles, so exact equality isn't meaningful.
private static let epsilon: CGFloat = 0.0001

static func direction(startX: CGFloat, currentX: CGFloat) -> FourFingerSwipeDirection? {
let movement = currentX - startX
if movement >= normalizedDisplacementThreshold - epsilon { return .right }
if movement <= -normalizedDisplacementThreshold + epsilon { return .left }
return nil
}
}

/// Tracks raw NSTouch events on whichever view owns it to detect a four-finger
/// horizontal swipe independent of AppKit's own gesture recognizers or of
/// macOS actually completing a Space switch. Delivery is scoped to the key
/// window per AppKit's touch-event model — this fires reliably while the
/// panel itself is key (e.g. right after the user opened it), which is
/// exactly the case this exists for: collapsing an open panel when the user
/// swipes away from it.
protocol FourFingerSwipeObserving: NSView {
var onFourFingerSwipeThresholdCrossed: (() -> Void)? { get set }
}

extension FourFingerSwipeObserving {
func fourFingerSwipeTouchesBegan(_ event: NSEvent, state: inout FourFingerSwipeTrackingState) {
let touches = event.touches(matching: .touching, in: self)
guard touches.count == 4 else {
state.reset()
return
}
state.startX = Self.averageX(of: touches)
state.triggered = false
}

func fourFingerSwipeTouchesMoved(_ event: NSEvent, state: inout FourFingerSwipeTrackingState) {
guard !state.triggered, let startX = state.startX else { return }
let touches = event.touches(matching: .touching, in: self)
guard touches.count == 4 else { return }
let currentX = Self.averageX(of: touches)
guard FourFingerSwipeGesture.direction(startX: startX, currentX: currentX) != nil else { return }
state.triggered = true
onFourFingerSwipeThresholdCrossed?()
}

private static func averageX(of touches: Set<NSTouch>) -> CGFloat {
touches.reduce(CGFloat.zero) { $0 + $1.normalizedPosition.x } / CGFloat(touches.count)
}
}

struct FourFingerSwipeTrackingState {
var startX: CGFloat?
var triggered = false

mutating func reset() {
startX = nil
triggered = false
}
}
11 changes: 11 additions & 0 deletions Sources/CodeIsland/IslandSurface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ enum IslandSurface: Equatable {

var isExpanded: Bool { self != .collapsed }

/// Whether this surface may be silently collapsed by something other than
/// direct user action (e.g. clicking outside the panel, switching desktops)
/// — approval/question cards represent a pending request the user still
/// needs to act on and must stay put until explicitly resolved.
var canAutoCollapse: Bool {
switch self {
case .collapsed, .approvalCard, .questionCard: return false
case .sessionList, .completionCard: return true
}
}

/// 当前 surface 关联的 session ID(如有)
var sessionId: String? {
switch self {
Expand Down
Loading