From c0b15b132a0c589c74790eed8eff25093136aac4 Mon Sep 17 00:00:00 2001 From: MtlPhil <76601115+MtlPhil@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:36:23 -0400 Subject: [PATCH 1/4] Center the watch BG with IOB and COB icons, and fix crown scrubbing on the chart page The trend arrow no longer shifts the BG digits or the delta off center, and IOB and COB share one line under the delta with syringe and fork-and-knife icons. On the chart page, crown focus is now requested after the page becomes focusable, so the crown scrubs instead of paging. A crown button in the toolbar shows the feature and toggles it. --- LoopFollowWatch/ChartPage.swift | 34 ++++++++++++++----- LoopFollowWatch/GlucosePage.swift | 54 +++++++++++++++++-------------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index d354cdd3a..0ed2e8b96 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -4,9 +4,9 @@ import SwiftUI import WatchKit -/// The full chart on its own page. Tap the chart to scrub back in time with the -/// Digital Crown; tap again to return to now. The button in the corner changes -/// the time span. +/// The full chart on its own page. Tap the chart or the crown button to scrub back +/// in time with the Digital Crown; tap again to return to now. The button in the +/// other corner changes the time span. struct ChartPage: View { let state: WatchState @@ -33,17 +33,30 @@ struct ChartPage: View { // Keeps the axis labels clear of the page indicator. .padding(.trailing, 10) .contentShape(Rectangle()) - .onTapGesture { - isScrubbing.toggle() - scrubSteps = 0 - crownFocused = isScrubbing - } + .onTapGesture(perform: toggleScrubbing) } .focusable(isScrubbing) .focused($crownFocused) + // Focus can only move once the page has become focusable, so it follows the toggle rather than + // being set in the same tap; otherwise the crown keeps paging instead of scrubbing. + .onChange(of: isScrubbing) { _, scrubbing in + crownFocused = scrubbing + } + .onChange(of: crownFocused) { _, focused in + if !focused { + isScrubbing = false + } + } .digitalCrownRotation($scrubSteps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) } .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: toggleScrubbing) { + Image(systemName: CrownSymbol.increase) + } + .tint(isScrubbing ? .green : nil) + .accessibilityLabel(isScrubbing ? "Stop scrubbing" : "Scrub with the Digital Crown") + } ToolbarItem(placement: .topBarTrailing) { Button("\(Int(spanHours)) h") { let index = WatchChartView.spans.firstIndex(of: spanHours) ?? 1 @@ -55,6 +68,11 @@ struct ChartPage: View { } } + private func toggleScrubbing() { + isScrubbing.toggle() + scrubSteps = 0 + } + /// Time and BG at the marker. private func scrubHeader(now: Date) -> some View { let marker = WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index ed5ba466a..756113950 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -4,7 +4,7 @@ import Charts import SwiftUI -/// Main page: one large BG value with trend and delta, a two-hour sparkline, +/// Main page: one large BG value with trend and delta, IOB and COB, a two-hour sparkline, /// and the remote commands in the bottom toolbar. struct GlucosePage: View { let state: WatchState @@ -26,14 +26,6 @@ struct GlucosePage: View { } } .toolbar { - ToolbarItem(placement: .topBarLeading) { - if !isLuminanceReduced, let iob = state.snapshot?.iob { - Text("IOB \(WatchFormat.iob(iob))") - .font(.footnote) - .lineLimit(1) - .minimumScaleFactor(0.6) - } - } ToolbarItem(placement: .topBarTrailing) { age(now: context.date) } @@ -51,16 +43,20 @@ struct GlucosePage: View { if let snapshot = state.snapshot { let isStale = now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter VStack(spacing: -4) { - HStack(alignment: .center, spacing: 4) { - Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) - .font(.system(size: 56, weight: .black)) - .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) - .strikethrough(isStale, pattern: .solid, color: .red) - .lineLimit(1) - .minimumScaleFactor(0.5) - Text(WatchFormat.trendArrow(snapshot.trend)) - .font(.system(size: 28, weight: .black)) - } + // The arrow hangs off the number's trailing edge so the digits, and the delta under them, stay centered. + Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) + .font(.system(size: 56, weight: .black)) + .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) + .strikethrough(isStale, pattern: .solid, color: .red) + .lineLimit(1) + .minimumScaleFactor(0.5) + .overlay(alignment: .trailing) { + Text(WatchFormat.trendArrow(snapshot.trend)) + .font(.system(size: 28, weight: .black)) + .fixedSize() + .alignmentGuide(.trailing) { $0[.leading] - 4 } + } + .padding(.horizontal, 34) Text(WatchFormat.delta(snapshot.delta, unit: snapshot.unit)) .font(.title3) .foregroundStyle(.secondary) @@ -103,11 +99,21 @@ struct GlucosePage: View { Label("Not looping", systemImage: "exclamationmark.triangle.fill") .font(.footnote) .foregroundStyle(.red) - } else if let cob = snapshot.cob { - Text("COB \(WatchFormat.cob(cob))") - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) + } else if snapshot.iob != nil || snapshot.cob != nil { + HStack(spacing: 12) { + if let iob = snapshot.iob { + Text("\(Image(systemName: "syringe")) \(WatchFormat.iob(iob))") + .accessibilityLabel("Insulin on board \(WatchFormat.iob(iob))") + } + if let cob = snapshot.cob { + Text("\(Image(systemName: "fork.knife")) \(WatchFormat.cob(cob))") + .accessibilityLabel("Carbs on board \(WatchFormat.cob(cob))") + } + } + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.7) } } } From dfd71afd2ac10723e5f2a7f2e14f0774b237909a Mon Sep 17 00:00:00 2001 From: MtlPhil <76601115+MtlPhil@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:51:43 -0400 Subject: [PATCH 2/4] Show BG, carbs and insulin under the watch chart while scrubbing The readout moves below the chart so it no longer collides with the toolbar buttons, and shows a range-colored dot with the BG plus any carbs and boluses that fall within the marker's five-minute crown step. --- LoopFollowWatch/ChartPage.swift | 50 ++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index 0ed2e8b96..f05991501 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -21,9 +21,6 @@ struct ChartPage: View { var body: some View { TimelineView(.periodic(from: .now, by: 60)) { context in VStack(spacing: 2) { - if isScrubbing { - scrubHeader(now: context.date) - } WatchChartView( state: state, now: context.date, @@ -34,6 +31,10 @@ struct ChartPage: View { .padding(.trailing, 10) .contentShape(Rectangle()) .onTapGesture(perform: toggleScrubbing) + // Below the chart, clear of the toolbar buttons that float over the top of the page. + if isScrubbing { + scrubDetails(now: context.date) + } } .focusable(isScrubbing) .focused($crownFocused) @@ -73,23 +74,46 @@ struct ChartPage: View { scrubSteps = 0 } - /// Time and BG at the marker. - private func scrubHeader(now: Date) -> some View { + /// Time, BG, carbs and insulin at the marker. Carbs and boluses count when they fall within the + /// marker's five-minute crown step, so each entry shows at exactly one step. + private func scrubDetails(now: Date) -> some View { let marker = WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) let nearest = state.readings.min { abs($0.date.timeIntervalSince(marker)) < abs($1.date.timeIntervalSince(marker)) } let reading = nearest.flatMap { abs($0.date.timeIntervalSince(marker)) <= 450 ? $0 : nil } - return HStack { + let carbs = total(of: state.carbs, near: marker) + let insulin = total(of: state.boluses, near: marker) + return HStack(spacing: 8) { Text(marker.formatted(date: .omitted, time: .shortened)) .foregroundStyle(.secondary) - Spacer() - if let reading { - Text(WatchFormat.glucose(reading.v, unit: state.snapshot?.unit ?? .mgdl)) - .foregroundStyle(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) - } else { - Text("—") + HStack(spacing: 3) { + if let reading { + Circle() + .fill(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) + .frame(width: 7, height: 7) + Text(WatchFormat.glucose(reading.v, unit: state.snapshot?.unit ?? .mgdl)) + } else { + Text("—") + } + } + if let carbs { + Text("\(Image(systemName: "fork.knife")) \(WatchFormat.cob(carbs))") + .accessibilityLabel("Carbs \(WatchFormat.cob(carbs))") + } + if let insulin { + Text("\(Image(systemName: "syringe")) \(WatchFormat.insulin(insulin)) U") + .accessibilityLabel("Bolus \(WatchFormat.insulin(insulin)) U") } } - .font(.headline) + .font(.footnote) .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.6) + .padding(.trailing, 10) + } + + /// Sum of the entries within half a crown step of the marker, or nil when there are none. + private func total(of entries: [WatchReading], near marker: Date) -> Double? { + let matching = entries.filter { abs($0.date.timeIntervalSince(marker)) < 150 } + return matching.isEmpty ? nil : matching.reduce(0) { $0 + $1.v } } } From 5ed0f40e571cd38a77be8a7b12a4350012607f4e Mon Sep 17 00:00:00 2001 From: MtlPhil <76601115+MtlPhil@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:44:05 -0400 Subject: [PATCH 3/4] Show overrides on the watch: a band on the chart and the running one on the glucose page The phone now sends the override periods of the last 24 hours with their name and insulin needs, taking the percentage from the matching preset when Nightscout does not carry it. The chart draws them as a thin green band along the top, and the glucose page shows the running override's name and percentage under IOB and COB. --- LoopFollow/Watch/WatchStateBuilder.swift | 19 +++++++++++++++++++ LoopFollowWatch/GlucosePage.swift | 19 ++++++++++++++++++- LoopFollowWatch/WatchChartView.swift | 15 ++++++++++++++- Tests/WatchProtocolTests.swift | 13 +++++++++++++ WatchShared/WatchState.swift | 22 ++++++++++++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index eaf59df86..21d89021c 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -27,6 +27,10 @@ enum WatchStateBuilder { .filter { $0.date >= cutoff } .map { WatchReading(t: $0.date, v: $0.value) } + let overrides = (vc?.overrideGraphData ?? []) + .filter { $0.endDate >= cutoff } + .map { WatchOverride(start: $0.date, end: $0.endDate, name: $0.reason.isEmpty ? "Override" : $0.reason, percent: overridePercent($0)) } + var predictions: [WatchPredictionLine] = [] if let vc { let lines: [(WatchPredictionLine.Kind, [ShareGlucoseData])] = [ @@ -55,6 +59,7 @@ enum WatchStateBuilder { cone: cone, boluses: boluses, carbs: carbs, + overrides: overrides, lowMgdl: thresholds.low, highMgdl: thresholds.high, colorBGText: Storage.shared.colorBGText.value, @@ -167,6 +172,20 @@ enum WatchStateBuilder { ) } + /// Insulin needs of an override, from its Nightscout entry or else from the preset of the same name, + /// since Trio does not upload the factor with the override. + private static func overridePercent(_ override: DataStructs.overrideStruct) -> Int? { + var factor = override.insulNeedsScaleFactor + if factor == 1 { + if let loop = ProfileManager.shared.loopOverrides.first(where: { $0.name == override.reason }) { + factor = loop.insulinNeedsScaleFactor + } else if let percentage = ProfileManager.shared.trioOverrides.first(where: { $0.name == override.reason })?.percentage { + factor = percentage / 100 + } + } + return factor == 1 ? nil : Int(round(factor * 100)) + } + private static func loopOverridePresets() -> [WatchPreset] { ProfileManager.shared.loopOverrides.map { override in var parts: [String] = [] diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index 756113950..4a4741feb 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -4,7 +4,7 @@ import Charts import SwiftUI -/// Main page: one large BG value with trend and delta, IOB and COB, a two-hour sparkline, +/// Main page: one large BG value with trend and delta, IOB and COB, the running override, a two-hour sparkline, /// and the remote commands in the bottom toolbar. struct GlucosePage: View { let state: WatchState @@ -21,6 +21,7 @@ struct GlucosePage: View { glucose(now: context.date) if !isLuminanceReduced { details + overrideLine(now: context.date) Sparkline(state: state, now: context.date) .frame(minHeight: 36) } @@ -117,6 +118,22 @@ struct GlucosePage: View { } } } + + /// The running override's name and insulin needs, in the green of the overrides button and chart band. + @ViewBuilder + private func overrideLine(now: Date) -> some View { + let active = state.activeOverride(at: now) + // The phone's periods end on time; the watch's own fetch only has the snapshot's name. + if let name = state.overrides == nil ? state.snapshot?.override : active?.name { + let percent = active?.percent.map { " · \($0) %" } ?? "" + Text("\(Image(systemName: "slider.horizontal.3")) \(name)\(percent)") + .font(.footnote) + .foregroundStyle(.green) + .lineLimit(1) + .minimumScaleFactor(0.7) + .accessibilityLabel("Override \(name)\(active?.percent.map { ", insulin needs \($0) percent" } ?? "")") + } + } } /// Override, treatments and temp target, as in Apple's bottom-toolbar pattern. diff --git a/LoopFollowWatch/WatchChartView.swift b/LoopFollowWatch/WatchChartView.swift index b6be4c090..71cc3225f 100644 --- a/LoopFollowWatch/WatchChartView.swift +++ b/LoopFollowWatch/WatchChartView.swift @@ -5,7 +5,8 @@ import Charts import SwiftUI /// BG chart in the phone's colors: readings colored by range, dashed low/high -/// lines, prediction lines or cone in their phone colors, boluses and carbs as markers. +/// lines, prediction lines or cone in their phone colors, boluses and carbs as markers, +/// and overrides as a green band along the top. struct WatchChartView: View { let state: WatchState let now: Date @@ -24,6 +25,7 @@ struct WatchChartView: View { (line.kind, line.points.filter { window.contains($0.date) }) } let cone = state.cone.filter { window.contains($0.date) } + let overrides = (state.overrides ?? []).filter { $0.endDate > window.lowerBound && $0.startDate < window.upperBound } let top = yTop(readings: readings, predictions: predictions.flatMap(\.1) + cone.map { WatchReading(t: $0.t, v: $0.high) }) Chart { @@ -46,6 +48,17 @@ struct WatchChartView: View { .foregroundStyle(Color.blue.opacity(0.4)) } + // A thin band along the top edge, like the override strip on the phone's chart. + ForEach(overrides, id: \.start) { override in + RectangleMark( + xStart: .value("Start", max(override.startDate, window.lowerBound)), + xEnd: .value("End", min(override.endDate, window.upperBound)), + y: .value("Override", top), + height: .fixed(4) + ) + .foregroundStyle(Color.green.opacity(0.8)) + } + if let marker = markerDate { RuleMark(x: .value("Marker", marker)) .foregroundStyle(Color.white.opacity(0.8)) diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index 1854e27b8..ebb5231b0 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -19,6 +19,10 @@ struct WatchProtocolTests { cone: [WatchConePoint(t: t0.timeIntervalSince1970 + 300, low: 120, high: 150)], boluses: [WatchReading(t: t0.timeIntervalSince1970 - 600, v: 2.5)], carbs: [WatchReading(t: t0.timeIntervalSince1970 - 600, v: 30)], + overrides: [ + WatchOverride(start: t0.timeIntervalSince1970 - 7200, end: t0.timeIntervalSince1970 - 3600, name: "Sleep", percent: nil), + WatchOverride(start: t0.timeIntervalSince1970 - 1800, end: t0.timeIntervalSince1970 + 1800, name: "Exercise", percent: 80), + ], lowMgdl: 70, highMgdl: 180, colorBGText: true, @@ -52,6 +56,15 @@ struct WatchProtocolTests { #expect(WatchState.decode(data) == original) } + @Test("The active override is the one whose period contains the time") + func activeOverride() { + let state = state() + #expect(state.activeOverride(at: t0)?.name == "Exercise") + #expect(state.activeOverride(at: t0.addingTimeInterval(-5400))?.name == "Sleep") + #expect(state.activeOverride(at: t0.addingTimeInterval(-3000)) == nil) + #expect(state.activeOverride(at: t0.addingTimeInterval(1800)) == nil) + } + @Test("Commands keep their action, id and creation time") func commandRoundTrip() throws { let actions: [WatchCommand.Action] = [ diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index 095784b9f..91c73c69c 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -24,6 +24,8 @@ struct WatchState: Codable, Equatable { var cone: [WatchConePoint] var boluses: [WatchReading] var carbs: [WatchReading] + /// Override periods in the last 24 hours, oldest first; nil from the watch's own fetch. + var overrides: [WatchOverride]? /// Range thresholds used for coloring, in mg/dL (same as the phone). var lowMgdl: Double var highMgdl: Double @@ -65,6 +67,26 @@ struct WatchReading: Codable, Equatable { var date: Date { Date(timeIntervalSince1970: t) } } +/// One override period, as drawn in the band at the top of the phone's chart. +struct WatchOverride: Codable, Equatable { + /// Unix epoch seconds. + let start: TimeInterval + let end: TimeInterval + let name: String + /// Insulin needs as a percentage of normal; nil when unchanged. + let percent: Int? + + var startDate: Date { Date(timeIntervalSince1970: start) } + var endDate: Date { Date(timeIntervalSince1970: end) } +} + +extension WatchState { + /// The override running at `date`, if any. + func activeOverride(at date: Date) -> WatchOverride? { + overrides?.last { $0.startDate <= date && date < $0.endDate } + } +} + /// The phone's statistics panel, formatted on the phone. struct WatchStats: Codable, Equatable { var lowPercent: String From bbc06292fd78d7eac21ba80cec7dfd9074183697 Mon Sep 17 00:00:00 2001 From: MtlPhil <76601115+MtlPhil@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:43:10 -0400 Subject: [PATCH 4/4] Show the override's name and percentage under the watch chart The line shows the running override, or the one at the marker while scrubbing, and shares its view with the glucose page. --- LoopFollowWatch/ChartPage.swift | 12 ++++++++++-- LoopFollowWatch/GlucosePage.swift | 25 +++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index f05991501..9af911995 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -6,7 +6,7 @@ import WatchKit /// The full chart on its own page. Tap the chart or the crown button to scrub back /// in time with the Digital Crown; tap again to return to now. The button in the -/// other corner changes the time span. +/// other corner changes the time span. The override line under the chart follows the marker. struct ChartPage: View { let state: WatchState @@ -35,6 +35,10 @@ struct ChartPage: View { if isScrubbing { scrubDetails(now: context.date) } + if let override = state.activeOverride(at: isScrubbing ? markerDate(now: context.date) : context.date) { + OverrideLine(name: override.name, percent: override.percent) + .padding(.trailing, 10) + } } .focusable(isScrubbing) .focused($crownFocused) @@ -77,7 +81,7 @@ struct ChartPage: View { /// Time, BG, carbs and insulin at the marker. Carbs and boluses count when they fall within the /// marker's five-minute crown step, so each entry shows at exactly one step. private func scrubDetails(now: Date) -> some View { - let marker = WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) + let marker = markerDate(now: now) let nearest = state.readings.min { abs($0.date.timeIntervalSince(marker)) < abs($1.date.timeIntervalSince(marker)) } let reading = nearest.flatMap { abs($0.date.timeIntervalSince(marker)) <= 450 ? $0 : nil } let carbs = total(of: state.carbs, near: marker) @@ -111,6 +115,10 @@ struct ChartPage: View { .padding(.trailing, 10) } + private func markerDate(now: Date) -> Date { + WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) + } + /// Sum of the entries within half a crown step of the marker, or nil when there are none. private func total(of entries: [WatchReading], near marker: Date) -> Double? { let matching = entries.filter { abs($0.date.timeIntervalSince(marker)) < 150 } diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index 4a4741feb..d07086dd2 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -119,23 +119,32 @@ struct GlucosePage: View { } } - /// The running override's name and insulin needs, in the green of the overrides button and chart band. + /// The running override's name and insulin needs. @ViewBuilder private func overrideLine(now: Date) -> some View { let active = state.activeOverride(at: now) // The phone's periods end on time; the watch's own fetch only has the snapshot's name. if let name = state.overrides == nil ? state.snapshot?.override : active?.name { - let percent = active?.percent.map { " · \($0) %" } ?? "" - Text("\(Image(systemName: "slider.horizontal.3")) \(name)\(percent)") - .font(.footnote) - .foregroundStyle(.green) - .lineLimit(1) - .minimumScaleFactor(0.7) - .accessibilityLabel("Override \(name)\(active?.percent.map { ", insulin needs \($0) percent" } ?? "")") + OverrideLine(name: name, percent: active?.percent) } } } +/// An override's name and insulin needs, in the green of the overrides button and chart band. +struct OverrideLine: View { + let name: String + let percent: Int? + + var body: some View { + Text("\(Image(systemName: "slider.horizontal.3")) \(name)\(percent.map { " · \($0) %" } ?? "")") + .font(.footnote) + .foregroundStyle(.green) + .lineLimit(1) + .minimumScaleFactor(0.7) + .accessibilityLabel("Override \(name)\(percent.map { ", insulin needs \($0) percent" } ?? "")") + } +} + /// Override, treatments and temp target, as in Apple's bottom-toolbar pattern. private struct RemoteToolbar: View { let state: WatchState