Skip to content
Draft
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
48 changes: 48 additions & 0 deletions Ice/Bridging/Bridging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ extension Bridging {
}
return rect
}

/// Marks a window as sticky across spaces.
///
/// This uses the private WindowServer sticky tag expected by the overlay
/// panels, so it should stay narrowly scoped to those panels.
static func setStickyAcrossSpaces(_ windowID: CGWindowID) {
var tags: UInt64 = 1 << 11
let result = CGSSetWindowTags(CGSMainConnectionID(), windowID, &tags, 1)
if result != .success {
Logger.bridging.error("CGSSetWindowTags failed with error \(result.logString)")
}
}
}

// MARK: Private Window List Helpers
Expand Down Expand Up @@ -244,6 +256,42 @@ extension Bridging {
let type = CGSSpaceGetType(CGSMainConnectionID(), spaceID)
return type == .fullscreen
}

/// Returns a Boolean value that indicates whether the current space for
/// the display with the given stable identifier is a fullscreen space.
///
/// This differs from ``activeSpaceID`` in multi-display setups, where each
/// display can show a different current space.
static func isCurrentSpaceFullscreen(forDisplayWithIdentifier displayIdentifier: String?) -> Bool? {
guard
let displayIdentifier,
let managedDisplays = CGSCopyManagedDisplaySpaces(CGSMainConnectionID())?.takeRetainedValue() as? [[String: Any]]
else {
return nil
}
guard
let managedDisplay = managedDisplays.first(where: { $0["Display Identifier"] as? String == displayIdentifier }),
let currentSpace = managedDisplay["Current Space"] as? [String: Any],
let rawType = currentSpace["type"].flatMap({ rawSpaceType(from: $0) }),
let type = CGSSpaceType(rawValue: rawType)
else {
return nil
}
return type == .fullscreen
}

private static func rawSpaceType(from value: Any) -> UInt32? {
switch value {
case let value as UInt32:
return value
case let value as Int:
return UInt32(value)
case let value as NSNumber:
return value.uint32Value
default:
return nil
}
}
}

// MARK: - Process Responsivity
Expand Down
11 changes: 11 additions & 0 deletions Ice/Bridging/Shims/Private.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ func CGSCopySpacesForWindows(
_ windowIDs: CFArray
) -> Unmanaged<CFArray>?

@_silgen_name("CGSCopyManagedDisplaySpaces")
func CGSCopyManagedDisplaySpaces(_ cid: CGSConnectionID) -> Unmanaged<CFArray>?

