From 374226c44c78eb414e37173f964892bad9bc6878 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 28 Jan 2026 18:52:53 -0500 Subject: [PATCH] color fixes/arrow changes --- .../Containers/MenuBarGuideOverlayView.swift | 105 ++++++++++------- Gaze/Views/MenuBar/MenuBarContentView.swift | 109 ++++++++++++------ 2 files changed, 135 insertions(+), 79 deletions(-) diff --git a/Gaze/Views/Containers/MenuBarGuideOverlayView.swift b/Gaze/Views/Containers/MenuBarGuideOverlayView.swift index 1b2c667..656eddf 100644 --- a/Gaze/Views/Containers/MenuBarGuideOverlayView.swift +++ b/Gaze/Views/Containers/MenuBarGuideOverlayView.swift @@ -14,9 +14,11 @@ final class MenuBarGuideOverlayPresenter { static let shared = MenuBarGuideOverlayPresenter() private var window: NSWindow? - private var displayLink: CVDisplayLink? private var lastWindowFrame: CGRect = .zero + private var checkTimer: Timer? private var onboardingWindowObserver: NSObjectProtocol? + private var currentStartPoint: CGPoint = .zero + private var previousStartPoint: CGPoint = .zero func updateVisibility(isVisible: Bool) { if isVisible { @@ -28,7 +30,8 @@ final class MenuBarGuideOverlayPresenter { } func hide() { - stopDisplayLink() + checkTimer?.invalidate() + checkTimer = nil window?.orderOut(nil) window?.close() window = nil @@ -37,7 +40,7 @@ final class MenuBarGuideOverlayPresenter { private func show() { if let window { window.orderFrontRegardless() - startDisplayLink() + startCheckTimer() return } @@ -58,48 +61,26 @@ final class MenuBarGuideOverlayPresenter { overlayWindow.isReleasedWhenClosed = false overlayWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary] - let overlayView = MenuBarGuideOverlayView() + let overlayView = MenuBarGuideOverlayView( + currentStart: currentStartPoint, + previousStart: previousStartPoint + ) overlayWindow.contentView = NSHostingView(rootView: overlayView) overlayWindow.orderFrontRegardless() window = overlayWindow - startDisplayLink() + startCheckTimer() } - private func startDisplayLink() { - guard displayLink == nil else { return } - - var link: CVDisplayLink? - 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.fromOpaque(userInfo) - .takeUnretainedValue() - DispatchQueue.main.async { - presenter.checkAndRedraw() - } - return kCVReturnSuccess - }, Doc) - - CVDisplayLinkStart(displayLink) - self.displayLink = displayLink + private func startCheckTimer() { + checkTimer?.invalidate() + checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in + self?.checkWindowFrame() + } } - private func stopDisplayLink() { - guard let displayLink else { return } - CVDisplayLinkStop(displayLink) - self.displayLink = nil - } - - private func checkAndRedraw() { + private func checkWindowFrame() { guard let onboardingWindow = NSApp.windows.first(where: { $0.identifier == WindowIdentifiers.onboarding @@ -109,13 +90,27 @@ final class MenuBarGuideOverlayPresenter { let currentFrame = onboardingWindow.frame if currentFrame != lastWindowFrame { 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() } } private func redraw() { guard let window else { return } - let overlayView = MenuBarGuideOverlayView() + + let overlayView = MenuBarGuideOverlayView( + currentStart: currentStartPoint, + previousStart: previousStartPoint + ) window.contentView = NSHostingView(rootView: overlayView) } @@ -125,6 +120,18 @@ final class MenuBarGuideOverlayPresenter { 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 onboardingWindowObserver = NotificationCenter.default.addObserver( forName: NSWindow.willCloseNotification, @@ -144,6 +151,9 @@ final class MenuBarGuideOverlayPresenter { } struct MenuBarGuideOverlayView: View { + var currentStart: CGPoint + var previousStart: CGPoint + var body: some View { GeometryReader { proxy in let size = proxy.size @@ -154,7 +164,7 @@ struct MenuBarGuideOverlayView: View { let target = targetPoint(from: locationResult.frame) let start = startPoint(screenSize: size, screenFrame: screen.frame) - HandDrawnArrowShape(start: start, end: target) + HandDrawnArrowShape(start: start, end: target, previousStart: previousStart) .stroke( Color.accentColor.opacity(0.85), 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) } + + 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 { let start: CGPoint let end: CGPoint + let previousStart: CGPoint func path(in rect: CGRect) -> 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 // 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 let ctrl1 = CGPoint( @@ -242,6 +262,9 @@ struct HandDrawnArrowShape: Shape { } #Preview("Menu Bar Guide Overlay") { - MenuBarGuideOverlayView() - .frame(width: 1200, height: 800) + MenuBarGuideOverlayView( + currentStart: .zero, + previousStart: .zero + ) + .frame(width: 1200, height: 800) } diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index 1ea8033..99f1ce3 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -109,42 +109,8 @@ struct MenuBarContentView: View { // Controls VStack(spacing: 4) { - Button(action: { - if let engine = timerEngine { - 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()) + PauseAllButton(timerEngine: timerEngine) + SettingsButton(onOpenSettings: onOpenSettings) } .padding(.vertical, 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 { @State private var isHovering = false @@ -218,7 +249,9 @@ struct IncompleteOnboardingView: View { .padding(.horizontal, 8) } } + struct QuitRow: View { + @Environment(\.colorScheme) private var colorScheme @State private var isHovering = false let onQuit: () -> Void @@ -228,9 +261,9 @@ struct QuitRow: View { Button(action: onQuit) { HStack { Image(systemName: "power") - .foregroundStyle(isHovering ? .white : .red) + .foregroundStyle(isHovering && colorScheme == .light ? .white : .red) Text("Quit Gaze") - .foregroundStyle(isHovering ? .white : .primary) + .foregroundStyle(isHovering && colorScheme == .light ? .white : .primary) Spacer() } .padding(.horizontal, 8)