Skip to content

Commit 640c328

Browse files
committed
New Experimental UI Settings for menu bar style and functionality
1 parent 9314802 commit 640c328

File tree

3 files changed

+212
-11
lines changed

3 files changed

+212
-11
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import AppKit
2+
import SwiftUI
3+
4+
struct ExperimentalUISettings {
5+
var displayStyle: MenuBarStyle {
6+
get {
7+
if let value = UserDefaults.standard.string(forKey: ExperimentalUISettingsItems.displayStyle.rawValue) {
8+
return MenuBarStyle(rawValue: value) ?? .monospacedText
9+
} else {
10+
return .monospacedText
11+
}
12+
}
13+
set {
14+
UserDefaults.standard.setValue(newValue.rawValue, forKey: ExperimentalUISettingsItems.displayStyle.rawValue)
15+
UserDefaults.standard.synchronize()
16+
}
17+
}
18+
var filterEmptyWorkspacesFromMenu: Bool {
19+
get {
20+
return UserDefaults.standard.bool(forKey: ExperimentalUISettingsItems.filterEmptyWorkspacesFromMenu.rawValue)
21+
}
22+
set {
23+
UserDefaults.standard.setValue(newValue, forKey: ExperimentalUISettingsItems.filterEmptyWorkspacesFromMenu.rawValue)
24+
UserDefaults.standard.synchronize()
25+
}
26+
}
27+
}
28+
29+
enum ExperimentalUISettingsItems: String {
30+
case displayStyle
31+
case filterEmptyWorkspacesFromMenu
32+
}
33+
34+
@MainActor
35+
func getExperimentalUISettingsMenu(viewModel: TrayMenuModel) -> some View {
36+
Menu {
37+
Text("Menu bar display style:")
38+
ForEach(MenuBarStyle.allCases) { item in
39+
Button {
40+
viewModel.experimentalUISettings.displayStyle = item
41+
} label: {
42+
Toggle(isOn: .constant(viewModel.experimentalUISettings.displayStyle == item)) {
43+
Text(item.title)
44+
}
45+
}
46+
}.id(viewModel.experimentalUISettings.displayStyle)
47+
Divider()
48+
Text("Menu content:")
49+
Button {
50+
viewModel.experimentalUISettings.filterEmptyWorkspacesFromMenu.toggle()
51+
} label: {
52+
Toggle(isOn: .constant(viewModel.experimentalUISettings.filterEmptyWorkspacesFromMenu)) {
53+
Text("Filter empty workspaces")
54+
}.id(viewModel.experimentalUISettings.filterEmptyWorkspacesFromMenu)
55+
}
56+
} label: {
57+
Text("Experimental UI Settings")
58+
}
59+
}