@_silgen_name("CGSSpaceGetType")
func CGSSpaceGetType(
_ cid: CGSConnectionID,
Expand Down Expand Up @@ -127,3 +130,11 @@ func CGSGetScreenRectForWindow(
_ wid: CGWindowID,
_ outRect: inout CGRect
) -> CGError

@_silgen_name("CGSSetWindowTags")
func CGSSetWindowTags(
_ cid: CGSConnectionID,
_ wid: CGWindowID,
_ tags: UnsafePointer<UInt64>,
_ tagCount: Int32
) -> CGError
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Ice
//

import AppKit
import CoreGraphics
import Foundation

Expand All @@ -13,6 +14,8 @@ struct MenuBarAppearanceConfigurationV2: Hashable {
var shapeKind: MenuBarShapeKind
var fullShapeInfo: MenuBarFullShapeInfo
var splitShapeInfo: MenuBarSplitShapeInfo
var screenShapeInfo: ScreenShapeInfo
var screenShapeOverrides: [String: ScreenShapeInfo]
var isInset: Bool
var isDynamic: Bool

Expand Down Expand Up @@ -45,11 +48,25 @@ extension MenuBarAppearanceConfigurationV2 {
shapeKind: .none,
fullShapeInfo: .default,
splitShapeInfo: .default,
screenShapeInfo: .default,
screenShapeOverrides: [:],
isInset: true,
isDynamic: false
)
}

// MARK: Screen shape helpers
extension MenuBarAppearanceConfigurationV2 {
/// Returns the screen shape settings for the given screen, falling back
/// to the default ``screenShapeInfo`` when there is no override.
func effectiveScreenShapeInfo(for screen: NSScreen) -> ScreenShapeInfo {
if let id = screen.stableIdentifier, let override = screenShapeOverrides[id] {
return override
}
return screenShapeInfo
}
}

extension MenuBarAppearanceConfigurationV2: Codable {
private enum CodingKeys: CodingKey {
case lightModeConfiguration
Expand All @@ -58,6 +75,8 @@ extension MenuBarAppearanceConfigurationV2: Codable {
case shapeKind
case fullShapeInfo
case splitShapeInfo
case screenShapeInfo
case screenShapeOverrides
case isInset
case isDynamic
}
Expand All @@ -71,6 +90,8 @@ extension MenuBarAppearanceConfigurationV2: Codable {
shapeKind: container.decodeIfPresent(MenuBarShapeKind.self, forKey: .shapeKind) ?? Self.defaultConfiguration.shapeKind,
fullShapeInfo: container.decodeIfPresent(MenuBarFullShapeInfo.self, forKey: .fullShapeInfo) ?? Self.defaultConfiguration.fullShapeInfo,
splitShapeInfo: container.decodeIfPresent(MenuBarSplitShapeInfo.self, forKey: .splitShapeInfo) ?? Self.defaultConfiguration.splitShapeInfo,
screenShapeInfo: container.decodeIfPresent(ScreenShapeInfo.self, forKey: .screenShapeInfo) ?? Self.defaultConfiguration.screenShapeInfo,
screenShapeOverrides: container.decodeIfPresent([String: ScreenShapeInfo].self, forKey: .screenShapeOverrides) ?? Self.defaultConfiguration.screenShapeOverrides,
isInset: container.decodeIfPresent(Bool.self, forKey: .isInset) ?? Self.defaultConfiguration.isInset,
isDynamic: container.decodeIfPresent(Bool.self, forKey: .isDynamic) ?? Self.defaultConfiguration.isDynamic
)
Expand All @@ -84,6 +105,8 @@ extension MenuBarAppearanceConfigurationV2: Codable {
try container.encode(shapeKind, forKey: .shapeKind)
try container.encode(fullShapeInfo, forKey: .fullShapeInfo)
try container.encode(splitShapeInfo, forKey: .splitShapeInfo)
try container.encode(screenShapeInfo, forKey: .screenShapeInfo)
try container.encode(screenShapeOverrides, forKey: .screenShapeOverrides)
try container.encode(isInset, forKey: .isInset)
try container.encode(isDynamic, forKey: .isDynamic)
}
Expand All @@ -96,6 +119,7 @@ struct MenuBarAppearancePartialConfiguration: Hashable {
var hasBorder: Bool
var borderColor: CGColor
var borderWidth: Double
var appearanceKind: MenuBarAppearanceKind
var tintKind: MenuBarTintKind
var tintColor: CGColor
var tintGradient: CustomGradient
Expand All @@ -108,8 +132,9 @@ extension MenuBarAppearancePartialConfiguration {
hasBorder: false,
borderColor: .black,
borderWidth: 1,
tintKind: .none,
tintColor: .black,
appearanceKind: .none,
tintKind: .solid,
tintColor: CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1),
tintGradient: .defaultMenuBarTint
)
}
Expand All @@ -124,19 +149,34 @@ extension MenuBarAppearancePartialConfiguration: Codable {
case shapeKind
case fullShapeInfo
case splitShapeInfo
case appearanceKind
case tintKind
case tintColor
case tintGradient
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let tintKind = try container.decodeIfPresent(MenuBarTintKind.self, forKey: .tintKind) ?? Self.defaultConfiguration.tintKind
let appearanceKind: MenuBarAppearanceKind = {
if let stored = try? container.decodeIfPresent(MenuBarAppearanceKind.self, forKey: .appearanceKind) {
return stored
}
// Legacy data has no appearanceKind. Derive it: a stored tintKind of
// .none means the user had no color treatment; .solid/.gradient maps
// to the pre-existing overlay-style behavior, i.e. .tint.
return tintKind == .none ? .none : .tint
}()
// Normalize legacy .none tintKind to .solid since the picker no longer
// offers .none. The appearanceKind above carries the intent.
let resolvedTintKind: MenuBarTintKind = tintKind == .none ? .solid : tintKind
try self.init(
hasShadow: container.decodeIfPresent(Bool.self, forKey: .hasShadow) ?? Self.defaultConfiguration.hasShadow,
hasBorder: container.decodeIfPresent(Bool.self, forKey: .hasBorder) ?? Self.defaultConfiguration.hasBorder,
borderColor: container.decodeIfPresent(CodableColor.self, forKey: .borderColor)?.cgColor ?? Self.defaultConfiguration.borderColor,
borderWidth: container.decodeIfPresent(Double.self, forKey: .borderWidth) ?? Self.defaultConfiguration.borderWidth,
tintKind: container.decodeIfPresent(MenuBarTintKind.self, forKey: .tintKind) ?? Self.defaultConfiguration.tintKind,
appearanceKind: appearanceKind,
tintKind: resolvedTintKind,
tintColor: container.decodeIfPresent(CodableColor.self, forKey: .tintColor)?.cgColor ?? Self.defaultConfiguration.tintColor,
tintGradient: container.decodeIfPresent(CustomGradient.self, forKey: .tintGradient) ?? Self.defaultConfiguration.tintGradient
)
Expand All @@ -148,6 +188,7 @@ extension MenuBarAppearancePartialConfiguration: Codable {
try container.encode(hasBorder, forKey: .hasBorder)
try container.encode(CodableColor(cgColor: borderColor), forKey: .borderColor)
try container.encode(borderWidth, forKey: .borderWidth)
try container.encode(appearanceKind, forKey: .appearanceKind)
try container.encode(tintKind, forKey: .tintKind)
try container.encode(CodableColor(cgColor: tintColor), forKey: .tintColor)
try container.encode(tintGradient, forKey: .tintGradient)
Expand Down
Loading