Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions a-bar/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,30 @@ class AppDelegate: NSObject, NSApplicationDelegate {
barWindows[key] = barWindow
barWindow.makeKeyAndOrderFront(nil)
}

// Create left bar if configured
if displayConfig.leftBar != nil {
let key = BarWindowKey(displayIndex: displayIndex, position: .left)
let barWindow = BarWindow(
screen: screen,
displayIndex: displayIndex,
position: .left
)
barWindows[key] = barWindow
barWindow.makeKeyAndOrderFront(nil)
}

// Create right bar if configured
if displayConfig.rightBar != nil {
let key = BarWindowKey(displayIndex: displayIndex, position: .right)
let barWindow = BarWindow(
screen: screen,
displayIndex: displayIndex,
position: .right
)
barWindows[key] = barWindow
barWindow.makeKeyAndOrderFront(nil)
}
}
}

Expand Down
233 changes: 188 additions & 45 deletions a-bar/BarView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import SwiftUI

/// Orientation for widget layout
enum WidgetOrientation {
case horizontal
case vertical
}

/// Main bar view containing all widgets arranged in sections
struct BarView: View {
let displayIndex: Int
Expand All @@ -25,61 +31,144 @@ struct BarView: View {
layoutManager.barLayout(forDisplay: displayIndex, position: position)
}

/// Whether this bar is vertical (left or right edge)
private var isVertical: Bool {
position.isVertical
}

/// Widget orientation based on bar position
private var orientation: WidgetOrientation {
isVertical ? .vertical : .horizontal
}

// The body of the view constructs the layout of the bar using an HStack to arrange the left, center, and right sections. Each section contains its respective widgets, which are rendered using the WidgetContainer view. The bar's background and optional border are also applied here based on user settings.
var body: some View {
GeometryReader { geometry in
let borderEnabled = globalSettings.showBorder
HStack(spacing: 0) {
// Left section
HStack(spacing: globalSettings.barElementGap) {
ForEach(leftWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex)
}

if isVertical {
verticalBarContent
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(barBackground)
.overlay(verticalBorderOverlay(enabled: borderEnabled))
} else {
horizontalBarContent
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(barBackground)
.overlay(horizontalBorderOverlay(enabled: borderEnabled))
}
}
}

// MARK: - Horizontal Bar Layout (Top/Bottom)

@ViewBuilder
private var horizontalBarContent: some View {
HStack(spacing: 0) {
// Left section
HStack(spacing: globalSettings.barElementGap) {
ForEach(leftWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex, orientation: orientation)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)

// Center section
HStack(spacing: globalSettings.barElementGap) {
ForEach(centerWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex)
}
// Center section
HStack(spacing: globalSettings.barElementGap) {
ForEach(centerWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex, orientation: orientation)
}
}

// Right section
HStack(spacing: globalSettings.barElementGap) {
ForEach(rightWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex)
}
// Right section
HStack(spacing: globalSettings.barElementGap) {
ForEach(rightWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex, orientation: orientation)
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.horizontal, 8)
}

// MARK: - Vertical Bar Layout (Left/Right)

@ViewBuilder
private var verticalBarContent: some View {
VStack(spacing: 0) {
// Top section (mapped from left)
VStack(spacing: globalSettings.barElementGap) {
ForEach(leftWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex, orientation: orientation)
}
}
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity, alignment: .top)

// Middle section (mapped from center)
VStack(spacing: globalSettings.barElementGap) {
ForEach(centerWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex, orientation: orientation)
}
}
.frame(maxWidth: .infinity)

// Bottom section (mapped from right)
VStack(spacing: globalSettings.barElementGap) {
ForEach(rightWidgets) { widget in
WidgetContainer(widget: widget, displayIndex: displayIndex, orientation: orientation)
}
}
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity, alignment: .bottom)
}
.padding(.vertical, 8)
.padding(.horizontal, 4)
}

// MARK: - Border Overlays

@ViewBuilder
private func horizontalBorderOverlay(enabled: Bool) -> some View {
if enabled {
// Border at bottom for top bar, at top for bottom bar
if position == .top {
VStack(spacing: 0) {
Spacer(minLength: 0)
Rectangle()
.fill(theme.minor)
.frame(height: 1)
}
} else {
VStack(spacing: 0) {
Rectangle()
.fill(theme.minor)
.frame(height: 1)
Spacer(minLength: 0)
}
}
}
}

