mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
Something in the way
This commit is contained in:
parent
018050081d
commit
9bd445986d
19 changed files with 2395 additions and 1 deletions
|
|
@ -0,0 +1,96 @@
|
|||
import SwiftUI
|
||||
import AtollExtensionKit
|
||||
|
||||
struct ExtensionLiveActivityStandaloneView: View {
|
||||
let payload: ExtensionLiveActivityPayload
|
||||
let notchWidth: CGFloat
|
||||
let notchHeight: CGFloat
|
||||
let isHovering: Bool
|
||||
|
||||
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
|
||||
private var contentHeight: CGFloat {
|
||||
max(0, notchHeight - (isHovering ? 0 : 12))
|
||||
}
|
||||
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
||||
|
||||
var body: some View {
|
||||
let baseWingWidth = max(contentHeight, 44)
|
||||
let trailingWidth = ExtensionLayoutMetrics.trailingWidth(
|
||||
for: payload,
|
||||
baseWidth: baseWingWidth,
|
||||
maxWidth: notchWidth * 0.42
|
||||
)
|
||||
let centerWidth = max(96, notchWidth - baseWingWidth - trailingWidth)
|
||||
|
||||
return HStack(spacing: 0) {
|
||||
ExtensionCompositeIconView(
|
||||
leading: descriptor.leadingIcon,
|
||||
badge: descriptor.badgeIcon,
|
||||
accent: accentColor,
|
||||
size: contentHeight
|
||||
)
|
||||
.frame(width: baseWingWidth, height: contentHeight)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.black)
|
||||
.frame(width: centerWidth, height: contentHeight)
|
||||
.overlay(centerContent)
|
||||
|
||||
ExtensionMusicWingView(payload: payload, notchHeight: contentHeight)
|
||||
.frame(width: trailingWidth, height: contentHeight)
|
||||
}
|
||||
.frame(width: notchWidth, height: notchHeight + (isHovering ? 8 : 0))
|
||||
.animation(.smooth(duration: 0.25), value: payload.id)
|
||||
}
|
||||
|
||||
private var centerContent: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(descriptor.title)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
if let subtitle = descriptor.subtitle, !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(Color.white.opacity(0.7))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtensionMusicWingView: View {
|
||||
let payload: ExtensionLiveActivityPayload
|
||||
let notchHeight: CGFloat
|
||||
|
||||
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
|
||||
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
if case .none = descriptor.trailingContent {
|
||||
EmptyView()
|
||||
} else {
|
||||
ExtensionTrailingContentView(content: descriptor.trailingContent, accent: accentColor)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
if let indicator = descriptor.progressIndicator, indicator != .none {
|
||||
ExtensionProgressIndicatorView(
|
||||
indicator: indicator,
|
||||
progress: descriptor.progress,
|
||||
accent: accentColor,
|
||||
estimatedDuration: descriptor.estimatedDuration
|
||||
)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
} else {
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.padding(.trailing, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
import SwiftUI
|
||||
import AtollExtensionKit
|
||||
|
||||
struct ExtensionLockScreenWidgetView: View {
|
||||
let payload: ExtensionLockScreenWidgetPayload
|
||||
|
||||
private var descriptor: AtollLockScreenWidgetDescriptor { payload.descriptor }
|
||||
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
backgroundView
|
||||
contentView
|
||||
.padding(.horizontal, descriptor.layoutStyle == .circular ? 10 : 16)
|
||||
.padding(.vertical, descriptor.layoutStyle == .circular ? 10 : 12)
|
||||
}
|
||||
.frame(width: descriptor.size.width, height: descriptor.size.height)
|
||||
.clipShape(RoundedRectangle(cornerRadius: descriptor.cornerRadius, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: descriptor.cornerRadius, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.04), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var contentView: some View {
|
||||
switch descriptor.layoutStyle {
|
||||
case .inline, .card, .custom:
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(Array(descriptor.content.enumerated()), id: \.offset) { index, element in
|
||||
view(for: element)
|
||||
.frame(maxWidth: .infinity, alignment: alignment(for: element))
|
||||
.accessibilityIdentifier("extension-widget-element-\(payload.id)-\(index)")
|
||||
}
|
||||
}
|
||||
case .circular:
|
||||
ZStack {
|
||||
ForEach(Array(descriptor.content.enumerated()), id: \.offset) { index, element in
|
||||
view(for: element)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.accessibilityIdentifier("extension-widget-element-\(payload.id)-\(index)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func view(for element: AtollWidgetContentElement) -> some View {
|
||||
switch element {
|
||||
case let .text(text, font: font, color: color, alignment: _):
|
||||
Text(text)
|
||||
.font(font.swiftUIFont())
|
||||
.foregroundStyle((color?.swiftUIColor) ?? Color.white.opacity(0.9))
|
||||
.lineLimit(2)
|
||||
case let .icon(iconDescriptor, tint):
|
||||
ExtensionIconView(
|
||||
descriptor: iconDescriptor,
|
||||
tint: (tint?.swiftUIColor) ?? accentColor,
|
||||
size: CGSize(width: 28, height: 28),
|
||||
cornerRadius: 8
|
||||
)
|
||||
case let .progress(indicator, value, color):
|
||||
ExtensionProgressIndicatorView(
|
||||
indicator: indicator,
|
||||
progress: value,
|
||||
accent: (color?.swiftUIColor) ?? accentColor,
|
||||
estimatedDuration: nil
|
||||
)
|
||||
case let .graph(data, color, size):
|
||||
ExtensionGraphView(data: data, color: color.swiftUIColor, size: size)
|
||||
.frame(width: size.width, height: size.height)
|
||||
case let .gauge(value, minValue, maxValue, style, color):
|
||||
ExtensionGaugeView(
|
||||
value: value,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
style: style,
|
||||
accent: (color?.swiftUIColor) ?? accentColor
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
case let .spacer(height):
|
||||
Color.clear
|
||||
.frame(height: height)
|
||||
case let .divider(color, thickness):
|
||||
Rectangle()
|
||||
.fill(color.swiftUIColor.opacity(0.4))
|
||||
.frame(height: thickness)
|
||||
}
|
||||
}
|
||||
|
||||
private func alignment(for element: AtollWidgetContentElement) -> Alignment {
|
||||
switch element {
|
||||
case let .text(_, _, _, alignment):
|
||||
return alignment.swiftUI
|
||||
default:
|
||||
return .leading
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var backgroundView: some View {
|
||||
switch descriptor.material {
|
||||
case .frosted:
|
||||
RoundedRectangle(cornerRadius: descriptor.cornerRadius, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
case .liquid:
|
||||
RoundedRectangle(cornerRadius: descriptor.cornerRadius, style: .continuous)
|
||||
.fill(.regularMaterial)
|
||||
case .solid:
|
||||
RoundedRectangle(cornerRadius: descriptor.cornerRadius, style: .continuous)
|
||||
.fill(accentColor.opacity(0.9))
|
||||
case .semiTransparent:
|
||||
RoundedRectangle(cornerRadius: descriptor.cornerRadius, style: .continuous)
|
||||
.fill(accentColor.opacity(0.35))
|
||||
case .clear:
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ExtensionGraphView: View {
|
||||
let data: [Double]
|
||||
let color: Color
|
||||
let size: CGSize
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
let minValue = data.min() ?? 0
|
||||
let maxValue = data.max() ?? 1
|
||||
let range = max(maxValue - minValue, 0.0001)
|
||||
let step = proxy.size.width / CGFloat(max(data.count - 1, 1))
|
||||
Path { path in
|
||||
for (index, value) in data.enumerated() {
|
||||
let x = CGFloat(index) * step
|
||||
let normalized = (value - minValue) / range
|
||||
let y = proxy.size.height - (CGFloat(normalized) * proxy.size.height)
|
||||
if index == 0 {
|
||||
path.move(to: CGPoint(x: x, y: y))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: x, y: y))
|
||||
}
|
||||
}
|
||||
}
|
||||
.stroke(color, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
|
||||
}
|
||||
.frame(width: size.width, height: size.height)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ExtensionGaugeView: View {
|
||||
let value: Double
|
||||
let minValue: Double
|
||||
let maxValue: Double
|
||||
let style: AtollWidgetContentElement.GaugeStyle
|
||||
let accent: Color
|
||||
|
||||
var body: some View {
|
||||
switch style {
|
||||
case .circular:
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.15), lineWidth: 5)
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(normalizedValue))
|
||||
.stroke(accent, style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.smooth(duration: 0.3), value: normalizedValue)
|
||||
Text("\(Int(normalizedValue * 100))%")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.monospacedDigit()
|
||||
}
|
||||
.frame(width: 54, height: 54)
|
||||
case .linear:
|
||||
GeometryReader { proxy in
|
||||
Capsule()
|
||||
.fill(Color.white.opacity(0.2))
|
||||
.frame(height: 8)
|
||||
.overlay(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(accent)
|
||||
.frame(width: proxy.size.width * CGFloat(normalizedValue), height: 8)
|
||||
.animation(.smooth(duration: 0.3), value: normalizedValue)
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
}
|
||||
}
|
||||
|
||||
private var normalizedValue: Double {
|
||||
guard maxValue > minValue else { return 0 }
|
||||
return min(max((value - minValue) / (maxValue - minValue), 0), 1)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AtollWidgetContentElement.TextAlignment {
|
||||
var swiftUI: Alignment {
|
||||
switch self {
|
||||
case .leading: return .leading
|
||||
case .center: return .center
|
||||
case .trailing: return .trailing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
import SwiftUI
|
||||
import AppKit
|
||||
import AtollExtensionKit
|
||||
|
||||
// MARK: - Color Conversion
|
||||
|
||||
extension AtollColorDescriptor {
|
||||
var swiftUIColor: Color {
|
||||
if isAccent {
|
||||
return .accentColor
|
||||
}
|
||||
return Color(red: red, green: green, blue: blue, opacity: alpha)
|
||||
}
|
||||
|
||||
var nsColor: NSColor {
|
||||
if isAccent {
|
||||
return NSColor.controlAccentColor
|
||||
}
|
||||
return NSColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Font Conversion
|
||||
|
||||
extension AtollFontDescriptor {
|
||||
func swiftUIFont() -> Font {
|
||||
let design = self.design.swiftUI
|
||||
let weight = self.weight.swiftUI
|
||||
var font = Font.system(size: size, weight: weight, design: design)
|
||||
if isMonospacedDigit {
|
||||
font = font.monospacedDigit()
|
||||
}
|
||||
return font
|
||||
}
|
||||
|
||||
func nsFont() -> NSFont {
|
||||
let weight = weight.nsFont
|
||||
let font: NSFont
|
||||
switch design {
|
||||
case .serif:
|
||||
font = NSFont.userFont(ofSize: size) ?? NSFont.systemFont(ofSize: size, weight: weight)
|
||||
case .rounded:
|
||||
if let descriptor = NSFont.systemFont(ofSize: size, weight: weight).fontDescriptor.withDesign(.rounded) {
|
||||
font = NSFont(descriptor: descriptor, size: size) ?? NSFont.systemFont(ofSize: size, weight: weight)
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: size, weight: weight)
|
||||
}
|
||||
case .monospaced:
|
||||
font = NSFont.monospacedSystemFont(ofSize: size, weight: weight)
|
||||
case .default:
|
||||
font = NSFont.systemFont(ofSize: size, weight: weight)
|
||||
}
|
||||
return font
|
||||
}
|
||||
}
|
||||
|
||||
private extension AtollFontWeight {
|
||||
var swiftUI: Font.Weight {
|
||||
switch self {
|
||||
case .ultraLight: return .ultraLight
|
||||
case .thin: return .thin
|
||||
case .light: return .light
|
||||
case .regular: return .regular
|
||||
case .medium: return .medium
|
||||
case .semibold: return .semibold
|
||||
case .bold: return .bold
|
||||
case .heavy: return .heavy
|
||||
case .black: return .black
|
||||
}
|
||||
}
|
||||
|
||||
var nsFont: NSFont.Weight {
|
||||
switch self {
|
||||
case .ultraLight: return .ultraLight
|
||||
case .thin: return .thin
|
||||
case .light: return .light
|
||||
case .regular: return .regular
|
||||
case .medium: return .medium
|
||||
case .semibold: return .semibold
|
||||
case .bold: return .bold
|
||||
case .heavy: return .heavy
|
||||
case .black: return .black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AtollFontDesign {
|
||||
var swiftUI: Font.Design {
|
||||
switch self {
|
||||
case .default: return .default
|
||||
case .serif: return .serif
|
||||
case .rounded: return .rounded
|
||||
case .monospaced: return .monospaced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Icon Rendering
|
||||
|
||||
struct ExtensionIconView: View {
|
||||
let descriptor: AtollIconDescriptor
|
||||
let tint: Color
|
||||
let size: CGSize
|
||||
let cornerRadius: CGFloat
|
||||
|
||||
var body: some View {
|
||||
switch descriptor {
|
||||
case let .symbol(name, glyphSize, weight):
|
||||
Image(systemName: name)
|
||||
.font(.system(size: glyphSize, weight: weight.swiftUI))
|
||||
.foregroundStyle(tint)
|
||||
.frame(width: size.width, height: size.height)
|
||||
case let .image(data, targetSize, radius):
|
||||
if let image = NSImage(data: data) {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: targetSize.width, height: targetSize.height)
|
||||
.clipShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
|
||||
} else {
|
||||
fallbackSymbol
|
||||
}
|
||||
case let .appIcon(bundleIdentifier, targetSize, radius):
|
||||
if let icon = AppIconAsNSImage(for: bundleIdentifier) {
|
||||
Image(nsImage: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: targetSize.width, height: targetSize.height)
|
||||
.clipShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
|
||||
} else {
|
||||
fallbackSymbol
|
||||
}
|
||||
case .lottie, .none:
|
||||
fallbackSymbol
|
||||
}
|
||||
}
|
||||
|
||||
private var fallbackSymbol: some View {
|
||||
Image(systemName: "app.dashed")
|
||||
.font(.system(size: size.width * 0.6, weight: .medium))
|
||||
.foregroundStyle(tint)
|
||||
.frame(width: size.width, height: size.height)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: cornerRadius(for: descriptor), style: .continuous)
|
||||
.fill(tint.opacity(0.15))
|
||||
)
|
||||
}
|
||||
|
||||
private func cornerRadius(for descriptor: AtollIconDescriptor) -> CGFloat {
|
||||
switch descriptor {
|
||||
case .image(_, _, let radius): return radius
|
||||
case .appIcon(_, _, let radius): return radius
|
||||
default: return cornerRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtensionCompositeIconView: View {
|
||||
let leading: AtollIconDescriptor
|
||||
let badge: AtollIconDescriptor?
|
||||
let accent: Color
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ExtensionIconView(
|
||||
descriptor: leading,
|
||||
tint: accent,
|
||||
size: CGSize(width: size, height: size),
|
||||
cornerRadius: size * 0.18
|
||||
)
|
||||
if let badge {
|
||||
ExtensionIconView(
|
||||
descriptor: badge,
|
||||
tint: .white,
|
||||
size: CGSize(width: max(size * 0.35, 12), height: max(size * 0.35, 12)),
|
||||
cornerRadius: size * 0.12
|
||||
)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.7))
|
||||
.frame(width: size * 0.4, height: size * 0.4)
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Progress Rendering
|
||||
|
||||
struct ExtensionProgressIndicatorView: View {
|
||||
let indicator: AtollProgressIndicator
|
||||
let progress: Double
|
||||
let accent: Color
|
||||
let estimatedDuration: TimeInterval?
|
||||
|
||||
var body: some View {
|
||||
switch indicator {
|
||||
case let .ring(diameter, strokeWidth):
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.15), lineWidth: strokeWidth)
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(max(0, min(progress, 1))))
|
||||
.stroke(accent, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.smooth(duration: 0.25), value: progress)
|
||||
}
|
||||
.frame(width: diameter, height: diameter)
|
||||
case let .bar(width, height, cornerRadius):
|
||||
Capsule()
|
||||
.fill(Color.white.opacity(0.18))
|
||||
.frame(width: width ?? 80, height: height)
|
||||
.overlay(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(accent)
|
||||
.frame(width: (width ?? 80) * CGFloat(max(0, min(progress, 1))), height: height)
|
||||
.animation(.smooth(duration: 0.25), value: progress)
|
||||
}
|
||||
case let .percentage(font):
|
||||
Text("\(Int(progress * 100))%")
|
||||
.font(font.swiftUIFont())
|
||||
.foregroundStyle(accent)
|
||||
.monospacedDigit()
|
||||
case let .countdown(font):
|
||||
Text(countdownText)
|
||||
.font(font.swiftUIFont())
|
||||
.foregroundStyle(accent)
|
||||
.monospacedDigit()
|
||||
case .lottie:
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(accent)
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private var countdownText: String {
|
||||
guard let estimatedDuration else { return formatPercent }
|
||||
let remaining = max(estimatedDuration * (1 - progress), 0)
|
||||
let hours = Int(remaining) / 3600
|
||||
let minutes = (Int(remaining) % 3600) / 60
|
||||
let seconds = Int(remaining) % 60
|
||||
if hours > 0 {
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
return String(format: "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
private var formatPercent: String {
|
||||
"\(Int(progress * 100))%"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Trailing Content Rendering
|
||||
|
||||
struct ExtensionTrailingContentView: View {
|
||||
let content: AtollTrailingContent
|
||||
let accent: Color
|
||||
|
||||
var body: some View {
|
||||
switch content {
|
||||
case let .text(value, font: font):
|
||||
Text(value)
|
||||
.font(font.swiftUIFont())
|
||||
.foregroundStyle(accent)
|
||||
.lineLimit(1)
|
||||
case let .icon(descriptor):
|
||||
ExtensionIconView(
|
||||
descriptor: descriptor,
|
||||
tint: accent,
|
||||
size: CGSize(width: 26, height: 26),
|
||||
cornerRadius: 6
|
||||
)
|
||||
case let .spectrum(color: colorDescriptor):
|
||||
Rectangle()
|
||||
.fill((colorDescriptor.isAccent ? accent : colorDescriptor.swiftUIColor).gradient)
|
||||
.frame(width: 48, height: 14)
|
||||
.mask {
|
||||
AudioSpectrumView(isPlaying: .constant(true))
|
||||
.frame(width: 16, height: 12)
|
||||
}
|
||||
case .animation:
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(accent)
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout Metrics
|
||||
|
||||
enum ExtensionLayoutMetrics {
|
||||
static func trailingWidth(for payload: ExtensionLiveActivityPayload, baseWidth: CGFloat, maxWidth: CGFloat? = nil) -> CGFloat {
|
||||
var width = baseWidth
|
||||
width = max(width, widthForTrailing(content: payload.descriptor.trailingContent, baseWidth: baseWidth))
|
||||
if let indicator = payload.descriptor.progressIndicator {
|
||||
width = max(width, widthForProgress(indicator))
|
||||
}
|
||||
if let maxWidth {
|
||||
width = min(width, maxWidth)
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
private static func widthForTrailing(content: AtollTrailingContent, baseWidth: CGFloat) -> CGFloat {
|
||||
switch content {
|
||||
case let .text(text, font: font):
|
||||
let measured = ExtensionTextMeasurer.width(for: text, font: font.nsFont())
|
||||
return max(baseWidth, measured + 32)
|
||||
case .icon:
|
||||
return max(baseWidth, 52)
|
||||
case .spectrum:
|
||||
return max(baseWidth, 56)
|
||||
case let .animation(data: _, size: size):
|
||||
return max(baseWidth, size.width + 16)
|
||||
case .none:
|
||||
return baseWidth
|
||||
}
|
||||
}
|
||||
|
||||
private static func widthForProgress(_ indicator: AtollProgressIndicator) -> CGFloat {
|
||||
switch indicator {
|
||||
case let .ring(diameter, _):
|
||||
return diameter + 20
|
||||
case let .bar(width, _, _):
|
||||
return (width ?? 72) + 12
|
||||
case .percentage:
|
||||
return 60
|
||||
case .countdown:
|
||||
return 74
|
||||
case let .lottie(_, size):
|
||||
return size.width + 16
|
||||
case .none:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ExtensionTextMeasurer {
|
||||
static func width(for text: String, font: NSFont) -> CGFloat {
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: font]
|
||||
return max(1, text.size(withAttributes: attributes).width)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue