oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2
3/// Compact call controls retain their Button labels for VoiceOver and tooltips.
4///
5/// With `overlay` set, the button takes the look of controls over video
6/// (see `CallOverlay`) in that state, and `color` and `emphasized` are
7/// not used: the Mac's control row is one set of buttons in two places,
8/// the classic layout's row and the full-window call's bar.
9public struct CallActionStyle: ButtonStyle {
10 public var color: Color
11 public var emphasized: Bool
12 public var overlay: CallOverlayButtonStyle.Tone?
13 @Environment(\.isEnabled) private var isEnabled
14
15 public init(
16 color: Color = CallsAppearance.accent, emphasized: Bool = false,
17 overlay: CallOverlayButtonStyle.Tone? = nil
18 ) {
19 self.color = color
20 self.emphasized = emphasized
21 self.overlay = overlay
22 }
23
24 @ViewBuilder public func makeBody(configuration: Configuration) -> some View {
25 if let overlay {
26 OverlayButtonBody(
27 label: configuration.label, tone: overlay, size: 44, round: false,
28 pressed: configuration.isPressed, enabled: isEnabled)
29 } else {
30 configuration.label.labelStyle(.iconOnly).font(.title3)
31 .modifier(CallGlyphSizing(.action))
32 .frame(width: 44, height: 44)
33 .foregroundStyle(emphasized ? .white : color)
34 .background(emphasized ? color : color.opacity(0.12), in: RoundedRectangle(cornerRadius: 14))
35 .opacity(!isEnabled ? 0.4 : configuration.isPressed ? 0.65 : 1)
36 .contentShape(RoundedRectangle(cornerRadius: 14))
37 }
38 }
39}
40
41// MARK: - Over the picture
42
43/// The look of everything the full-window call puts over its video — the
44/// control bar and its buttons, the title pill, the name labels and the
45/// notices — as one system (0.3.7): borderless glass panes, dark whatever
46/// the system appearance, because what is under them is always video, and
47/// video can be anything from a black room to a white wall.
48///
49/// Each surface is Liquid Glass where the system has it (macOS 26, iOS
50/// 26), and the ultra-thin material before that, with black at `scrim`
51/// laid on it. The glass and the blur only soften; the black does the
52/// work, so the contrast holds over anything and even where neither is
53/// drawn: white under 62 % black is #616161, so white on a surface is
54/// 6.2:1 at worst and 21:1 at best (over black). Rendered, the glass over
55/// white measures #5B5B5B (6.8:1). No hairline edges anywhere: a surface
56/// is told from the picture by its tone, and from black video by the
57/// glass's own light. `CallOverlayContrastTests` holds the ratios.
58public enum CallOverlay {
59 /// The black laid over the glass or material.
60 public static let scrim: Double = 0.62
61 /// A resting button has no fill of its own: a white symbol on the pane.
62 /// Under the pointer it lightens by this, and pressed by `pressFill`;
63 /// white on the hovered pane is still 4.9:1 at worst.
64 public static let hoverFill = Color.white.opacity(0.1)
65 public static let pressFill = Color.white.opacity(0.18)
66 /// A control that is on — a panel open, the screen being shared: a
67 /// near-white capsule with a near-black symbol, 15:1 between them and
68 /// 6.8:1 from the pane at its lightest.
69 public static let onFill = Color(white: 0.94)
70 public static let onSymbol = Color(white: 0.1)
71 /// Microphone or camera off: an amber capsule, with the slashed symbol
72 /// in white (5.7:1). A warning that others cannot hear or see you, and
73 /// distinct from Leave's red and from the white of a control that is on.
74 public static let offFill = Color(red: 0.65, green: 0.30, blue: 0.02)
75 /// Leave and End call: a red capsule deep enough for white on it to read
76 /// (5.0:1; the system red would be 3.4:1).
77 public static let leaveFill = Color(red: 0.84, green: 0.16, blue: 0.14)
78 /// The unread count on Chat: white on violet, 6.0:1.
79 public static let badgeFill = Color(red: 0.49, green: 0.23, blue: 0.85)
80 /// Secondary text on a surface: the status line, a quiet notice. 5.0:1
81 /// over the worst-case surface.
82 public static let secondaryText = Color.white.opacity(0.85)
83 /// A notice that must be seen (you are sharing your screen): a light
84 /// amber, 4.6:1 over the worst-case surface.
85 public static let alertText = Color(red: 1.0, green: 0.85, blue: 0.6)
86 /// A panel over the picture that is read rather than glanced at (the
87 /// Devices panel): opaque, so nothing of the video shows through the
88 /// text. The dark appearance's own label colours sit on it at full
89 /// contrast (primary 17:1, secondary 7.9:1).
90 public static let panelFill = Color(white: 0.12)
91}
92
93private struct CallOverlayGlassKey: EnvironmentKey {
94 static let defaultValue = true
95}
96
97extension EnvironmentValues {
98 /// Whether the overlay's surfaces may use Liquid Glass where the system
99 /// has it. True in the apps; the snapshots turn it off to show the
100 /// material every older system draws instead.
101 public var callOverlayGlass: Bool {
102 get { self[CallOverlayGlassKey.self] }
103 set { self[CallOverlayGlassKey.self] = newValue }
104 }
105}
106
107/// A surface over the picture: borderless glass — Liquid Glass on macOS
108/// and iOS 26, the ultra-thin material before — with black at
109/// `CallOverlay.scrim` on it. Only the background is made dark, so a sheet
110/// or popover the content opens keeps the person's own appearance.
111///
112/// `rim: false` leaves Liquid Glass out even where the system has it: the
113/// glass lights its own edge, a faint light hairline that a small pill
114/// over a dark picture shows plainly (the sharing notice, 2026-09-28).
115public struct CallOverlaySurface<S: InsettableShape>: ViewModifier {
116 let shape: S
117 let rim: Bool
118 @Environment(\.callOverlayGlass) private var glass
119 public init(_ shape: S, rim: Bool = true) {
120 self.shape = shape
121 self.rim = rim
122 }
123 public func body(content: Content) -> some View {
124 if glass, rim, #available(macOS 26, iOS 26, *) {
125 content
126 .background(shape.fill(Color.black.opacity(CallOverlay.scrim)))
127 .glassEffect(.regular, in: shape)
128 } else {
129 content
130 .background {
131 ZStack {
132 shape.fill(.ultraThinMaterial)
133 shape.fill(Color.black.opacity(CallOverlay.scrim))
134 }
135 .environment(\.colorScheme, .dark)
136 }
137 }
138 }
139}
140
141extension View {
142 /// See `CallOverlaySurface`.
143 public func callOverlaySurface<S: InsettableShape>(_ shape: S, rim: Bool = true) -> some View {
144 modifier(CallOverlaySurface(shape, rim: rim))
145 }
146}
147
148/// A button over the picture, in one of four states. At rest it is only a
149/// white symbol on the pane, lightening under the pointer; a filled
150/// capsule appears behind it only in a state that needs one, and the state
151/// is always the symbol as well as the fill: a muted microphone is slashed
152/// and amber, a control that is on is white with a dark symbol.
153public struct CallOverlayButtonStyle: ButtonStyle {
154 public enum Tone: Sendable {
155 /// Resting: a white symbol on the pane.
156 case normal
157 /// On: a panel open, the screen being shared.
158 case on
159 /// Microphone or camera off.
160 case off
161 /// Leave, End call.
162 case leave
163 }
164
165 public var tone: Tone
166 public var size: CGFloat
167 public var round: Bool
168 @Environment(\.isEnabled) private var isEnabled
169
170 public init(_ tone: Tone = .normal, size: CGFloat = 44, round: Bool = false) {
171 self.tone = tone
172 self.size = size
173 self.round = round
174 }
175
176 public func makeBody(configuration: Configuration) -> some View {
177 OverlayButtonBody(
178 label: configuration.label, tone: tone, size: size, round: round,
179 pressed: configuration.isPressed, enabled: isEnabled)
180 }
181}
182
183private struct OverlayButtonBody<Label: View>: View {
184 let label: Label
185 let tone: CallOverlayButtonStyle.Tone
186 let size: CGFloat
187 let round: Bool
188 let pressed: Bool
189 let enabled: Bool
190 @State private var hovering = false
191
192 var body: some View {
193 // Leave is wider than it is tall, so it reads as a capsule and not as
194 // one more round state behind a symbol.
195 let width = tone == .leave && !round ? size * 1.4 : size
196 label.labelStyle(.iconOnly)
197 .font(round ? .title2.weight(.semibold) : .title3.weight(.medium))
198 .modifier(CallGlyphSizing(round ? .round : .bar))
199 .frame(width: width, height: size)
200 .foregroundStyle(tone == .on ? CallOverlay.onSymbol : .white)
201 .background(fill, in: Capsule())
202 .opacity(!enabled ? 0.45 : pressed && tone != .normal ? 0.75 : 1)
203 .contentShape(Capsule())
204 .onHover { hovering = $0 }
205 }
206
207 private var fill: Color {
208 switch tone {
209 case .normal:
210 guard enabled else { return .clear }
211 return pressed ? CallOverlay.pressFill : hovering ? CallOverlay.hoverFill : .clear
212 case .on: return CallOverlay.onFill
213 case .off: return CallOverlay.offFill
214 case .leave: return CallOverlay.leaveFill
215 }
216 }
217}
218