110 lines
3.6 KiB
Swift
110 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
/// Custom SwiftUI Shape that draws the characteristic MacBook notch outline.
|
|
/// Both top and bottom corner radii are animatable, enabling smooth transitions
|
|
/// between the compact closed state and the expanded open state.
|
|
///
|
|
/// The shape uses quadratic Bezier curves to produce the distinctive
|
|
/// top-edge cut-ins of the closed notch, and a clean rounded-bottom
|
|
/// rectangle when open (topCornerRadius approaches 0).
|
|
struct NotchShape: Shape {
|
|
|
|
/// Radius applied to the top-left and top-right transitions where the notch
|
|
/// curves away from the screen edge. When close to 0, the top corners become
|
|
/// sharp and the shape is a rectangle with rounded bottom corners.
|
|
var topCornerRadius: CGFloat
|
|
|
|
/// Radius applied to the bottom-left and bottom-right inner corners.
|
|
var bottomCornerRadius: CGFloat
|
|
|
|
// MARK: - Animatable conformance
|
|
|
|
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
|
get { AnimatablePair(topCornerRadius, bottomCornerRadius) }
|
|
set {
|
|
topCornerRadius = newValue.first
|
|
bottomCornerRadius = newValue.second
|
|
}
|
|
}
|
|
|
|
// MARK: - Path
|
|
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
|
|
let minX = rect.minX
|
|
let maxX = rect.maxX
|
|
let minY = rect.minY
|
|
let maxY = rect.maxY
|
|
let width = rect.width
|
|
let height = rect.height
|
|
|
|
let topR = min(topCornerRadius, width / 4, height / 2)
|
|
let botR = min(bottomCornerRadius, width / 4, height / 2)
|
|
|
|
// Start at the top-left corner of the rect
|
|
path.move(to: CGPoint(x: minX, y: minY))
|
|
|
|
if topR > 0.5 {
|
|
// Leave the screen edge horizontally, then turn into the side wall.
|
|
path.addQuadCurve(
|
|
to: CGPoint(x: minX + topR, y: minY + topR),
|
|
control: CGPoint(x: minX + topR, y: minY)
|
|
)
|
|
} else {
|
|
path.addLine(to: CGPoint(x: minX, y: minY))
|
|
}
|
|
|
|
// Left edge down to bottom-left corner area
|
|
path.addLine(to: CGPoint(x: minX + topR, y: maxY - botR))
|
|
|
|
// Bottom-left inner corner
|
|
path.addQuadCurve(
|
|
to: CGPoint(x: minX + topR + botR, y: maxY),
|
|
control: CGPoint(x: minX + topR, y: maxY)
|
|
)
|
|
|
|
// Bottom edge across
|
|
path.addLine(to: CGPoint(x: maxX - topR - botR, y: maxY))
|
|
|
|
// Bottom-right inner corner
|
|
path.addQuadCurve(
|
|
to: CGPoint(x: maxX - topR, y: maxY - botR),
|
|
control: CGPoint(x: maxX - topR, y: maxY)
|
|
)
|
|
|
|
// Right edge up to the top-right transition
|
|
path.addLine(to: CGPoint(x: maxX - topR, y: minY + topR))
|
|
|
|
if topR > 0.5 {
|
|
// Mirror the top-left transition.
|
|
path.addQuadCurve(
|
|
to: CGPoint(x: maxX, y: minY),
|
|
control: CGPoint(x: maxX - topR, y: minY)
|
|
)
|
|
} else {
|
|
path.addLine(to: CGPoint(x: maxX, y: minY))
|
|
}
|
|
|
|
path.closeSubpath()
|
|
return path
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience initializers
|
|
|
|
extension NotchShape {
|
|
|
|
/// Closed-state shape with tight corner radii that mimic the physical notch.
|
|
static var closed: NotchShape {
|
|
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
|
|
}
|
|
|
|
/// Open-state shape: no top-edge cut-ins, just rounded bottom corners.
|
|
/// topCornerRadius is near-zero so the top becomes effectively flat and the panel
|
|
/// extends flush to the top edge of the screen.
|
|
static var opened: NotchShape {
|
|
NotchShape(topCornerRadius: 0, bottomCornerRadius: 24)
|
|
}
|
|
}
|