waterui 0.3.0

A modern UI framework for Rust
import CWaterUI

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

/// Component for Metadata<Scale>.
///
/// Applies a scale transform to the wrapped view around the specified anchor point.
/// Scales are purely visual and do not affect layout.
@MainActor
final class WuiScale: PlatformView, WuiComponent {
  static var rawId: CWaterUI.WuiTypeId { waterui_metadata_scale_id() }

  private let contentView: any WuiComponent
  private var scaleXObservation: WuiComputedObservation<Float>?
  private var scaleYObservation: WuiComputedObservation<Float>?

  // Current transform values
  private var currentScaleX: CGFloat = 1.0
  private var currentScaleY: CGFloat = 1.0
  private let anchor: CGPoint
  private var lastBoundsSize: CGSize = .zero

  var stretchAxis: WuiStretchAxis {
    contentView.stretchAxis
  }

  required init(anyview: OpaquePointer, env: WuiEnvironment) {
    let metadata = waterui_force_as_metadata_scale(anyview)

    // Convert anchor from normalized (0-1) to CGPoint
    self.anchor = CGPoint(
      x: CGFloat(metadata.value.anchor.x),
      y: CGFloat(metadata.value.anchor.y)
    )

    // Resolve the content
    self.contentView = WuiAnyView.resolve(anyview: metadata.content, env: env)

    super.init(frame: .zero)

    // Enable layer for transforms
    #if canImport(AppKit)
      wantsLayer = true
      contentView.wantsLayer = true
    #endif

    contentView.translatesAutoresizingMaskIntoConstraints = true
    addSubview(contentView)

    // Setup watchers for reactive transform properties
    setupWatchers(metadata.value)
  }

  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private func setupWatchers(_ scale: WuiScale_Struct) {
    let xObservation = WuiComputedObservation(WuiComputed<Float>(scale.x)) {
      [weak self] value, metadata in
      guard let self else { return }
      self.currentScaleX = CGFloat(value)
      #if canImport(UIKit)
        withPlatformAnimation(metadata) {
          self.applyTransform()
        }
      #elseif canImport(AppKit)
        let animation = parseAnimation(metadata.getAnimation())
        self.applyTransform(animation: animation)
      #endif
    }

    let yObservation = WuiComputedObservation(WuiComputed<Float>(scale.y)) {
      [weak self] value, metadata in
      guard let self else { return }
      self.currentScaleY = CGFloat(value)
      #if canImport(UIKit)
        withPlatformAnimation(metadata) {
          self.applyTransform()
        }
      #elseif canImport(AppKit)
        let animation = parseAnimation(metadata.getAnimation())
        self.applyTransform(animation: animation)
      #endif
    }
    scaleXObservation = xObservation
    scaleYObservation = yObservation
    currentScaleX = CGFloat(xObservation.value)
    currentScaleY = CGFloat(yObservation.value)
    applyTransform()
  }

  private func applyTransform() {
    #if canImport(UIKit)
      let transform = CGAffineTransform(scaleX: currentScaleX, y: currentScaleY)
      contentView.transform = transform
    #elseif canImport(AppKit)
      let layer = transformedContentLayer()
      let transform = CATransform3DMakeScale(currentScaleX, currentScaleY, 1.0)
      wuiSetLayerTransformWithoutImplicitAnimation(layer, transform)
    #endif
    invalidateCapturedRendering()
  }

  #if canImport(AppKit)
    private func applyTransform(animation: Animation) {
      let layer = transformedContentLayer()
      let toTransform = CATransform3DMakeScale(currentScaleX, currentScaleY, 1.0)
      wuiApplyLayerTransform(layer, to: toTransform, animation: animation, key: "wuiScale")
      invalidateCapturedRendering()
    }

    private func transformedContentLayer() -> CALayer {
      wuiLayoutTransformedContent(
        contentView,
        in: bounds,
        anchor: anchor,
        lastBoundsSize: &lastBoundsSize
      )
      guard let layer = contentView.layer else {
        fatalError("WuiScale content must be layer-backed after transformed layout")
      }
      return layer
    }
  #endif

  func layoutPriority() -> Int32 {
    contentView.layoutPriority()
  }

  func sizeThatFits(_ proposal: WuiProposalSize) -> CGSize {
    // Transform doesn't affect layout size
    contentView.sizeThatFits(proposal)
  }

  #if canImport(UIKit)
    override func layoutSubviews() {
      super.layoutSubviews()
      // Set anchorPoint and position for proper scale pivot
      contentView.layer.anchorPoint = anchor
      contentView.bounds = CGRect(origin: .zero, size: bounds.size)
      contentView.center = CGPoint(
        x: bounds.width * anchor.x,
        y: bounds.height * anchor.y
      )
    }
  #elseif canImport(AppKit)
    nonisolated override var isFlipped: Bool { true }

    override func layout() {
      super.layout()

      if wuiLayoutTransformedContent(
        contentView,
        in: bounds,
        anchor: anchor,
        lastBoundsSize: &lastBoundsSize
      ) {
        applyTransform()
      }
    }
  #endif
}

// Type alias for the FFI scale struct
private typealias WuiScale_Struct = CWaterUI.WuiScale