| 1 | import CoreGraphics |
| 2 | |
| 3 | /// How the full-window call's control bar fits the width it has (2026-10-03: |
| 4 | /// Paul's 0.4.6 bar ran off the left of the window, mute and camera cut |
| 5 | /// off). The bar is always whole and centred: first its spacing tightens, |
| 6 | /// then the controls that matter least move into a ⋯ menu, one at a time — |
| 7 | /// never mute, camera, chat or Leave. |
| 8 | public struct CallBarFit: Equatable, Sendable { |
| 9 | /// A control the bar can hold. `essential` ones never move into ⋯. |
| 10 | public enum Control: String, CaseIterable, Sendable { |
| 11 | case mute, camera, share, devices, layout, link, chat, leave |
| 12 | |
| 13 | public var essential: Bool { [.mute, .camera, .chat, .leave].contains(self) } |
| 14 | |
| 15 | /// Its width on the bar: every button is 44 pt round; Leave is a |
| 16 | /// capsule 1.4 times as wide (OverlayButtonBody). |
| 17 | var width: CGFloat { self == .leave ? CallBarFit.button * 1.4 : CallBarFit.button } |
| 18 | } |
| 19 | |
| 20 | public static let button: CGFloat = 44 |
| 21 | /// The widest the bar grows, and its margin from the window's edges. |
| 22 | public static let maxWidth: CGFloat = 760 |
| 23 | public static let margin: CGFloat = 16 |
| 24 | /// The order the optional controls move into ⋯: the least used first. |
| 25 | public static let overflowOrder: [Control] = [.link, .layout, .devices, .share] |
| 26 | |
| 27 | /// One step of tightening: the gap between buttons, the least room |
| 28 | /// between the left group and chat, and the pane's padding. |
| 29 | public struct Spacing: Equatable, Sendable { |
| 30 | public var between: CGFloat |
| 31 | public var gap: CGFloat |
| 32 | public var padding: CGFloat |
| 33 | } |
| 34 | |
| 35 | public static let roomy = Spacing(between: 10, gap: 24, padding: 10) |
| 36 | public static let snug = Spacing(between: 6, gap: 12, padding: 8) |
| 37 | public static let tight = Spacing(between: 4, gap: 4, padding: 6) |
| 38 | |
| 39 | public var spacing: Spacing |
| 40 | /// The controls in ⋯, in the bar's own order; empty when everything fits. |
| 41 | public var overflow: [Control] |
| 42 | /// The pane's width: as wide as it may be, never wider than it has. |
| 43 | public var width: CGFloat |
| 44 | |
| 45 | /// The bar for a window `windowWidth` wide showing `controls` (Share |
| 46 | /// meeting link is only there for a member; Video layout only here). |
| 47 | public static func fit(windowWidth: CGFloat, controls: [Control]) -> CallBarFit { |
| 48 | let room = max(0, windowWidth - 2 * margin) |
| 49 | for spacing in [roomy, snug, tight] where needed(controls, spacing, more: false) <= room { |
| 50 | return CallBarFit(spacing: spacing, overflow: [], width: min(maxWidth, room)) |
| 51 | } |
| 52 | var shown = controls |
| 53 | var moved: [Control] = [] |
| 54 | for control in overflowOrder where shown.contains(control) { |
| 55 | shown.removeAll { $0 == control } |
| 56 | moved.append(control) |
| 57 | if needed(shown, tight, more: true) <= room { break } |
| 58 | } |
| 59 | let order = controls.filter { moved.contains($0) } |
| 60 | return CallBarFit(spacing: tight, overflow: order, width: min(maxWidth, room)) |
| 61 | } |
| 62 | |
| 63 | /// The least width `controls` take with `spacing`, with a ⋯ button if |
| 64 | /// `more`: the buttons, a gap between each (the spacer before chat is |
| 65 | /// one more child of the row), the spacer's own least, and the padding. |
| 66 | public static func needed(_ controls: [Control], _ spacing: Spacing, more: Bool) -> CGFloat { |
| 67 | let buttons = controls.reduce(0) { $0 + $1.width } + (more ? button : 0) |
| 68 | let children = controls.count + (more ? 1 : 0) + 1 |
| 69 | return buttons + CGFloat(children - 1) * spacing.between + spacing.gap + 2 * spacing.padding |
| 70 | } |
| 71 | |
| 72 | public func shows(_ control: Control) -> Bool { !overflow.contains(control) } |
| 73 | } |
| 74 | |