Update DynamicIsland Configuration

This commit is contained in:
Hariharan Mudaliar 2026-03-02 23:14:54 +05:30
commit b900804acb
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
8 changed files with 333 additions and 11 deletions

View file

@ -80,6 +80,8 @@ struct ContentView: View {
@Default(.enableCapsLockIndicator) var enableCapsLockIndicator
@Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities
@Default(.showStandardMediaControls) var showStandardMediaControls
@Default(.externalDisplayStyle) var externalDisplayStyle
@Default(.hideNonNotchUntilHover) var hideNonNotchUntilHover
// Dynamic sizing based on view type and graph count with smooth transitions
var dynamicNotchSize: CGSize {
@ -231,8 +233,52 @@ struct ContentView: View {
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 {
let interactionsEnabled = !lockScreenManager.isLocked
let isIslandMode = isDynamicIslandMode
let notchHorizontalPadding: CGFloat = {
guard vm.notchState == .open else {
return activeCornerRadiusInsets.closed.bottom
@ -249,6 +295,8 @@ struct ContentView: View {
return vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0
}()
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) {
let mainLayout = NotchLayout()
@ -256,7 +304,7 @@ struct ContentView: View {
.padding(.horizontal, notchHorizontalPadding)
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
.background(.black)
.clipShape(currentNotchShape)
.clipShape(resolvedClipShape)
.compositingGroup()
.shadow(
color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow])
@ -264,7 +312,16 @@ struct ContentView: View {
: .clear,
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)
// Hide off-screen when non-notch hide-until-hover is active
.offset(y: shouldHideUntilHover && !isHovering
? -(vm.closedNotchSize.height + pillTopOffset + currentShadowPadding + 10)
: 0
)
mainLayout
.conditionalModifier(!useModernCloseAnimation) { view in
@ -288,7 +345,7 @@ struct ContentView: View {
}
.conditionalModifier(interactionsEnabled) { view in
view
.contentShape(currentNotchShape)
.contentShape(resolvedClipShape)
.onHover { hovering in
handleHover(hovering)
}
@ -410,8 +467,8 @@ struct ContentView: View {
}
.frame(
maxWidth: dynamicNotchSize.width,
maxHeight: dynamicNotchSize.height + currentShadowPadding,
maxWidth: dynamicNotchSize.width + (isDynamicIslandMode ? dynamicIslandShadowInset * 2 : 0),
maxHeight: dynamicNotchSize.height + currentShadowPadding + (isDynamicIslandMode ? dynamicIslandTopOffset : 0),
alignment: .top
)
.frame(maxHeight: .infinity, alignment: .top)

View file

@ -224,7 +224,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
-> NSWindow
{
// 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(
contentRect: NSRect(
@ -317,10 +318,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
isStatsTabActive: coordinator.currentView == .stats,
secondRowProgress: coordinator.statsSecondRowExpansion
)
return addShadowPadding(
var result = addShadowPadding(
to: adjustedContentSize,
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) {
@ -332,15 +348,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if Defaults[.showOnAllDisplays] {
for (screen, window) in windows {
if force || window.frame.size != size {
resizeWindow(window, on: screen, to: size, animated: animated)
let screenSize = adjustedSizeForScreen(size, screen: screen)
if force || window.frame.size != screenSize {
resizeWindow(window, on: screen, to: screenSize, animated: animated)
}
}
} else if let window {
let screen = window.screen ?? NSScreen.screens.first { $0.frame.intersects(window.frame) } ?? NSScreen.main ?? NSScreen.screens.first
guard let screen else { return }
if force || window.frame.size != size {
resizeWindow(window, on: screen, to: size, animated: animated)
let screenSize = adjustedSizeForScreen(size, screen: screen)
if force || window.frame.size != screenSize {
resizeWindow(window, on: screen, to: screenSize, animated: animated)
}
}
}

View file

@ -1591,6 +1591,10 @@
}
}
},
"±10 Seconds" : {
"comment" : "Display name for the \"±10 Seconds\" music skip behavior.",
"isCommentAutoGenerated" : true
},
"0" : {
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
"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" : {
"localizations" : {
"ar" : {
@ -2252,6 +2260,10 @@
}
}
},
"Adaptive gradient" : {
"comment" : "Name of the timer icon color mode that uses an adaptive gradient.",
"isCommentAutoGenerated" : true
},
"Add" : {
"localizations" : {
"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." : {
"localizations" : {
"pt-BR" : {
@ -4252,6 +4268,10 @@
}
}
},
"Bar" : {
"comment" : "Localized name for the \"Bar\" timer progress style.",
"isCommentAutoGenerated" : true
},
"Battery" : {
"localizations" : {
"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" : {
"extractionState" : "stale",
"localizations" : {
@ -8491,6 +8515,10 @@
}
}
},
"Digital" : {
"comment" : "The name of the \"Digital\" style of reminder presentation.",
"isCommentAutoGenerated" : true
},
"Dimensions" : {
"localizations" : {
"pt-BR" : {
@ -9256,6 +9284,10 @@
}
}
},
"Dynamic Island" : {
"comment" : "Description of the dynamic island style.",
"isCommentAutoGenerated" : true
},
"Dynamic Island Progress Bars" : {
"comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.",
"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" : {
"comment" : "Localized name of the \"Favorites\" tab in the clipboard.",
"isCommentAutoGenerated" : true
@ -11861,6 +11897,10 @@
}
}
},
"Green" : {
"comment" : "Display name for the \"Green\" Caps Lock Indicator Tint Mode.",
"isCommentAutoGenerated" : true
},
"Height:" : {
"extractionState" : "stale",
"localizations" : {
@ -15527,7 +15567,6 @@
}
},
"Minutes" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
@ -17737,6 +17776,10 @@
}
}
},
"Now Playing" : {
"comment" : "Localized name for each media controller type.",
"isCommentAutoGenerated" : true
},
"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
@ -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." : {
"comment" : "A description of the \"Panel\" clipboard display mode.",
"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" : {
"localizations" : {
"pt-BR" : {
@ -19697,6 +19748,10 @@
}
}
},
"Ring" : {
"comment" : "Localized name for the \"Ring\" timer progress style.",
"isCommentAutoGenerated" : true
},
"Rotation:" : {
"extractionState" : "stale",
"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." : {
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
"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" : {
"localizations" : {
"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" : {
"localizations" : {
"ar" : {
@ -22200,6 +22271,10 @@
}
}
},
"Spotify" : {
"comment" : "Add more media player types here.",
"isCommentAutoGenerated" : true
},
"Square" : {
"localizations" : {
"ar" : {
@ -22302,12 +22377,20 @@
},
"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. 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" : {
"localizations" : {
@ -23657,6 +23740,10 @@
},
"shouldTranslate" : false
},
"Track Skip" : {
"comment" : "Name of the music skip behavior option that skips to the next track.",
"isCommentAutoGenerated" : true
},
"Transform" : {
"extractionState" : "stale",
"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." : {
"localizations" : {
"pt-BR" : {
@ -25567,6 +25658,10 @@
"comment" : "A footer note explaining the app's funding.",
"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: " : {
"localizations" : {
"ar" : {

View 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)
}

View file

@ -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: "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: "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")),
// Live Activities
@ -905,6 +907,8 @@ struct GeneralSettings: View {
@Default(.musicGestureBehavior) var musicGestureBehavior
@Default(.reverseSwipeGestures) var reverseSwipeGestures
@Default(.reverseScrollGestures) var reverseScrollGestures
@Default(.externalDisplayStyle) var externalDisplayStyle
@Default(.hideNonNotchUntilHover) var hideNonNotchUntilHover
private func highlightID(_ title: String) -> String {
SettingsTab.general.highlightID(for: title)
@ -1118,6 +1122,24 @@ struct GeneralSettings: View {
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: {
Text("Notch behavior")
}

View file

@ -29,6 +29,36 @@ public enum Style {
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 {
case normal
case menu

View file

@ -699,6 +699,11 @@ extension Defaults.Keys {
static let enableHaptics = Key<Bool>("enableHaptics", default: true)
static let openNotchOnHover = Key<Bool>("openNotchOnHover", default: true)
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>(
"notchHeightMode",
default: WindowHeightMode.matchRealNotchSize

View file

@ -104,6 +104,51 @@ func addShadowPadding(to size: CGSize, isMinimalistic: Bool) -> CGSize {
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 {
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))