import SwiftUI /// The Glass theme, natively. /// /// Not a port. ipx's Glass is already an approximation of what UIKit gives away: its panels are /// `color-mix(panel 70%, transparent)` under `backdrop-filter: blur(24px) saturate(180%)` with an /// inset top highlight, which is a material; and its palette names Apple's own colours in its own /// comments. Built here it is the thing the theme was imitating, with real backdrop sampling, /// vibrancy and dynamic type, and light and dark at no cost. /// /// The values are taken from web/app.css so the two agree. Where ipx had to name a hex because /// CSS has no system colours, this names the system colour instead. enum Glass { // MARK: - colour /// Text. ipx calls these `--fg`, `--dim` and `--faint`, and its comments give them away as /// Apple's label colours; the system versions adapt to contrast settings as well as to dark. static let text = Color.primary static let dim = Color.secondary static let faint = Color(uiColor: .tertiaryLabel) /// `--accent`. ipx has to use #409cff in dark and #0055aa in light because a single blue /// cannot clear AA over its wash; the system blue already varies with appearance. static let accent = Color.accentColor /// `--accent2`, the amber the EQ bars and the unread dot use. static let highlight = Color(uiColor: .systemOrange) static let good = Color(uiColor: .systemGreen) static let bad = Color(uiColor: .systemRed) /// `--line`, the hairline between panels. static let line = Color(uiColor: .separator) // MARK: - surfaces /// What `backdrop-filter: blur(24px) saturate(180%)` over a 70% panel was reaching for. static let panel: Material = .ultraThinMaterial /// The toolbar and list headers, which ipx frosts a little more strongly because the list /// scrolls under them -- the one place the blur has something to blur. static let sticky: Material = .thinMaterial /// `box-shadow: inset 0 1px 0 var(--glass-hi)`: the light catching the top edge of a pane. /// It is what stops a material reading as flat grey. struct TopHighlight: ViewModifier { func body(content: Content) -> some View { content.overlay(alignment: .top) { Rectangle() .fill(Color.white.opacity(0.14)) .frame(height: 1) .blendMode(.plusLighter) .allowsHitTesting(false) } } } // MARK: - the wash /// The three radial gradients ipx lays under everything, so the frosting has colour to pick /// up. Without it a material over a flat background is just grey. struct Wash: View { @Environment(\.colorScheme) private var scheme private var base: Color { scheme == .dark ? Color(hex: 0x0b0d14) : Color(hex: 0xeef1f7) } var body: some View { GeometryReader { geo in let w = geo.size.width, h = geo.size.height ZStack { base // Positions and sizes as the stylesheet has them: 8% 0%, 92% 18%, 60% 100%. blob(Color(hex: 0x4078ff).opacity(scheme == .dark ? 0.30 : 0.18), at: CGPoint(x: w * 0.08, y: 0), size: CGSize(width: w * 1.2, height: h * 1.1)) blob(Color(hex: 0xaf52de).opacity(scheme == .dark ? 0.24 : 0.14), at: CGPoint(x: w * 0.92, y: h * 0.18), size: CGSize(width: w, height: h)) blob(Color(hex: 0x30b0c7).opacity(scheme == .dark ? 0.20 : 0.12), at: CGPoint(x: w * 0.60, y: h), size: CGSize(width: w * 1.2, height: h * 1.2)) } .ignoresSafeArea() } } private func blob(_ colour: Color, at centre: CGPoint, size: CGSize) -> some View { RadialGradient(colors: [colour, colour.opacity(0)], center: .center, startRadius: 0, endRadius: max(size.width, size.height) / 2) .frame(width: size.width, height: size.height) .position(centre) .blendMode(.plusLighter) } } // MARK: - metrics /// Corner radii, from the stylesheet: cards 20, toasts 14, controls 10. enum Radius { static let card: CGFloat = 20 static let toast: CGFloat = 14 static let control: CGFloat = 10 } } extension View { /// A frosted pane with the light on its top edge, as every panel in Glass has. func glassPane(_ material: Material = Glass.panel) -> some View { background(material).modifier(Glass.TopHighlight()) } } extension Color { /// Only for the wash, where the colour is the design rather than a semantic role and there /// is no system equivalent to ask for. init(hex: UInt32) { self.init(.sRGB, red: Double((hex >> 16) & 0xff) / 255, green: Double((hex >> 8) & 0xff) / 255, blue: Double(hex & 0xff) / 255, opacity: 1) } }