InfoRowSettingsView.swift 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. // LoopFollow
  2. // InfoRowSettingsView.swift
  3. import SwiftUI
  4. struct InfoRowSettingsView: View {
  5. @Binding var item: InfoDisplayItem
  6. var body: some View {
  7. Form {
  8. Section {
  9. Toggle("Show in Info Display", isOn: $item.isVisible)
  10. }
  11. if let config = item.type.colorConfig {
  12. Section(
  13. header: Text("Color"),
  14. footer: Text(colorFooter(config))
  15. ) {
  16. Toggle("Enable coloring", isOn: enabledBinding(config))
  17. if item.coloring.enabled {
  18. thresholdRow(title: "Yellow at", value: $item.coloring.warning, default: config.defaultWarning, config: config)
  19. thresholdRow(title: "Red at", value: $item.coloring.urgent, default: config.defaultUrgent, config: config)
  20. if let thresholdWarning = thresholdWarning(config) {
  21. Label(thresholdWarning, systemImage: "exclamationmark.triangle.fill")
  22. .font(.footnote)
  23. .foregroundStyle(.orange)
  24. }
  25. }
  26. }
  27. }
  28. }
  29. .navigationBarTitle(item.type.name, displayMode: .inline)
  30. .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
  31. }
  32. /// Turning coloring on seeds any unset threshold, so the steppers and the
  33. /// stored values always agree.
  34. private func enabledBinding(_ config: InfoColorConfig) -> Binding<Bool> {
  35. Binding(
  36. get: { item.coloring.enabled },
  37. set: { isOn in
  38. if isOn {
  39. item.coloring.warning = item.coloring.warning ?? config.defaultWarning
  40. item.coloring.urgent = item.coloring.urgent ?? config.defaultUrgent
  41. }
  42. item.coloring.enabled = isOn
  43. }
  44. )
  45. }
  46. private func thresholdRow(title: String, value: Binding<Double?>, default defaultValue: Double, config: InfoColorConfig) -> some View {
  47. SettingsStepperRow(
  48. title: title,
  49. range: config.range,
  50. step: config.step,
  51. value: Binding(
  52. get: { value.wrappedValue ?? defaultValue },
  53. set: { value.wrappedValue = $0 }
  54. ),
  55. format: { formatted($0, config) }
  56. )
  57. }
  58. private func formatted(_ value: Double, _ config: InfoColorConfig) -> String {
  59. let number = Localizer.formatToLocalizedString(
  60. value,
  61. maxFractionDigits: config.fractionDigits,
  62. minFractionDigits: config.fractionDigits
  63. )
  64. return "\(number) \(config.unit)"
  65. }
  66. /// Non-blocking sanity check: red should be more severe than yellow, in the
  67. /// metric's fixed direction. `nil` when consistent or a threshold is unset.
  68. private func thresholdWarning(_ config: InfoColorConfig) -> String? {
  69. guard item.coloring.enabled,
  70. let warning = item.coloring.warning,
  71. let urgent = item.coloring.urgent
  72. else { return nil }
  73. switch config.direction {
  74. case .above:
  75. return urgent < warning ? "Red should be at or above yellow." : nil
  76. case .below:
  77. return urgent > warning ? "Red should be at or below yellow." : nil
  78. }
  79. }
  80. private func colorFooter(_ config: InfoColorConfig) -> String {
  81. switch config.direction {
  82. case .above:
  83. return "The value turns yellow at or above the yellow level and red at or above the red level. In range it shows green. A visual cue only — it never triggers an alarm."
  84. case .below:
  85. return "The value turns yellow at or below the yellow level and red at or below the red level. In range it shows green. A visual cue only — it never triggers an alarm."
  86. }
  87. }
  88. }