加这个设置时只改了三处调用 activeScreen() 的地方,漏了控制器里的 followCursorAcrossScreens() —— 它直接调 screenUnderCursor(),名字对不上 所以 grep 没找到。用户在界面上关掉跟随,岛照样跑到外接屏压住浏览器标签栏。 除了补 guard,把 NotchGeometry.screenUnderCursor() 收成 private: 绕过设置去取屏现在在编译期就不成立,而不是靠下一个人记得加判断。 新增的那条测试特意标注了它的局限——检出能力取决于跑测试时光标在哪块屏, 单屏机器上永远绿。真正的护栏是 private 那条,别把它当保证。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
4.5 KiB
Swift
98 lines
4.5 KiB
Swift
import AppKit
|
||
|
||
/// 一块屏幕的刘海几何。
|
||
///
|
||
/// **所有布局都必须写成这里几个属性的函数,绝不能出现字面量。**
|
||
/// 用户改显示分辨率缩放时刘海的 pt 尺寸会变;换机器更是完全不同的值。
|
||
///
|
||
/// 本机实测(16" M4 Max,默认缩放 1728×1117):
|
||
/// - 内建屏:notchWidth = 185,notchHeight = 32,菜单栏高 33,中心 X = 864
|
||
/// - 外接 LG 4K(1920×1080):无刘海,safeAreaInsets.top = 0
|
||
///
|
||
/// 注意实测值和常见的"200×37"传闻并不一致 —— 这正是不能硬编码的理由。
|
||
public struct NotchGeometry: Equatable, Sendable {
|
||
public let screenFrame: CGRect
|
||
public let notchWidth: CGFloat
|
||
public let notchHeight: CGFloat
|
||
/// 刘海的**真实**中心(屏幕坐标)。
|
||
///
|
||
/// 不能直接用 `screenFrame.midX`:本机实测左侧可用区宽 771、右侧 772,
|
||
/// 刘海中心是 863.5 而屏幕中心是 864 —— 差半个点。数值不大,但岛是**贴着**
|
||
/// 刘海画的,任何偏移都会在交界处露出一条不对称的边。既然系统把两侧的
|
||
/// 真实宽度都给了,就没有理由去假设它对称。
|
||
public let notchCenterX: CGFloat
|
||
|
||
public var hasNotch: Bool { notchWidth > 0 && notchHeight > 0 }
|
||
|
||
/// 岛的对称轴。有刘海时贴刘海中心,无刘海时用屏幕中心。
|
||
public var centerX: CGFloat { hasNotch ? notchCenterX : screenFrame.midX }
|
||
|
||
/// 屏幕顶边(Cocoa 坐标系,y 向上)。
|
||
public var topY: CGFloat { screenFrame.maxY }
|
||
|
||
public init(
|
||
screenFrame: CGRect,
|
||
notchWidth: CGFloat,
|
||
notchHeight: CGFloat,
|
||
notchCenterX: CGFloat? = nil
|
||
) {
|
||
self.screenFrame = screenFrame
|
||
self.notchWidth = notchWidth
|
||
self.notchHeight = notchHeight
|
||
self.notchCenterX = notchCenterX ?? screenFrame.midX
|
||
}
|
||
|
||
public init(screen: NSScreen) {
|
||
self.screenFrame = screen.frame
|
||
|
||
// auxiliaryTopLeftArea / auxiliaryTopRightArea 是刘海**两侧**的可用菜单栏区域。
|
||
// 两者都存在才说明有刘海;无刘海的屏幕这两个属性返回 nil。
|
||
if let left = screen.auxiliaryTopLeftArea,
|
||
let right = screen.auxiliaryTopRightArea {
|
||
let width = max(0, screen.frame.width - left.width - right.width)
|
||
self.notchWidth = width
|
||
self.notchHeight = screen.safeAreaInsets.top
|
||
// 左侧可用区的右边界就是刘海的左沿,加半个刘海宽即中心。
|
||
self.notchCenterX = left.maxX + width / 2
|
||
} else {
|
||
self.notchWidth = 0
|
||
self.notchHeight = 0
|
||
self.notchCenterX = screen.frame.midX
|
||
}
|
||
}
|
||
|
||
/// 当前应该承载岛的屏幕。
|
||
///
|
||
/// 默认取内建屏(有刘海那块)—— 心智模型是"岛长在刘海上",
|
||
/// 它不该因为鼠标移到副屏就跟着跑。找不到带刘海的屏幕就退回主屏。
|
||
public static func preferredScreen() -> NSScreen? {
|
||
NSScreen.screens.first { $0.auxiliaryTopLeftArea != nil } ?? NSScreen.main
|
||
}
|
||
|
||
/// 岛该出现在哪块屏上。
|
||
///
|
||
/// `followsCursor = false`(默认)固定在刘海屏;true 才跟着光标走。
|
||
///
|
||
/// 两边都有代价,所以做成设置而不是选一个:
|
||
/// - 固定在刘海屏:用户在外接屏工作时看不见岛("我可能关注其他屏幕就没看到")
|
||
/// - 跟随光标:外接屏没刘海,岛走悬浮胶囊形态,正好压在浏览器标签栏那一带,
|
||
/// **底下本来就有东西**,用户点标签页点不动
|
||
///
|
||
/// 默认取前者:看不见只是少了个提醒,点不动是功能被夺走,后者更不可接受。
|
||
public static func screen(followsCursor: Bool) -> NSScreen? {
|
||
followsCursor ? (screenUnderCursor() ?? preferredScreen()) : preferredScreen()
|
||
}
|
||
|
||
/// **刻意是 private。** 想拿屏幕请走 `IslandPlacementStore.screen()`。
|
||
///
|
||
/// 它以前是 public,于是控制器里的 `followCursorAcrossScreens()` 直接调了它,
|
||
/// 绕过「固定在刘海屏」这个设置 —— 用户在设置里关掉跟随,岛照样跟着光标跑。
|
||
/// 加设置那次没发现,因为搜的是 `activeScreen()` 的调用点,名字对不上。
|
||
///
|
||
/// 收成 private 之后,这条绕行路在编译期就不存在了。
|
||
private static func screenUnderCursor() -> NSScreen? {
|
||
let point = NSEvent.mouseLocation
|
||
return NSScreen.screens.first { $0.frame.contains(point) } ?? NSScreen.main
|
||
}
|
||
}
|