Skip to content

Commit

Permalink
Implement dynamic menu bar appearance
Browse files Browse the repository at this point in the history
Closes #393
  • Loading branch information
jordanbaird committed Oct 12, 2024
1 parent bda9d86 commit 326893d
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 97 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//
// MenuBarAppearanceConfiguration.swift
// MenuBarAppearanceConfigurationV1.swift
// Ice
//

import CoreGraphics
import Foundation

/// Configuration for the menu bar's appearance.
struct MenuBarAppearanceConfiguration: Hashable {
struct MenuBarAppearanceConfigurationV1: Hashable {
var hasShadow: Bool
var hasBorder: Bool
var isInset: Bool
Expand All @@ -28,12 +28,12 @@ struct MenuBarAppearanceConfiguration: Hashable {
}
}

/// Creates a configuration by migrating from the deprecated
/// appearance-related keys stored in `UserDefaults`, storing
/// the new configuration and deleting the deprecated keys.
/// Creates a configuration by migrating from the deprecated appearance-related
/// keys stored in `UserDefaults`, storing the new configuration and deleting
/// the deprecated keys.
static func migrate(encoder: JSONEncoder, decoder: JSONDecoder) throws -> Self {
// try to load an already-migrated configuration first;
// otherwise, load each value from the deprecated keys
// Try to load an already migrated configuration first. Otherwise, load each
// value from the deprecated keys.
if let data = Defaults.data(forKey: .menuBarAppearanceConfiguration) {
return try decoder.decode(Self.self, from: data)
} else {
Expand Down Expand Up @@ -67,11 +67,11 @@ struct MenuBarAppearanceConfiguration: Hashable {
configuration.splitShapeInfo = try decoder.decode(MenuBarSplitShapeInfo.self, from: splitShapeData)
}

// store the configuration to complete the migration
// Store the configuration to complete the migration.
let configurationData = try encoder.encode(configuration)
Defaults.set(configurationData, forKey: .menuBarAppearanceConfiguration)

// remove the deprecated keys
// Remove the deprecated keys.
let keys: [Defaults.Key] = [
.menuBarHasShadow,
.menuBarHasBorder,
Expand All @@ -94,8 +94,8 @@ struct MenuBarAppearanceConfiguration: Hashable {
}

// MARK: Default Configuration
extension MenuBarAppearanceConfiguration {
static let defaultConfiguration = MenuBarAppearanceConfiguration(
extension MenuBarAppearanceConfigurationV1 {
static let defaultConfiguration = MenuBarAppearanceConfigurationV1(
hasShadow: false,
hasBorder: false,
isInset: true,
Expand All @@ -110,8 +110,8 @@ extension MenuBarAppearanceConfiguration {
)
}

// MARK: MenuBarAppearanceConfiguration: Codable
extension MenuBarAppearanceConfiguration: Codable {
// MARK: MenuBarAppearanceConfigurationV1: Codable
extension MenuBarAppearanceConfigurationV1: Codable {
private enum CodingKeys: CodingKey {
case hasShadow
case hasBorder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// MenuBarAppearanceConfigurationV2.swift
// Ice
//

import CoreGraphics
import Foundation

struct MenuBarAppearanceConfigurationV2: Hashable {
var lightModeConfiguration: MenuBarAppearancePartialConfiguration
var darkModeConfiguration: MenuBarAppearancePartialConfiguration
var staticConfiguration: MenuBarAppearancePartialConfiguration
var shapeKind: MenuBarShapeKind
var fullShapeInfo: MenuBarFullShapeInfo
var splitShapeInfo: MenuBarSplitShapeInfo
var isInset: Bool
var isDynamic: Bool

var hasRoundedShape: Bool {
switch shapeKind {
case .none: false
case .full: fullShapeInfo.hasRoundedShape
case .split: splitShapeInfo.hasRoundedShape
}
}

var current: MenuBarAppearancePartialConfiguration {
if isDynamic {
switch SystemAppearance.current {
case .light: lightModeConfiguration
case .dark: darkModeConfiguration
}
} else {
staticConfiguration
}
}
}

// MARK: Default Configuration
extension MenuBarAppearanceConfigurationV2 {
static let defaultConfiguration = MenuBarAppearanceConfigurationV2(
lightModeConfiguration: .defaultConfiguration,
darkModeConfiguration: .defaultConfiguration,
staticConfiguration: .defaultConfiguration,
shapeKind: .none,
fullShapeInfo: .default,
splitShapeInfo: .default,
isInset: true,
isDynamic: false
)
}

extension MenuBarAppearanceConfigurationV2: Codable {
private enum CodingKeys: CodingKey {
case lightModeConfiguration
case darkModeConfiguration
case staticConfiguration
case shapeKind
case fullShapeInfo
case splitShapeInfo
case isInset
case isDynamic
}

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
lightModeConfiguration: container.decodeIfPresent(MenuBarAppearancePartialConfiguration.self, forKey: .lightModeConfiguration) ?? Self.defaultConfiguration.lightModeConfiguration,
darkModeConfiguration: container.decodeIfPresent(MenuBarAppearancePartialConfiguration.self, forKey: .darkModeConfiguration) ?? Self.defaultConfiguration.darkModeConfiguration,
staticConfiguration: container.decodeIfPresent(MenuBarAppearancePartialConfiguration.self, forKey: .staticConfiguration) ?? Self.defaultConfiguration.staticConfiguration,
shapeKind: container.decodeIfPresent(MenuBarShapeKind.self, forKey: .shapeKind) ?? Self.defaultConfiguration.shapeKind,
fullShapeInfo: container.decodeIfPresent(MenuBarFullShapeInfo.self, forKey: .fullShapeInfo) ?? Self.defaultConfiguration.fullShapeInfo,
splitShapeInfo: container.decodeIfPresent(MenuBarSplitShapeInfo.self, forKey: .splitShapeInfo) ?? Self.defaultConfiguration.splitShapeInfo,
isInset: container.decodeIfPresent(Bool.self, forKey: .isInset) ?? Self.defaultConfiguration.isInset,
isDynamic: container.decodeIfPresent(Bool.self, forKey: .isDynamic) ?? Self.defaultConfiguration.isDynamic
)
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(lightModeConfiguration, forKey: .lightModeConfiguration)
try container.encode(darkModeConfiguration, forKey: .darkModeConfiguration)
try container.encode(staticConfiguration, forKey: .staticConfiguration)
try container.encode(shapeKind, forKey: .shapeKind)
try container.encode(fullShapeInfo, forKey: .fullShapeInfo)
try container.encode(splitShapeInfo, forKey: .splitShapeInfo)
try container.encode(isInset, forKey: .isInset)
try container.encode(isDynamic, forKey: .isDynamic)
}
}

// MARK: - MenuBarAppearancePartialConfiguration

struct MenuBarAppearancePartialConfiguration: Hashable {
var hasShadow: Bool
var hasBorder: Bool
var borderColor: CGColor
var borderWidth: Double
var tintKind: MenuBarTintKind
var tintColor: CGColor
var tintGradient: CustomGradient
}

// MARK: Default Partial Configuration
extension MenuBarAppearancePartialConfiguration {
static let defaultConfiguration = MenuBarAppearancePartialConfiguration(
hasShadow: false,
hasBorder: false,
borderColor: .black,
borderWidth: 1,
tintKind: .none,
tintColor: .black,
tintGradient: .defaultMenuBarTint
)
}

// MARK: MenuBarAppearancePartialConfiguration: Codable
extension MenuBarAppearancePartialConfiguration: Codable {
private enum CodingKeys: CodingKey {
case hasShadow
case hasBorder
case borderColor
case borderWidth
case shapeKind
case fullShapeInfo
case splitShapeInfo
case tintKind
case tintColor
case tintGradient
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
hasShadow: container.decodeIfPresent(Bool.self, forKey: .hasShadow) ?? Self.defaultConfiguration.hasShadow,
hasBorder: container.decodeIfPresent(Bool.self, forKey: .hasBorder) ?? Self.defaultConfiguration.hasBorder,
borderColor: container.decodeIfPresent(CodableColor.self, forKey: .borderColor)?.cgColor ?? Self.defaultConfiguration.borderColor,
borderWidth: container.decodeIfPresent(Double.self, forKey: .borderWidth) ?? Self.defaultConfiguration.borderWidth,
tintKind: container.decodeIfPresent(MenuBarTintKind.self, forKey: .tintKind) ?? Self.defaultConfiguration.tintKind,
tintColor: container.decodeIfPresent(CodableColor.self, forKey: .tintColor)?.cgColor ?? Self.defaultConfiguration.tintColor,
tintGradient: container.decodeIfPresent(CustomGradient.self, forKey: .tintGradient) ?? Self.defaultConfiguration.tintGradient
)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(hasShadow, forKey: .hasShadow)
try container.encode(hasBorder, forKey: .hasBorder)
try container.encode(CodableColor(cgColor: borderColor), forKey: .borderColor)
try container.encode(borderWidth, forKey: .borderWidth)
try container.encode(tintKind, forKey: .tintKind)
try container.encode(CodableColor(cgColor: tintColor), forKey: .tintColor)
try container.encode(tintGradient, forKey: .tintGradient)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,21 @@ struct MenuBarAppearanceEditor: View {
private var mainForm: some View {
IceForm {
IceSection {
tintPicker
shadowToggle
isDynamicToggle
}
IceSection {
borderToggle
borderColor
borderWidth
if appearanceManager.configuration.isDynamic {
VStack(alignment: .leading) {
Text("Light Appearance")
.font(.headline)
MenuBarPartialAppearanceEditor(configuration: appearanceManager.bindings.configuration.lightModeConfiguration)
}
VStack(alignment: .leading) {
Text("Dark Appearance")
.font(.headline)
MenuBarPartialAppearanceEditor(configuration: appearanceManager.bindings.configuration.darkModeConfiguration)
}
} else {
MenuBarPartialAppearanceEditor(configuration: appearanceManager.bindings.configuration.staticConfiguration)
}
IceSection("Menu Bar Shape") {
shapePicker
Expand All @@ -106,36 +114,74 @@ struct MenuBarAppearanceEditor: View {
}
}

@ViewBuilder
private var isDynamicToggle: some View {
Toggle("Use dynamic appearance", isOn: appearanceManager.bindings.configuration.isDynamic)
.annotation("Apply different settings based on the current system appearance")
}

@ViewBuilder
private var cannotEdit: some View {
Text("Ice cannot edit the appearance of automatically hidden menu bars")
.font(.title3)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}

@ViewBuilder
private var shapePicker: some View {
MenuBarShapePicker()
.fixedSize(horizontal: false, vertical: true)
}

@ViewBuilder
private var isInset: some View {
if appearanceManager.configuration.shapeKind != .none {
Toggle(
"Use inset shape on screens with notch",
isOn: appearanceManager.bindings.configuration.isInset
)
}
}
}

struct MenuBarPartialAppearanceEditor: View {
@Binding var configuration: MenuBarAppearancePartialConfiguration

var body: some View {
IceSection {
tintPicker
shadowToggle
}
IceSection {
borderToggle
borderColor
borderWidth
}
}

@ViewBuilder
private var tintPicker: some View {
IceLabeledContent("Tint") {
HStack {
IcePicker("Tint", selection: appearanceManager.bindings.configuration.tintKind) {
IcePicker("Tint", selection: $configuration.tintKind) {
ForEach(MenuBarTintKind.allCases) { tintKind in
Text(tintKind.localized).icePickerID(tintKind)
}
}
.labelsHidden()

switch appearanceManager.configuration.tintKind {
switch configuration.tintKind {
case .none:
EmptyView()
case .solid:
CustomColorPicker(
selection: appearanceManager.bindings.configuration.tintColor,
selection: $configuration.tintColor,
supportsOpacity: false,
mode: .crayon
)
case .gradient:
CustomGradientPicker(
gradient: appearanceManager.bindings.configuration.tintGradient,
gradient: $configuration.tintGradient,
supportsOpacity: false,
allowsEmptySelections: false,
mode: .crayon
Expand All @@ -148,20 +194,20 @@ struct MenuBarAppearanceEditor: View {

@ViewBuilder
private var shadowToggle: some View {
Toggle("Shadow", isOn: appearanceManager.bindings.configuration.hasShadow)
Toggle("Shadow", isOn: $configuration.hasShadow)
}

@ViewBuilder
private var borderToggle: some View {
Toggle("Border", isOn: appearanceManager.bindings.configuration.hasBorder)
Toggle("Border", isOn: $configuration.hasBorder)
}

@ViewBuilder
private var borderColor: some View {
if appearanceManager.configuration.hasBorder {
if configuration.hasBorder {
IceLabeledContent("Border Color") {
CustomColorPicker(
selection: appearanceManager.bindings.configuration.borderColor,
selection: $configuration.borderColor,
supportsOpacity: true,
mode: .crayon
)
Expand All @@ -171,31 +217,15 @@ struct MenuBarAppearanceEditor: View {

@ViewBuilder
private var borderWidth: some View {
if appearanceManager.configuration.hasBorder {
if configuration.hasBorder {
IcePicker(
"Border Width",
selection: appearanceManager.bindings.configuration.borderWidth
selection: $configuration.borderWidth
) {
Text("1").icePickerID(1.0)
Text("2").icePickerID(2.0)
Text("3").icePickerID(3.0)
}
}
}

@ViewBuilder
private var shapePicker: some View {
MenuBarShapePicker()
.fixedSize(horizontal: false, vertical: true)
}

@ViewBuilder
private var isInset: some View {
if appearanceManager.configuration.shapeKind != .none {
Toggle(
"Use inset shape on screens with notch",
isOn: appearanceManager.bindings.configuration.isInset
)
}
}
}
Loading

0 comments on commit 326893d

Please sign in to comment.