@ViewBuilder
private func verticalBorderOverlay(enabled: Bool) -> some View {
if enabled {
// Border on right edge for left bar, on left edge for right bar
if position == .left {
HStack(spacing: 0) {
Spacer(minLength: 0)
Rectangle()
.fill(theme.minor)
.frame(width: 1)
}
} else {
HStack(spacing: 0) {
Rectangle()
.fill(theme.minor)
.frame(width: 1)
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
// A minimal padding is necessary on the built-in screen as it has rounded corners
// Without it, the content might be clipped or appear too close to the edges
.padding(.horizontal, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(barBackground)
.overlay(
Group {
if borderEnabled {
// Border at top for bottom bar, at bottom for top bar
if position == .top {
VStack(spacing: 0) {
Spacer(minLength: 0)
Rectangle()
.fill(theme.minor)
.frame(height: 1)
}
} else {
VStack(spacing: 0) {
Rectangle()
.fill(theme.minor)
.frame(height: 1)
Spacer(minLength: 0)
}
}
}
}, alignment: position == .top ? .bottom : .top
)
}
}

Expand Down Expand Up @@ -108,6 +197,7 @@ struct BarView: View {
struct WidgetContainer: View {
let widget: WidgetInstance
let displayIndex: Int
var orientation: WidgetOrientation = .horizontal

@EnvironmentObject var settings: SettingsManager
@EnvironmentObject var yabaiService: YabaiService
Expand Down Expand Up @@ -162,5 +252,58 @@ struct WidgetContainer: View {
}
}
}
.environment(\.widgetOrientation, orientation)
}
}

// MARK: - Widget Orientation Environment Key

struct WidgetOrientationKey: EnvironmentKey {
static let defaultValue: WidgetOrientation = .horizontal
}

extension EnvironmentValues {
var widgetOrientation: WidgetOrientation {
get { self[WidgetOrientationKey.self] }
set { self[WidgetOrientationKey.self] = newValue }
}
}

// MARK: - Adaptive Stack for Orientation-Aware Layout

/// A stack that switches between HStack and VStack based on widget orientation
struct AdaptiveStack<Content: View>: View {
@Environment(\.widgetOrientation) var orientation

let horizontalSpacing: CGFloat
let verticalSpacing: CGFloat
let horizontalAlignment: VerticalAlignment
let verticalAlignment: HorizontalAlignment
@ViewBuilder let content: () -> Content

init(
hSpacing: CGFloat = 4,
vSpacing: CGFloat = 2,
hAlignment: VerticalAlignment = .center,
vAlignment: HorizontalAlignment = .center,
@ViewBuilder content: @escaping () -> Content
) {
self.horizontalSpacing = hSpacing
self.verticalSpacing = vSpacing
self.horizontalAlignment = hAlignment
self.verticalAlignment = vAlignment
self.content = content
}

var body: some View {
if orientation == .vertical {
VStack(alignment: verticalAlignment, spacing: verticalSpacing) {
content()
}
} else {
HStack(alignment: horizontalAlignment, spacing: horizontalSpacing) {
content()
}
}
}
}
42 changes: 29 additions & 13 deletions a-bar/BarWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class BarWindow: NSPanel {
// The hosting view that contains the SwiftUI content of the bar
private var hostingView: NSHostingView<AnyView>?

// Initialize the bar window with the appropriate screen, display index, and position (top or bottom)
// Initialize the bar window with the appropriate screen, display index, and position (top, bottom, left, or right)
init(screen: NSScreen, displayIndex: Int, position: BarPosition) {
self.barScreen = screen
self.displayIndex = displayIndex
Expand All @@ -19,30 +19,46 @@ class BarWindow: NSPanel {
// Calculate frame for the bar
let settings = SettingsManager.shared.settings.global
let barHeight = settings.barHeight
let barWidth = settings.barWidth
// Padding is hardcoded for now, but can be made user-configurable if needed. It provides spacing from the screen edges.
let padding: CGFloat = 0

let screenFrame = screen.frame

let barY: CGFloat
let barFrame: NSRect
switch position {
case .top:
// Position the bar at the top of the screen, accounting for the menu bar height and padding
barY = screenFrame.maxY - barHeight - padding
barFrame = NSRect(
x: screenFrame.minX + padding,
y: screenFrame.maxY - barHeight - padding,
width: screenFrame.width - (padding * 2),
height: barHeight
)
case .bottom:
// Position the bar at the bottom of the screen, accounting for padding
barY = screenFrame.minY + padding
barFrame = NSRect(
x: screenFrame.minX + padding,
y: screenFrame.minY + padding,
width: screenFrame.width - (padding * 2),
height: barHeight
)
case .left:
barFrame = NSRect(
x: screenFrame.minX + padding,
y: screenFrame.minY + padding,
width: barWidth,
height: screenFrame.height - (padding * 2)
)
case .right:
barFrame = NSRect(
x: screenFrame.maxX - barWidth - padding,
y: screenFrame.minY + padding,
width: barWidth,
height: screenFrame.height - (padding * 2)
)
}

// The bar spans the full width of the screen, minus any horizontal padding
let barFrame = NSRect(
x: screenFrame.minX + padding,
y: barY,
width: screenFrame.width - (padding * 2),
height: barHeight
)

//
super.init(
contentRect: barFrame,
styleMask: [.borderless, .nonactivatingPanel, .hudWindow],
Expand Down
6 changes: 6 additions & 0 deletions a-bar/Models/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ class SettingsManager: ObservableObject {
validated.global.barHeight = 34
}

if validated.global.barWidth < 40 || validated.global.barWidth > 300 {
print("⚠️ Invalid barWidth (\(validated.global.barWidth)), using default")
validated.global.barWidth = 120
}

if validated.global.fontSize < 6 || validated.global.fontSize > 72 {
print("⚠️ Invalid fontSize (\(validated.global.fontSize)), using default")
validated.global.fontSize = 11
Expand Down Expand Up @@ -392,6 +397,7 @@ struct GlobalSettings: Codable, Equatable {
var barEnabled: Bool = true
var launchAtLogin: Bool = false
var barHeight: CGFloat = 34
var barWidth: CGFloat = 120 // Width for vertical bars (left/right)
var fontSize: CGFloat = 11
var fontName: String = ""
var barPadding: CGFloat = 4
Expand Down
Loading