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/// Spec §15.19: the mark a video tile carries while its peer is muted.
4/// A slashed microphone and the word beside it, so the state is a shape
5/// and a word and never a colour. The tile folds it into its own
6/// accessibility label, so this hides itself from assistive technology
7/// rather than being read twice.
8public struct MutedBadge: View {
9 public init() {}
10 public var body: some View {
11 Label("Muted", systemImage: "mic.slash.fill")
12 .font(.caption.bold()).foregroundStyle(.white)
13 .padding(.horizontal, 8).padding(.vertical, 4)
14 .callOverlaySurface(Capsule())
15 .accessibilityHidden(true)
16 }
17}
18
19/// What a tile's accessibility label says: the name, and "muted" when it
20/// is. Shared by the Mac and iPhone tiles so VoiceOver hears one phrase.
21public func tileAccessibilityLabel(_ label: String, muted: Bool) -> String {
22 muted ? "\(label), muted" : label
23}
24
25/// The full-window call's name label: small, and carrying the muted mark
26/// inside it — shape and word, as §15.19 has it — rather than in a
27/// second badge, so a tile keeps one quiet
28/// label in its corner that can stay while the controls come and go.
29/// Hidden from assistive technology, as the tile's own label says both.
30///
31/// A narrow tile (a filmstrip thumbnail, most of all on an iPhone held
32/// upright) has no room for "Ana García | Muted": there the word goes and
33/// the slashed microphone stays, and the name gives up its end to an
34/// ellipsis rather than running off the tile (0.4.1). The tile's own
35/// accessibility label still says "muted".
36public struct TileNameLabel: View {
37 let label: String
38 let muted: Bool
39 public init(label: String, muted: Bool) {
40 self.label = label
41 self.muted = muted
42 }
43 public var body: some View {
44 ViewThatFits(in: .horizontal) {
45 row(word: true).fixedSize()
46 row(word: false).fixedSize(horizontal: false, vertical: true)
47 }
48 .font(.caption.weight(.semibold)).foregroundStyle(.white)
49 .padding(.horizontal, 7).padding(.vertical, 3)
50 // The overlay's glass, as the controls have: white on it is 6.2:1
51 // even over a white wall.
52 .callOverlaySurface(Capsule())
53 .accessibilityHidden(true)
54 }
55
56 private func row(word: Bool) -> some View {
57 HStack(spacing: 5) {
58 Text(label).lineLimit(1).truncationMode(.tail)
59 if muted {
60 Group {
61 if word {
62 Label("Muted", systemImage: "mic.slash.fill").labelStyle(.titleAndIcon)
63 } else {
64 Image(systemName: "mic.slash.fill")
65 }
66 }
67 .padding(.leading, 5)
68 .overlay(alignment: .leading) {
69 Rectangle().fill(.white.opacity(0.5)).frame(width: 1, height: 10)
70 }
71 .fixedSize()
72 .layoutPriority(1)
73 }
74 }
75 }
76}
77
78/// What a tile shows where its picture is not: a symbol, and the words
79/// for it when there is room. Centred in the whole tile, as FaceTime has
80/// it, with the name label over the tile's bottom edge (2026-10-05, Paul:
81/// "Vertical centering here too"; until then, from 0.4.1, it was centred
82/// in the space above the label, which left it high). Only if the tile is
83/// so short that the centred placeholder would reach the label is it
84/// moved up, by no more than that (`PlaceholderPlacement`). On a tile too
85/// short for the words between the label and the top, the symbol stands
86/// alone, then smaller. The tile's accessibility value carries the words
87/// either way.
88public struct TilePlaceholder: View {
89 let symbol: String
90 let words: String?
91 let iconFont: Font
92 let wordsFont: Font
93 let spacing: CGFloat
94 let reserve: CGFloat
95
96 public init(
97 symbol: String, words: String?, iconFont: Font = .largeTitle, wordsFont: Font = .callout,
98 spacing: CGFloat = 12, reserve: CGFloat
99 ) {
100 self.symbol = symbol
101 self.words = words
102 self.iconFont = iconFont
103 self.wordsFont = wordsFont
104 self.spacing = spacing
105 self.reserve = reserve
106 }
107
108 public var body: some View {
109 PlaceholderLayout(reserve: reserve) {
110 ViewThatFits(in: .vertical) {
111 if let words {
112 VStack(spacing: spacing) {
113 Image(systemName: symbol).font(iconFont)
114 Text(words).font(wordsFont).multilineTextAlignment(.center).lineLimit(2)
115 .padding(.horizontal, 12)
116 }
117 }
118 Image(systemName: symbol).font(iconFont)
119 Image(systemName: symbol).font(.body)
120 }
121 }
122 .foregroundStyle(.white.opacity(0.8))
123 .accessibilityHidden(true)
124 }
125}
126
127/// Where a tile's placeholder goes, apart from any view so tests hold it:
128/// centred in the tile, unless that would bring it within `gap` of the
129/// label block at the bottom, when it rises just clear of it, and never
130/// above the tile's top.
131public enum PlaceholderPlacement {
132 /// Air kept between the placeholder and the top of the label block.
133 public static let gap: CGFloat = 4
134
135 /// The height the placeholder may take and still clear the label: what
136 /// chooses between the words, the symbol, and the smaller symbol.
137 public static func room(tile: CGFloat, reserve: CGFloat) -> CGFloat {
138 max(0, tile - clearance(reserve))
139 }
140
141 /// The placeholder's top, from the tile's top, for content `height` tall.
142 public static func top(tile: CGFloat, content height: CGFloat, reserve: CGFloat) -> CGFloat {
143 let centred = (tile - height) / 2
144 let clear = tile - clearance(reserve) - height
145 return max(0, min(centred, clear))
146 }
147
148 static func clearance(_ reserve: CGFloat) -> CGFloat { reserve > 0 ? reserve + gap : 0 }
149}
150
151/// Fills the tile and puts its one subview where `PlaceholderPlacement`
152/// says, offered the height it may take so `ViewThatFits` chooses what
153/// fits there.
154struct PlaceholderLayout: Layout {
155 let reserve: CGFloat
156 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
157 proposal.replacingUnspecifiedDimensions()
158 }
159 func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
160 guard let content = subviews.first else { return }
161 let room = PlaceholderPlacement.room(tile: bounds.height, reserve: reserve)
162 let size = content.sizeThatFits(ProposedViewSize(width: bounds.width, height: room))
163 let top = PlaceholderPlacement.top(tile: bounds.height, content: size.height, reserve: reserve)
164 content.place(
165 at: CGPoint(x: bounds.midX, y: bounds.minY + top), anchor: .top,
166 proposal: ProposedViewSize(width: bounds.width, height: size.height))
167 }
168}
169
170/// A tile's name label block, measured, so its placeholder can keep
171/// clear of it (see `TilePlaceholder`).
172extension View {
173 /// Reports this view's height as the tile's label block.
174 public func tileLabelBlock(_ height: Binding<CGFloat>) -> some View {
175 onGeometryChange(for: CGFloat.self) { $0.size.height } action: { height.wrappedValue = $0 }
176 }
177}
178