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. /// /// Each gradient fills the whole view and is placed by its centre, rather than being sized /// into a box and then moved: a box has edges, and they showed as straight seams down the /// window. They composite normally, as the CSS does. Blending them plusLighter added the /// three together into something far louder than the theme -- it is a wash, not a light show. struct Wash: View { @Environment(\.colorScheme) private var scheme private var base: Color { scheme == .dark ? Color(hex: 0x0b0d14) : Color(hex: 0xeef1f7) } /// The stylesheet's alphas are for colour seen through a frosted panel. Most of this is /// seen in the open, where the same values are too much. private var strength: Double { scheme == .dark ? 0.5 : 0.3 } var body: some View { GeometryReader { geo in let reach = max(geo.size.width, geo.size.height) ZStack { base blob(0x4078ff, 0.30, at: UnitPoint(x: 0.08, y: 0.00), reach * 0.75) blob(0xaf52de, 0.24, at: UnitPoint(x: 0.92, y: 0.18), reach * 0.65) blob(0x30b0c7, 0.20, at: UnitPoint(x: 0.60, y: 1.00), reach * 0.75) } } .ignoresSafeArea() } private func blob(_ hex: UInt32, _ alpha: Double, at centre: UnitPoint, _ radius: Double) -> some View { RadialGradient( gradient: Gradient(colors: [Color(hex: hex).opacity(alpha * strength), Color(hex: hex).opacity(0)]), center: centre, startRadius: 0, endRadius: radius) } } // 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) } }