diff --git a/NotchIA.xcodeproj/project.pbxproj b/NotchIA.xcodeproj/project.pbxproj index 31e0426..dd71f50 100644 --- a/NotchIA.xcodeproj/project.pbxproj +++ b/NotchIA.xcodeproj/project.pbxproj @@ -1277,7 +1277,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1285,7 +1285,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NotchIAXPCHelper; INFOPLIST_KEY_NSHumanReadableCopyright = ""; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia.NotchIAXPCHelper; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -1303,7 +1303,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1311,7 +1311,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NotchIAXPCHelper; INFOPLIST_KEY_NSHumanReadableCopyright = ""; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia.NotchIAXPCHelper; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -1462,7 +1462,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"NotchIA/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1490,7 +1490,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1518,7 +1518,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_POSTPROCESSING = YES; DEVELOPMENT_ASSET_PATHS = "\"NotchIA/Preview Content\""; @@ -1546,7 +1546,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/NotchIA/ContentView.swift b/NotchIA/ContentView.swift index 58bd590..541fa92 100644 --- a/NotchIA/ContentView.swift +++ b/NotchIA/ContentView.swift @@ -450,7 +450,7 @@ struct ContentView: View { private func updateShelfNotchSizeForSummary() { guard vm.notchState == .open, coordinator.currentView == .shelf else { return } - let target = openNotchSize(for: .shelf) + let target = openNotchSize(for: .shelf, screenUUID: vm.screenUUID) guard vm.notchSize != target else { return } withAnimation(.smooth) { diff --git a/NotchIA/NotchIAApp.swift b/NotchIA/NotchIAApp.swift index 5aec4b6..89eda34 100644 --- a/NotchIA/NotchIAApp.swift +++ b/NotchIA/NotchIAApp.swift @@ -279,10 +279,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func setupDragDetectorForScreen(_ screen: NSScreen) { guard let uuid = screen.displayUUID else { return } - + let screenFrame = screen.frame - let notchHeight = openNotchSize.height - let notchWidth = openNotchSize.width + // Détection drag basée sur l'encoche du défaut (.media), scalée par la + // classe physique de cet écran. Évite que la zone de hover dépasse + // visuellement l'encoche sur les petits écrans (13" Air). + let referenceSize = openNotchSize(for: .media, screenUUID: uuid) + let notchHeight = referenceSize.height + let notchWidth = referenceSize.width // Create notch region at the top-center of the screen where an open notch would occupy let notchRegion = CGRect( diff --git a/NotchIA/models/NotchIAViewModel.swift b/NotchIA/models/NotchIAViewModel.swift index 4a14928..e983973 100644 --- a/NotchIA/models/NotchIAViewModel.swift +++ b/NotchIA/models/NotchIAViewModel.swift @@ -72,7 +72,7 @@ class NotchIAViewModel: NSObject, ObservableObject { .sink { [weak self] newView in guard let self = self, self.notchState == .open else { return } withAnimation(.smooth) { - self.notchSize = openNotchSize(for: newView) + self.notchSize = openNotchSize(for: newView, screenUUID: self.screenUUID) } } .store(in: &cancellables) @@ -176,7 +176,7 @@ class NotchIAViewModel: NSObject, ObservableObject { } func open() { - self.notchSize = openNotchSize(for: coordinator.currentView) + self.notchSize = openNotchSize(for: coordinator.currentView, screenUUID: self.screenUUID) self.notchState = .open // Force music information update when notch is opened diff --git a/NotchIA/sizing/matters.swift b/NotchIA/sizing/matters.swift index 2277145..f9d5f5d 100644 --- a/NotchIA/sizing/matters.swift +++ b/NotchIA/sizing/matters.swift @@ -5,6 +5,7 @@ // Created by Harsh Vardhan Goswami on 05/08/24. // +import CoreGraphics import Defaults import Foundation import SwiftUI @@ -12,34 +13,46 @@ import SwiftUI let downloadSneakSize: CGSize = .init(width: 65, height: 1) let shadowPadding: CGFloat = 20 -let openNotchSize: CGSize = .init(width: 560, height: 176) -let mediaOpenNotchSize: CGSize = .init(width: openNotchSize.width, height: 195) -let calendarOpenNotchSize: CGSize = .init(width: openNotchSize.width, height: 240) -let shelfSummaryOpenNotchSize: CGSize = .init(width: openNotchSize.width, height: 360) -let digestOpenNotchSize: CGSize = .init(width: 590, height: 272) + +// MARK: - Tailles d'encoche de base (baseline 16" Pro) +// +// Ces tailles sont la BASE non-scalée — utilisées uniquement pour allouer +// la NSWindow (qui doit pouvoir contenir le plus grand contenu possible +// sur la plus grande machine). La taille VISIBLE de l'encoche dans la +// fenêtre est calculée par `openNotchSize(for:screenUUID:)` ci-dessous, +// avec un scale qui dépend de la diagonale physique de l'écran. +// → Un MacBook Air 13" voit une encoche plus étroite qu'un Pro 16", +// indépendamment du réglage Affichage macOS choisi par l'utilisateur. + +private let baseOpenNotchSize: CGSize = .init(width: 560, height: 176) +private let baseMediaOpenNotchSize: CGSize = .init(width: baseOpenNotchSize.width, height: 195) +private let baseCalendarOpenNotchSize: CGSize = .init(width: baseOpenNotchSize.width, height: 240) +private let baseShelfSummaryOpenNotchSize: CGSize = .init(width: baseOpenNotchSize.width, height: 360) +private let baseDigestOpenNotchSize: CGSize = .init(width: 590, height: 272) // Concentration keeps its generous cards but the notch itself stays // short — PomodoroView is wrapped in a ScrollView, so the bottom row // is reached by scrolling instead of stretching the notch off-screen. // 236pt shows the whole top row (status + progression) plus a peek of // the commands/settings row that signals the page scrolls. -let pomodoroOpenNotchSize: CGSize = .init(width: 590, height: 236) +private let basePomodoroOpenNotchSize: CGSize = .init(width: 590, height: 236) + let maximumOpenNotchSize: CGSize = .init( width: [ - openNotchSize, - mediaOpenNotchSize, - calendarOpenNotchSize, - shelfSummaryOpenNotchSize, - digestOpenNotchSize, - pomodoroOpenNotchSize - ].map(\.width).max() ?? openNotchSize.width, + baseOpenNotchSize, + baseMediaOpenNotchSize, + baseCalendarOpenNotchSize, + baseShelfSummaryOpenNotchSize, + baseDigestOpenNotchSize, + basePomodoroOpenNotchSize + ].map(\.width).max() ?? baseOpenNotchSize.width, height: [ - openNotchSize, - mediaOpenNotchSize, - calendarOpenNotchSize, - shelfSummaryOpenNotchSize, - digestOpenNotchSize, - pomodoroOpenNotchSize - ].map(\.height).max() ?? openNotchSize.height + baseOpenNotchSize, + baseMediaOpenNotchSize, + baseCalendarOpenNotchSize, + baseShelfSummaryOpenNotchSize, + baseDigestOpenNotchSize, + basePomodoroOpenNotchSize + ].map(\.height).max() ?? baseOpenNotchSize.height ) let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14)) let openOuterHorizontalPadding: CGFloat = max(cornerRadiusInsets.opened.top, cornerRadiusInsets.opened.bottom) + 12 @@ -80,7 +93,68 @@ enum MusicPlayerImageSizes { static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20)) } -@MainActor func openNotchSize(for view: NotchViews) -> CGSize { +// MARK: - Classe physique d'écran +// +// Détecte la diagonale physique de l'écran via `CGDisplayScreenSize` (qui +// retourne les dimensions en mm rapportées par l'EDID). Cette valeur est +// INDÉPENDANTE du réglage Affichage choisi par l'utilisateur dans +// Réglages Système → Moniteurs ("Default", "More Space", "Larger Text"…). +// Conséquence : deux MacBook Air 13" différents (un en Default, l'autre +// en More Space) obtiennent la même classe `.compact13` et donc la même +// taille d'encoche visible. + +enum ScreenSizeClass { + case compact13 // MacBook Air 13.6" (M2/M3) + case small14 // MacBook Pro 14.2" (M2/M3/M4 Pro/Max) + case medium15 // MacBook Air 15.3" (M2/M3) + case large16 // MacBook Pro 16.2" (M2/M3/M4 Pro/Max), écrans externes + + /// Multiplier appliqué à la LARGEUR de l'encoche ouverte. Les hauteurs + /// restent constantes — le contenu (texte 11-13pt, icônes 16pt, + /// contrôles 30pt) doit rester lisible. Seule la largeur excessive sur + /// les petits écrans était le problème. + /// + /// Calibrage (baseline = 560pt sur 16" Pro) : + /// 13" → 480pt (-14%) : ~33% de l'écran sur 1469pt (au lieu de 38%) + /// 14" → 520pt (- 7%) : ~34% de l'écran sur 1512pt (au lieu de 37%) + /// 15" → 540pt (- 4%) : ~32% de l'écran sur 1680pt (au lieu de 33%) + /// 16" → 560pt (réf) : ~32% de l'écran sur 1728pt + var widthScale: CGFloat { + switch self { + case .compact13: return 480.0 / 560.0 + case .small14: return 520.0 / 560.0 + case .medium15: return 540.0 / 560.0 + case .large16: return 1.0 + } + } +} + +@MainActor func screenSizeClass(screenUUID: String? = nil) -> ScreenSizeClass { + let screen = screenUUID.flatMap { NSScreen.screen(withUUID: $0) } ?? NSScreen.main + return screenSizeClass(screen) +} + +@MainActor func screenSizeClass(_ screen: NSScreen?) -> ScreenSizeClass { + guard let screen, + let raw = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")], + let displayID = (raw as? NSNumber)?.uint32Value + else { return .large16 } + + let sizeMM = CGDisplayScreenSize(displayID) + // Les écrans externes / virtuels retournent souvent (0, 0) — on tombe + // sur le baseline maximal (16"), comportement actuel non-régressif. + guard sizeMM.width > 0, sizeMM.height > 0 else { return .large16 } + + let diagonalInches = sqrt(sizeMM.width * sizeMM.width + sizeMM.height * sizeMM.height) / 25.4 + switch diagonalInches { + case ..<13.8: return .compact13 // MBA 13.6" + case ..<14.6: return .small14 // MBP 14.2" + case ..<15.7: return .medium15 // MBA 15.3" + default: return .large16 // MBP 16.2" + externes + } +} + +@MainActor func openNotchSize(for view: NotchViews, screenUUID: String? = nil) -> CGSize { // Per user feedback: each tab uses the height it actually needs. // - Concentration: dedicated compact size (its own layout) // - Calendar: 280pt — needs that much room to absorb the full @@ -91,25 +165,34 @@ enum MusicPlayerImageSizes { // (or if it failed) the notch stays compact; it stretches only // when the finished text actually appears. // - Everything else: original compact 176pt + // + // La largeur est ensuite scalée par la classe physique de l'écran + // (compact13/small14/medium15/large16) — voir `ScreenSizeClass.widthScale`. + let scale = screenSizeClass(screenUUID: screenUUID).widthScale + + func scaled(_ base: CGSize) -> CGSize { + CGSize(width: (base.width * scale).rounded(), height: base.height) + } + switch view { case .digest: - return digestOpenNotchSize + return scaled(baseDigestOpenNotchSize) case .pomodoro: - return pomodoroOpenNotchSize + return scaled(basePomodoroOpenNotchSize) case .media: // Slight bump (176 → 195) so the music player has a touch // more vertical breathing room without making it stand out // dramatically vs the other compact tabs. - return mediaOpenNotchSize + return scaled(baseMediaOpenNotchSize) case .calendar: - return calendarOpenNotchSize + return scaled(baseCalendarOpenNotchSize) case .shelf: if SummaryViewModel.shared.isShowingSummaryText { - return shelfSummaryOpenNotchSize + return scaled(baseShelfSummaryOpenNotchSize) } - return openNotchSize // 560×176 + return scaled(baseOpenNotchSize) // 560×176 baseline, scalé default: - return openNotchSize // 560×176 + return scaled(baseOpenNotchSize) // 560×176 baseline, scalé } } @@ -119,11 +202,11 @@ enum MusicPlayerImageSizes { if let uuid = screenUUID { selectedScreen = NSScreen.screen(withUUID: uuid) } - + if let screen = selectedScreen { return screen.frame } - + return nil }