mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-29 08:04:32 +00:00
Update DynamicIsland Configuration
This commit is contained in:
parent
ddda04369d
commit
b900804acb
8 changed files with 333 additions and 11 deletions
|
|
@ -80,6 +80,8 @@ struct ContentView: View {
|
||||||
@Default(.enableCapsLockIndicator) var enableCapsLockIndicator
|
@Default(.enableCapsLockIndicator) var enableCapsLockIndicator
|
||||||
@Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities
|
@Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities
|
||||||
@Default(.showStandardMediaControls) var showStandardMediaControls
|
@Default(.showStandardMediaControls) var showStandardMediaControls
|
||||||
|
@Default(.externalDisplayStyle) var externalDisplayStyle
|
||||||
|
@Default(.hideNonNotchUntilHover) var hideNonNotchUntilHover
|
||||||
|
|
||||||
// Dynamic sizing based on view type and graph count with smooth transitions
|
// Dynamic sizing based on view type and graph count with smooth transitions
|
||||||
var dynamicNotchSize: CGSize {
|
var dynamicNotchSize: CGSize {
|
||||||
|
|
@ -231,8 +233,52 @@ struct ContentView: View {
|
||||||
return NotchShape(topCornerRadius: topRadius, bottomCornerRadius: bottomRadius)
|
return NotchShape(topCornerRadius: topRadius, bottomCornerRadius: bottomRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the current screen should render as a Dynamic Island pill
|
||||||
|
/// rather than the standard notch shape. Always false on physical notch screens.
|
||||||
|
private var isDynamicIslandMode: Bool {
|
||||||
|
shouldUseDynamicIslandMode(for: vm.screen ?? coordinator.selectedScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the current screen lacks a physical notch.
|
||||||
|
private var isNonNotchScreen: Bool {
|
||||||
|
let screenName = vm.screen ?? coordinator.selectedScreen
|
||||||
|
guard let screen = NSScreen.screens.first(where: { $0.localizedName == screenName }) else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return screen.safeAreaInsets.top <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the notch/island should hide off-screen when closed on a non-notch display.
|
||||||
|
private var shouldHideUntilHover: Bool {
|
||||||
|
hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pill shape for Dynamic Island mode with animated corner radius transitions.
|
||||||
|
private var currentPillShape: DynamicIslandPillShape {
|
||||||
|
let radius: CGFloat
|
||||||
|
if vm.notchState == .open {
|
||||||
|
radius = enableMinimalisticUI
|
||||||
|
? minimalisticCornerRadiusInsets.opened.top
|
||||||
|
: dynamicIslandPillCornerRadiusInsets.opened
|
||||||
|
} else {
|
||||||
|
// Use half the closed height for a true capsule shape
|
||||||
|
radius = max(vm.closedNotchSize.height / 2, dynamicIslandPillCornerRadiusInsets.closed.standard)
|
||||||
|
}
|
||||||
|
return DynamicIslandPillShape(cornerRadius: radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the clip/content shape per-screen: pill on non-notch screens
|
||||||
|
/// when dynamic island mode is active, standard notch shape otherwise.
|
||||||
|
private var resolvedClipShape: AnyShape {
|
||||||
|
if isDynamicIslandMode {
|
||||||
|
return AnyShape(currentPillShape)
|
||||||
|
}
|
||||||
|
return AnyShape(currentNotchShape)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let interactionsEnabled = !lockScreenManager.isLocked
|
let interactionsEnabled = !lockScreenManager.isLocked
|
||||||
|
let isIslandMode = isDynamicIslandMode
|
||||||
let notchHorizontalPadding: CGFloat = {
|
let notchHorizontalPadding: CGFloat = {
|
||||||
guard vm.notchState == .open else {
|
guard vm.notchState == .open else {
|
||||||
return activeCornerRadiusInsets.closed.bottom
|
return activeCornerRadiusInsets.closed.bottom
|
||||||
|
|
@ -249,6 +295,8 @@ struct ContentView: View {
|
||||||
return vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0
|
return vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0
|
||||||
}()
|
}()
|
||||||
let notchBottomPadding = currentShadowPadding + hoverAreaPadding
|
let notchBottomPadding = currentShadowPadding + hoverAreaPadding
|
||||||
|
// Extra top padding to detach pill from screen edge in Dynamic Island mode
|
||||||
|
let pillTopOffset: CGFloat = isIslandMode ? dynamicIslandTopOffset : 0
|
||||||
|
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
let mainLayout = NotchLayout()
|
let mainLayout = NotchLayout()
|
||||||
|
|
@ -256,7 +304,7 @@ struct ContentView: View {
|
||||||
.padding(.horizontal, notchHorizontalPadding)
|
.padding(.horizontal, notchHorizontalPadding)
|
||||||
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.clipShape(currentNotchShape)
|
.clipShape(resolvedClipShape)
|
||||||
.compositingGroup()
|
.compositingGroup()
|
||||||
.shadow(
|
.shadow(
|
||||||
color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow])
|
color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow])
|
||||||
|
|
@ -264,7 +312,16 @@ struct ContentView: View {
|
||||||
: .clear,
|
: .clear,
|
||||||
radius: Defaults[.cornerRadiusScaling] ? 10 : 5
|
radius: Defaults[.cornerRadiusScaling] ? 10 : 5
|
||||||
)
|
)
|
||||||
|
// Extra horizontal inset for Dynamic Island mode so the shadow
|
||||||
|
// is not clipped by the outer frame constraint
|
||||||
|
.padding(.horizontal, isIslandMode ? dynamicIslandShadowInset : 0)
|
||||||
|
.padding(.top, pillTopOffset)
|
||||||
.padding(.bottom, notchBottomPadding)
|
.padding(.bottom, notchBottomPadding)
|
||||||
|
// Hide off-screen when non-notch hide-until-hover is active
|
||||||
|
.offset(y: shouldHideUntilHover && !isHovering
|
||||||
|
? -(vm.closedNotchSize.height + pillTopOffset + currentShadowPadding + 10)
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
|
||||||
mainLayout
|
mainLayout
|
||||||
.conditionalModifier(!useModernCloseAnimation) { view in
|
.conditionalModifier(!useModernCloseAnimation) { view in
|
||||||
|
|
@ -288,7 +345,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
.conditionalModifier(interactionsEnabled) { view in
|
.conditionalModifier(interactionsEnabled) { view in
|
||||||
view
|
view
|
||||||
.contentShape(currentNotchShape)
|
.contentShape(resolvedClipShape)
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
handleHover(hovering)
|
handleHover(hovering)
|
||||||
}
|
}
|
||||||
|
|
@ -410,8 +467,8 @@ struct ContentView: View {
|
||||||
|
|
||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: dynamicNotchSize.width,
|
maxWidth: dynamicNotchSize.width + (isDynamicIslandMode ? dynamicIslandShadowInset * 2 : 0),
|
||||||
maxHeight: dynamicNotchSize.height + currentShadowPadding,
|
maxHeight: dynamicNotchSize.height + currentShadowPadding + (isDynamicIslandMode ? dynamicIslandTopOffset : 0),
|
||||||
alignment: .top
|
alignment: .top
|
||||||
)
|
)
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
-> NSWindow
|
-> NSWindow
|
||||||
{
|
{
|
||||||
// Use the current required size instead of always using openNotchSize
|
// Use the current required size instead of always using openNotchSize
|
||||||
let requiredSize = calculateRequiredNotchSize()
|
let baseSize = calculateRequiredNotchSize()
|
||||||
|
let requiredSize = adjustedSizeForScreen(baseSize, screen: screen)
|
||||||
|
|
||||||
let window = DynamicIslandWindow(
|
let window = DynamicIslandWindow(
|
||||||
contentRect: NSRect(
|
contentRect: NSRect(
|
||||||
|
|
@ -317,10 +318,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
isStatsTabActive: coordinator.currentView == .stats,
|
isStatsTabActive: coordinator.currentView == .stats,
|
||||||
secondRowProgress: coordinator.statsSecondRowExpansion
|
secondRowProgress: coordinator.statsSecondRowExpansion
|
||||||
)
|
)
|
||||||
return addShadowPadding(
|
var result = addShadowPadding(
|
||||||
to: adjustedContentSize,
|
to: adjustedContentSize,
|
||||||
isMinimalistic: Defaults[.enableMinimalisticUI]
|
isMinimalistic: Defaults[.enableMinimalisticUI]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjusts a base notch size for a specific screen by adding Dynamic Island
|
||||||
|
/// shadow insets and top-offset only when the screen lacks a physical notch
|
||||||
|
/// and the user has chosen the Dynamic Island style.
|
||||||
|
private func adjustedSizeForScreen(_ baseSize: CGSize, screen: NSScreen) -> CGSize {
|
||||||
|
guard shouldUseDynamicIslandMode(for: screen.localizedName) else {
|
||||||
|
return baseSize
|
||||||
|
}
|
||||||
|
var adjusted = baseSize
|
||||||
|
adjusted.width += dynamicIslandShadowInset * 2
|
||||||
|
adjusted.height += dynamicIslandTopOffset
|
||||||
|
return adjusted
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) {
|
func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) {
|
||||||
|
|
@ -332,15 +348,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
if Defaults[.showOnAllDisplays] {
|
if Defaults[.showOnAllDisplays] {
|
||||||
for (screen, window) in windows {
|
for (screen, window) in windows {
|
||||||
if force || window.frame.size != size {
|
let screenSize = adjustedSizeForScreen(size, screen: screen)
|
||||||
resizeWindow(window, on: screen, to: size, animated: animated)
|
if force || window.frame.size != screenSize {
|
||||||
|
resizeWindow(window, on: screen, to: screenSize, animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let window {
|
} else if let window {
|
||||||
let screen = window.screen ?? NSScreen.screens.first { $0.frame.intersects(window.frame) } ?? NSScreen.main ?? NSScreen.screens.first
|
let screen = window.screen ?? NSScreen.screens.first { $0.frame.intersects(window.frame) } ?? NSScreen.main ?? NSScreen.screens.first
|
||||||
guard let screen else { return }
|
guard let screen else { return }
|
||||||
if force || window.frame.size != size {
|
let screenSize = adjustedSizeForScreen(size, screen: screen)
|
||||||
resizeWindow(window, on: screen, to: size, animated: animated)
|
if force || window.frame.size != screenSize {
|
||||||
|
resizeWindow(window, on: screen, to: screenSize, animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1591,6 +1591,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"±10 Seconds" : {
|
||||||
|
"comment" : "Display name for the \"±10 Seconds\" music skip behavior.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"0" : {
|
"0" : {
|
||||||
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -1907,6 +1911,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Accent" : {
|
||||||
|
"comment" : "Name of the Caps Lock Indicator Tint Mode option that uses the accent color.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Accent color" : {
|
"Accent color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -2252,6 +2260,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Adaptive gradient" : {
|
||||||
|
"comment" : "Name of the timer icon color mode that uses an adaptive gradient.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -3841,6 +3853,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Apple Music" : {
|
||||||
|
"comment" : "Names for the different media player types available in the app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -4252,6 +4268,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Bar" : {
|
||||||
|
"comment" : "Localized name for the \"Bar\" timer progress style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Battery" : {
|
"Battery" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -6234,6 +6254,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Classic notch shape that blends into the top screen edge" : {
|
||||||
|
"comment" : "Description of a notch shape that blends into the top screen edge.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Clear" : {
|
"Clear" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -8491,6 +8515,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Digital" : {
|
||||||
|
"comment" : "The name of the \"Digital\" style of reminder presentation.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Dimensions" : {
|
"Dimensions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -9256,6 +9284,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Dynamic Island" : {
|
||||||
|
"comment" : "Description of the dynamic island style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Dynamic Island Progress Bars" : {
|
"Dynamic Island Progress Bars" : {
|
||||||
"comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.",
|
"comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -10814,6 +10846,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"External display style" : {
|
||||||
|
"comment" : "A picker that lets the user choose the style of the external display.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Favorites" : {
|
"Favorites" : {
|
||||||
"comment" : "Localized name of the \"Favorites\" tab in the clipboard.",
|
"comment" : "Localized name of the \"Favorites\" tab in the clipboard.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|
@ -11861,6 +11897,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Green" : {
|
||||||
|
"comment" : "Display name for the \"Green\" Caps Lock Indicator Tint Mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Height:" : {
|
"Height:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -15527,7 +15567,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Minutes" : {
|
"Minutes" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17737,6 +17776,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Now Playing" : {
|
||||||
|
"comment" : "Localized name for each media controller type.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Off" : {
|
"Off" : {
|
||||||
"comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.",
|
"comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|
@ -18103,6 +18146,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Panel" : {
|
||||||
|
"comment" : "Display name for the \"Panel\" clipboard display mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Panel mode shows clipboard in a floating window near the notch." : {
|
"Panel mode shows clipboard in a floating window near the notch." : {
|
||||||
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -18232,6 +18279,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Pill-shaped island with rounded corners, similar to iPhone's Dynamic Island" : {
|
||||||
|
"comment" : "Description of the Dynamic Island style for external displays.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Pin items from history to save them here" : {
|
"Pin items from history to save them here" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -19697,6 +19748,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Ring" : {
|
||||||
|
"comment" : "Localized name for the \"Ring\" timer progress style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Rotation:" : {
|
"Rotation:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -20517,6 +20572,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Separate Tab" : {
|
||||||
|
"comment" : "Name of the clipboard display mode option that opens the clipboard in a separate tab.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
||||||
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -21571,6 +21630,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Shows screen assistant as a dropdown attached to the AI button" : {
|
||||||
|
"comment" : "Description of the \"popover\" display mode for the screen assistant.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Shows screen assistant in a floating panel near the notch" : {
|
||||||
|
"comment" : "Description of the \"Panel\" screen assistant display mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Shows the model's reasoning process before the final answer" : {
|
"Shows the model's reasoning process before the final answer" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -21633,6 +21700,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Skip forward or backward by ten seconds" : {
|
||||||
|
"comment" : "Description of the \"Skip forward or backward by ten seconds\" music skip behavior option.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Slider color" : {
|
"Slider color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22200,6 +22271,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Spotify" : {
|
||||||
|
"comment" : "Add more media player types here.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Square" : {
|
"Square" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22302,12 +22377,20 @@
|
||||||
},
|
},
|
||||||
"Standard" : {
|
"Standard" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Standard Notch" : {
|
||||||
|
"comment" : "Text for the \"Standard Notch\" option in the external display style picker.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Standard notch media controls are hidden while this toggle is off." : {
|
"Standard notch media controls are hidden while this toggle is off." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Standard previous/next track controls" : {
|
||||||
|
"comment" : "Description of a music skip behavior option that uses standard previous/next track controls.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -23657,6 +23740,10 @@
|
||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
"Track Skip" : {
|
||||||
|
"comment" : "Name of the music skip behavior option that skips to the next track.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Transform" : {
|
"Transform" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -25243,6 +25330,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"When enabled, the notch slides up and hides on external (non-notch) displays until you hover over it." : {
|
||||||
|
"comment" : "A description of the feature that hides the notch on non-notch displays.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {
|
"When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -25567,6 +25658,10 @@
|
||||||
"comment" : "A footer note explaining the app's funding.",
|
"comment" : "A footer note explaining the app's funding.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Youtube Music" : {
|
||||||
|
"comment" : "Name for the \"Youtube Music\" media controller type in settings.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"YouTube Music requires this third-party app to be installed: " : {
|
"YouTube Music requires this third-party app to be installed: " : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
|
||||||
50
DynamicIsland/components/Notch/DynamicIslandPillShape.swift
Normal file
50
DynamicIsland/components/Notch/DynamicIslandPillShape.swift
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Atoll (DynamicIsland)
|
||||||
|
* Copyright (C) 2024-2026 Atoll Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A pill-shaped (capsule-like) clip shape used for the Dynamic Island mode
|
||||||
|
/// on external and non-notched displays. Uses `.continuous` rounded corners
|
||||||
|
/// for an Apple-style squircle appearance, inspired by DynamicNotchKit's
|
||||||
|
/// floating style.
|
||||||
|
struct DynamicIslandPillShape: Shape {
|
||||||
|
var cornerRadius: CGFloat
|
||||||
|
|
||||||
|
var animatableData: CGFloat {
|
||||||
|
get { cornerRadius }
|
||||||
|
set { cornerRadius = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
func path(in rect: CGRect) -> Path {
|
||||||
|
let resolvedRadius = min(cornerRadius, min(rect.width, rect.height) / 2)
|
||||||
|
return Path(roundedRect: rect, cornerRadius: resolvedRadius, style: .continuous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
DynamicIslandPillShape(cornerRadius: 16)
|
||||||
|
.fill(.black)
|
||||||
|
.frame(width: 185, height: 32)
|
||||||
|
|
||||||
|
DynamicIslandPillShape(cornerRadius: 24)
|
||||||
|
.fill(.black)
|
||||||
|
.frame(width: 640, height: 200)
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
}
|
||||||
|
|
@ -606,6 +606,8 @@ struct SettingsView: View {
|
||||||
SettingsSearchEntry(tab: .general, title: "Extend hover area", keywords: ["hover", "cursor"], highlightID: SettingsTab.general.highlightID(for: "Extend hover area")),
|
SettingsSearchEntry(tab: .general, title: "Extend hover area", keywords: ["hover", "cursor"], highlightID: SettingsTab.general.highlightID(for: "Extend hover area")),
|
||||||
SettingsSearchEntry(tab: .general, title: "Enable haptics", keywords: ["haptic", "feedback"], highlightID: SettingsTab.general.highlightID(for: "Enable haptics")),
|
SettingsSearchEntry(tab: .general, title: "Enable haptics", keywords: ["haptic", "feedback"], highlightID: SettingsTab.general.highlightID(for: "Enable haptics")),
|
||||||
SettingsSearchEntry(tab: .general, title: "Open notch on hover", keywords: ["hover to open", "auto open"], highlightID: SettingsTab.general.highlightID(for: "Open notch on hover")),
|
SettingsSearchEntry(tab: .general, title: "Open notch on hover", keywords: ["hover to open", "auto open"], highlightID: SettingsTab.general.highlightID(for: "Open notch on hover")),
|
||||||
|
SettingsSearchEntry(tab: .general, title: "External display style", keywords: ["dynamic island", "pill", "external display", "non-notch", "floating", "capsule"], highlightID: SettingsTab.general.highlightID(for: "External display style")),
|
||||||
|
SettingsSearchEntry(tab: .general, title: "Hide until hovered", keywords: ["hide", "hover", "external", "non-notch", "auto hide", "slide"], highlightID: SettingsTab.general.highlightID(for: "Hide until hovered")),
|
||||||
SettingsSearchEntry(tab: .general, title: "Notch display height", keywords: ["display height", "menu bar size"], highlightID: SettingsTab.general.highlightID(for: "Notch display height")),
|
SettingsSearchEntry(tab: .general, title: "Notch display height", keywords: ["display height", "menu bar size"], highlightID: SettingsTab.general.highlightID(for: "Notch display height")),
|
||||||
|
|
||||||
// Live Activities
|
// Live Activities
|
||||||
|
|
@ -905,6 +907,8 @@ struct GeneralSettings: View {
|
||||||
@Default(.musicGestureBehavior) var musicGestureBehavior
|
@Default(.musicGestureBehavior) var musicGestureBehavior
|
||||||
@Default(.reverseSwipeGestures) var reverseSwipeGestures
|
@Default(.reverseSwipeGestures) var reverseSwipeGestures
|
||||||
@Default(.reverseScrollGestures) var reverseScrollGestures
|
@Default(.reverseScrollGestures) var reverseScrollGestures
|
||||||
|
@Default(.externalDisplayStyle) var externalDisplayStyle
|
||||||
|
@Default(.hideNonNotchUntilHover) var hideNonNotchUntilHover
|
||||||
|
|
||||||
private func highlightID(_ title: String) -> String {
|
private func highlightID(_ title: String) -> String {
|
||||||
SettingsTab.general.highlightID(for: title)
|
SettingsTab.general.highlightID(for: title)
|
||||||
|
|
@ -1118,6 +1122,24 @@ struct GeneralSettings: View {
|
||||||
NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
|
NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Picker("External display style", selection: $externalDisplayStyle) {
|
||||||
|
ForEach(ExternalDisplayStyle.allCases) { style in
|
||||||
|
Text(style.localizedName)
|
||||||
|
.tag(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: externalDisplayStyle) {
|
||||||
|
NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
|
||||||
|
}
|
||||||
|
.settingsHighlight(id: highlightID("External display style"))
|
||||||
|
Text(externalDisplayStyle.description)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Defaults.Toggle("Hide until hovered on non-notch displays", key: .hideNonNotchUntilHover)
|
||||||
|
.settingsHighlight(id: highlightID("Hide until hovered"))
|
||||||
|
Text("When enabled, the notch slides up and hides on external (non-notch) displays until you hover over it.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Notch behavior")
|
Text("Notch behavior")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,36 @@ public enum Style {
|
||||||
case floating
|
case floating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls how Atoll renders on external and non-notched displays.
|
||||||
|
/// - `notch`: Standard notch shape (concave top corners blending into the screen edge).
|
||||||
|
/// - `dynamicIsland`: Pill-shaped island with continuously rounded corners,
|
||||||
|
/// inspired by DynamicNotchKit's floating style. Only applies to screens
|
||||||
|
/// that do NOT have a physical notch.
|
||||||
|
enum ExternalDisplayStyle: String, CaseIterable, Defaults.Serializable, Identifiable {
|
||||||
|
case notch = "Standard Notch"
|
||||||
|
case dynamicIsland = "Dynamic Island"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var localizedName: String {
|
||||||
|
switch self {
|
||||||
|
case .notch:
|
||||||
|
return String(localized: "Standard Notch")
|
||||||
|
case .dynamicIsland:
|
||||||
|
return String(localized: "Dynamic Island")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .notch:
|
||||||
|
return String(localized: "Classic notch shape that blends into the top screen edge")
|
||||||
|
case .dynamicIsland:
|
||||||
|
return String(localized: "Pill-shaped island with rounded corners, similar to iPhone's Dynamic Island")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum ContentType: Int, Codable, Hashable, Equatable {
|
public enum ContentType: Int, Codable, Hashable, Equatable {
|
||||||
case normal
|
case normal
|
||||||
case menu
|
case menu
|
||||||
|
|
|
||||||
|
|
@ -699,6 +699,11 @@ extension Defaults.Keys {
|
||||||
static let enableHaptics = Key<Bool>("enableHaptics", default: true)
|
static let enableHaptics = Key<Bool>("enableHaptics", default: true)
|
||||||
static let openNotchOnHover = Key<Bool>("openNotchOnHover", default: true)
|
static let openNotchOnHover = Key<Bool>("openNotchOnHover", default: true)
|
||||||
static let extendHoverArea = Key<Bool>("extendHoverArea", default: false)
|
static let extendHoverArea = Key<Bool>("extendHoverArea", default: false)
|
||||||
|
static let externalDisplayStyle = Key<ExternalDisplayStyle>(
|
||||||
|
"externalDisplayStyle",
|
||||||
|
default: .notch
|
||||||
|
)
|
||||||
|
static let hideNonNotchUntilHover = Key<Bool>("hideNonNotchUntilHover", default: false)
|
||||||
static let notchHeightMode = Key<WindowHeightMode>(
|
static let notchHeightMode = Key<WindowHeightMode>(
|
||||||
"notchHeightMode",
|
"notchHeightMode",
|
||||||
default: WindowHeightMode.matchRealNotchSize
|
default: WindowHeightMode.matchRealNotchSize
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,51 @@ func addShadowPadding(to size: CGSize, isMinimalistic: Bool) -> CGSize {
|
||||||
CGSize(width: size.width, height: size.height + notchShadowPaddingValue(isMinimalistic: isMinimalistic))
|
CGSize(width: size.width, height: size.height + notchShadowPaddingValue(isMinimalistic: isMinimalistic))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines whether a specific screen should render the Dynamic Island pill
|
||||||
|
/// shape instead of the standard notch shape.
|
||||||
|
///
|
||||||
|
/// Returns `true` only when ALL of these conditions are met:
|
||||||
|
/// 1. The user has selected `.dynamicIsland` in `externalDisplayStyle`
|
||||||
|
/// 2. The screen does NOT have a physical notch (safeAreaInsets.top == 0)
|
||||||
|
///
|
||||||
|
/// Screens with a physical notch always use the standard notch shape.
|
||||||
|
func shouldUseDynamicIslandMode(for screenName: String?) -> Bool {
|
||||||
|
guard Defaults[.externalDisplayStyle] == .dynamicIsland else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedScreen: NSScreen? = NSScreen.main
|
||||||
|
if let screenName {
|
||||||
|
selectedScreen = NSScreen.screens.first(where: { $0.localizedName == screenName })
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let screen = selectedScreen else {
|
||||||
|
// No screen found — fallback to standard notch
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Physical notch screens always use standard notch shape
|
||||||
|
return screen.safeAreaInsets.top <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Corner radius insets for the Dynamic Island pill shape.
|
||||||
|
/// - closed: half the closed notch height for a true capsule look
|
||||||
|
/// - opened: generous radius for smooth expanded pill
|
||||||
|
let dynamicIslandPillCornerRadiusInsets: (opened: CGFloat, closed: (standard: CGFloat, minimalistic: CGFloat)) = (
|
||||||
|
opened: 24,
|
||||||
|
closed: (standard: 16, minimalistic: 16)
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Vertical offset from the top screen edge for the Dynamic Island pill.
|
||||||
|
/// Creates a visual gap so the pill floats below the menu bar, mimicking
|
||||||
|
/// the iPhone's Dynamic Island detachment from the physical screen edge.
|
||||||
|
let dynamicIslandTopOffset: CGFloat = 6
|
||||||
|
|
||||||
|
/// Extra horizontal padding applied OUTSIDE the pill clip shape in Dynamic
|
||||||
|
/// Island mode so the drop shadow has room to render without being clipped
|
||||||
|
/// by the outer frame constraint.
|
||||||
|
let dynamicIslandShadowInset: CGFloat = 14
|
||||||
|
|
||||||
enum MusicPlayerImageSizes {
|
enum MusicPlayerImageSizes {
|
||||||
static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
|
static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
|
||||||
static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20))
|
static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue