InfoDisplayItem.swift 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. // LoopFollow
  2. // InfoDisplayItem.swift
  3. import SwiftUI
  4. /// One configurable row in the Info Display: which info it shows, whether it is
  5. /// visible, and its optional threshold-based coloring. The ordered array of
  6. /// these replaces the old parallel `infoSort` / `infoVisible` storage.
  7. struct InfoDisplayItem: Codable, Equatable, Identifiable {
  8. var type: InfoType
  9. var isVisible: Bool
  10. var coloring: InfoColoring
  11. var id: Int { type.rawValue }
  12. }
  13. extension Array where Element == InfoDisplayItem {
  14. func item(for type: InfoType) -> InfoDisplayItem? {
  15. first { $0.type == type }
  16. }
  17. func isVisible(_ type: InfoType) -> Bool {
  18. item(for: type)?.isVisible ?? false
  19. }
  20. }
  21. /// Which direction is "concerning" for a metric. Fixed per InfoType (battery is
  22. /// bad when low, sensor age is bad when high) rather than user-configurable.
  23. enum InfoColorDirection {
  24. case above // color as the value rises (e.g. SAGE, IOB)
  25. case below // color as the value falls (e.g. Battery, Pump)
  26. }
  27. /// What threshold coloring looks like for one InfoType: the concerning
  28. /// direction, the unit shown next to the value, and the bounds the threshold
  29. /// steppers move in. Only types that expose one of these can be colored.
  30. struct InfoColorConfig {
  31. let direction: InfoColorDirection
  32. let unit: String
  33. let range: ClosedRange<Double>
  34. let step: Double
  35. let defaultWarning: Double
  36. let defaultUrgent: Double
  37. /// Decimals to show, derived from the step (0.5 → 1, 5 → 0).
  38. var fractionDigits: Int {
  39. step.truncatingRemainder(dividingBy: 1) == 0 ? 0 : 1
  40. }
  41. }
  42. /// Per-row, opt-in color thresholds. Purely visual — never triggers an alarm.
  43. struct InfoColoring: Codable, Equatable {
  44. var enabled: Bool = false
  45. var warning: Double? = nil // yellow
  46. var urgent: Double? = nil // red
  47. /// Resolves the color for a raw numeric value, given the metric's fixed
  48. /// direction. Returns `nil` only when coloring is off (callers map that to
  49. /// the default text color); when enabled, an in-range value reads green,
  50. /// mirroring the app's BG text coloring.
  51. func color(for value: Double, direction: InfoColorDirection) -> Color? {
  52. guard enabled else { return nil }
  53. switch direction {
  54. case .above:
  55. if let urgent, value >= urgent { return .red }
  56. if let warning, value >= warning { return .yellow }
  57. case .below:
  58. if let urgent, value <= urgent { return .red }
  59. if let warning, value <= warning { return .yellow }
  60. }
  61. return .green
  62. }
  63. }