color fixes/arrow changes

This commit is contained in:
Michael Freno
2026-01-28 18:52:53 -05:00
parent 39d19d13d5
commit 374226c44c
2 changed files with 135 additions and 79 deletions

View File

@@ -14,9 +14,11 @@ final class MenuBarGuideOverlayPresenter {
static let shared = MenuBarGuideOverlayPresenter() static let shared = MenuBarGuideOverlayPresenter()
private var window: NSWindow? private var window: NSWindow?
private var displayLink: CVDisplayLink?
private var lastWindowFrame: CGRect = .zero private var lastWindowFrame: CGRect = .zero
private var checkTimer: Timer?
private var onboardingWindowObserver: NSObjectProtocol? private var onboardingWindowObserver: NSObjectProtocol?
private var currentStartPoint: CGPoint = .zero
private var previousStartPoint: CGPoint = .zero
func updateVisibility(isVisible: Bool) { func updateVisibility(isVisible: Bool) {
if isVisible { if isVisible {
@@ -28,7 +30,8 @@ final class MenuBarGuideOverlayPresenter {
} }
func hide() { func hide() {
stopDisplayLink() checkTimer?.invalidate()
checkTimer = nil
window?.orderOut(nil) window?.orderOut(nil)
window?.close() window?.close()
window = nil window = nil
@@ -37,7 +40,7 @@ final class MenuBarGuideOverlayPresenter {
private func show() { private func show() {
if let window { if let window {
window.orderFrontRegardless() window.orderFrontRegardless()
startDisplayLink() startCheckTimer()
return return
} }
@@ -58,48 +61,26 @@ final class MenuBarGuideOverlayPresenter {
overlayWindow.isReleasedWhenClosed = false overlayWindow.isReleasedWhenClosed = false
overlayWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary] overlayWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
let overlayView = MenuBarGuideOverlayView() let overlayView = MenuBarGuideOverlayView(
currentStart: currentStartPoint,
previousStart: previousStartPoint
)
overlayWindow.contentView = NSHostingView(rootView: overlayView) overlayWindow.contentView = NSHostingView(rootView: overlayView)
overlayWindow.orderFrontRegardless() overlayWindow.orderFrontRegardless()
window = overlayWindow window = overlayWindow
startDisplayLink() startCheckTimer()
} }
private func startDisplayLink() { private func startCheckTimer() {
guard displayLink == nil else { return } checkTimer?.invalidate()
checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
var link: CVDisplayLink? self?.checkWindowFrame()
CVDisplayLinkCreateWithActiveCGDisplays(&link) }
guard let displayLink = link else { return }
let Doc = Unmanaged.passUnretained(self).toOpaque()
CVDisplayLinkSetOutputCallback(
displayLink,
{ _, _, _, _, _, userInfo -> CVReturn in
guard let userInfo = userInfo else { return kCVReturnSuccess }
let presenter = Unmanaged<MenuBarGuideOverlayPresenter>.fromOpaque(userInfo)
.takeUnretainedValue()
DispatchQueue.main.async {
presenter.checkAndRedraw()
}
return kCVReturnSuccess
}, Doc)
CVDisplayLinkStart(displayLink)
self.displayLink = displayLink
} }
private func stopDisplayLink() { private func checkWindowFrame() {
guard let displayLink else { return }
CVDisplayLinkStop(displayLink)
self.displayLink = nil
}
private func checkAndRedraw() {
guard guard
let onboardingWindow = NSApp.windows.first(where: { let onboardingWindow = NSApp.windows.first(where: {
$0.identifier == WindowIdentifiers.onboarding $0.identifier == WindowIdentifiers.onboarding
@@ -109,13 +90,27 @@ final class MenuBarGuideOverlayPresenter {
let currentFrame = onboardingWindow.frame let currentFrame = onboardingWindow.frame
if currentFrame != lastWindowFrame { if currentFrame != lastWindowFrame {
lastWindowFrame = currentFrame lastWindowFrame = currentFrame
let screenSize = NSScreen.main?.frame ?? .zero
let windowFrame = onboardingWindow.frame
let textRightX = windowFrame.midX + 40
let textY = screenSize.maxY - windowFrame.maxY + 255
let newStart = CGPoint(x: textRightX, y: textY)
previousStartPoint = currentStartPoint
currentStartPoint = newStart
redraw() redraw()
} }
} }
private func redraw() { private func redraw() {
guard let window else { return } guard let window else { return }
let overlayView = MenuBarGuideOverlayView()
let overlayView = MenuBarGuideOverlayView(
currentStart: currentStartPoint,
previousStart: previousStartPoint
)
window.contentView = NSHostingView(rootView: overlayView) window.contentView = NSHostingView(rootView: overlayView)
} }
@@ -125,6 +120,18 @@ final class MenuBarGuideOverlayPresenter {
NotificationCenter.default.removeObserver(observer) NotificationCenter.default.removeObserver(observer)
} }
guard
let onboardingWindow = NSApp.windows.first(where: {
$0.identifier == WindowIdentifiers.onboarding
})
else { return }
// Set up KVO for window frame changes
onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) {
[weak self] _, _ in
self?.checkWindowFrame()
}
// Add observer for when the onboarding window is closed // Add observer for when the onboarding window is closed
onboardingWindowObserver = NotificationCenter.default.addObserver( onboardingWindowObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification, forName: NSWindow.willCloseNotification,
@@ -144,6 +151,9 @@ final class MenuBarGuideOverlayPresenter {
} }
struct MenuBarGuideOverlayView: View { struct MenuBarGuideOverlayView: View {
var currentStart: CGPoint
var previousStart: CGPoint
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
let size = proxy.size let size = proxy.size
@@ -154,7 +164,7 @@ struct MenuBarGuideOverlayView: View {
let target = targetPoint(from: locationResult.frame) let target = targetPoint(from: locationResult.frame)
let start = startPoint(screenSize: size, screenFrame: screen.frame) let start = startPoint(screenSize: size, screenFrame: screen.frame)
HandDrawnArrowShape(start: start, end: target) HandDrawnArrowShape(start: start, end: target, previousStart: previousStart)
.stroke( .stroke(
Color.accentColor.opacity(0.85), Color.accentColor.opacity(0.85),
style: StrokeStyle(lineWidth: 3.5, lineCap: .round, lineJoin: .round) style: StrokeStyle(lineWidth: 3.5, lineCap: .round, lineJoin: .round)
@@ -183,19 +193,29 @@ struct MenuBarGuideOverlayView: View {
} }
return CGPoint(x: screenSize.width * 0.5, y: screenSize.height * 0.45) return CGPoint(x: screenSize.width * 0.5, y: screenSize.height * 0.45)
} }
private func interpolate(from: CGPoint, to: CGPoint, factor: CGFloat) -> CGPoint {
CGPoint(x: from.x + (to.x - from.x) * factor, y: from.y + (to.y - from.y) * factor)
}
} }
struct HandDrawnArrowShape: Shape { struct HandDrawnArrowShape: Shape {
let start: CGPoint let start: CGPoint
let end: CGPoint let end: CGPoint
let previousStart: CGPoint
func path(in rect: CGRect) -> Path { func path(in rect: CGRect) -> Path {
var path = Path() var path = Path()
// Interpolate start point for smooth transition
let currentStart = CGPoint(
x: previousStart.x + (start.x - previousStart.x) * 0.3,
y: previousStart.y + (start.y - previousStart.y) * 0.3)
// Create a path that starts going DOWN, then curves back UP to the target // Create a path that starts going DOWN, then curves back UP to the target
// This creates a more playful, hand-drawn feel // This creates a more playful, hand-drawn feel
let dx = end.x - start.x let dx = end.x - currentStart.x
// First control point: go DOWN and slightly toward target // First control point: go DOWN and slightly toward target
let ctrl1 = CGPoint( let ctrl1 = CGPoint(
@@ -242,6 +262,9 @@ struct HandDrawnArrowShape: Shape {
} }
#Preview("Menu Bar Guide Overlay") { #Preview("Menu Bar Guide Overlay") {
MenuBarGuideOverlayView() MenuBarGuideOverlayView(
.frame(width: 1200, height: 800) currentStart: .zero,
previousStart: .zero
)
.frame(width: 1200, height: 800)
} }

View File

@@ -109,42 +109,8 @@ struct MenuBarContentView: View {
// Controls // Controls
VStack(spacing: 4) { VStack(spacing: 4) {
Button(action: { PauseAllButton(timerEngine: timerEngine)
if let engine = timerEngine { SettingsButton(onOpenSettings: onOpenSettings)
if isAllPaused(timerEngine: engine) {
engine.resume()
} else {
engine.pause()
}
}
}) {
HStack {
Image(
systemName: timerEngine.map { isAllPaused(timerEngine: $0) }
?? false
? "play.circle" : "pause.circle")
Text(
timerEngine.map { isAllPaused(timerEngine: $0) } ?? false
? "Resume All Timers" : "Pause All Timers")
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
Button(action: {
onOpenSettings()
}) {
HStack {
Image(systemName: "gearshape")
Text("Settings...")
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -189,6 +155,71 @@ struct MenuBarContentView: View {
} }
} }
} }
struct PauseAllButton: View {
var timerEngine: TimerEngine?
@Environment(\.colorScheme) private var colorScheme
@State private var isHovered = false
private var isAllPaused: Bool {
guard let engine = timerEngine else { return false }
let activeStates = engine.timerStates.values.filter { $0.isActive }
return !activeStates.isEmpty && activeStates.allSatisfy { $0.isPaused }
}
var body: some View {
Button(action: {
if let engine = timerEngine {
if isAllPaused {
engine.resume()
} else {
engine.pause()
}
}
}) {
HStack {
Image(
systemName: isAllPaused ? "play.circle" : "pause.circle")
Text(
isAllPaused ? "Resume All Timers" : "Pause All Timers")
Spacer()
}
.foregroundStyle(isHovered && colorScheme == .light ? .white : .primary)
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
.onHover { hovering in
isHovered = hovering
}
}
}
struct SettingsButton: View {
var onOpenSettings: () -> Void
@Environment(\.colorScheme) private var colorScheme
@State private var isHovered = false
var body: some View {
Button(action: {
onOpenSettings()
}) {
HStack {
Image(systemName: "gearshape")
Text("Settings...")
Spacer()
}
.foregroundStyle(isHovered && colorScheme == .light ? .white : .primary)
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.buttonStyle(MenuBarHoverButtonStyle())
.onHover { hovering in
isHovered = hovering
}
}
}
struct IncompleteOnboardingView: View { struct IncompleteOnboardingView: View {
@State private var isHovering = false @State private var isHovering = false
@@ -218,7 +249,9 @@ struct IncompleteOnboardingView: View {
.padding(.horizontal, 8) .padding(.horizontal, 8)
} }
} }
struct QuitRow: View { struct QuitRow: View {
@Environment(\.colorScheme) private var colorScheme
@State private var isHovering = false @State private var isHovering = false
let onQuit: () -> Void let onQuit: () -> Void
@@ -228,9 +261,9 @@ struct QuitRow: View {
Button(action: onQuit) { Button(action: onQuit) {
HStack { HStack {
Image(systemName: "power") Image(systemName: "power")
.foregroundStyle(isHovering ? .white : .red) .foregroundStyle(isHovering && colorScheme == .light ? .white : .red)
Text("Quit Gaze") Text("Quit Gaze")
.foregroundStyle(isHovering ? .white : .primary) .foregroundStyle(isHovering && colorScheme == .light ? .white : .primary)
Spacer() Spacer()
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)