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:
@@ -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,30 +39,30 @@ 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,
|
||||||
|
elapsed: elapsed,
|
||||||
|
remaining: remaining,
|
||||||
|
overshoot: overshoot,
|
||||||
|
isPaused: isPaused,
|
||||||
|
menuBarH: menuBarH,
|
||||||
|
notchW: notchW
|
||||||
|
)
|
||||||
|
|
||||||
|
if isExpanded {
|
||||||
|
expandedBody(
|
||||||
|
active: active,
|
||||||
step: step,
|
step: step,
|
||||||
elapsed: elapsed,
|
elapsed: elapsed,
|
||||||
remaining: remaining,
|
remaining: remaining,
|
||||||
overshoot: overshoot,
|
overshoot: overshoot,
|
||||||
isPaused: isPaused,
|
isPaused: isPaused
|
||||||
menuBarH: menuBarH,
|
|
||||||
notchW: notchW
|
|
||||||
)
|
)
|
||||||
if isExpanded {
|
.padding(.top, menuBarH)
|
||||||
expandedBody(
|
.transition(.opacity)
|
||||||
active: active,
|
|
||||||
step: step,
|
|
||||||
elapsed: elapsed,
|
|
||||||
remaining: remaining,
|
|
||||||
overshoot: overshoot,
|
|
||||||
isPaused: isPaused
|
|
||||||
)
|
|
||||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user