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()
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<MenuBarGuideOverlayPresenter>.fromOpaque(userInfo)
.takeUnretainedValue()
DispatchQueue.main.async {
presenter.checkAndRedraw()
private func startCheckTimer() {
checkTimer?.invalidate()
checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
self?.checkWindowFrame()
}
return kCVReturnSuccess
}, Doc)
CVDisplayLinkStart(displayLink)
self.displayLink = displayLink
}
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()
MenuBarGuideOverlayView(
currentStart: .zero,
previousStart: .zero
)
.frame(width: 1200, height: 800)
}

View File

@@ -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)