mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +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(.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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" : {
|
||||
|
|
|
|||
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: "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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue