oficina / meet Public

Calls fixtures: a picture on the stage's tile, and stage snapshots

A tile's placeholder is all an offline snapshot of Speaker view could
show on the stage, black on a black window, so the band left under the
filmstrip and the bar did not show. fixtureStagePicture (Mac
environment; iPhone -CallsFixtureStagePicture busy) draws a fixture
frame as the stage tile's video, as a camera fills a tile and a screen
is fitted. CallsInterop --stage-snapshots draws Speaker view with a
camera, a camera that is off and a shared screen on the stage, controls
shown and hidden, at 1000 x 640 and 760 x 540; Grid; and the mini window
at 200, 320 and 480 and its tab. Off screen: this Mac's screencapture is
refused, so renderLive cannot run here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 2b7f90c7e9893568e89f37cf69230857c4eba69e parent b5a2b80
6 files changed, +123 −3
  • native/apple/Sources/CallsInterop/Interop.swift +6 −0
  • native/apple/Sources/CallsInterop/StageSnapshots.swift +63 −0
  • native/apple/Sources/CallsKit/CallFixtures.swift +40 −0
  • native/apple/Sources/CallsKit/MacVideoTile.swift +4 −1
  • native/ios/Sources/CallsPhone.swift +4 −1
  • native/ios/Sources/PhoneCallFixture.swift +6 −1
