import Foundation import SwiftUI /// Described calls for the offline snapshots of the call views (Mac, from /// CallsInterop) and the iPhone's UI test of its controls (a debug build /// launched with `-CallsCallFixture `). No server, no media: the /// tiles show their placeholders, which is what an off-screen capture /// would show of video anyway. @_spi(Fixtures) public enum CallFixture: String, CaseIterable, Sendable { /// One other person, muted, on the stage. case focus /// Four people in the call. case grid /// Somebody sharing a screen, with two cameras beside it, and you /// sharing too. case screen /// Somebody waiting to be let in, and the connection coming back. case notices /// Two people who have just joined (spec §15.24): one still connecting /// and unable to hear you yet, the other's browser blocking sound. case connecting /// The focus call in a meeting someone named: its title is shown, /// centred where a window's title would be (CallTitle). case named /// Nobody else here yet: "Waiting for others", and you in the corner /// (Paul's 0.4.6 screenshot, 2026-10-03). case alone public static let title = "Call, 24 Sep 10:21" public static let namedTitle = "Board meeting" @MainActor public func apply(to call: Call) { let me = #"{"peer":"me","name":"Paul Campbell","kind":"member"}"# let ana = #"{"peer":"ana","name":"Ana García","kind":"member","media":{"muted":true,"cameraOff":false}}"# let tom = #"{"peer":"tom","name":"Tom Byrne","kind":"guest","media":{"muted":false,"cameraOff":true}}"# let li = #"{"peer":"li","name":"Li Wei","kind":"member"}"# func roster(_ phase: String, _ peers: [String], waiting: [String] = []) -> String { #"{"phase":"\#(phase)","peer":"me","boot":"fixture","peers":[\#(peers.joined(separator: ","))],"waiting":[\#(waiting.joined(separator: ","))]}"# } switch self { case .focus: call.showFixture(title: Self.title, state: roster("admitted", [me, ana])) case .grid: call.showFixture(title: Self.title, state: roster("admitted", [me, ana, tom, li])) case .screen: call.showFixture( title: Self.title, state: roster("admitted", [me, ana, tom, li]), screens: ["li"], sharingScreen: true) case .notices: let knock = #"{"peer":"sam","name":"Sam Kelly","kind":"guest"}"# call.showFixture( title: Self.title, status: "Reconnecting…", state: roster("reconnecting", [me, ana], waiting: [knock]), muted: true, floor: true) case .connecting: let sam = #"{"peer":"sam","name":"Samuel Okafor","kind":"member","media":{"muted":false,"cameraOff":false,"hearing":[]}}"# let bea = #"{"peer":"bea","name":"Bea Ruiz","kind":"guest","media":{"muted":false,"cameraOff":true,"audioBlocked":true,"hearing":[]}}"# call.showFixture(title: Self.title, state: roster("admitted", [me, ana, sam, bea])) call.showFixtureConnecting(["sam", "bea"]) case .named: call.showFixture(title: Self.namedTitle, state: roster("admitted", [me, ana]), named: true) case .alone: call.showFixture(title: Self.title, state: roster("admitted", [me])) } } } /// How loudly everyone is speaking, for the snapshots of the glow behind /// the names: silent, and low, middling and loud speech. @_spi(Fixtures) public enum FixtureSpeaking: String, CaseIterable, Sendable { case silent, low, mid, high public var decibels: Float { switch self { case .silent: -60 case .low: -29 case .mid: -21 case .high: -12 } } @MainActor public func apply(to call: Call) { call.showFixtureSpeaking(level: pow(10, decibels / 20)) } } /// What the snapshots put behind the call's overlay in place of video, to /// show that the controls read over anything: black, white, and a busy /// frame with light and dark and colour and fine detail in it. @_spi(Fixtures) public enum FixtureBackdrop: String, CaseIterable, Sendable { case black, white, busy } /// Your own state, for the snapshots of the controls in each. @_spi(Fixtures) public enum FixtureSelf: String, CaseIterable, Sendable { case normal, muted, cameraOff, sharing @MainActor public func apply(to call: Call) { call.showFixtureSelf( muted: self == .muted, cameraOff: self == .cameraOff, sharingScreen: self == .sharing) } } extension FixtureBackdrop { /// The frame itself, filling whatever it is given. @_spi(Fixtures) @MainActor @ViewBuilder public var view: some View { switch self { case .black: Color.black case .white: Color.white case .busy: BusyFrame() } } } /// A stand-in for a busy picture: a bright window and a dark room, a /// saturated poster, and stripes as fine as a patterned shirt, across the /// whole frame so the bar and the pills each land on some of everything. private struct BusyFrame: View { var body: some View { GeometryReader { geometry in let size = geometry.size ZStack { LinearGradient( colors: [Color(white: 0.95), Color(red: 0.55, green: 0.42, blue: 0.3), Color(white: 0.08)], startPoint: .topLeading, endPoint: .bottomTrailing) Rectangle().fill(Color(red: 0.98, green: 0.85, blue: 0.2)) .frame(width: size.width * 0.28, height: size.height * 0.5) .position(x: size.width * 0.2, y: size.height * 0.62) Rectangle().fill(Color(red: 0.1, green: 0.35, blue: 0.85)) .frame(width: size.width * 0.22, height: size.height * 0.7) .position(x: size.width * 0.72, y: size.height * 0.55) HStack(spacing: 6) { ForEach(0..<60, id: \.self) { index in Rectangle().fill(index.isMultiple(of: 2) ? Color.white : Color.black) .frame(width: 6) } } .frame(width: size.width * 0.45, height: size.height * 0.35) .position(x: size.width * 0.45, y: size.height * 0.88) Circle().fill(Color(red: 0.9, green: 0.2, blue: 0.3)) .frame(width: size.height * 0.3) .position(x: size.width * 0.9, y: size.height * 0.12) } .clipped() } } } private struct FixtureBackdropKey: EnvironmentKey { static let defaultValue: FixtureBackdrop? = nil } extension EnvironmentValues { /// A fixture's frame in place of the call's stage; nil in a real call. @_spi(Fixtures) public var fixtureBackdrop: FixtureBackdrop? { get { self[FixtureBackdropKey.self] } set { self[FixtureBackdropKey.self] = newValue } } } private struct FixtureStagePictureKey: EnvironmentKey { static let defaultValue: FixtureBackdrop? = nil } extension EnvironmentValues { /// A fixture's frame as the picture on the stage's tile, so snapshots of /// Speaker view show how a picture sits under the filmstrip and the bar, /// which a tile's placeholder cannot; nil in a real call. @_spi(Fixtures) public var fixtureStagePicture: FixtureBackdrop? { get { self[FixtureStagePictureKey.self] } set { self[FixtureStagePictureKey.self] = newValue } } } /// A fixture's frame drawn as a tile draws video: placed by `VideoFrame` /// (a camera's 16:9 picture filling the tile with headroom, a screen's /// 16:10 fitted whole) and, around a screen, the same frame soft and /// dimmed as `ScreenBackdrop` draws it. @_spi(Fixtures) public struct FixtureVideo: View { let picture: FixtureBackdrop let screen: Bool public init(picture: FixtureBackdrop, screen: Bool) { self.picture = picture self.screen = screen } public var body: some View { GeometryReader { geometry in let bounds = geometry.size let video = screen ? CGSize(width: 16, height: 10) : CGSize(width: 16, height: 9) let placed = VideoFrame.place(video: video, in: bounds, fit: screen) ZStack(alignment: .topLeading) { if screen { CallsAppearance.stageSurface picture.view .frame(width: bounds.width, height: bounds.height) .scaleEffect(1.15) .blur(radius: 24) .opacity(BackdropSampler.dimmed) } picture.view .frame(width: placed.width, height: placed.height) .offset(x: placed.minX, y: placed.minY) } } .clipped() } } /// The warm rim on a tile's name at one level, over each kind of picture /// it has to read on (0.4.3): the rim mock-up's own dark and bright tiles, /// for comparing with it, then white and the busy frame. The name is the /// tiles' own `TileSpeakingName`, full size and compact. @_spi(Fixtures) public struct WarmRimGallery: View { let level: FixtureSpeaking @State private var glow = GlowLevel() public init(level: FixtureSpeaking) { self.level = level } public enum Picture: String, CaseIterable, Sendable { case mockDark, mockBright, white, busy } public var body: some View { VStack(spacing: 10) { ForEach(Picture.allCases, id: \.self) { picture in ZStack(alignment: .bottomLeading) { Self.view(picture) HStack(spacing: 12) { TileSpeakingName(label: "Vicky Walsh", muted: false, compact: false, glow: glow) TileSpeakingName(label: "Tony Keane", muted: false, compact: true, glow: glow) } .padding(14) } .frame(width: 360, height: 150) .clipShape(RoundedRectangle(cornerRadius: 16)) } } .padding(10) .onAppear { glow.showFixture(level: pow(10, level.decibels / 20)) } } @MainActor @ViewBuilder static func view(_ picture: Picture) -> some View { switch picture { case .mockDark: // speaking-indicator-options/index.html's .tile, as near as SwiftUI // draws its gradients. ZStack { LinearGradient( stops: [ .init(color: Color(red: 0x3b / 255, green: 0x36 / 255, blue: 0x32 / 255), location: 0), .init(color: Color(red: 0x1d / 255, green: 0x1b / 255, blue: 0x1a / 255), location: 0.6), .init(color: Color(red: 0x12 / 255, green: 0x12 / 255, blue: 0x12 / 255), location: 1), ], startPoint: .topLeading, endPoint: .bottomTrailing) EllipticalGradient( colors: [Color(red: 1, green: 214 / 255, blue: 170 / 255).opacity(0.35), .clear], center: UnitPoint(x: 0.55, y: 0.4), startRadiusFraction: 0, endRadiusFraction: 0.5) EllipticalGradient( colors: [Color(red: 90 / 255, green: 110 / 255, blue: 140 / 255).opacity(0.45), .clear], center: UnitPoint(x: 0.3, y: 0.8), startRadiusFraction: 0, endRadiusFraction: 0.35) } case .mockBright: ZStack { LinearGradient( stops: [ .init(color: Color(red: 0xe9 / 255, green: 0xe4 / 255, blue: 0xdc / 255), location: 0), .init(color: Color(red: 0xc9 / 255, green: 0xc2 / 255, blue: 0xb7 / 255), location: 0.6), .init(color: Color(red: 0xa9 / 255, green: 0xa1 / 255, blue: 0x96 / 255), location: 1), ], startPoint: .topLeading, endPoint: .bottomTrailing) EllipticalGradient( colors: [Color.white.opacity(0.9), .clear], center: UnitPoint(x: 0.55, y: 0.4), startRadiusFraction: 0, endRadiusFraction: 0.5) } case .white: FixtureBackdrop.white.view case .busy: FixtureBackdrop.busy.view } } } #if os(macOS) /// Where the full-window call's control bar last laid out, in its /// window's coordinates (top-left origin), for the bar layout gate /// (`CallsInterop --bar-layout`): the glass pane, and the row of /// buttons on it, which overflow the pane if it is too narrow for them. @_spi(Fixtures) @MainActor public enum CallBarProbe { public static var pane: CGRect = .zero /// The first button on the bar and the last (Leave). public static var first: CGRect = .zero public static var last: CGRect = .zero public static var row: CGRect { first.union(last) } } /// Records a bar button's frame for CallBarProbe; nothing else reads it. struct CallBarEndProbe: ViewModifier { let last: Bool func body(content: Content) -> some View { content.background( GeometryReader { button in Color.clear .onAppear { record(button.frame(in: .global)) } .onChange(of: button.frame(in: .global)) { _, now in record(now) } }) } @MainActor private func record(_ frame: CGRect) { if last { CallBarProbe.last = frame } else { CallBarProbe.first = frame } } } #endif