waterui 0.3.0

A modern UI framework for Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
import CWaterUI

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

enum LazyStackAxis: Int32 {
  case unsupported = 0
  case vertical = 1
  case horizontal = 2
}

// MARK: - Proposal and Layout Types

public struct WuiProposalSize {
  public var width: Float?
  public var height: Float?

  public init(width: Float? = nil, height: Float? = nil) {
    self.width = width
    self.height = height
  }

  init(_ raw: CWaterUI.WuiProposalSize) {
    self.width = raw.width.isNaN ? nil : raw.width
    self.height = raw.height.isNaN ? nil : raw.height
  }

  public init(size: CGSize) {
    self.width = size.width.isNaN ? nil : Float(size.width)
    self.height = size.height.isNaN ? nil : Float(size.height)
  }

  func toCStruct() -> CWaterUI.WuiProposalSize {
    CWaterUI.WuiProposalSize(
      width: width ?? .nan,
      height: height ?? .nan
    )
  }
}

struct WuiPoint {
  var x: Float
  var y: Float

  init(_ point: CGPoint) {
    self.x = Float(point.x)
    self.y = Float(point.y)
  }

  init(_ raw: CWaterUI.WuiPoint) {
    self.x = raw.x
    self.y = raw.y
  }

  func toCStruct() -> CWaterUI.WuiPoint {
    CWaterUI.WuiPoint(x: x, y: y)
  }

  var cgPoint: CGPoint {
    CGPoint(x: CGFloat(x), y: CGFloat(y))
  }
}

struct WuiSize {
  var width: Float
  var height: Float

  init(width: Float, height: Float) {
    self.width = width
    self.height = height
  }

  init(_ size: CGSize) {
    self.width = Float(size.width)
    self.height = Float(size.height)
  }

  init(_ raw: CWaterUI.WuiSize) {
    self.width = raw.width
    self.height = raw.height
  }

  func toCStruct() -> CWaterUI.WuiSize {
    CWaterUI.WuiSize(width: width, height: height)
  }

  var cgSize: CGSize {
    CGSize(width: CGFloat(width), height: CGFloat(height))
  }
}

struct WuiRect {
  var origin: WuiPoint
  var size: WuiSize

  init(_ rect: CGRect) {
    self.origin = WuiPoint(rect.origin)
    self.size = WuiSize(rect.size)
  }

  init(_ raw: CWaterUI.WuiRect) {
    self.origin = WuiPoint(raw.origin)
    self.size = WuiSize(raw.size)
  }

  func toCStruct() -> CWaterUI.WuiRect {
    CWaterUI.WuiRect(origin: origin.toCStruct(), size: size.toCStruct())
  }

  var cgRect: CGRect {
    CGRect(origin: origin.cgPoint, size: size.cgSize)
  }
}

public struct WuiHorizontalGuide {
  var alignment: CWaterUI.WuiHorizontalAlignment
  var value: Float

  init(_ raw: CWaterUI.WuiHorizontalGuide) {
    self.alignment = raw.alignment
    self.value = raw.value
  }

  func toCStruct() -> CWaterUI.WuiHorizontalGuide {
    CWaterUI.WuiHorizontalGuide(alignment: alignment, value: value)
  }
}

public struct WuiVerticalGuide {
  var alignment: CWaterUI.WuiVerticalAlignment
  var value: Float

  init(alignment: CWaterUI.WuiVerticalAlignment, value: Float) {
    self.alignment = alignment
    self.value = value
  }

  init(_ raw: CWaterUI.WuiVerticalGuide) {
    self.alignment = raw.alignment
    self.value = raw.value
  }

  func toCStruct() -> CWaterUI.WuiVerticalGuide {
    CWaterUI.WuiVerticalGuide(alignment: alignment, value: value)
  }
}

public struct WuiViewDimensions {
  var size: WuiSize
  var horizontalGuides: [WuiHorizontalGuide]
  var verticalGuides: [WuiVerticalGuide]

  init(
    size: CGSize,
    horizontalGuides: [WuiHorizontalGuide] = [],
    verticalGuides: [WuiVerticalGuide] = []
  ) {
    self.size = WuiSize(size)
    self.horizontalGuides = horizontalGuides
    self.verticalGuides = verticalGuides
  }

