diff --git a/FuelBoard/FuelSavingsViews.swift b/FuelBoard/FuelSavingsViews.swift index b507ea4..907ae6e 100644 --- a/FuelBoard/FuelSavingsViews.swift +++ b/FuelBoard/FuelSavingsViews.swift @@ -160,6 +160,12 @@ struct ListingFuelSavingsCapsule: View { /// Station rows carry the figure; the Key sheet's legend shows the wording only. 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 { result.listingVerdict(showsAmount: showsAmount) } @@ -185,6 +191,13 @@ struct ListingFuelSavingsCapsule: View { .multilineTextAlignment(.leading) // Never compress or truncate: the capsule wraps and the row grows taller. .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 // 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)) .accessibilityElement(children: .combine) .accessibilityLabel(verdict.spokenText) + .onPreferenceChange(CapsuleTextHeightKey.self) { textHeight = $0 } } /// 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) } + /// 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 /// 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 - /// when "(approx.)" pushes it onto a second line. + /// radius does not. Keyed to the DRAWN line count, so a wrapped "Closest + /// station" gets the same rounded rect as a figure pushed onto its own line. private var shape: AnyShape { - verdict.lines.count > 1 + drawnLineCount > 1 ? AnyShape(RoundedRectangle(cornerRadius: 8)) : 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 /// white text and icon. ///