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.
This commit is contained in:
Julien Calixte
2026-05-15 12:05:35 +02:00
parent 030ab950eb
commit fe75df4e0e
2 changed files with 39 additions and 36 deletions

View File

@@ -5,15 +5,15 @@ struct NotchOverlayContent: View {
@Environment(Clock.self) private var clock @Environment(Clock.self) private var clock
let state: NotchOverlayState let state: NotchOverlayState
let geometry: NotchGeometry
var body: some View { var body: some View {
if let active = store.activeRecord, if let geometry = state.geometry,
let active = store.activeRecord,
let stepId = active.record.currentStepId, let stepId = active.record.currentStepId,
let step = active.task.step(id: stepId), let step = active.task.step(id: stepId),
let range = active.record.stepRecords[stepId] let range = active.record.stepRecords[stepId]
{ {
island(active: active, step: step, range: range) island(active: active, step: step, range: range, geometry: geometry)
} else { } else {
Color.clear Color.clear
} }
@@ -22,7 +22,8 @@ struct NotchOverlayContent: View {
private func island( private func island(
active: (task: TaskPlan, record: TaskRecord), active: (task: TaskPlan, record: TaskRecord),
step: Step, step: Step,
range: TimeRange range: TimeRange,
geometry: NotchGeometry
) -> some View { ) -> some View {
let elapsed = clock.now.timeIntervalSince(range.start) let elapsed = clock.now.timeIntervalSince(range.start)
let estimationSec = Double(step.estimation) * 60 let estimationSec = Double(step.estimation) * 60
@@ -38,7 +39,6 @@ struct NotchOverlayContent: View {
.fill(Color.black, style: FillStyle(eoFill: true)) .fill(Color.black, style: FillStyle(eoFill: true))
.shadow(color: .black.opacity(isExpanded ? 0.35 : 0), radius: 12, x: 0, y: 6) .shadow(color: .black.opacity(isExpanded ? 0.35 : 0), radius: 12, x: 0, y: 6)
VStack(spacing: 0) {
wings( wings(
step: step, step: step,
elapsed: elapsed, elapsed: elapsed,
@@ -48,6 +48,7 @@ struct NotchOverlayContent: View {
menuBarH: menuBarH, menuBarH: menuBarH,
notchW: notchW notchW: notchW
) )
if isExpanded { if isExpanded {
expandedBody( expandedBody(
active: active, active: active,
@@ -57,11 +58,11 @@ struct NotchOverlayContent: View {
overshoot: overshoot, overshoot: overshoot,
isPaused: isPaused 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) .animation(.spring(response: 0.32, dampingFraction: 0.82), value: isExpanded)
.onHover { hover in .onHover { hover in
state.isExpanded = hover state.isExpanded = hover

View File

@@ -6,6 +6,7 @@ import Observation
@Observable @Observable
final class NotchOverlayState { final class NotchOverlayState {
var isExpanded = false var isExpanded = false
var geometry: NotchGeometry?
} }
struct NotchGeometry: Equatable { struct NotchGeometry: Equatable {
@@ -66,9 +67,15 @@ final class NotchOverlayController {
} }
private func sync() { private func sync() {
let shouldShow = store.activeRecord != nil && notchGeometry() != nil let geometry = notchGeometry()
if shouldShow { let shouldShow = store.activeRecord != nil && geometry != nil
if shouldShow, let geometry {
let geometryChanged = state.geometry != geometry
state.geometry = geometry
ensurePanel() ensurePanel()
if geometryChanged {
updateFrame(animated: false)
}
} else { } else {
tearDown() tearDown()
} }
@@ -99,7 +106,7 @@ final class NotchOverlayController {
} }
private func ensurePanel() { private func ensurePanel() {
guard let geometry = notchGeometry() else { return } guard let geometry = state.geometry else { return }
if panel != nil { return } if panel != nil { return }
let panel = NSPanel( let panel = NSPanel(
@@ -116,12 +123,14 @@ final class NotchOverlayController {
panel.ignoresMouseEvents = false panel.ignoresMouseEvents = false
panel.isMovable = false panel.isMovable = false
let content = NotchOverlayContent(state: state, geometry: geometry) let content = NotchOverlayContent(state: state)
.environment(store) .environment(store)
.environment(clock) .environment(clock)
let hosting = NSHostingController(rootView: content) let hosting = NSHostingController(rootView: content)
hosting.view.wantsLayer = true hosting.view.wantsLayer = true
hosting.view.layer?.backgroundColor = .clear hosting.view.layer?.backgroundColor = .clear
hosting.view.layerContentsPlacement = .topLeft
hosting.view.layerContentsRedrawPolicy = .duringViewResize
panel.contentViewController = hosting panel.contentViewController = hosting
panel.orderFront(nil) panel.orderFront(nil)
@@ -129,22 +138,15 @@ final class NotchOverlayController {
} }
private func updateFrame(animated: Bool) { 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) let newFrame = panelFrame(geometry: geometry, expanded: state.isExpanded)
if animated { if panel.frame == newFrame { return }
NSAnimationContext.runAnimationGroup { ctx in panel.setFrame(newFrame, display: true, animate: animated)
ctx.duration = 0.32
ctx.timingFunction = CAMediaTimingFunction(name: .easeOut)
ctx.allowsImplicitAnimation = true
panel.animator().setFrame(newFrame, display: true)
}
} else {
panel.setFrame(newFrame, display: true)
}
} }
private func tearDown() { private func tearDown() {
state.isExpanded = false state.isExpanded = false
state.geometry = nil
panel?.orderOut(nil) panel?.orderOut(nil)
panel?.close() panel?.close()
panel = nil panel = nil