waterui 0.3.0

A modern UI framework for Rust
//
//  Color.swift
//  waterui-swift
//
//  Created by Lexo Liu on 10/21/24.
//
import CWaterUI

#if canImport(UIKit)
  import UIKit
#elseif canImport(AppKit)
  import AppKit
#endif

func wuiLinearToSrgb(_ linear: Float) -> Float {
  if linear <= 0.003_130_8 {
    return linear * 12.92
  }
  return 1.055 * pow(linear, 1.0 / 2.4) - 0.055
}

func wuiSrgbToLinear(_ srgb: Float) -> Float {
  if srgb <= 0.040_45 {
    return srgb / 12.92
  }
  return pow((srgb + 0.055) / 1.055, 2.4)
}

private func wuiClampUnit(_ value: Float) -> Float {
  min(max(value, 0.0), 1.0)
}

private func wuiHeadroomScale(_ headroom: Float) -> Float {
  precondition(headroom.isFinite && headroom >= 0, "Color headroom must be finite and non-negative")
  return 1.0 + headroom
}

private func wuiLinearComponents(_ color: WuiResolvedColor) -> (Float, Float, Float, Float) {
  let scale = wuiHeadroomScale(color.headroom)
  return (
    color.red * scale,
    color.green * scale,
    color.blue * scale,
    wuiClampUnit(color.opacity)
  )
}

@MainActor
class WuiColor {
  private var inner: OpaquePointer?
  init(_ inner: OpaquePointer) {
    self.inner = inner
  }

  func resolve(in env: WuiEnvironment) -> WuiComputed<WuiResolvedColor> {
    guard let inner else {
      fatalError("WuiColor pointer was already consumed")
    }
    let computed = waterui_resolve_color(inner, env.inner)
    return WuiComputed(computed!)
  }

  func intoInner() -> OpaquePointer {
    guard let inner else {
      fatalError("WuiColor pointer was already consumed")
    }
    self.inner = nil
    return inner
  }

  @MainActor deinit {
    if let inner {
      waterui_drop_color(inner)
    }
  }
}

#if canImport(UIKit)
  extension WuiResolvedColor {
    func toUIColor(allowHdr: Bool = true) -> UIColor {
      let alpha = wuiClampUnit(self.opacity)

      if allowHdr {
        let exposure = CGFloat(wuiHeadroomScale(self.headroom))

        // UIColor's HDR initializer expects SDR (nominal [0..1]) components. Our resolved color is stored
        // in linear space, so convert to sRGB before constructing the base SDR color.
        var r = wuiLinearToSrgb(self.red)
        var g = wuiLinearToSrgb(self.green)
        var b = wuiLinearToSrgb(self.blue)

        // If upstream provides linear components outside [0..1], normalize into the exposure to keep
        // the base SDR color in-range while preserving the intended output.
        let maxLinear = max(self.red, self.green, self.blue)
        if maxLinear > 1.0 {
          let mergedExposure = max(exposure, CGFloat(maxLinear))
          let inv = 1.0 / Float(mergedExposure)
          r = wuiLinearToSrgb(self.red * inv)
          g = wuiLinearToSrgb(self.green * inv)
          b = wuiLinearToSrgb(self.blue * inv)
          return UIColor(
            red: CGFloat(wuiClampUnit(r)),
            green: CGFloat(wuiClampUnit(g)),
            blue: CGFloat(wuiClampUnit(b)),
            alpha: CGFloat(alpha),
            linearExposure: mergedExposure
          )
        }

        return UIColor(
          red: CGFloat(wuiClampUnit(r)),
          green: CGFloat(wuiClampUnit(g)),
          blue: CGFloat(wuiClampUnit(b)),
          alpha: CGFloat(alpha),
          linearExposure: exposure
        )
      }

      let (r, g, b, a) = wuiLinearComponents(self)

      guard let colorSpace = CGColorSpace(name: CGColorSpace.linearSRGB),
        let cgColor = CGColor(
          colorSpace: colorSpace,
          components: [
            CGFloat(wuiClampUnit(r)),
            CGFloat(wuiClampUnit(g)),
            CGFloat(wuiClampUnit(b)),
            CGFloat(a),
          ]
        )
      else {
        fatalError("Core Graphics could not create a linear sRGB color")
      }
      return UIColor(cgColor: cgColor)
    }

    static func fromUIColor(_ color: UIColor) -> WuiResolvedColor {
      var headroom: Float = 0.0
      var sourceColor = color
      let exposure = color.linearExposure
      if exposure > 1.0 {
        headroom = Float(exposure - 1.0)
        sourceColor = color.standardDynamicRange
      }

      if let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
        let converted = sourceColor.cgColor.converted(
          to: colorSpace,
          intent: .defaultIntent,
          options: nil
        ),
        let components = converted.components,
        components.count >= 3
      {
        let alpha = components.count > 3 ? components[3] : 1.0
        return WuiResolvedColor(
          red: Float(components[0]),
          green: Float(components[1]),
          blue: Float(components[2]),
          opacity: Float(alpha),
          headroom: headroom
        )
      }

      var r: CGFloat = 0
      var g: CGFloat = 0
      var b: CGFloat = 0
      var a: CGFloat = 0
      guard sourceColor.getRed(&r, green: &g, blue: &b, alpha: &a) else {
        fatalError("UIColor cannot convert to extended linear sRGB")
      }
      return WuiResolvedColor(
        red: wuiSrgbToLinear(Float(r)),
        green: wuiSrgbToLinear(Float(g)),
        blue: wuiSrgbToLinear(Float(b)),
        opacity: Float(a),
        headroom: headroom
      )
    }
  }