Sources/AppBundle/MenuBar.swift

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene { // todo should it
1313
Divider()
1414
if let token: RunSessionGuard = .isServerEnabled {
1515
Text("Workspaces:")
16-
ForEach(viewModel.workspaces, id: \.name) { workspace in
16+
let filteredWorkspaces = viewModel.workspaces.filter {
17+
viewModel.experimentalUISettings.filterEmptyWorkspacesFromMenu ? !$0.suffix.isEmpty : true
18+
}
19+
ForEach(filteredWorkspaces, id: \.name) { workspace in
1720
Button {
1821
Task {
1922
try await runSession(.menuBarButton, token) { _ = Workspace.get(byName: workspace.name).focusWorkspace() }
@@ -26,6 +29,8 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene { // todo should it
2629
}
2730
Divider()
2831
}
32+
getExperimentalUISettingsMenu(viewModel: viewModel)
33+
Divider()
2934
Button(viewModel.isEnabled ? "Disable" : "Enable") {
3035
Task {
3136
try await runSession(.menuBarButton, .forceRun) { () throws in
@@ -62,25 +67,38 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene { // todo should it
6267
}.keyboardShortcut("Q", modifiers: .command)
6368
} label: {
6469
if viewModel.isEnabled {
65-
MonospacedText(viewModel.trayText)
70+
switch viewModel.experimentalUISettings.displayStyle {
71+
case .systemText:
72+
Text(viewModel.trayText)
73+
case .monospacedText:
74+
MenuBarLabel(viewModel.trayText)
75+
case .squares:
76+
MenuBarLabel(viewModel.trayText, trayItems: viewModel.trayItems)
77+
case .i3:
78+
MenuBarLabel(viewModel.trayText, trayItems: viewModel.trayItems, workspaces: viewModel.workspaces)
79+
}
6680
} else {
67-
MonospacedText("⏸️")
81+
MenuBarLabel("⏸️")
6882
}
6983
}
7084
}
7185

72-
struct MonospacedText: View {
86+
@MainActor
87+
struct MenuBarLabel: View {
7388
@Environment(\.colorScheme) var colorScheme: ColorScheme
7489
var text: String
75-
init(_ text: String) { self.text = text }
90+
var trayItems: [TrayItem]?
91+
var workspaces: [WorkspaceViewModel]?
92+
93+
init(_ text: String, trayItems: [TrayItem]? = nil, workspaces: [WorkspaceViewModel]? = nil) {
94+
self.text = text
95+
self.trayItems = trayItems
96+
self.workspaces = workspaces
97+
}
7698

7799
var body: some View {
78100
if #available(macOS 14, *) { // https://github.com/nikitabobko/AeroSpace/issues/1122
79-
let renderer = ImageRenderer(
80-
content: Text(text)
81-
.font(.system(.largeTitle, design: .monospaced))
82-
.foregroundStyle(colorScheme == .light ? Color.black : Color.white)
83-
)
101+
let renderer = ImageRenderer(content: menuBarContent)
84102
if let cgImage = renderer.cgImage {
85103
// Using scale: 1 results in a blurry image for unknown reasons
86104
Image(cgImage, scale: 2, label: Text(text))
@@ -92,6 +110,96 @@ struct MonospacedText: View {
92110
Text(text)
93111
}
94112
}
113+
114+
@ViewBuilder
115+
var menuBarContent: some View {
116+
let color = colorScheme == .light ? Color.black : Color.white
117+
if let trayItems {
118+
HStack(spacing: 4) {
119+
ForEach(trayItems, id: \.name) { item in
120+
if item.name.containsEmoji() {
121+
// If workspace name contains emojis we use the plain emoji in text to avoid visibility issues scaling the emoji to fit the squares
122+
Text(item.name)
123+
.font(.system(.largeTitle, design: .monospaced))
124+
.foregroundStyle(color)
125+
.bold()
126+
} else {
127+
Image(systemName: item.systemImageName)
128+
.resizable()
129+
.aspectRatio(contentMode: .fit)
130+
.foregroundStyle(color)
131+
if item.type == .mode {
132+
Text(":")
133+
.font(.system(.largeTitle, design: .monospaced))
134+
.foregroundStyle(color)
135+
.bold()
136+
}
137+
}
138+
}
139+
if workspaces != nil {
140+
let otherWorkspaces = Workspace.all.filter { workspace in
141+
!workspace.isEffectivelyEmpty && !trayItems.contains(where: { item in item.name == workspace.name })
142+
}
143+
if !otherWorkspaces.isEmpty {
144+
Group {
145+
Text("|")
146+
.font(.system(.largeTitle, design: .monospaced))
147+
.foregroundStyle(color)
148+
.bold()
149+
.padding(.bottom, 2)
150+
ForEach(otherWorkspaces, id: \.name) { item in
151+
if item.name.containsEmoji() {
152+
Text(item.name)
153+
.font(.system(.largeTitle, design: .monospaced))
154+
.foregroundStyle(color)
155+
.bold()
156+
} else {
157+
Image(systemName: "\(item.name.lowercased()).square")
158+
.resizable()
159+
.aspectRatio(contentMode: .fit)
160+
.foregroundStyle(color)
161+
}
162+
}
163+
}
164+
.opacity(0.6)
165+
}
166+
}
167+
}
168+
.frame(height: 40)
169+
} else {
170+
Text(text)
171+
.font(.system(.largeTitle, design: .monospaced))
172+
.foregroundStyle(colorScheme == .light ? Color.black : Color.white)
173+
}
174+
}
175+
}
176+
177+
enum MenuBarStyle: String, CaseIterable, Identifiable, Equatable, Hashable {
178+
case monospacedText
179+
case systemText
180+
case squares
181+
case i3
182+
var id: Int {
183+
return self.hashValue
184+
}
185+
var title: String {
186+
switch self {
187+
case .monospacedText:
188+
"Monospaced font"
189+
case .systemText:
190+
"System font"
191+
case .squares:
192+
"Square images"
193+
case .i3:
194+
"i3 style"
195+
}
196+
}
197+
}
198+
199+
extension String {
200+
func containsEmoji() -> Bool {
201+
unicodeScalars.contains { $0.properties.isEmoji && $0.properties.isEmojiPresentation }
202+
}
95203
}
96204

97205
func getTextEditorToOpenConfig() -> URL {

Sources/AppBundle/TrayMenuModel.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ public class TrayMenuModel: ObservableObject {
77
private init() {}
88

99
@Published var trayText: String = ""
10+
@Published var trayItems: [TrayItem] = []
1011
/// Is "layouting" enabled
1112
@Published var isEnabled: Bool = true
1213
@Published var workspaces: [WorkspaceViewModel] = []
14+
@Published var experimentalUISettings: ExperimentalUISettings = ExperimentalUISettings()
1315
}
1416

1517
@MainActor func updateTrayText() {
@@ -25,10 +27,42 @@ public class TrayMenuModel: ObservableObject {
2527
let monitor = $0.isVisible || !$0.isEffectivelyEmpty ? " - \($0.workspaceMonitor.name)" : ""
2628
return WorkspaceViewModel(name: $0.name, suffix: monitor, isFocused: focus.workspace == $0)
2729
}
30+
var items = sortedMonitors.map {
31+
TrayItem(type: .monitor, name: $0.activeWorkspace.name, isActive: $0.activeWorkspace == focus.workspace && sortedMonitors.count > 1)
32+
}
33+
let mode = activeMode?.takeIf { $0 != mainModeId }?.first?.lets { TrayItem(type: .mode, name: String($0), isActive: true) }
34+
if let mode {
35+
items.insert(mode, at: 0)
36+
}
37+
TrayMenuModel.shared.trayItems = items
2838
}
2939

30-
struct WorkspaceViewModel {
40+
struct WorkspaceViewModel: Hashable {
3141
let name: String
3242
let suffix: String
3343
let isFocused: Bool
3444
}
45+
46+
enum TrayItemType: String {
47+
case mode
48+
case monitor
49+
}
50+
51+
struct TrayItem: Hashable {
52+
let type: TrayItemType
53+
let name: String
54+
let isActive: Bool
55+
var systemImageName: String {
56+
let lowercasedName = name.lowercased()
57+
switch type {
58+
case .mode:
59+
return "\(lowercasedName).circle"
60+
case .monitor:
61+
if isActive {
62+
return "\(lowercasedName).square.fill"
63+
} else {
64+
return "\(lowercasedName).square"
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)