From fe75df4e0ec1b05427512213937a6d638569e90b Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Fri, 15 May 2026 12:05:35 +0200 Subject: [PATCH] refactor(macos): route notch geometry through overlay state Store NotchGeometry on NotchOverlayState so the SwiftUI content can read it reactively instead of receiving it via initializer. Lets the controller drive frame updates when geometry changes and simplifies the panel resize to NSPanel's built-in animate flag. --- .../NotchOverlay/NotchOverlayContent.swift | 43 ++++++++++--------- .../NotchOverlay/NotchOverlayController.swift | 32 +++++++------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayContent.swift b/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayContent.swift index b8ba87e..ec1ab55 100644 --- a/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayContent.swift +++ b/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayContent.swift @@ -5,15 +5,15 @@ struct NotchOverlayContent: View { @Environment(Clock.self) private var clock let state: NotchOverlayState - let geometry: NotchGeometry var body: some View { - if let active = store.activeRecord, + if let geometry = state.geometry, + let active = store.activeRecord, let stepId = active.record.currentStepId, let step = active.task.step(id: stepId), let range = active.record.stepRecords[stepId] { - island(active: active, step: step, range: range) + island(active: active, step: step, range: range, geometry: geometry) } else { Color.clear } @@ -22,7 +22,8 @@ struct NotchOverlayContent: View { private func island( active: (task: TaskPlan, record: TaskRecord), step: Step, - range: TimeRange + range: TimeRange, + geometry: NotchGeometry ) -> some View { let elapsed = clock.now.timeIntervalSince(range.start) let estimationSec = Double(step.estimation) * 60 @@ -38,30 +39,30 @@ struct NotchOverlayContent: View { .fill(Color.black, style: FillStyle(eoFill: true)) .shadow(color: .black.opacity(isExpanded ? 0.35 : 0), radius: 12, x: 0, y: 6) - VStack(spacing: 0) { - wings( + wings( + step: step, + elapsed: elapsed, + remaining: remaining, + overshoot: overshoot, + isPaused: isPaused, + menuBarH: menuBarH, + notchW: notchW + ) + + if isExpanded { + expandedBody( + active: active, step: step, elapsed: elapsed, remaining: remaining, overshoot: overshoot, - isPaused: isPaused, - menuBarH: menuBarH, - notchW: notchW + isPaused: isPaused ) - if isExpanded { - expandedBody( - active: active, - step: step, - elapsed: elapsed, - remaining: remaining, - overshoot: overshoot, - isPaused: isPaused - ) - .transition(.opacity.combined(with: .move(edge: .top))) - } + .padding(.top, menuBarH) + .transition(.opacity) } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .animation(.spring(response: 0.32, dampingFraction: 0.82), value: isExpanded) .onHover { hover in state.isExpanded = hover diff --git a/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayController.swift b/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayController.swift index 455a3c1..d22a091 100644 --- a/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayController.swift +++ b/macos/Sources/FailWell/Views/NotchOverlay/NotchOverlayController.swift @@ -6,6 +6,7 @@ import Observation @Observable final class NotchOverlayState { var isExpanded = false + var geometry: NotchGeometry? } struct NotchGeometry: Equatable { @@ -66,9 +67,15 @@ final class NotchOverlayController { } private func sync() { - let shouldShow = store.activeRecord != nil && notchGeometry() != nil - if shouldShow { + let geometry = notchGeometry() + let shouldShow = store.activeRecord != nil && geometry != nil + if shouldShow, let geometry { + let geometryChanged = state.geometry != geometry + state.geometry = geometry ensurePanel() + if geometryChanged { + updateFrame(animated: false) + } } else { tearDown() } @@ -99,7 +106,7 @@ final class NotchOverlayController { } private func ensurePanel() { - guard let geometry = notchGeometry() else { return } + guard let geometry = state.geometry else { return } if panel != nil { return } let panel = NSPanel( @@ -116,12 +123,14 @@ final class NotchOverlayController { panel.ignoresMouseEvents = false panel.isMovable = false - let content = NotchOverlayContent(state: state, geometry: geometry) + let content = NotchOverlayContent(state: state) .environment(store) .environment(clock) let hosting = NSHostingController(rootView: content) hosting.view.wantsLayer = true hosting.view.layer?.backgroundColor = .clear + hosting.view.layerContentsPlacement = .topLeft + hosting.view.layerContentsRedrawPolicy = .duringViewResize panel.contentViewController = hosting panel.orderFront(nil) @@ -129,22 +138,15 @@ final class NotchOverlayController { } private func updateFrame(animated: Bool) { - guard let panel, let geometry = notchGeometry() else { return } + guard let panel, let geometry = state.geometry else { return } let newFrame = panelFrame(geometry: geometry, expanded: state.isExpanded) - if animated { - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.32 - ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) - ctx.allowsImplicitAnimation = true - panel.animator().setFrame(newFrame, display: true) - } - } else { - panel.setFrame(newFrame, display: true) - } + if panel.frame == newFrame { return } + panel.setFrame(newFrame, display: true, animate: animated) } private func tearDown() { state.isExpanded = false + state.geometry = nil panel?.orderOut(nil) panel?.close() panel = nil