#elseif canImport(AppKit)
  extension WuiResolvedColor {
    func toNSColor(allowHdr: Bool = true) -> NSColor {
      let alpha = wuiClampUnit(self.opacity)
      let base = (self.red, self.green, self.blue)

      if allowHdr, self.headroom > 0.0 {
        guard let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
          let cgColor = CGColor(
            colorSpace: colorSpace,
            components: [CGFloat(base.0), CGFloat(base.1), CGFloat(base.2), CGFloat(alpha)]
          ),
          let baseColor = NSColor(cgColor: cgColor)
        else {
          fatalError("AppKit rejected a valid HDR color")
        }
        return baseColor.applyingContentHeadroom(CGFloat(wuiHeadroomScale(self.headroom)))
      }

      let (r, g, b, a) = wuiLinearComponents(self)

      if allowHdr {
        guard let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
          let cgColor = CGColor(
            colorSpace: colorSpace,
            components: [CGFloat(r), CGFloat(g), CGFloat(b), CGFloat(a)]
          ),
          let color = NSColor(cgColor: cgColor)
        else {
          fatalError("AppKit rejected a valid extended-linear sRGB CGColor")
        }
        return color
      }

      guard let colorSpace = CGColorSpace(name: CGColorSpace.linearSRGB),
        let cgColor = CGColor(
          colorSpace: colorSpace,
          components: [
            CGFloat(wuiClampUnit(r)),
            CGFloat(wuiClampUnit(g)),
            CGFloat(wuiClampUnit(b)),
            CGFloat(a),
          ]
        ),
        let color = NSColor(cgColor: cgColor)
      else {
        fatalError("AppKit rejected a valid linear sRGB color")
      }
      return color
    }

    static func fromNSColor(_ color: NSColor) -> WuiResolvedColor {
      var headroom: Float = 0.0
      var sourceColor = color
      let exposure = color.linearExposure
      if exposure > 1.0 {
        headroom = Float(exposure - 1.0)
        sourceColor = color.standardDynamicRange
      }

      func components(in colorSpace: NSColorSpace) -> (CGFloat, CGFloat, CGFloat, CGFloat)? {
        guard let converted = sourceColor.usingColorSpace(colorSpace),
          let components = converted.cgColor.components,
          components.count >= 3
        else {
          return nil
        }
        let alpha = components.count > 3 ? components[3] : 1.0
        return (components[0], components[1], components[2], alpha)
      }

      if let (r, g, b, a) = components(in: .extendedSRGB) {
        return WuiResolvedColor(
          red: wuiSrgbToLinear(Float(r)),
          green: wuiSrgbToLinear(Float(g)),
          blue: wuiSrgbToLinear(Float(b)),
          opacity: Float(a),
          headroom: headroom
        )
      }

      if let (r, g, b, a) = components(in: .sRGB) {
        return WuiResolvedColor(
          red: wuiSrgbToLinear(Float(r)),
          green: wuiSrgbToLinear(Float(g)),
          blue: wuiSrgbToLinear(Float(b)),
          opacity: Float(a),
          headroom: headroom
        )
      }

      fatalError("WaterUI: NSColor '\(color)' could not be resolved to an RGB color space.")
    }
  }
#endif