  init(_ raw: CWaterUI.WuiViewDimensions) {
    self.size = WuiSize(raw.size)
    self.horizontalGuides = WuiArray<CWaterUI.WuiHorizontalGuide>(raw.horizontal_guides)
      .map(WuiHorizontalGuide.init)
    self.verticalGuides = WuiArray<CWaterUI.WuiVerticalGuide>(raw.vertical_guides)
      .map(WuiVerticalGuide.init)
  }

  var cgSize: CGSize {
    size.cgSize
  }

  func toCStruct() -> CWaterUI.WuiViewDimensions {
    let horizontalArray = WuiArray(array: horizontalGuides.map { $0.toCStruct() })
    let verticalArray = WuiArray(array: verticalGuides.map { $0.toCStruct() })
    return CWaterUI.WuiViewDimensions(
      size: size.toCStruct(),
      horizontal_guides: unsafeBitCast(
        horizontalArray.intoInner(),
        to: CWaterUI.WuiArray_WuiHorizontalGuide.self
      ),
      vertical_guides: unsafeBitCast(
        verticalArray.intoInner(),
        to: CWaterUI.WuiArray_WuiVerticalGuide.self
      )
    )
  }
}

// MARK: - Layout Engine

@MainActor
private final class WuiLayoutInvalidationTarget {
  weak var owner: PlatformView?

  func invalidate() {
    guard let owner else { return }
    owner.invalidateIntrinsicContentSize()
    #if canImport(UIKit)
      owner.setNeedsLayout()
    #elseif canImport(AppKit)
      owner.needsLayout = true
    #endif
    owner.invalidateCapturedRendering()
  }
}

@MainActor
final class WuiLayout {
  private var inner: OpaquePointer
  private let invalidationWatcher: OpaquePointer
  private let invalidationTarget: WuiLayoutInvalidationTarget

  init(inner: OpaquePointer) {
    self.inner = inner
    let invalidationTarget = WuiLayoutInvalidationTarget()
    self.invalidationTarget = invalidationTarget
    let callback = WuiRedrawCallbackBox { [weak invalidationTarget] in
      invalidationTarget?.invalidate()
    }
    self.invalidationWatcher = waterui_layout_watch_invalidation(
      inner,
      Unmanaged.passRetained(callback).toOpaque(),
      wuiRedrawWakeCallback,
      wuiRedrawDropCallback
    )!
  }

  @MainActor deinit {
    waterui_layout_watcher_drop(invalidationWatcher)
    waterui_drop_layout(inner)
  }

  func setOwner(_ owner: PlatformView) {
    invalidationTarget.owner = owner
  }

  func measure(
    proposal: WuiProposalSize,
    children: CachedSubViewArray
  ) -> WuiViewDimensions {
    let dimensions = waterui_layout_measure(inner, proposal.toCStruct(), children.ffiArray)
    return WuiViewDimensions(dimensions)
  }

  /// Place children within the given bounds.
  /// Returns a rect for each child specifying its position and size.
  func place(
    bounds: CGRect,
    children: CachedSubViewArray
  ) -> [CGRect] {
    let boundsRaw = WuiRect(bounds).toCStruct()
    let rects = waterui_layout_place(inner, boundsRaw, children.ffiArray)
    let rawArray = unsafeBitCast(rects, to: CWaterUI.WuiArray.self)
    let bridged = WuiArray<CWaterUI.WuiRect>(c: rawArray)
    return bridged.map { WuiRect($0).cgRect }
  }

  func lazyStackAxis() -> LazyStackAxis {
    switch waterui_layout_lazy_stack_axis(inner) {
    case WuiLazyStackAxis_Unsupported: return .unsupported
    case WuiLazyStackAxis_Vertical: return .vertical
    case WuiLazyStackAxis_Horizontal: return .horizontal
    default: fatalError("Unknown WaterUI lazy stack axis")
    }
  }

  func lazyStackSpacing() -> Float {
    waterui_layout_lazy_stack_spacing(inner)
  }

  func lazyStackHorizontalAlignment() -> CWaterUI.WuiHorizontalAlignment {
    waterui_layout_lazy_stack_horizontal_alignment(inner)
  }

  func lazyStackVerticalAlignment() -> CWaterUI.WuiVerticalAlignment {
    waterui_layout_lazy_stack_vertical_alignment(inner)
  }
}

// MARK: - SubView Proxy

