mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update HUDs for external display parity
This commit is contained in:
parent
e0da879823
commit
9c044237c1
4 changed files with 100 additions and 34 deletions
|
|
@ -257,22 +257,33 @@ struct ContentView: View {
|
||||||
/// Whether the current screen should render as a Dynamic Island pill
|
/// Whether the current screen should render as a Dynamic Island pill
|
||||||
/// rather than the standard notch shape. Always false on physical notch screens.
|
/// rather than the standard notch shape. Always false on physical notch screens.
|
||||||
private var isDynamicIslandMode: Bool {
|
private var isDynamicIslandMode: Bool {
|
||||||
shouldUseDynamicIslandMode(for: vm.screen ?? coordinator.selectedScreen)
|
shouldUseDynamicIslandMode(for: currentScreenName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentScreenName: String {
|
||||||
|
vm.screen ?? coordinator.selectedScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the current screen lacks a physical notch.
|
/// Whether the current screen lacks a physical notch.
|
||||||
private var isNonNotchScreen: Bool {
|
private var isNonNotchScreen: Bool {
|
||||||
let screenName = vm.screen ?? coordinator.selectedScreen
|
guard let screen = NSScreen.screens.first(where: { $0.localizedName == currentScreenName }) else {
|
||||||
guard let screen = NSScreen.screens.first(where: { $0.localizedName == screenName }) else {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return screen.safeAreaInsets.top <= 0
|
return screen.safeAreaInsets.top <= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the global sneak peek is visible on this specific screen.
|
||||||
|
private var isSneakPeekVisibleOnCurrentScreen: Bool {
|
||||||
|
guard coordinator.sneakPeek.show else { return false }
|
||||||
|
guard Defaults[.showOnAllDisplays] else { return true }
|
||||||
|
guard let targetScreenName = coordinator.sneakPeek.targetScreenName else { return true }
|
||||||
|
return currentScreenName == targetScreenName
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the notch/island should hide off-screen when closed on a non-notch display.
|
/// Whether the notch/island should hide off-screen when closed on a non-notch display.
|
||||||
/// Temporarily reveals the notch when a sneakPeek HUD (volume, brightness, music, etc.) is active.
|
/// Temporarily reveals the notch when a sneakPeek HUD (volume, brightness, music, etc.) is active.
|
||||||
private var shouldHideUntilHover: Bool {
|
private var shouldHideUntilHover: Bool {
|
||||||
hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed && !coordinator.sneakPeek.show
|
hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed && !isSneakPeekVisibleOnCurrentScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pill shape for Dynamic Island mode with animated corner radius transitions.
|
/// Pill shape for Dynamic Island mode with animated corner radius transitions.
|
||||||
|
|
@ -696,7 +707,7 @@ struct ContentView: View {
|
||||||
.frame(width: 76, alignment: .trailing)
|
.frame(width: 76, alignment: .trailing)
|
||||||
}
|
}
|
||||||
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
||||||
} else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && !coordinator.sneakPeek.type.isExtensionPayload && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) {
|
} else if isSneakPeekVisibleOnCurrentScreen && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && !coordinator.sneakPeek.type.isExtensionPayload && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) {
|
||||||
InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
|
InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
|
||||||
.transition(
|
.transition(
|
||||||
coordinator.sneakPeek.type == .capsLock
|
coordinator.sneakPeek.type == .capsLock
|
||||||
|
|
@ -743,7 +754,7 @@ struct ContentView: View {
|
||||||
Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight)
|
Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
if coordinator.sneakPeek.show {
|
if isSneakPeekVisibleOnCurrentScreen {
|
||||||
if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .capsLock) && !coordinator.sneakPeek.type.isExtensionPayload && !Defaults[.inlineHUD] && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) {
|
if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .capsLock) && !coordinator.sneakPeek.type.isExtensionPayload && !Defaults[.inlineHUD] && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) {
|
||||||
SystemEventIndicatorModifier(eventType: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, sendEventBack: { _ in
|
SystemEventIndicatorModifier(eventType: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, sendEventBack: { _ in
|
||||||
//
|
//
|
||||||
|
|
@ -1637,7 +1648,7 @@ struct ContentView: View {
|
||||||
let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive && !enableMinimalisticUI
|
let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive && !enableMinimalisticUI
|
||||||
|
|
||||||
guard vm.notchState == .closed,
|
guard vm.notchState == .closed,
|
||||||
!coordinator.sneakPeek.show,
|
!isSneakPeekVisibleOnCurrentScreen,
|
||||||
(Defaults[.openNotchOnHover] || shouldFocusTimerTab) else { return }
|
(Defaults[.openNotchOnHover] || shouldFocusTimerTab) else { return }
|
||||||
|
|
||||||
hoverTask = Task {
|
hoverTask = Task {
|
||||||
|
|
@ -1647,7 +1658,7 @@ struct ContentView: View {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard self.vm.notchState == .closed,
|
guard self.vm.notchState == .closed,
|
||||||
self.isHovering,
|
self.isHovering,
|
||||||
!self.coordinator.sneakPeek.show else { return }
|
!self.isSneakPeekVisibleOnCurrentScreen else { return }
|
||||||
|
|
||||||
if shouldFocusTimerTab {
|
if shouldFocusTimerTab {
|
||||||
withAnimation(.smooth) {
|
withAnimation(.smooth) {
|
||||||
|
|
@ -2162,7 +2173,7 @@ struct ContentView: View {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private func shouldFixSizeForSneakPeek() -> Bool {
|
private func shouldFixSizeForSneakPeek() -> Bool {
|
||||||
guard coordinator.sneakPeek.show else { return false }
|
guard isSneakPeekVisibleOnCurrentScreen else { return false }
|
||||||
let style = resolvedSneakPeekStyle()
|
let style = resolvedSneakPeekStyle()
|
||||||
|
|
||||||
// Check for extension sneak peek
|
// Check for extension sneak peek
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ struct sneakPeek {
|
||||||
var subtitle: String = ""
|
var subtitle: String = ""
|
||||||
var accentColor: Color?
|
var accentColor: Color?
|
||||||
var styleOverride: SneakPeekStyle? = nil
|
var styleOverride: SneakPeekStyle? = nil
|
||||||
|
var targetScreenName: String? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BrowserType {
|
enum BrowserType {
|
||||||
|
|
@ -323,7 +324,8 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
title: String = "",
|
title: String = "",
|
||||||
subtitle: String = "",
|
subtitle: String = "",
|
||||||
accentColor: Color? = nil,
|
accentColor: Color? = nil,
|
||||||
styleOverride: SneakPeekStyle? = nil
|
styleOverride: SneakPeekStyle? = nil,
|
||||||
|
onScreen targetScreen: NSScreen? = nil
|
||||||
) {
|
) {
|
||||||
let resolvedDuration: TimeInterval
|
let resolvedDuration: TimeInterval
|
||||||
switch type {
|
switch type {
|
||||||
|
|
@ -360,6 +362,7 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
self.sneakPeek.subtitle = subtitle
|
self.sneakPeek.subtitle = subtitle
|
||||||
self.sneakPeek.accentColor = accentColor
|
self.sneakPeek.accentColor = accentColor
|
||||||
self.sneakPeek.styleOverride = styleOverride
|
self.sneakPeek.styleOverride = styleOverride
|
||||||
|
self.sneakPeek.targetScreenName = targetScreen?.localizedName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
let category = classifyControlTarget(osd.controlTarget, systemIconID: osd.systemIconID)
|
let category = classifyControlTarget(osd.controlTarget, systemIconID: osd.systemIconID)
|
||||||
let normalizedValue = normalizeValue(osd.value, maxValue: osd.maxValue)
|
let normalizedValue = normalizeValue(osd.value, maxValue: osd.maxValue)
|
||||||
let targetScreen = resolveScreen(for: osd.displayID)
|
let targetScreen = resolveScreen(for: osd.displayID)
|
||||||
let isExternalDisplay = isExternal(displayID: osd.displayID)
|
let isExternalDisplay = isExternal(displayID: osd.displayID, resolvedScreen: targetScreen)
|
||||||
|
|
||||||
NSLog("📺 BetterDisplay OSD: target=\(osd.controlTarget ?? "nil") displayID=\(osd.displayID.map(String.init) ?? "nil") resolvedScreen=\(targetScreen?.localizedName ?? "nil") isExternal=\(isExternalDisplay) value=\(osd.value ?? -1)")
|
NSLog("📺 BetterDisplay OSD: target=\(osd.controlTarget ?? "nil") displayID=\(osd.displayID.map(String.init) ?? "nil") resolvedScreen=\(targetScreen?.localizedName ?? "nil") isExternal=\(isExternalDisplay) value=\(osd.value ?? -1)")
|
||||||
|
|
||||||
|
|
@ -266,7 +266,8 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
status: true,
|
status: true,
|
||||||
type: .volume,
|
type: .volume,
|
||||||
value: value,
|
value: value,
|
||||||
icon: ""
|
icon: "",
|
||||||
|
onScreen: targetScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +290,8 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
status: true,
|
status: true,
|
||||||
type: .brightness,
|
type: .brightness,
|
||||||
value: value,
|
value: value,
|
||||||
icon: icon
|
icon: icon,
|
||||||
|
onScreen: targetScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,28 +304,50 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
NSLog("📺 BetterDisplay resolveScreen: displayID is nil, falling back to all screens")
|
NSLog("📺 BetterDisplay resolveScreen: displayID is nil, falling back to all screens")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let directDisplayID = UInt32(exactly: displayID),
|
||||||
|
let matchedScreen = NSScreen.screens.first(where: { screenNumber(for: $0) == directDisplayID }) {
|
||||||
|
return matchedScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for payloads that use 1-based display indexes (1, 2, ...)
|
||||||
|
if displayID > 0 {
|
||||||
|
let index = displayID - 1
|
||||||
|
if NSScreen.screens.indices.contains(index) {
|
||||||
|
let fallback = NSScreen.screens[index]
|
||||||
|
NSLog("📺 BetterDisplay resolveScreen: displayID=\(displayID) resolved via index fallback to '\(fallback.localizedName)'")
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let target = UInt32(displayID)
|
let target = UInt32(displayID)
|
||||||
let availableScreens = NSScreen.screens.map { screen -> (String, UInt32) in
|
let availableScreens = NSScreen.screens.map { screen -> (String, UInt32) in
|
||||||
let num = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0
|
let num = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0
|
||||||
return (screen.localizedName, num)
|
return (screen.localizedName, num)
|
||||||
}
|
}
|
||||||
NSLog("📺 BetterDisplay resolveScreen: looking for displayID=\(displayID) (UInt32=\(target)) among screens: \(availableScreens)")
|
NSLog("📺 BetterDisplay resolveScreen: looking for displayID=\(displayID) (UInt32=\(target)) among screens: \(availableScreens)")
|
||||||
let matched = NSScreen.screens.first { screen in
|
NSLog("📺 BetterDisplay resolveScreen: no match found for displayID=\(displayID), HUD will show on all screens")
|
||||||
guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
|
return nil
|
||||||
return false
|
|
||||||
}
|
|
||||||
return screenNumber.uint32Value == target
|
|
||||||
}
|
|
||||||
if matched == nil {
|
|
||||||
NSLog("📺 BetterDisplay resolveScreen: no match found for displayID=\(displayID), HUD will show on all screens")
|
|
||||||
}
|
|
||||||
return matched
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the given displayID refers to an external (non-built-in) display.
|
/// Whether the given displayID refers to an external (non-built-in) display.
|
||||||
private func isExternal(displayID: Int?) -> Bool {
|
private func isExternal(displayID: Int?, resolvedScreen: NSScreen?) -> Bool {
|
||||||
guard let displayID else { return false }
|
if let resolvedScreen,
|
||||||
return CGDisplayIsBuiltin(UInt32(displayID)) == 0
|
let resolvedDisplayID = screenNumber(for: resolvedScreen) {
|
||||||
|
return CGDisplayIsBuiltin(resolvedDisplayID) == 0
|
||||||
|
}
|
||||||
|
guard let displayID,
|
||||||
|
let displayIDValue = UInt32(exactly: displayID) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return CGDisplayIsBuiltin(displayIDValue) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func screenNumber(for screen: NSScreen) -> UInt32? {
|
||||||
|
guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return screenNumber.uint32Value
|
||||||
}
|
}
|
||||||
|
|
||||||
private func classifyControlTarget(_ target: String?, systemIconID: Int?) -> BetterDisplayControlCategory {
|
private func classifyControlTarget(_ target: String?, systemIconID: Int?) -> BetterDisplayControlCategory {
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ final class LunarManager: ObservableObject {
|
||||||
|
|
||||||
private func routeToHUD(_ data: LunarDisplayData) {
|
private func routeToHUD(_ data: LunarDisplayData) {
|
||||||
let targetScreen = resolveScreen(for: data.display)
|
let targetScreen = resolveScreen(for: data.display)
|
||||||
let isExternal = isExternalDisplay(data.display)
|
let isExternal = isExternalDisplay(data.display, resolvedScreen: targetScreen)
|
||||||
|
|
||||||
// Nits-only events are informational (no user-facing HUD needed).
|
// Nits-only events are informational (no user-facing HUD needed).
|
||||||
let hasActionableChange = data.brightness != nil || data.contrast != nil || data.volume != nil || data.mute != nil
|
let hasActionableChange = data.brightness != nil || data.contrast != nil || data.volume != nil || data.mute != nil
|
||||||
|
|
@ -342,7 +342,8 @@ final class LunarManager: ObservableObject {
|
||||||
status: true,
|
status: true,
|
||||||
type: .volume,
|
type: .volume,
|
||||||
value: value,
|
value: value,
|
||||||
icon: ""
|
icon: "",
|
||||||
|
onScreen: targetScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -364,7 +365,8 @@ final class LunarManager: ObservableObject {
|
||||||
status: true,
|
status: true,
|
||||||
type: .brightness,
|
type: .brightness,
|
||||||
value: value,
|
value: value,
|
||||||
icon: icon
|
icon: icon,
|
||||||
|
onScreen: targetScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -378,16 +380,42 @@ final class LunarManager: ObservableObject {
|
||||||
|
|
||||||
/// Resolve a Lunar display ID (CGDirectDisplayID) to the matching NSScreen.
|
/// Resolve a Lunar display ID (CGDirectDisplayID) to the matching NSScreen.
|
||||||
private func resolveScreen(for displayID: Int) -> NSScreen? {
|
private func resolveScreen(for displayID: Int) -> NSScreen? {
|
||||||
let target = UInt32(displayID)
|
if let directDisplayID = UInt32(exactly: displayID),
|
||||||
return NSScreen.screens.first { screen in
|
let matchedScreen = NSScreen.screens.first(where: { screenNumber(for: $0) == directDisplayID }) {
|
||||||
guard let num = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { return false }
|
return matchedScreen
|
||||||
return num.uint32Value == target
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for integrations that send 1-based display indexes (e.g. 1, 2)
|
||||||
|
if displayID > 0 {
|
||||||
|
let index = displayID - 1
|
||||||
|
if NSScreen.screens.indices.contains(index) {
|
||||||
|
let fallback = NSScreen.screens[index]
|
||||||
|
NSLog("🌙 Lunar: display=\(displayID) resolved via index fallback to '\(fallback.localizedName)'")
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("🌙 Lunar: unable to resolve display=\(displayID), HUD will show on all screens")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the given displayID refers to an external (non-built-in) display.
|
/// Whether the given displayID refers to an external (non-built-in) display.
|
||||||
private func isExternalDisplay(_ displayID: Int) -> Bool {
|
private func isExternalDisplay(_ displayID: Int, resolvedScreen: NSScreen?) -> Bool {
|
||||||
CGDisplayIsBuiltin(UInt32(displayID)) == 0
|
if let resolvedScreen,
|
||||||
|
let resolvedDisplayID = screenNumber(for: resolvedScreen) {
|
||||||
|
return CGDisplayIsBuiltin(resolvedDisplayID) == 0
|
||||||
|
}
|
||||||
|
if let displayIDValue = UInt32(exactly: displayID) {
|
||||||
|
return CGDisplayIsBuiltin(displayIDValue) == 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func screenNumber(for screen: NSScreen) -> UInt32? {
|
||||||
|
guard let num = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return num.uint32Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Reconnection
|
// MARK: - Reconnection
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue