Give the closest capsule the rounded rect whenever it wraps

The capsule's shape was keyed to the verdict's declared line count, so
"Closest station" — one declared line — always drew as a Capsule while
its text wrapped to two lines in the row's narrow column, leaving the
semicircular ends crowding the words they wrapped. Saves never showed
this because its amount line makes the declared count two.

The shape now follows the DRAWN height: a GeometryReader behind the
text block reports what it actually rendered, divided by the caption's
line height taken through ScaledMetric so the test holds at every
Dynamic Type size. One drawn line keeps the capsule; two or more get
the same 8pt rounded rect the other verdicts use. The first layout
pass, before there is a height, falls back to the declared count.
This commit is contained in:
FuelBoard Contributor
2026-10-18 20:16:30 +01:00
parent 244f9be640
commit 6edab35400
+35 -3
View File
@@ -160,6 +160,12 @@ struct ListingFuelSavingsCapsule: View {
/// Station rows carry the figure; the Key sheet's legend shows the wording only. /// Station rows carry the figure; the Key sheet's legend shows the wording only.
var showsAmount: Bool = true var showsAmount: Bool = true
/// The height the text actually drew, so the shape can follow the drawn line
/// count rather than the data's. See `drawnLineCount`.
@State private var textHeight: CGFloat = 0
/// One caption line's height, scaled with Dynamic Type, to divide that by.
@ScaledMetric(relativeTo: .caption) private var captionLineHeight: CGFloat = 14
private var verdict: FuelSavingsResult.ListingVerdict { private var verdict: FuelSavingsResult.ListingVerdict {
result.listingVerdict(showsAmount: showsAmount) result.listingVerdict(showsAmount: showsAmount)
} }
@@ -185,6 +191,13 @@ struct ListingFuelSavingsCapsule: View {
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
// Never compress or truncate: the capsule wraps and the row grows taller. // Never compress or truncate: the capsule wraps and the row grows taller.
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.background(
GeometryReader { proxy in
Color.clear
.preference(key: CapsuleTextHeightKey.self,
value: proxy.size.height)
}
)
} }
// White on the muted fill. These capsules carry a figure the eye is asked // White on the muted fill. These capsules carry a figure the eye is asked
// to read at a glance, which is why they are the only filled-and-white // to read at a glance, which is why they are the only filled-and-white
@@ -200,6 +213,7 @@ struct ListingFuelSavingsCapsule: View {
.background(shape.fill(fill)) .background(shape.fill(fill))
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
.accessibilityLabel(verdict.spokenText) .accessibilityLabel(verdict.spokenText)
.onPreferenceChange(CapsuleTextHeightKey.self) { textHeight = $0 }
} }
/// Saves and Costs only — the two verdicts that carry a figure. See the /// Saves and Costs only — the two verdicts that carry a figure. See the
@@ -216,12 +230,22 @@ struct ListingFuelSavingsCapsule: View {
ListingCapsuleFill.color(for: result.status) ListingCapsuleFill.color(for: result.status)
} }
/// How many lines the text DREW, not how many the verdict declares.
/// "Closest station" is one declared line that wraps to two in the row's
/// narrow column, and the shape has to follow what is on screen or the pill's
/// semicircular ends crowd the text they are wrapping. Falls back to the
/// declared count on the first layout pass, before there is a height.
private var drawnLineCount: Int {
guard textHeight > 0, captionLineHeight > 0 else { return verdict.lines.count }
return max(1, Int((textHeight / captionLineHeight).rounded()))
}
/// One line keeps the capsule. Two or more switch to a rounded rect: a pill's /// One line keeps the capsule. Two or more switch to a rounded rect: a pill's
/// semicircular ends crowd the text corners at that height, while an 8pt /// semicircular ends crowd the text corners at that height, while an 8pt
/// radius does not. Keyed to the LINE COUNT, so Break even flips shape only /// radius does not. Keyed to the DRAWN line count, so a wrapped "Closest
/// when "(approx.)" pushes it onto a second line. /// station" gets the same rounded rect as a figure pushed onto its own line.
private var shape: AnyShape { private var shape: AnyShape {
verdict.lines.count > 1 drawnLineCount > 1
? AnyShape(RoundedRectangle(cornerRadius: 8)) ? AnyShape(RoundedRectangle(cornerRadius: 8))
: AnyShape(Capsule()) : AnyShape(Capsule())
} }
@@ -236,6 +260,14 @@ struct ListingFuelSavingsCapsule: View {
} }
} }
/// Carries the drawn height of a capsule's text block up to `drawnLineCount`.
private struct CapsuleTextHeightKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
/// The station listing's verdict-capsule fills: muted semantic colours carrying /// The station listing's verdict-capsule fills: muted semantic colours carrying
/// white text and icon. /// white text and icon.
/// ///