@MainActor
final class CachedSubViewArray {
  private static let vtable = CWaterUI.WuiArrayVTable(
    drop: { _ in },
    slice: { data in
      guard let data else {
        return WuiArraySlice(head: nil, len: 0)
      }

      let cache = Unmanaged<CachedSubViewArray>.fromOpaque(data).takeUnretainedValue()
      return WuiArraySlice(head: cache.baseAddress, len: UInt(cache.subviews.count))
    }
  )

  private let proxies: [SubViewProxy]
  private let subviews: ContiguousArray<CWaterUI.WuiSubView>
  private let baseAddress: UnsafeMutableRawPointer?

  init(_ proxies: [SubViewProxy]) {
    self.proxies = proxies
    let subviews = ContiguousArray(proxies.map { $0.toBorrowedWuiSubView() })
    self.baseAddress = subviews.withUnsafeBufferPointer { buffer in
      UnsafeMutableRawPointer(mutating: buffer.baseAddress)
    }
    self.subviews = subviews
  }

  var ffiArray: CWaterUI.WuiArray_WuiSubView {
    let raw = CWaterUI.WuiArray(
      data: Unmanaged.passUnretained(self).toOpaque(),
      vtable: Self.vtable
    )
    return unsafeBitCast(raw, to: CWaterUI.WuiArray_WuiSubView.self)
  }

  func resetMeasurements() {
    for proxy in proxies {
      proxy.resetMeasurementCache()
    }
  }
}

/// A proxy for child views that provides measurement via callback.
/// This mirrors Rust's SubView trait.
@MainActor
final class SubViewProxy {
  private struct ProposalCacheKey: Hashable {
    private static let none = UInt32.max

    let width: UInt32
    let height: UInt32

    init(_ proposal: WuiProposalSize) {
      self.width = proposal.width.map { $0.bitPattern } ?? Self.none
      self.height = proposal.height.map { $0.bitPattern } ?? Self.none
    }
  }

  /// Closure that measures the child given a proposal.
  private let measure: (WuiProposalSize) -> WuiViewDimensions
  /// Which axis this view stretches to fill available space
  let stretchAxis: WuiStretchAxis
  /// Layout priority (higher = measured first)
  let priority: Int32
  private var measurementCache: [ProposalCacheKey: WuiViewDimensions] = [:]
  private var activeMeasurements = Set<ProposalCacheKey>()

  init(
    stretchAxis: WuiStretchAxis = .none,
    priority: Int32 = 0,
    measure: @escaping (WuiProposalSize) -> WuiViewDimensions
  ) {
    self.measure = measure
    self.stretchAxis = stretchAxis
    self.priority = priority
  }

  func toBorrowedWuiSubView() -> CWaterUI.WuiSubView {
    let vtable = CWaterUI.WuiSubViewVTable(
      measure: { contextPtr, proposal in
        guard let contextPtr = contextPtr else {
          return WuiViewDimensions(size: .zero).toCStruct()
        }
        let proxy = Unmanaged<SubViewProxy>.fromOpaque(contextPtr).takeUnretainedValue()
        let swiftProposal = WuiProposalSize(proposal)
        return proxy.measureCached(swiftProposal).toCStruct()
      },
      drop: { _ in }
    )

    return CWaterUI.WuiSubView(
      context: Unmanaged.passUnretained(self).toOpaque(),
      vtable: vtable,
      stretch_axis: stretchAxis.ffiValue,
      priority: priority
    )
  }

  func resetMeasurementCache() {
    measurementCache.removeAll(keepingCapacity: true)
    activeMeasurements.removeAll(keepingCapacity: true)
  }

  private func measureCached(_ proposal: WuiProposalSize) -> WuiViewDimensions {
    let key = ProposalCacheKey(proposal)
    if let cached = measurementCache[key] {
      return cached
    }
    guard activeMeasurements.insert(key).inserted else {
      fatalError("WaterUI: recursive layout measurement for proposal \(proposal)")
    }
    let dimensions = measure(proposal)
    activeMeasurements.remove(key)
    measurementCache[key] = dimensions
    return dimensions
  }
}

// MARK: - CGFloat Extensions

extension CGFloat {
  /// Checks if the value is a valid, finite number suitable for layout calculations.
  var isValidForLayout: Bool {
    !isNaN && !isInfinite
  }
}

extension CGRect {
  /// Checks if the rect's origin and size are composed of valid, finite numbers.
  var isValidForLayout: Bool {
    origin.x.isValidForLayout && origin.y.isValidForLayout && size.width.isValidForLayout
      && size.height.isValidForLayout
  }
}