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
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,7 +39,6 @@ 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(
step: step,
elapsed: elapsed,
@@ -48,6 +48,7 @@ struct NotchOverlayContent: View {
menuBarH: menuBarH,
notchW: notchW
)
if isExpanded {
expandedBody(
active: active,
@@ -57,11 +58,11 @@ struct NotchOverlayContent: View {
overshoot: overshoot,
isPaused: isPaused
)
.transition(.opacity.combined(with: .move(edge: .top)))
.padding(.top, menuBarH)
.transition(.opacity)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.animation(.spring(response: 0.32, dampingFraction: 0.82), value: isExpanded)
.onHover { hover in
state.isExpanded = hover

View File

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