aboutsummaryrefslogtreecommitdiff
path: root/Sources/phbar/Models/HEX.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/phbar/Models/HEX.swift')
-rw-r--r--Sources/phbar/Models/HEX.swift114
1 files changed, 114 insertions, 0 deletions
diff --git a/Sources/phbar/Models/HEX.swift b/Sources/phbar/Models/HEX.swift
new file mode 100644
index 0000000..fbbdf39
--- /dev/null
+++ b/Sources/phbar/Models/HEX.swift
@@ -0,0 +1,114 @@
+import SwiftUI
+
+/// A color described by a hexadecimal color code.
+///
+/// `HEX` is a lightweight, string-backed representation: the code is stored
+/// verbatim and parsed only when converted to a SwiftUI color via `Color(hex:)`
+/// or `Color.hex(_:)`, which resolve it in the sRGB color space.
+///
+/// Supported formats (the leading `#` is optional):
+/// - `#RGB`
+/// - `#RGBA`
+/// - `#RRGGBB`
+/// - `#RRGGBBAA`
+///
+/// Single-digit components are expanded by repeating the digit; for example,
+/// `#3aF` is read as `#33AAFF`. When present, the trailing digits encode alpha
+/// (opacity); otherwise opacity defaults to `1`.
+struct HEX {
+ /// The raw hex code, exactly as supplied. It is not validated or normalized;
+ /// parsing is performed lazily by `Color.hex(_:)`.
+ var code: String
+
+ /// Creates a hex color from the given code.
+ ///
+ /// - Parameter code: A hex color code. Stored verbatim; see ``HEX`` for the
+ /// accepted formats.
+ init(code: String) {
+ self.code = code
+ }
+}
+
+extension SwiftUI.Color {
+ /// Creates a color from a ``HEX`` value.
+ init(hex: HEX) {
+ self = .hex(hex.code)
+ }
+
+ /// Creates a color from a hex code, resolving it in the sRGB color space.
+ ///
+ /// Supported formats (the leading `#` is optional): `#RGB`, `#RGBA`,
+ /// `#RRGGBB`, and `#RRGGBBAA`. Single-digit components are expanded by
+ /// repeating the digit. When present, the trailing digits encode alpha
+ /// (opacity); otherwise opacity defaults to `1`. Malformed input resolves to
+ /// opaque black.
+ ///
+ /// - Parameter code: A hex color code.
+ static func hex(_ code: String) -> SwiftUI.Color {
+ var sanitized = code.trimmingCharacters(in: .whitespacesAndNewlines)
+ if sanitized.hasPrefix("#") {
+ sanitized.removeFirst()
+ }
+
+ var rgba: UInt64 = 0
+ Scanner(string: sanitized).scanHexInt64(&rgba)
+
+ let red: Double
+ let green: Double
+ let blue: Double
+ let opacity: Double
+
+ switch sanitized.count {
+ case 8: // RRGGBBAA
+ red = Double((rgba >> 24) & 0xFF) / 255.0
+ green = Double((rgba >> 16) & 0xFF) / 255.0
+ blue = Double((rgba >> 8) & 0xFF) / 255.0
+ opacity = Double(rgba & 0xFF) / 255.0
+ case 6: // RRGGBB
+ red = Double((rgba >> 16) & 0xFF) / 255.0
+ green = Double((rgba >> 8) & 0xFF) / 255.0
+ blue = Double(rgba & 0xFF) / 255.0
+ opacity = 1.0
+ case 4: // RGBA
+ red = Double(((rgba >> 12) & 0xF) * 0x11) / 255.0
+ green = Double(((rgba >> 8) & 0xF) * 0x11) / 255.0
+ blue = Double(((rgba >> 4) & 0xF) * 0x11) / 255.0
+ opacity = Double((rgba & 0xF) * 0x11) / 255.0
+ case 3: // RGB
+ red = Double(((rgba >> 8) & 0xF) * 0x11) / 255.0
+ green = Double(((rgba >> 4) & 0xF) * 0x11) / 255.0
+ blue = Double((rgba & 0xF) * 0x11) / 255.0
+ opacity = 1.0
+ default:
+ red = 0.0
+ green = 0.0
+ blue = 0.0
+ opacity = 1.0
+ }
+
+ return Color(.sRGB, red: red, green: green, blue: blue, opacity: opacity)
+ }
+}
+
+extension SwiftUI.Color.Resolved {
+ /// The hex code for this resolved color.
+ ///
+ /// Components are clamped to `0...1` and quantized to 8 bits. The result is a
+ /// 6-digit `#RRGGBB` code, or an 8-digit `#RRGGBBAA` code when the color is
+ /// not fully opaque.
+ var hex: HEX {
+ let r = Int((min(max(Double(red), 0), 1) * 255).rounded())
+ let g = Int((min(max(Double(green), 0), 1) * 255).rounded())
+ let b = Int((min(max(Double(blue), 0), 1) * 255).rounded())
+
+ let code: String
+ if opacity < 1 {
+ let a = Int((min(max(Double(opacity), 0), 1) * 255).rounded())
+ code = String(format: "#%02X%02X%02X%02X", r, g, b, a)
+ } else {
+ code = String(format: "#%02X%02X%02X", r, g, b)
+ }
+
+ return HEX(code: code)
+ }
+}