diff --git a/native/apple/Sources/CallsInterop/Interop.swift b/native/apple/Sources/CallsInterop/Interop.swift
index 603c858..6abf335 100644
--- a/native/apple/Sources/CallsInterop/Interop.swift
+++ b/native/apple/Sources/CallsInterop/Interop.swift
@@ -52,6 +52,12 @@ struct Input: Decodable {
} catch { exit(1) }
return
}
+ if CommandLine.arguments.count == 3, CommandLine.arguments[1] == "--stage-snapshots" {
+ do {
+ try await StageSnapshots.save(to: URL(fileURLWithPath: CommandLine.arguments[2]))
+ } catch { exit(1) }
+ return
+ }
if CommandLine.arguments.count == 3, CommandLine.arguments[1] == "--bar-layout" {
exit(await BarLayoutHarness.run(to: URL(fileURLWithPath: CommandLine.arguments[2])) ? 0 : 1)
}
diff --git a/native/apple/Sources/CallsInterop/StageSnapshots.swift b/native/apple/Sources/CallsInterop/StageSnapshots.swift
new file mode 100644
index 0000000..9337f76
--- /dev/null
+++ b/native/apple/Sources/CallsInterop/StageSnapshots.swift
@@ -0,0 +1,63 @@
+import AppKit
+@_spi(Fixtures) import CallsKit
+import SwiftUI
+
+/// The stage and every tile's placeholder (2026-10-05), drawn off screen
+/// (no glass and no Metal video, as the other offline fixtures), with a busy frame as the picture on the stage's
+/// tile so the filmstrip, the names and the bar show over a picture:
+/// Speaker view with a camera on the stage, with a camera that is off on
+/// it, and with a shared screen on it, controls shown and hidden, at the
+/// default window and the smallest; Grid; and the mini window at 200, 320
+/// and 480 with someone whose camera is off, and its tab.
+@MainActor enum StageSnapshots {
+ static func save(to folder: URL) async throws {
+ NSApplication.shared.setActivationPolicy(.prohibited)
+ try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
+ func shot(
+ _ name: String, _ fixture: CallFixture = .grid, grid: Bool = false, shown: Bool,
+ size: CGSize = CGSize(width: 1000, height: 640), speaker: String?
+ ) async throws {
+ let call = Call(playAudio: false)
+ fixture.apply(to: call)
+ call.levels.speaker.showFixture(speaker)
+ try await draw(
+ MacCallView(
+ call: call, grid: grid, style: .fullWindow, controlsShown: shown, leave: {}, rejoin: {}
+ )
+ .tint(CallsAppearance.accent)
+ .environment(\.fixtureStagePicture, .busy),
+ size: size, to: folder.appendingPathComponent("mac-\(name).png"))
+ }
+ for shown in [true, false] {
+ let state = shown ? "controls" : "hidden"
+ try await shot("stage-camera-\(state)", shown: shown, speaker: "ana")
+ try await shot("stage-camera-off-\(state)", shown: shown, speaker: "tom")
+ try await shot("stage-screen-\(state)", .screen, shown: shown, speaker: "ana")
+ try await shot(
+ "stage-camera-760x540-\(state)", shown: shown, size: CGSize(width: 760, height: 540),
+ speaker: "ana")
+ try await shot("grid-\(state)", grid: true, shown: shown, speaker: "ana")
+ }
+ for width in [MiniWindowRules.minimumWidth, MiniWindowRules.size.width, 480] {
+ for (who, label) in [("tom", "camera-off"), (nil, "self")] as [(String?, String)] {
+ let call = Call(playAudio: false)
+ CallFixture.grid.apply(to: call)
+ call.levels.speaker.showFixture(who)
+ if who == nil { call.showFixtureSelf(muted: false, cameraOff: true, sharingScreen: false) }
+ try await draw(
+ MiniWindowSnapshot.window(call: call, hovering: false),
+ size: CGSize(width: width, height: width / MiniWindowRules.aspect),
+ to: folder.appendingPathComponent("mini-\(Int(width))-\(label).png"))
+ }
+ }
+ let call = Call(playAudio: false)
+ CallFixture.grid.apply(to: call)
+ try await draw(
+ MiniWindowSnapshot.tab(call: call), size: MiniWindowTab.size,
+ to: folder.appendingPathComponent("mini-tab.png"))
+ }
+
+ private static func draw(_ content: some View, size: CGSize, to file: URL) async throws {
+ try await LobbySnapshots.render(content, scheme: .dark, size: size, to: file)
+ }
+}
diff --git a/native/apple/Sources/CallsKit/CallFixtures.swift b/native/apple/Sources/CallsKit/CallFixtures.swift
index 89f70b0..f409157 100644
--- a/native/apple/Sources/CallsKit/CallFixtures.swift
+++ b/native/apple/Sources/CallsKit/CallFixtures.swift
@@ -157,6 +157,46 @@ extension EnvironmentValues {
}
}
+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: a camera's 16:9 picture
+/// filling the tile, a screen's 16:10 fitted whole.
+@_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 scale =
+ screen
+ ? min(bounds.width / video.width, bounds.height / video.height)
+ : max(bounds.width / video.width, bounds.height / video.height)
+ let size = CGSize(width: video.width * scale, height: video.height * scale)
+ picture.view
+ .frame(width: size.width, height: size.height)
+ .position(x: bounds.width / 2, y: bounds.height / 2)
+ }
+ .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
diff --git a/native/apple/Sources/CallsKit/MacVideoTile.swift b/native/apple/Sources/CallsKit/MacVideoTile.swift
index 5d2bd88..1d3744e 100644
--- a/native/apple/Sources/CallsKit/MacVideoTile.swift
+++ b/native/apple/Sources/CallsKit/MacVideoTile.swift
@@ -43,6 +43,7 @@
/// The height of the name label and what is over it, lift included,
/// which the placeholder stays above.
@State private var labelBlock: CGFloat = 0
+ @Environment(\.fixtureStagePicture) private var fixturePicture
var body: some View {
TileAudioReader(audio) { info in tile(info) }
@@ -51,7 +52,9 @@
private func tile(_ info: TileAudioInfo?) -> some View {
ZStack {
backdrop
- if let track, !cameraOff, placeholder == nil {
+ if let fixturePicture, onStage, track == nil, !cameraOff, placeholder == nil {
+ FixtureVideo(picture: fixturePicture, screen: screen)
+ } else if let track, !cameraOff, placeholder == nil {
MacVideoView(track: track, fit: screen).scaleEffect(x: mirrored ? -1 : 1, y: 1)
} else {
TilePlaceholder(
diff --git a/native/ios/Sources/CallsPhone.swift b/native/ios/Sources/CallsPhone.swift
index 926362b..c212fa0 100644
--- a/native/ios/Sources/CallsPhone.swift
+++ b/native/ios/Sources/CallsPhone.swift
@@ -425,13 +425,16 @@ struct PhoneVideoTile: View {
/// The height of the name label and what is over it, which the
/// placeholder stays above.
@State private var labelBlock: CGFloat = 0
+ @Environment(\.fixtureStagePicture) private var fixturePicture
var body: some View {
TileAudioReader(audio) { info in tile(info) }
}
private func tile(_ info: TileAudioInfo?) -> some View {
ZStack {
backdrop
- if let track, !cameraOff, placeholder == nil {
+ if let fixturePicture, onStage, track == nil, !cameraOff, placeholder == nil {
+ FixtureVideo(picture: fixturePicture, screen: screen)
+ } else if let track, !cameraOff, placeholder == nil {
PhoneVideo(track: track, fit: screen).scaleEffect(x: mirrored ? -1 : 1, y: 1)
} else {
TilePlaceholder(
diff --git a/native/ios/Sources/PhoneCallFixture.swift b/native/ios/Sources/PhoneCallFixture.swift
index 714f657..f3e4284 100644
--- a/native/ios/Sources/PhoneCallFixture.swift
+++ b/native/ios/Sources/PhoneCallFixture.swift
@@ -8,7 +8,9 @@
/// optionally `-CallsFixtureControls shown|hidden` to pin the controls,
/// `-CallsFixtureMore YES` to open the controls menu, `-CallsFixtureDark
/// YES`, `-CallsFixtureClassic YES`, `-CallsFixtureBackdrop black|white|busy`
- /// for a frame in place of the stage, `-CallsFixtureSelf
+ /// for a frame in place of the stage, `-CallsFixtureStagePicture
+ /// black|white|busy` for a frame as the picture on the stage's tile (the
+ /// filmstrip and labels over it), `-CallsFixtureSelf
/// muted|cameraOff|sharing` for your own state, `-CallsFixtureSpeaking
/// silent|low|mid|high` for everyone's speaking glow, and
/// `-CallsFixtureNoGlass YES` for the material older systems draw in
@@ -45,6 +47,9 @@
.environment(
\.fixtureBackdrop,
defaults.string(forKey: "CallsFixtureBackdrop").flatMap(FixtureBackdrop.init(rawValue:)))
+ .environment(
+ \.fixtureStagePicture,
+ defaults.string(forKey: "CallsFixtureStagePicture").flatMap(FixtureBackdrop.init(rawValue:)))
.environment(\.callOverlayGlass, !defaults.bool(forKey: "CallsFixtureNoGlass"))
.preferredColorScheme(defaults.bool(forKey: "CallsFixtureDark") ? .dark : .light)
.onAppear {