Files
wuguanganandClaude Opus 5 61aaea6062 fix: 关掉「跟随光标」后灵动岛仍然跟着光标跑
加这个设置时只改了三处调用 activeScreen() 的地方,漏了控制器里的
followCursorAcrossScreens() —— 它直接调 screenUnderCursor(),名字对不上
所以 grep 没找到。用户在界面上关掉跟随,岛照样跑到外接屏压住浏览器标签栏。

除了补 guard,把 NotchGeometry.screenUnderCursor() 收成 private:
绕过设置去取屏现在在编译期就不成立,而不是靠下一个人记得加判断。

新增的那条测试特意标注了它的局限——检出能力取决于跑测试时光标在哪块屏,
单屏机器上永远绿。真正的护栏是 private 那条,别把它当保证。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:49:57 +08:00

98 lines
4.5 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}