Sources/OrgSwiftUI/OrgBadgeRowView.swift
49 lines · 1976 bytes
1import SwiftUI
2
3/// The row of status badges a README opens with.
4///
5/// Badges keep their own size — a badge is authored at the pixel size it is meant to be
6/// seen at, so `resizable()` would only blur it. They wrap to the next line when the row is
7/// wider than the column, which is what a long badge row does on the web.
8struct OrgBadgeRowView: View {
9 let badges: [OrgBadge]
10
11 /// The scale badge artwork is served at. Retina artwork is the only way a small raster
12 /// badge stays sharp, and a badge has no other channel to declare its scale.
13 static let scale: CGFloat = 2
14
15 var body: some View {
16 // `ViewThatFits` is not a wrap; `Layout` is more than a badge row needs. A flexible
17 // `HStack` inside a horizontal `ScrollView` keeps a long row reachable without
18 // guessing at line breaks.
19 ScrollView(.horizontal, showsIndicators: false) {
20 HStack(spacing: 6) {
21 ForEach(badges) { badge in
22 link(badge) {
23 AsyncImage(url: badge.source, scale: Self.scale) { phase in
24 switch phase {
25 case .success(let image):
26 image
27 case .failure:
28 Text(badge.alt).font(.footnote).foregroundStyle(.secondary)
29 default:
30 ProgressView().controlSize(.mini)
31 }
32 }
33 .accessibilityLabel(badge.alt)
34 }
35 }
36 }
37 }
38 }
39
40 /// A badge with no resolvable link is still shown, just not tappable.
41 @ViewBuilder
42 private func link(_ badge: OrgBadge, @ViewBuilder content: () -> some View) -> some View {
43 if let url = badge.link {
44 Link(destination: url, label: content)
45 } else {
46 content()
47 }
48 }
49}