@@ -26,6 +26,8 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene { // todo should it
2626 }
2727 Divider ( )
2828 }
29+ getExperimentalUISettingsMenu ( viewModel: viewModel)
30+ Divider ( )
2931 Button ( viewModel. isEnabled ? " Disable " : " Enable " ) {
3032 Task {
3133 try await runSession ( . menuBarButton, . forceRun) { ( ) throws in
@@ -62,25 +64,51 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene { // todo should it
6264 } . keyboardShortcut ( " Q " , modifiers: . command)
6365 } label: {
6466 if viewModel. isEnabled {
65- MonospacedText ( viewModel. trayText)
67+ switch viewModel. experimentalUISettings. displayStyle {
68+ case . systemText:
69+ MenuBarLabel ( viewModel. trayText, textStyle: . system)
70+ case . monospacedText:
71+ MenuBarLabel ( viewModel. trayText)
72+ case . squares:
73+ MenuBarLabel ( viewModel. trayText, trayItems: viewModel. trayItems)
74+ case . i3:
75+ MenuBarLabel ( viewModel. trayText, trayItems: viewModel. trayItems, workspaces: viewModel. workspaces)
76+ }
6677 } else {
67- MonospacedText ( " ⏸️ " )
78+ MenuBarLabel ( " ⏸️ " )
6879 }
6980 }
7081}
7182
72- struct MonospacedText : View {
83+ @MainActor
84+ struct MenuBarLabel : View {
7385 @Environment ( \. colorScheme) var colorScheme : ColorScheme
7486 var text : String
75- init ( _ text: String ) { self . text = text }
87+ var textStyle : MenuBarTextStyle
88+ var color : Color ?
89+ var trayItems : [ TrayItem ] ?
90+ var workspaces : [ WorkspaceViewModel ] ?
91+
92+ let hStackSpacing = CGFloat ( 4 )
93+ let itemHeight = CGFloat ( 40 )
94+ let itemBorderSize = CGFloat ( 4 )
95+ let itemCornerRadius = CGFloat ( 6 )
96+
97+ var finalColor : Color {
98+ return color ?? ( colorScheme == . dark ? Color . white : Color . black)
99+ }
100+
101+ init ( _ text: String , textStyle: MenuBarTextStyle = . monospaced, color: Color ? = nil , trayItems: [ TrayItem ] ? = nil , workspaces: [ WorkspaceViewModel ] ? = nil ) {
102+ self . text = text
103+ self . textStyle = textStyle
104+ self . color = color
105+ self . trayItems = trayItems
106+ self . workspaces = workspaces
107+ }
76108
77109 var body : some View {
78110 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- )
111+ let renderer = ImageRenderer ( content: menuBarContent)
84112 if let cgImage = renderer. cgImage {
85113 // Using scale: 1 results in a blurry image for unknown reasons
86114 Image ( cgImage, scale: 2 , label: Text ( text) )
@@ -92,6 +120,129 @@ struct MonospacedText: View {
92120 Text ( text)
93121 }
94122 }
123+
124+ var menuBarContent : some View {
125+ return ZStack {
126+ if let trayItems {
127+ HStack ( spacing: hStackSpacing) {
128+ ForEach ( trayItems, id: \. id) { item in
129+ itemView ( for: item)
130+ if item. type == . mode {
131+ Text ( " : " )
132+ . font ( . system( . largeTitle, design: textStyle. design) )
133+ . foregroundStyle ( finalColor)
134+ . bold ( )
135+ }
136+ }
137+ if workspaces != nil {
138+ let otherWorkspaces = Workspace . all. filter { workspace in
139+ !workspace. isEffectivelyEmpty && !trayItems. contains ( where: { item in item. type == . monitor && item. name == workspace. name } )
140+ }
141+ if !otherWorkspaces. isEmpty {
142+ Group {
143+ Text ( " | " )
144+ . font ( . system( . largeTitle, design: textStyle. design) )
145+ . foregroundStyle ( finalColor)
146+ . bold ( )
147+ . padding ( . bottom, 2 )
148+ ForEach ( otherWorkspaces, id: \. name) { item in
149+ itemView ( for: TrayItem ( type: . monitor, name: item. name, isActive: false ) )
150+ }
151+ }
152+ . opacity ( 0.6 )
153+ }
154+ }
155+ }
156+ . frame ( height: itemHeight)
157+ } else {
158+ HStack ( spacing: hStackSpacing) {
159+ Text ( text)
160+ . font ( . system( . largeTitle, design: textStyle. design) )
161+ . foregroundStyle ( finalColor)
162+ }
163+ }
164+ }
165+ }
166+
167+ @ViewBuilder
168+ fileprivate func itemView( for item: TrayItem ) -> some View {
169+ if item. name. containsEmoji ( ) {
170+ // If workspace name contains emojis we use the plain emoji in text to avoid visibility issues scaling the emoji to fit the squares
171+ Text ( item. name)
172+ . font ( . system( . largeTitle, design: textStyle. design) )
173+ . foregroundStyle ( finalColor)
174+ } else {
175+ if let imageName = item. systemImageName {
176+ Image ( systemName: imageName)
177+ . resizable ( )
178+ . aspectRatio ( contentMode: . fit)
179+ . symbolRenderingMode ( . monochrome)
180+ . foregroundStyle ( finalColor)
181+ } else {
182+ let text = Text ( item. name)
183+ . font ( . system( . largeTitle, design: textStyle. design) )
184+ . bold ( )
185+ . padding ( . horizontal, item. isActive ? itemBorderSize * 2 : itemBorderSize * 1.5 )
186+ . frame ( height: itemHeight)
187+ if item. isActive {
188+ ZStack {
189+ text. foregroundStyle ( . clear)
190+ . overlay (
191+ RoundedRectangle ( cornerRadius: itemCornerRadius, style: . circular)
192+ )
193+ text. blendMode ( . destinationOut)
194+ }
195+ . compositingGroup ( )
196+ . foregroundStyle ( finalColor)
197+ } else {
198+ text
199+ . padding ( . horizontal, itemBorderSize)
200+ . overlay (
201+ RoundedRectangle ( cornerRadius: itemCornerRadius, style: . circular)
202+ . strokeBorder ( lineWidth: itemBorderSize)
203+ )
204+ . foregroundStyle ( finalColor)
205+ }
206+ }
207+ }
208+ }
209+ }
210+
211+ enum MenuBarTextStyle : String {
212+ case monospaced
213+ case system
214+ var design : Font . Design {
215+ switch self {
216+ case . monospaced:
217+ return . monospaced
218+ case . system:
219+ return . default
220+ }
221+ }
222+ }
223+
224+ enum MenuBarStyle : String , CaseIterable , Identifiable , Equatable , Hashable {
225+ case monospacedText
226+ case systemText
227+ case squares
228+ case i3
229+ var id : Int {
230+ return self . hashValue
231+ }
232+ var title : String {
233+ switch self {
234+ case . monospacedText: " Monospaced font "
235+ case . systemText: " System font "
236+ case . squares: " Square images "
237+ case . i3: " i3 style "
238+ }
239+ }
240+ }
241+
242+ private extension String {
243+ func containsEmoji( ) -> Bool {
244+ unicodeScalars. contains { $0. properties. isEmoji && $0. properties. isEmojiPresentation }
245+ }
95246}
96247
97248func getTextEditorToOpenConfig( ) -> URL {
0 commit comments