Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ jobs:
- name: Run tests
run: bundle exec fastlane test

# Fork-only: hand back the snapshot references the run recorded or changed.
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
name: "Upload recorded snapshots"
if: ${{ always() }}
with:
name: recorded-snapshots
path: |
Tests/**/__Snapshots__/**
if-no-files-found: ignore

# Save the warmed DerivedData only from `main`, and only on a fresh key,
# so PR runs never evict the shared base cache other PRs restore from.
- uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down
1 change: 1 addition & 0 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2592,6 +2592,7 @@ The server only answers while the app is open, and stops when you leave it.";
"watch.complications.builder.show_unit" = "Show unit";
"watch.complications.builder.show_value" = "Show value";
"watch.complications.builder.show_when_inactive" = "Show when inactive";
"watch.complications.builder.show_when_inactive_footer" = "When off, the complication shows the Home Assistant logo while the watch display is dimmed, such as when your wrist is down.";
"watch.complications.builder.size_options" = "%1$@ options";
"watch.complications.builder.size_options_footer" = "These settings apply to the selected size. Switch the preview size above to customize each size independently.";
"watch.complications.builder.source" = "Source";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct WatchComplicationConfigurationSheet: View {
entityValueSection
familySection
slotSections
dimmedDisplaySection
}
// Floats over the form only — never over the navigation bar, so the Done button stays
// reachable.
Expand Down Expand Up @@ -183,6 +184,17 @@ struct WatchComplicationConfigurationSheet: View {
}
}

/// Whether the complication keeps its content on the face while the watch display is dimmed (wrist
/// down, Always On). Global rather than per size, like the colors: it is a privacy choice about the
/// complication itself, so it sits after the per-size sections. Internal for its tests.
var dimmedDisplaySection: some View {
Section {
Toggle(isOn: showWhenInactiveBinding) { Text(L10n.Watch.Complications.Builder.showWhenInactive) }
} footer: {
Text(L10n.Watch.Complications.Builder.showWhenInactiveFooter)
}
}

/// Gauge + color options shown inside the value slot's section: everything else that shapes how
/// the value element renders (the gauge/progress bar with its range and min/max labels, and the
/// complication's colors). Keeping these with the value keeps each section self-contained, one
Expand Down Expand Up @@ -502,6 +514,15 @@ struct WatchComplicationConfigurationSheet: View {
)
}

/// Shown while dimmed unless turned off; nil (configs saved before the toggle existed) reads as on.
/// Internal for its tests.
var showWhenInactiveBinding: Binding<Bool> {
Binding(
get: { viewModel.config.showsWhenInactive() },
set: { viewModel.config.showWhenInactive = $0 }
)
}

private func stringBinding(_ keyPath: WritableKeyPath<WatchComplicationConfig, String?>) -> Binding<String> {
Binding(
get: { viewModel.config[keyPath: keyPath] ?? "" },
Expand Down
2 changes: 2 additions & 0 deletions Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8433,6 +8433,8 @@ public enum L10n {
public static var showValue: String { return L10n.tr("Localizable", "watch.complications.builder.show_value") }
/// Show when inactive
public static var showWhenInactive: String { return L10n.tr("Localizable", "watch.complications.builder.show_when_inactive") }
/// When off, the complication shows the Home Assistant logo while the watch display is dimmed, such as when your wrist is down.
public static var showWhenInactiveFooter: String { return L10n.tr("Localizable", "watch.complications.builder.show_when_inactive_footer") }
/// %1$@ options
public static func sizeOptions(_ p1: Any) -> String {
return L10n.tr("Localizable", "watch.complications.builder.size_options", String(describing: p1))
Expand Down
6 changes: 6 additions & 0 deletions Sources/WatchWidgets/WatchWidgetComplicationSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ struct WatchWidgetComplicationSnapshot: Codable {
/// Whether the complication should render while the display is dimmed (default true).
var showsWhenInactive: Bool { showWhenInactive ?? true }

/// What the face draws for this complication: the built-in placeholder (the Home Assistant logo)
/// while the display is dimmed and the complication is set not to show then, otherwise itself.
func displayed(isLuminanceReduced: Bool) -> Self {
isLuminanceReduced && !showsWhenInactive ? .placeholder : self
}

// MARK: - Slot texts

// The top-level fields predate slots and are confusingly inverted (`title` carries the value,
Expand Down
16 changes: 12 additions & 4 deletions Sources/WatchWidgets/WatchWidgetsEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import WidgetKit
@available(watchOS 10.0, *)
struct WatchWidgetsEntryView: View {
let entry: WatchWidgetEntry
/// True while the display is dimmed: wrist down on an Always On watch.
@Environment(\.isLuminanceReduced) private var isLuminanceReduced

/// The complication as the face should draw it right now: swapped for the Home Assistant logo while
/// dimmed if the user turned "Show when inactive" off. The tap target stays the real complication's.
private var complication: WatchWidgetComplicationSnapshot? {
entry.complication?.displayed(isLuminanceReduced: isLuminanceReduced)
}

var body: some View {
content
Expand All @@ -31,13 +39,13 @@ struct WatchWidgetsEntryView: View {
private var content: some View {
switch entry.family {
case .accessoryInline:
InlineComplicationView(complication: entry.complication, family: entry.family)
InlineComplicationView(complication: complication, family: entry.family)
case .accessoryCorner:
CornerComplicationView(complication: entry.complication, family: entry.family)
CornerComplicationView(complication: complication, family: entry.family)
case .accessoryRectangular:
RectangularComplicationView(complication: entry.complication, family: entry.family)
RectangularComplicationView(complication: complication, family: entry.family)
default:
CircularComplicationView(complication: entry.complication, family: entry.family)
CircularComplicationView(complication: complication, family: entry.family)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@testable import HomeAssistant
@testable import Shared
import SharedTesting
import SwiftUI
import Testing

/// The "Show when inactive" choice in the complication's Customize sheet: what the toggle reads for a
/// config saved before it existed, that flipping it is what the watch later reads, and how the section
/// looks. The watch-side hiding lives in the widget extension, which no unit-test target builds.
@MainActor
struct WatchComplicationConfigurationSheetDimmedDisplayTests {
@Test func showWhenInactiveReadsOnUntilTurnedOff() {
let viewModel = WatchComplicationBuilderEditViewModel(
existing: WatchComplicationConfig(serverId: "server-1", entityId: "sensor.net_worth")
)
let sheet = WatchComplicationConfigurationSheet(viewModel: viewModel)

// Saved before the toggle existed: nothing stored, which must still mean "shown".
#expect(viewModel.config.showWhenInactive == nil)
#expect(sheet.showWhenInactiveBinding.wrappedValue)

sheet.showWhenInactiveBinding.wrappedValue = false
#expect(viewModel.config.showWhenInactive == false)
#expect(viewModel.config.showsWhenInactive() == false)
#expect(!sheet.showWhenInactiveBinding.wrappedValue)

sheet.showWhenInactiveBinding.wrappedValue = true
#expect(viewModel.config.showsWhenInactive())
}

@Test func dimmedDisplaySection() {
let viewModel = WatchComplicationBuilderEditViewModel(
existing: WatchComplicationConfig(serverId: "server-1", entityId: "sensor.net_worth")
)
assertLightDarkSnapshots(
of: Form { WatchComplicationConfigurationSheet(viewModel: viewModel).dimmedDisplaySection }
)
}
}
Loading