waterui 0.3.0

A modern UI framework for Rust
import CWaterUI

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

/// Component for Metadata<Cursor>.
///
/// Sets the cursor style when the pointer is over the wrapped view.
/// The cursor automatically resets when the pointer exits the view bounds.
@MainActor
final class WuiCursor: PlatformView, WuiComponent {
  static var rawId: CWaterUI.WuiTypeId { waterui_metadata_cursor_id() }

  private let contentView: any WuiComponent
  private var styleObservation: WuiComputedObservation<WuiCursorStyle>?
  private var currentStyle: WuiCursorStyle = WuiCursorStyle_Arrow

  #if canImport(AppKit)
    private var trackingArea: NSTrackingArea?
    private var isMouseInside = false
  #endif

  var stretchAxis: WuiStretchAxis {
    contentView.stretchAxis
  }

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

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

    super.init(frame: .zero)

    contentView.translatesAutoresizingMaskIntoConstraints = true
    addSubview(contentView)

    // Setup watcher for reactive cursor style
    setupWatcher(metadata.value)

    #if canImport(AppKit)
      setupTrackingAreaIfNeeded()
    #elseif canImport(UIKit)
      addInteraction(UIPointerInteraction(delegate: self))
    #endif
  }

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

  private func setupWatcher(_ cursor: CWaterUI.WuiCursor) {
    let observation = WuiComputedObservation(WuiComputed<WuiCursorStyle>(cursor.style)) {
      [weak self] value, _ in
      guard let self else { return }
      self.currentStyle = value
      #if canImport(AppKit)
        if self.isMouseInside {
          self.applyCursor()
        }
      #endif
    }
    styleObservation = observation
    currentStyle = observation.value
  }

  #if canImport(AppKit)
    private func setupTrackingAreaIfNeeded() {
      guard trackingArea == nil else { return }
      let options: NSTrackingArea.Options = [
        .mouseEnteredAndExited,
        .cursorUpdate,
        .activeInKeyWindow,
        .inVisibleRect,
      ]
      let area = NSTrackingArea(
        rect: .zero,
        options: options,
        owner: self,
        userInfo: nil
      )
      trackingArea = area
      addTrackingArea(area)
    }

    override func updateTrackingAreas() {
      super.updateTrackingAreas()
      setupTrackingAreaIfNeeded()
    }

    override func mouseEntered(with event: NSEvent) {
      super.mouseEntered(with: event)
      guard !isMouseInside else { return }
      isMouseInside = true
      applyCursor()
    }

    override func mouseExited(with event: NSEvent) {
      super.mouseExited(with: event)
      guard isMouseInside else { return }
      isMouseInside = false
      NSCursor.arrow.set()
    }

    override func cursorUpdate(with event: NSEvent) {
      super.cursorUpdate(with: event)
      applyCursor()
    }

    private func applyCursor() {
      let cursor = nsCursor(for: currentStyle)
      cursor.set()
    }

    private func nsCursor(for style: WuiCursorStyle) -> NSCursor {
      switch style {
      case WuiCursorStyle_Arrow:
        return .arrow
      case WuiCursorStyle_PointingHand:
        return .pointingHand
      case WuiCursorStyle_IBeam:
        return .iBeam
      case WuiCursorStyle_Crosshair:
        return .crosshair
      case WuiCursorStyle_OpenHand:
        return .openHand
      case WuiCursorStyle_ClosedHand:
        return .closedHand
      case WuiCursorStyle_NotAllowed:
        return .operationNotAllowed
      case WuiCursorStyle_ResizeLeft:
        return .resizeLeft
      case WuiCursorStyle_ResizeRight:
        return .resizeRight
      case WuiCursorStyle_ResizeUp:
        return .resizeUp
      case WuiCursorStyle_ResizeDown:
        return .resizeDown
      case WuiCursorStyle_ResizeLeftRight:
        return .resizeLeftRight
      case WuiCursorStyle_ResizeUpDown:
        return .resizeUpDown
      case WuiCursorStyle_Move:
        // macOS doesn't have a dedicated move cursor, use openHand
        return .openHand
      case WuiCursorStyle_Wait:
        guard let image = NSImage(systemSymbolName: "hourglass", accessibilityDescription: nil)
        else {
          fatalError("The system hourglass cursor image is unavailable")
        }
        return NSCursor(
          image: image,
          hotSpot: NSPoint(x: image.size.width / 2, y: image.size.height / 2)
        )
      case WuiCursorStyle_Copy:
        return .dragCopy
      default:
        fatalError("Unsupported WaterUI cursor style: \(style.rawValue)")
      }
    }
  #endif

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

  func sizeThatFits(_ proposal: WuiProposalSize) -> CGSize {
    contentView.sizeThatFits(proposal)
  }

  #if canImport(UIKit)
    override func layoutSubviews() {
      super.layoutSubviews()
      contentView.frame = bounds
    }
  #elseif canImport(AppKit)
    nonisolated override var isFlipped: Bool { true }

    override func layout() {
      super.layout()
      contentView.frame = bounds
    }
  #endif
}

#if canImport(UIKit)
  extension WuiCursor: UIPointerInteractionDelegate {
    func pointerInteraction(
      _ interaction: UIPointerInteraction,
      styleFor region: UIPointerRegion
    ) -> UIPointerStyle? {
      let shape = UIPointerShape.roundedRect(bounds)
      return UIPointerStyle(shape: shape)
    }
  }
#endif